Skip to content

postcss-content-alt-text: improve support for empty string alt text #1433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/postcss-content-alt-text/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `<img alt="">`, i.e. to represent an item that does not need a text alternative.

### 1.0.0

_July 7, 2024_
Expand Down
26 changes: 26 additions & 0 deletions plugins/postcss-content-alt-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugins/postcss-content-alt-text/dist/index.cjs
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion plugins/postcss-content-alt-text/dist/index.mjs
Original file line number Diff line number Diff line change
@@ -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};
40 changes: 5 additions & 35 deletions plugins/postcss-content-alt-text/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -34,42 +34,12 @@ const basePlugin: PluginCreator<basePluginOptions> = (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;
Expand Down
37 changes: 37 additions & 0 deletions plugins/postcss-content-alt-text/src/parse.ts
Original file line number Diff line number Diff line change
@@ -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<Array<ComponentValue>> {
const componentValues = parseListOfComponentValues(
tokenize({ css: str })
);

const parts: Array<Array<ComponentValue>> = []
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;
}
29 changes: 29 additions & 0 deletions plugins/postcss-content-alt-text/src/transform.ts
Original file line number Diff line number Diff line change
@@ -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<Array<ComponentValue>>, 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],
]]);
}
8 changes: 8 additions & 0 deletions plugins/postcss-content-alt-text/test/basic.css
Original file line number Diff line number Diff line change
Expand Up @@ -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: ">" / "";
}
9 changes: 9 additions & 0 deletions plugins/postcss-content-alt-text/test/basic.expect.css
Original file line number Diff line number Diff line change
Expand Up @@ -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: ">" / "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: ">" ;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: ">" / "";
}
4 changes: 4 additions & 0 deletions plugins/postcss-content-alt-text/test/examples/example.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.foo {
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}

.bar {
content: ">" / "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: ">" / "";
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.foo {
content: url(tree.jpg) "A beautiful tree in a dark forest";
}

.bar {
content: ">" ;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
content: url(tree.jpg) ;
content: url(tree.jpg) / "A beautiful tree in a dark forest";
}

.bar {
content: ">" ;
content: ">" / "";
}