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: ">" / ""; +}