diff --git a/plugins/postcss-content-alt-text/CHANGELOG.md b/plugins/postcss-content-alt-text/CHANGELOG.md
index 9336e189d..fe3bd6034 100644
--- a/plugins/postcss-content-alt-text/CHANGELOG.md
+++ b/plugins/postcss-content-alt-text/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changes to PostCSS Content Alt Text
+### Unreleased (patch)
+
+- Add specific handling of `content: ">" / "";` as this pattern is used in the same way as `
`, i.e. to represent an item that does not need a text alternative.
+
### 1.0.0
_July 7, 2024_
diff --git a/plugins/postcss-content-alt-text/README.md b/plugins/postcss-content-alt-text/README.md
index ca9a6f1b7..df10de030 100644
--- a/plugins/postcss-content-alt-text/README.md
+++ b/plugins/postcss-content-alt-text/README.md
@@ -13,12 +13,21 @@ npm install @csstools/postcss-content-alt-text --save-dev
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+.bar {
+ content: ">" / "";
+}
+
/* becomes */
.foo {
content: url(tree.jpg) "A beautiful tree in a dark forest";
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" ;
+ content: ">" / "";
+}
```
## Usage
@@ -67,11 +76,19 @@ postcssContentAltText({ preserve: false })
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+.bar {
+ content: ">" / "";
+}
+
/* becomes */
.foo {
content: url(tree.jpg) "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" ;
+}
```
### stripAltText
@@ -91,12 +108,21 @@ postcssContentAltText({ stripAltText: true })
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+.bar {
+ content: ">" / "";
+}
+
/* becomes */
.foo {
content: url(tree.jpg) ;
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" ;
+ content: ">" / "";
+}
```
[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
diff --git a/plugins/postcss-content-alt-text/dist/index.cjs b/plugins/postcss-content-alt-text/dist/index.cjs
index f016340ff..4ce766683 100644
--- a/plugins/postcss-content-alt-text/dist/index.cjs
+++ b/plugins/postcss-content-alt-text/dist/index.cjs
@@ -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;
+"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;
diff --git a/plugins/postcss-content-alt-text/dist/index.mjs b/plugins/postcss-content-alt-text/dist/index.mjs
index 0b488d05c..2faac285a 100644
--- a/plugins/postcss-content-alt-text/dist/index.mjs
+++ b/plugins/postcss-content-alt-text/dist/index.mjs
@@ -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};
+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};
diff --git a/plugins/postcss-content-alt-text/src/index.ts b/plugins/postcss-content-alt-text/src/index.ts
index ad5fcf778..9d9ac1d6c 100644
--- a/plugins/postcss-content-alt-text/src/index.ts
+++ b/plugins/postcss-content-alt-text/src/index.ts
@@ -1,8 +1,8 @@
-import { isTokenNode, parseListOfComponentValues, stringify } from '@csstools/css-parser-algorithms';
-import { isTokenDelim, tokenize } from '@csstools/css-tokenizer';
import postcssProgressiveCustomProperties from '@csstools/postcss-progressive-custom-properties';
import { hasFallback, hasSupportsAtRuleAncestor } from '@csstools/utilities';
import type { PluginCreator } from 'postcss';
+import { transform } from './transform';
+import { parse } from './parse';
/** postcss-content-alt-text plugin options */
export type basePluginOptions = {
@@ -34,42 +34,12 @@ const basePlugin: PluginCreator = (opts?: basePluginOptions)
return;
}
- const componentValues = parseListOfComponentValues(
- tokenize({ css: decl.value })
- );
-
- let slashCounter = 0;
-
- for (let i = (componentValues.length - 1); i >= 0; i--) {
- const componentValue = componentValues[i];
- if (!isTokenNode(componentValue)) {
- continue;
- }
-
- const token = componentValue.value;
- if (!isTokenDelim(token)) {
- continue;
- }
-
- if (token[4].value !== '/') {
- continue;
- }
-
- slashCounter++;
-
- if (opts?.stripAltText === true) {
- componentValues.splice(i, componentValues.length);
- } else {
- componentValues.splice(i, 1);
- }
- }
-
- if (slashCounter !== 1) {
- // Either too few or too many slashes
+ const parts = parse(decl.value);
+ if (parts.length !== 2) {
return;
}
- const modified = stringify([componentValues]);
+ const modified = transform(parts, opts?.stripAltText);
if (modified === decl.value) {
return;
diff --git a/plugins/postcss-content-alt-text/src/parse.ts b/plugins/postcss-content-alt-text/src/parse.ts
new file mode 100644
index 000000000..340f2e2fb
--- /dev/null
+++ b/plugins/postcss-content-alt-text/src/parse.ts
@@ -0,0 +1,37 @@
+import type { ComponentValue } from "@csstools/css-parser-algorithms";
+import { isTokenNode, parseListOfComponentValues } from "@csstools/css-parser-algorithms";
+import { isTokenDelim, tokenize } from "@csstools/css-tokenizer";
+
+export function parse(str: string): Array> {
+ const componentValues = parseListOfComponentValues(
+ tokenize({ css: str })
+ );
+
+ const parts: Array> = []
+ let lastSliceIndex = 0;
+
+ for (let i = (componentValues.length - 1); i >= 0; i--) {
+ const componentValue = componentValues[i];
+ if (!isTokenNode(componentValue)) {
+ continue;
+ }
+
+ const token = componentValue.value;
+ if (!isTokenDelim(token)) {
+ continue;
+ }
+
+ if (token[4].value !== '/') {
+ continue;
+ }
+
+ parts.push(componentValues.slice(lastSliceIndex, i));
+ lastSliceIndex = i + 1;
+ }
+
+ if (lastSliceIndex !== 0) {
+ parts.push(componentValues.slice(lastSliceIndex, componentValues.length));
+ }
+
+ return parts;
+}
diff --git a/plugins/postcss-content-alt-text/src/transform.ts b/plugins/postcss-content-alt-text/src/transform.ts
new file mode 100644
index 000000000..20a131f55
--- /dev/null
+++ b/plugins/postcss-content-alt-text/src/transform.ts
@@ -0,0 +1,29 @@
+import type { ComponentValue } from "@csstools/css-parser-algorithms";
+import { isTokenNode, isWhiteSpaceOrCommentNode, stringify } from "@csstools/css-parser-algorithms";
+import { isTokenString } from "@csstools/css-tokenizer";
+
+export function transform(parts: Array>, stripAltText?: boolean): string {
+ const firstPart = parts[0];
+ if (!firstPart.length) {
+ return '';
+ }
+
+ if (stripAltText) {
+ return stringify([firstPart]);
+ }
+
+ const relevantComponentValues = parts[1].filter((x) => !isWhiteSpaceOrCommentNode(x));
+ if (
+ relevantComponentValues.length === 1 &&
+ isTokenNode(relevantComponentValues[0]) &&
+ isTokenString(relevantComponentValues[0].value) &&
+ relevantComponentValues[0].value[4].value === ''
+ ) {
+ return stringify([firstPart]);
+ }
+
+ return stringify([[
+ ...firstPart,
+ ...parts[1],
+ ]]);
+}
diff --git a/plugins/postcss-content-alt-text/test/basic.css b/plugins/postcss-content-alt-text/test/basic.css
index 6872ef12d..b6a1edb18 100644
--- a/plugins/postcss-content-alt-text/test/basic.css
+++ b/plugins/postcss-content-alt-text/test/basic.css
@@ -41,3 +41,11 @@
content: "9" / "0";
}
}
+
+.ignore {
+ /*
+ * An empty string is often used for illustrative items that do not require alt text.
+ * Appending an empty string might be visually breaking without having any benefits at all.
+ */
+ content: ">" / "";
+}
diff --git a/plugins/postcss-content-alt-text/test/basic.expect.css b/plugins/postcss-content-alt-text/test/basic.expect.css
index b4cbd27b7..7edb8dcb5 100644
--- a/plugins/postcss-content-alt-text/test/basic.expect.css
+++ b/plugins/postcss-content-alt-text/test/basic.expect.css
@@ -50,3 +50,12 @@
content: "9" / "0";
}
}
+
+.ignore {
+ /*
+ * An empty string is often used for illustrative items that do not require alt text.
+ * Appending an empty string might be visually breaking without having any benefits at all.
+ */
+ content: ">" ;
+ content: ">" / "";
+}
diff --git a/plugins/postcss-content-alt-text/test/basic.preserve-false.expect.css b/plugins/postcss-content-alt-text/test/basic.preserve-false.expect.css
index 521a80f3d..11857876a 100644
--- a/plugins/postcss-content-alt-text/test/basic.preserve-false.expect.css
+++ b/plugins/postcss-content-alt-text/test/basic.preserve-false.expect.css
@@ -41,3 +41,11 @@
content: "9" / "0";
}
}
+
+.ignore {
+ /*
+ * An empty string is often used for illustrative items that do not require alt text.
+ * Appending an empty string might be visually breaking without having any benefits at all.
+ */
+ content: ">" ;
+}
diff --git a/plugins/postcss-content-alt-text/test/basic.strip-alt-text.expect.css b/plugins/postcss-content-alt-text/test/basic.strip-alt-text.expect.css
index 956530d79..c72518288 100644
--- a/plugins/postcss-content-alt-text/test/basic.strip-alt-text.expect.css
+++ b/plugins/postcss-content-alt-text/test/basic.strip-alt-text.expect.css
@@ -50,3 +50,12 @@
content: "9" / "0";
}
}
+
+.ignore {
+ /*
+ * An empty string is often used for illustrative items that do not require alt text.
+ * Appending an empty string might be visually breaking without having any benefits at all.
+ */
+ content: ">" ;
+ content: ">" / "";
+}
diff --git a/plugins/postcss-content-alt-text/test/examples/example.css b/plugins/postcss-content-alt-text/test/examples/example.css
index 0bcf7c152..e3e1babdf 100644
--- a/plugins/postcss-content-alt-text/test/examples/example.css
+++ b/plugins/postcss-content-alt-text/test/examples/example.css
@@ -1,3 +1,7 @@
.foo {
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" / "";
+}
diff --git a/plugins/postcss-content-alt-text/test/examples/example.expect.css b/plugins/postcss-content-alt-text/test/examples/example.expect.css
index d2f53dbb2..90ad84575 100644
--- a/plugins/postcss-content-alt-text/test/examples/example.expect.css
+++ b/plugins/postcss-content-alt-text/test/examples/example.expect.css
@@ -2,3 +2,8 @@
content: url(tree.jpg) "A beautiful tree in a dark forest";
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" ;
+ content: ">" / "";
+}
diff --git a/plugins/postcss-content-alt-text/test/examples/example.preserve-false.expect.css b/plugins/postcss-content-alt-text/test/examples/example.preserve-false.expect.css
index 677dc3d6d..b4e9a1088 100644
--- a/plugins/postcss-content-alt-text/test/examples/example.preserve-false.expect.css
+++ b/plugins/postcss-content-alt-text/test/examples/example.preserve-false.expect.css
@@ -1,3 +1,7 @@
.foo {
content: url(tree.jpg) "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" ;
+}
diff --git a/plugins/postcss-content-alt-text/test/examples/example.strip-alt-text.expect.css b/plugins/postcss-content-alt-text/test/examples/example.strip-alt-text.expect.css
index 99ab75472..bfc7956f0 100644
--- a/plugins/postcss-content-alt-text/test/examples/example.strip-alt-text.expect.css
+++ b/plugins/postcss-content-alt-text/test/examples/example.strip-alt-text.expect.css
@@ -2,3 +2,8 @@
content: url(tree.jpg) ;
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}
+
+.bar {
+ content: ">" ;
+ content: ">" / "";
+}