Skip to content

Commit d233755

Browse files
authored
postcss-content-alt-text: improve support for empty string alt text (#1433)
1 parent 26341df commit d233755

15 files changed

+155
-37
lines changed

plugins/postcss-content-alt-text/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to PostCSS Content Alt Text
22

3+
### Unreleased (patch)
4+
5+
- Add specific handling of `content: ">" / "";` as this pattern is used in the same way as `<img alt="">`, i.e. to represent an item that does not need a text alternative.
6+
37
### 1.0.0
48

59
_July 7, 2024_

plugins/postcss-content-alt-text/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ npm install @csstools/postcss-content-alt-text --save-dev
1313
content: url(tree.jpg) / "A beautiful tree in a dark forest";
1414
}
1515
16+
.bar {
17+
content: ">" / "";
18+
}
19+
1620
/* becomes */
1721
1822
.foo {
1923
content: url(tree.jpg) "A beautiful tree in a dark forest";
2024
content: url(tree.jpg) / "A beautiful tree in a dark forest";
2125
}
26+
27+
.bar {
28+
content: ">" ;
29+
content: ">" / "";
30+
}
2231
```
2332

2433
## Usage
@@ -67,11 +76,19 @@ postcssContentAltText({ preserve: false })
6776
content: url(tree.jpg) / "A beautiful tree in a dark forest";
6877
}
6978
79+
.bar {
80+
content: ">" / "";
81+
}
82+
7083
/* becomes */
7184
7285
.foo {
7386
content: url(tree.jpg) "A beautiful tree in a dark forest";
7487
}
88+
89+
.bar {
90+
content: ">" ;
91+
}
7592
```
7693

7794
### stripAltText
@@ -91,12 +108,21 @@ postcssContentAltText({ stripAltText: true })
91108
content: url(tree.jpg) / "A beautiful tree in a dark forest";
92109
}
93110
111+
.bar {
112+
content: ">" / "";
113+
}
114+
94115
/* becomes */
95116
96117
.foo {
97118
content: url(tree.jpg) ;
98119
content: url(tree.jpg) / "A beautiful tree in a dark forest";
99120
}
121+
122+
.bar {
123+
content: ">" ;
124+
content: ">" / "";
125+
}
100126
```
101127

102128
[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"use strict";var e=require("@csstools/css-parser-algorithms"),s=require("@csstools/css-tokenizer"),t=require("@csstools/postcss-progressive-custom-properties"),o=require("@csstools/utilities");const r={test:e=>e.includes("content:")&&e.includes("/")},basePlugin=t=>({postcssPlugin:"postcss-content-alt-text",Declaration(n){if("content"!==n.prop||!n.value.includes("/"))return;if(o.hasFallback(n))return;if(o.hasSupportsAtRuleAncestor(n,r))return;const i=e.parseListOfComponentValues(s.tokenize({css:n.value}));let c=0;for(let o=i.length-1;o>=0;o--){const r=i[o];if(!e.isTokenNode(r))continue;const n=r.value;s.isTokenDelim(n)&&("/"===n[4].value&&(c++,!0===t?.stripAltText?i.splice(o,i.length):i.splice(o,1)))}if(1!==c)return;const l=e.stringify([i]);l!==n.value&&(n.cloneBefore({value:l}),!1===t?.preserve&&n.remove())}});basePlugin.postcss=!0;const creator=e=>{const s=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0,stripAltText:!1},e);return s.enableProgressiveCustomProperties&&s.preserve?{postcssPlugin:"postcss-content-alt-text",plugins:[t(),basePlugin(s)]}:basePlugin(s)};creator.postcss=!0,module.exports=creator;
1+
"use strict";var e=require("@csstools/postcss-progressive-custom-properties"),s=require("@csstools/utilities"),t=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");function transform(e,s){const o=e[0];if(!o.length)return"";if(s)return t.stringify([o]);const n=e[1].filter((e=>!t.isWhiteSpaceOrCommentNode(e)));return 1===n.length&&t.isTokenNode(n[0])&&r.isTokenString(n[0].value)&&""===n[0].value[4].value?t.stringify([o]):t.stringify([[...o,...e[1]]])}function parse(e){const s=t.parseListOfComponentValues(r.tokenize({css:e})),o=[];let n=0;for(let e=s.length-1;e>=0;e--){const i=s[e];if(!t.isTokenNode(i))continue;const l=i.value;r.isTokenDelim(l)&&("/"===l[4].value&&(o.push(s.slice(n,e)),n=e+1))}return 0!==n&&o.push(s.slice(n,s.length)),o}const o={test:e=>e.includes("content:")&&e.includes("/")},basePlugin=e=>({postcssPlugin:"postcss-content-alt-text",Declaration(t){if("content"!==t.prop||!t.value.includes("/"))return;if(s.hasFallback(t))return;if(s.hasSupportsAtRuleAncestor(t,o))return;const r=parse(t.value);if(2!==r.length)return;const n=transform(r,e?.stripAltText);n!==t.value&&(t.cloneBefore({value:n}),!1===e?.preserve&&t.remove())}});basePlugin.postcss=!0;const creator=s=>{const t=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0,stripAltText:!1},s);return t.enableProgressiveCustomProperties&&t.preserve?{postcssPlugin:"postcss-content-alt-text",plugins:[e(),basePlugin(t)]}:basePlugin(t)};creator.postcss=!0,module.exports=creator;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import{parseListOfComponentValues as s,isTokenNode as e,stringify as t}from"@csstools/css-parser-algorithms";import{tokenize as o,isTokenDelim as r}from"@csstools/css-tokenizer";import n from"@csstools/postcss-progressive-custom-properties";import{hasFallback as c,hasSupportsAtRuleAncestor as i}from"@csstools/utilities";const l={test:s=>s.includes("content:")&&s.includes("/")},basePlugin=n=>({postcssPlugin:"postcss-content-alt-text",Declaration(p){if("content"!==p.prop||!p.value.includes("/"))return;if(c(p))return;if(i(p,l))return;const u=s(o({css:p.value}));let a=0;for(let s=u.length-1;s>=0;s--){const t=u[s];if(!e(t))continue;const o=t.value;r(o)&&("/"===o[4].value&&(a++,!0===n?.stripAltText?u.splice(s,u.length):u.splice(s,1)))}if(1!==a)return;const m=t([u]);m!==p.value&&(p.cloneBefore({value:m}),!1===n?.preserve&&p.remove())}});basePlugin.postcss=!0;const creator=s=>{const e=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0,stripAltText:!1},s);return e.enableProgressiveCustomProperties&&e.preserve?{postcssPlugin:"postcss-content-alt-text",plugins:[n(),basePlugin(e)]}:basePlugin(e)};creator.postcss=!0;export{creator as default};
1+
import s from"@csstools/postcss-progressive-custom-properties";import{hasFallback as t,hasSupportsAtRuleAncestor as e}from"@csstools/utilities";import{stringify as r,isWhiteSpaceOrCommentNode as o,isTokenNode as n,parseListOfComponentValues as c}from"@csstools/css-parser-algorithms";import{isTokenString as l,tokenize as i,isTokenDelim as u}from"@csstools/css-tokenizer";function transform(s,t){const e=s[0];if(!e.length)return"";if(t)return r([e]);const c=s[1].filter((s=>!o(s)));return 1===c.length&&n(c[0])&&l(c[0].value)&&""===c[0].value[4].value?r([e]):r([[...e,...s[1]]])}function parse(s){const t=c(i({css:s})),e=[];let r=0;for(let s=t.length-1;s>=0;s--){const o=t[s];if(!n(o))continue;const c=o.value;u(c)&&("/"===c[4].value&&(e.push(t.slice(r,s)),r=s+1))}return 0!==r&&e.push(t.slice(r,t.length)),e}const p={test:s=>s.includes("content:")&&s.includes("/")},basePlugin=s=>({postcssPlugin:"postcss-content-alt-text",Declaration(r){if("content"!==r.prop||!r.value.includes("/"))return;if(t(r))return;if(e(r,p))return;const o=parse(r.value);if(2!==o.length)return;const n=transform(o,s?.stripAltText);n!==r.value&&(r.cloneBefore({value:n}),!1===s?.preserve&&r.remove())}});basePlugin.postcss=!0;const creator=t=>{const e=Object.assign({enableProgressiveCustomProperties:!0,preserve:!0,stripAltText:!1},t);return e.enableProgressiveCustomProperties&&e.preserve?{postcssPlugin:"postcss-content-alt-text",plugins:[s(),basePlugin(e)]}:basePlugin(e)};creator.postcss=!0;export{creator as default};

plugins/postcss-content-alt-text/src/index.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { isTokenNode, parseListOfComponentValues, stringify } from '@csstools/css-parser-algorithms';
2-
import { isTokenDelim, tokenize } from '@csstools/css-tokenizer';
31
import postcssProgressiveCustomProperties from '@csstools/postcss-progressive-custom-properties';
42
import { hasFallback, hasSupportsAtRuleAncestor } from '@csstools/utilities';
53
import type { PluginCreator } from 'postcss';
4+
import { transform } from './transform';
5+
import { parse } from './parse';
66

77
/** postcss-content-alt-text plugin options */
88
export type basePluginOptions = {
@@ -34,42 +34,12 @@ const basePlugin: PluginCreator<basePluginOptions> = (opts?: basePluginOptions)
3434
return;
3535
}
3636

37-
const componentValues = parseListOfComponentValues(
38-
tokenize({ css: decl.value })
39-
);
40-
41-
let slashCounter = 0;
42-
43-
for (let i = (componentValues.length - 1); i >= 0; i--) {
44-
const componentValue = componentValues[i];
45-
if (!isTokenNode(componentValue)) {
46-
continue;
47-
}
48-
49-
const token = componentValue.value;
50-
if (!isTokenDelim(token)) {
51-
continue;
52-
}
53-
54-
if (token[4].value !== '/') {
55-
continue;
56-
}
57-
58-
slashCounter++;
59-
60-
if (opts?.stripAltText === true) {
61-
componentValues.splice(i, componentValues.length);
62-
} else {
63-
componentValues.splice(i, 1);
64-
}
65-
}
66-
67-
if (slashCounter !== 1) {
68-
// Either too few or too many slashes
37+
const parts = parse(decl.value);
38+
if (parts.length !== 2) {
6939
return;
7040
}
7141

72-
const modified = stringify([componentValues]);
42+
const modified = transform(parts, opts?.stripAltText);
7343

7444
if (modified === decl.value) {
7545
return;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ComponentValue } from "@csstools/css-parser-algorithms";
2+
import { isTokenNode, parseListOfComponentValues } from "@csstools/css-parser-algorithms";
3+
import { isTokenDelim, tokenize } from "@csstools/css-tokenizer";
4+
5+
export function parse(str: string): Array<Array<ComponentValue>> {
6+
const componentValues = parseListOfComponentValues(
7+
tokenize({ css: str })
8+
);
9+
10+
const parts: Array<Array<ComponentValue>> = []
11+
let lastSliceIndex = 0;
12+
13+
for (let i = (componentValues.length - 1); i >= 0; i--) {
14+
const componentValue = componentValues[i];
15+
if (!isTokenNode(componentValue)) {
16+
continue;
17+
}
18+
19+
const token = componentValue.value;
20+
if (!isTokenDelim(token)) {
21+
continue;
22+
}
23+
24+
if (token[4].value !== '/') {
25+
continue;
26+
}
27+
28+
parts.push(componentValues.slice(lastSliceIndex, i));
29+
lastSliceIndex = i + 1;
30+
}
31+
32+
if (lastSliceIndex !== 0) {
33+
parts.push(componentValues.slice(lastSliceIndex, componentValues.length));
34+
}
35+
36+
return parts;
37+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ComponentValue } from "@csstools/css-parser-algorithms";
2+
import { isTokenNode, isWhiteSpaceOrCommentNode, stringify } from "@csstools/css-parser-algorithms";
3+
import { isTokenString } from "@csstools/css-tokenizer";
4+
5+
export function transform(parts: Array<Array<ComponentValue>>, stripAltText?: boolean): string {
6+
const firstPart = parts[0];
7+
if (!firstPart.length) {
8+
return '';
9+
}
10+
11+
if (stripAltText) {
12+
return stringify([firstPart]);
13+
}
14+
15+
const relevantComponentValues = parts[1].filter((x) => !isWhiteSpaceOrCommentNode(x));
16+
if (
17+
relevantComponentValues.length === 1 &&
18+
isTokenNode(relevantComponentValues[0]) &&
19+
isTokenString(relevantComponentValues[0].value) &&
20+
relevantComponentValues[0].value[4].value === ''
21+
) {
22+
return stringify([firstPart]);
23+
}
24+
25+
return stringify([[
26+
...firstPart,
27+
...parts[1],
28+
]]);
29+
}

plugins/postcss-content-alt-text/test/basic.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,11 @@
4141
content: "9" / "0";
4242
}
4343
}
44+
45+
.ignore {
46+
/*
47+
* An empty string is often used for illustrative items that do not require alt text.
48+
* Appending an empty string might be visually breaking without having any benefits at all.
49+
*/
50+
content: ">" / "";
51+
}

plugins/postcss-content-alt-text/test/basic.expect.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,12 @@
5050
content: "9" / "0";
5151
}
5252
}
53+
54+
.ignore {
55+
/*
56+
* An empty string is often used for illustrative items that do not require alt text.
57+
* Appending an empty string might be visually breaking without having any benefits at all.
58+
*/
59+
content: ">" ;
60+
content: ">" / "";
61+
}

plugins/postcss-content-alt-text/test/basic.preserve-false.expect.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,11 @@
4141
content: "9" / "0";
4242
}
4343
}
44+
45+
.ignore {
46+
/*
47+
* An empty string is often used for illustrative items that do not require alt text.
48+
* Appending an empty string might be visually breaking without having any benefits at all.
49+
*/
50+
content: ">" ;
51+
}

plugins/postcss-content-alt-text/test/basic.strip-alt-text.expect.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,12 @@
5050
content: "9" / "0";
5151
}
5252
}
53+
54+
.ignore {
55+
/*
56+
* An empty string is often used for illustrative items that do not require alt text.
57+
* Appending an empty string might be visually breaking without having any benefits at all.
58+
*/
59+
content: ">" ;
60+
content: ">" / "";
61+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
.foo {
22
content: url(tree.jpg) / "A beautiful tree in a dark forest";
33
}
4+
5+
.bar {
6+
content: ">" / "";
7+
}

plugins/postcss-content-alt-text/test/examples/example.expect.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
content: url(tree.jpg) "A beautiful tree in a dark forest";
33
content: url(tree.jpg) / "A beautiful tree in a dark forest";
44
}
5+
6+
.bar {
7+
content: ">" ;
8+
content: ">" / "";
9+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
.foo {
22
content: url(tree.jpg) "A beautiful tree in a dark forest";
33
}
4+
5+
.bar {
6+
content: ">" ;
7+
}

plugins/postcss-content-alt-text/test/examples/example.strip-alt-text.expect.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
content: url(tree.jpg) ;
33
content: url(tree.jpg) / "A beautiful tree in a dark forest";
44
}
5+
6+
.bar {
7+
content: ">" ;
8+
content: ">" / "";
9+
}

0 commit comments

Comments
 (0)