diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml index 591bf1b46..e42dd65f3 100644 --- a/.github/ISSUE_TEMPLATE/css-issue.yml +++ b/.github/ISSUE_TEMPLATE/css-issue.yml @@ -76,6 +76,7 @@ body: - PostCSS Color Hex Alpha - PostCSS Color Mix Function - PostCSS Conditional Values + - PostCSS Content Alt Text - PostCSS Contrast Color Functions - PostCSS Custom Media Queries - PostCSS Custom Properties diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml index 665861495..554cdfcfe 100644 --- a/.github/ISSUE_TEMPLATE/plugin-issue.yml +++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml @@ -73,6 +73,7 @@ body: - PostCSS Color Hex Alpha - PostCSS Color Mix Function - PostCSS Conditional Values + - PostCSS Content Alt Text - PostCSS Contrast Color Functions - PostCSS Custom Media Queries - PostCSS Custom Properties diff --git a/.github/labeler.yml b/.github/labeler.yml index a3d24f37b..806e033cc 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -119,6 +119,12 @@ - plugins/postcss-conditional-values/** - experimental/postcss-conditional-values/** +"plugins/postcss-content-alt-text": + - changed-files: + - any-glob-to-any-file: + - plugins/postcss-content-alt-text/** + - experimental/postcss-content-alt-text/** + "plugins/postcss-contrast-color-function": - changed-files: - any-glob-to-any-file: diff --git a/package-lock.json b/package-lock.json index 04ccebf68..fa4b3bf36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2081,6 +2081,10 @@ "resolved": "plugins/postcss-conditional-values", "link": true }, + "node_modules/@csstools/postcss-content-alt-text": { + "resolved": "plugins/postcss-content-alt-text", + "link": true + }, "node_modules/@csstools/postcss-contrast-color-function": { "resolved": "plugins/postcss-contrast-color-function", "link": true @@ -10344,6 +10348,36 @@ "postcss": "^8.4" } }, + "plugins/postcss-content-alt-text": { + "name": "@csstools/postcss-content-alt-text", + "version": "0.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.2.0", + "@csstools/utilities": "^1.0.0" + }, + "devDependencies": { + "@csstools/postcss-tape": "*" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "plugins/postcss-contrast-color-function": { "name": "@csstools/postcss-contrast-color-function", "version": "1.0.5", diff --git a/plugins/postcss-content-alt-text/.gitignore b/plugins/postcss-content-alt-text/.gitignore new file mode 100644 index 000000000..e5b28db4a --- /dev/null +++ b/plugins/postcss-content-alt-text/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.html diff --git a/plugins/postcss-content-alt-text/.nvmrc b/plugins/postcss-content-alt-text/.nvmrc new file mode 100644 index 000000000..6ed5da955 --- /dev/null +++ b/plugins/postcss-content-alt-text/.nvmrc @@ -0,0 +1 @@ +v20.2.0 diff --git a/plugins/postcss-content-alt-text/CHANGELOG.md b/plugins/postcss-content-alt-text/CHANGELOG.md new file mode 100644 index 000000000..a64fbd269 --- /dev/null +++ b/plugins/postcss-content-alt-text/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changes to PostCSS Content Alt Text + +### Unreleased (major) + +- Initial version diff --git a/plugins/postcss-content-alt-text/INSTALL.md b/plugins/postcss-content-alt-text/INSTALL.md new file mode 100644 index 000000000..335fcbabd --- /dev/null +++ b/plugins/postcss-content-alt-text/INSTALL.md @@ -0,0 +1,235 @@ +# Installing PostCSS Content Alt Text + +[PostCSS Content Alt Text] runs in all Node environments, with special instructions for: + +- [Node](#node) +- [PostCSS CLI](#postcss-cli) +- [PostCSS Load Config](#postcss-load-config) +- [Webpack](#webpack) +- [Next.js](#nextjs) +- [Gulp](#gulp) +- [Grunt](#grunt) + + + +## Node + +Add [PostCSS Content Alt Text] to your project: + +```bash +npm install postcss @csstools/postcss-content-alt-text --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +// commonjs +const postcss = require('postcss'); +const postcssContentAltText = require('@csstools/postcss-content-alt-text'); + +postcss([ + postcssContentAltText(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +```js +// esm +import postcss from 'postcss'; +import postcssContentAltText from '@csstools/postcss-content-alt-text'; + +postcss([ + postcssContentAltText(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +## PostCSS CLI + +Add [PostCSS CLI] to your project: + +```bash +npm install postcss-cli @csstools/postcss-content-alt-text --save-dev +``` + +Use [PostCSS Content Alt Text] in your `postcss.config.js` configuration file: + +```js +const postcssContentAltText = require('@csstools/postcss-content-alt-text'); + +module.exports = { + plugins: [ + postcssContentAltText(/* pluginOptions */) + ] +} +``` + +## PostCSS Load Config + +If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config). + +```bash +npm install @csstools/postcss-content-alt-text --save-dev +``` + +`package.json`: + +```json +{ + "postcss": { + "plugins": { + "@csstools/postcss-content-alt-text": {} + } + } +} +``` + +`.postcssrc.json`: + +```json +{ + "plugins": { + "@csstools/postcss-content-alt-text": {} + } +} +``` + +_See the [README of `postcss-load-config`](https://github.com/postcss/postcss-load-config#usage) for more usage options._ + +## Webpack + +_Webpack version 5_ + +Add [PostCSS Loader] to your project: + +```bash +npm install postcss-loader @csstools/postcss-content-alt-text --save-dev +``` + +Use [PostCSS Content Alt Text] in your Webpack configuration: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + "style-loader", + { + loader: "css-loader", + options: { importLoaders: 1 }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + // Other plugins, + [ + "@csstools/postcss-content-alt-text", + { + // Options + }, + ], + ], + }, + }, + }, + ], + }, + ], + }, +}; +``` + +## Next.js + +Read the instructions on how to [customize the PostCSS configuration in Next.js](https://nextjs.org/docs/advanced-features/customizing-postcss-config) + +```bash +npm install @csstools/postcss-content-alt-text --save-dev +``` + +Use [PostCSS Content Alt Text] in your `postcss.config.json` file: + +```json +{ + "plugins": [ + "@csstools/postcss-content-alt-text" + ] +} +``` + +```json5 +{ + "plugins": [ + [ + "@csstools/postcss-content-alt-text", + { + // Optionally add plugin options + } + ] + ] +} +``` + +## Gulp + +Add [Gulp PostCSS] to your project: + +```bash +npm install gulp-postcss @csstools/postcss-content-alt-text --save-dev +``` + +Use [PostCSS Content Alt Text] in your Gulpfile: + +```js +const postcss = require('gulp-postcss'); +const postcssContentAltText = require('@csstools/postcss-content-alt-text'); + +gulp.task('css', function () { + var plugins = [ + postcssContentAltText(/* pluginOptions */) + ]; + + return gulp.src('./src/*.css') + .pipe(postcss(plugins)) + .pipe(gulp.dest('.')); +}); +``` + +## Grunt + +Add [Grunt PostCSS] to your project: + +```bash +npm install grunt-postcss @csstools/postcss-content-alt-text --save-dev +``` + +Use [PostCSS Content Alt Text] in your Gruntfile: + +```js +const postcssContentAltText = require('@csstools/postcss-content-alt-text'); + +grunt.loadNpmTasks('grunt-postcss'); + +grunt.initConfig({ + postcss: { + options: { + processors: [ + postcssContentAltText(/* pluginOptions */) + ] + }, + dist: { + src: '*.css' + } + } +}); +``` + +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS CLI]: https://github.com/postcss/postcss-cli +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Content Alt Text]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-content-alt-text +[Next.js]: https://nextjs.org diff --git a/plugins/postcss-content-alt-text/LICENSE.md b/plugins/postcss-content-alt-text/LICENSE.md new file mode 100644 index 000000000..e8ae93b9f --- /dev/null +++ b/plugins/postcss-content-alt-text/LICENSE.md @@ -0,0 +1,18 @@ +MIT No Attribution (MIT-0) + +Copyright © CSSTools Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/postcss-content-alt-text/README.md b/plugins/postcss-content-alt-text/README.md new file mode 100644 index 000000000..ca9a6f1b7 --- /dev/null +++ b/plugins/postcss-content-alt-text/README.md @@ -0,0 +1,109 @@ +# PostCSS Content Alt Text [PostCSS Logo][PostCSS] + +[npm version][npm-url] [Build Status][cli-url] [Discord][discord]

[Baseline Status][css-url] [CSS Standard Status][css-url] + +```bash +npm install @csstools/postcss-content-alt-text --save-dev +``` + +[PostCSS Content Alt Text] generates fallback values for `content` with alt text following the [CSS Generated Content Module]. + +```pcss +.foo { + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} + +/* becomes */ + +.foo { + content: url(tree.jpg) "A beautiful tree in a dark forest"; + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} +``` + +## Usage + +Add [PostCSS Content Alt Text] to your project: + +```bash +npm install postcss @csstools/postcss-content-alt-text --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssContentAltText = require('@csstools/postcss-content-alt-text'); + +postcss([ + postcssContentAltText(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +[PostCSS Content Alt Text] runs in all Node environments, with special +instructions for: + +- [Node](INSTALL.md#node) +- [PostCSS CLI](INSTALL.md#postcss-cli) +- [PostCSS Load Config](INSTALL.md#postcss-load-config) +- [Webpack](INSTALL.md#webpack) +- [Next.js](INSTALL.md#nextjs) +- [Gulp](INSTALL.md#gulp) +- [Grunt](INSTALL.md#grunt) + +## Options + +### preserve + +The `preserve` option determines whether the original notation +is preserved. By default, it is preserved. + +```js +postcssContentAltText({ preserve: false }) +``` + +```pcss +.foo { + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} + +/* becomes */ + +.foo { + content: url(tree.jpg) "A beautiful tree in a dark forest"; +} +``` + +### stripAltText + +The `stripAltText` option determines whether the alt text is removed from the value. +By default, it is not removed. +Instead it is added to the `content` value itself to ensure content is accessible. + +Only set this to `true` if you are sure the alt text is not needed. + +```js +postcssContentAltText({ stripAltText: true }) +``` + +```pcss +.foo { + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} + +/* becomes */ + +.foo { + content: url(tree.jpg) ; + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} +``` + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[css-url]: https://cssdb.org/#content-alt-text +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/postcss-content-alt-text + +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Content Alt Text]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-content-alt-text +[CSS Generated Content Module]: https://drafts.csswg.org/css-content/#content-property diff --git a/plugins/postcss-content-alt-text/api-extractor.json b/plugins/postcss-content-alt-text/api-extractor.json new file mode 100644 index 000000000..42058be51 --- /dev/null +++ b/plugins/postcss-content-alt-text/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.json" +} diff --git a/plugins/postcss-content-alt-text/dist/index.cjs b/plugins/postcss-content-alt-text/dist/index.cjs new file mode 100644 index 000000000..f016340ff --- /dev/null +++ b/plugins/postcss-content-alt-text/dist/index.cjs @@ -0,0 +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; diff --git a/plugins/postcss-content-alt-text/dist/index.d.ts b/plugins/postcss-content-alt-text/dist/index.d.ts new file mode 100644 index 000000000..2ebb7a3be --- /dev/null +++ b/plugins/postcss-content-alt-text/dist/index.d.ts @@ -0,0 +1,24 @@ +import type { PluginCreator } from 'postcss'; + +/** postcss-content-alt-text plugin options */ +export declare type basePluginOptions = { + /** Preserve the original notation. default: true */ + preserve: boolean; + /** Strip alt text */ + stripAltText: boolean; +}; + +declare const creator: PluginCreator; +export default creator; + +/** postcss-content-alt-text plugin options */ +export declare type pluginOptions = { + /** Preserve the original notation. default: true */ + preserve?: boolean; + /** Strip alt text */ + stripAltText?: boolean; + /** Enable "@csstools/postcss-progressive-custom-properties". default: true */ + enableProgressiveCustomProperties?: boolean; +}; + +export { } diff --git a/plugins/postcss-content-alt-text/dist/index.mjs b/plugins/postcss-content-alt-text/dist/index.mjs new file mode 100644 index 000000000..0b488d05c --- /dev/null +++ b/plugins/postcss-content-alt-text/dist/index.mjs @@ -0,0 +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}; diff --git a/plugins/postcss-content-alt-text/docs/README.md b/plugins/postcss-content-alt-text/docs/README.md new file mode 100644 index 000000000..dfecd731d --- /dev/null +++ b/plugins/postcss-content-alt-text/docs/README.md @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + +
+ +[] generates fallback values for `content` with alt text following the [CSS Generated Content Module]. + +```pcss + + +/* becomes */ + + +``` + + + + + +## Options + +### preserve + +The `preserve` option determines whether the original notation +is preserved. By default, it is preserved. + +```js +({ preserve: false }) +``` + +```pcss + + +/* becomes */ + + +``` + +### stripAltText + +The `stripAltText` option determines whether the alt text is removed from the value. +By default, it is not removed. +Instead it is added to the `content` value itself to ensure content is accessible. + +Only set this to `true` if you are sure the alt text is not needed. + +```js +({ stripAltText: true }) +``` + +```pcss + + +/* becomes */ + + +``` + + +[CSS Generated Content Module]: diff --git a/plugins/postcss-content-alt-text/package.json b/plugins/postcss-content-alt-text/package.json new file mode 100644 index 000000000..ac937239c --- /dev/null +++ b/plugins/postcss-content-alt-text/package.json @@ -0,0 +1,96 @@ +{ + "name": "@csstools/postcss-content-alt-text", + "description": "Generate fallback values for content with alt text", + "version": "0.0.0", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke", + "email": "romainmenke@gmail.com" + } + ], + "license": "MIT-0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": { + "default": "./dist/index.cjs" + } + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/postcss-progressive-custom-properties": "^3.2.0", + "@csstools/utilities": "^1.0.0" + }, + "peerDependencies": { + "postcss": "^8.4" + }, + "devDependencies": { + "@csstools/postcss-tape": "*" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.mjs", + "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs", + "lint": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run build && npm run test", + "test": "node --test", + "test:rewrite-expects": "REWRITE_EXPECTS=true node --test" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-content-alt-text#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/csstools/postcss-plugins.git", + "directory": "plugins/postcss-content-alt-text" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "accessibility", + "alt", + "content", + "css", + "csswg", + "fallback", + "postcss-plugin", + "w3c" + ], + "csstools": { + "cssdbId": "content-alt-text", + "exportName": "postcssContentAltText", + "humanReadableName": "PostCSS Content Alt Text", + "specUrl": "https://drafts.csswg.org/css-content/#content-property" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/plugins/postcss-content-alt-text/src/index.ts b/plugins/postcss-content-alt-text/src/index.ts new file mode 100644 index 000000000..ad5fcf778 --- /dev/null +++ b/plugins/postcss-content-alt-text/src/index.ts @@ -0,0 +1,122 @@ +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'; + +/** postcss-content-alt-text plugin options */ +export type basePluginOptions = { + /** Preserve the original notation. default: true */ + preserve: boolean, + /** Strip alt text */ + stripAltText: boolean, +}; + +const predicate = { + test: (str: string): boolean => { + return str.includes('content:') && str.includes('/'); + } +} + +const basePlugin: PluginCreator = (opts?: basePluginOptions) => { + return { + postcssPlugin: 'postcss-content-alt-text', + Declaration(decl): void { + if (decl.prop !== 'content' || !decl.value.includes('/')) { + return; + } + + if (hasFallback(decl)) { + return; + } + + if (hasSupportsAtRuleAncestor(decl, predicate)) { + 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 + return; + } + + const modified = stringify([componentValues]); + + if (modified === decl.value) { + return; + } + + decl.cloneBefore({ value: modified }); + + if (opts?.preserve === false) { + decl.remove(); + } + }, + }; +}; + +basePlugin.postcss = true; + + +/** postcss-content-alt-text plugin options */ +export type pluginOptions = { + /** Preserve the original notation. default: true */ + preserve?: boolean, + /** Strip alt text */ + stripAltText?: boolean, + /** Enable "@csstools/postcss-progressive-custom-properties". default: true */ + enableProgressiveCustomProperties?: boolean, +}; + +const creator: PluginCreator = (opts?: pluginOptions) => { + const options = Object.assign({ + enableProgressiveCustomProperties: true, + preserve: true, + stripAltText: false, + }, opts); + + if (options.enableProgressiveCustomProperties && options.preserve) { + return { + postcssPlugin: 'postcss-content-alt-text', + plugins: [ + postcssProgressiveCustomProperties(), + basePlugin(options), + ], + }; + } + + return basePlugin(options); +}; + +creator.postcss = true; + +export default creator; diff --git a/plugins/postcss-content-alt-text/test/_import.mjs b/plugins/postcss-content-alt-text/test/_import.mjs new file mode 100644 index 000000000..04228bc80 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/_import.mjs @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import plugin from '@csstools/postcss-content-alt-text'; + +test('import', () => { + plugin(); + assert.ok(plugin.postcss, 'should have "postcss flag"'); + assert.equal(typeof plugin, 'function', 'should return a function'); +}); + diff --git a/plugins/postcss-content-alt-text/test/_require.cjs b/plugins/postcss-content-alt-text/test/_require.cjs new file mode 100644 index 000000000..027c0494a --- /dev/null +++ b/plugins/postcss-content-alt-text/test/_require.cjs @@ -0,0 +1,9 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const plugin = require('@csstools/postcss-content-alt-text'); + +test('require', () => { + plugin(); + assert.ok(plugin.postcss, 'should have "postcss flag"'); + assert.equal(typeof plugin, 'function', 'should return a function'); +}); diff --git a/plugins/postcss-content-alt-text/test/_tape.mjs b/plugins/postcss-content-alt-text/test/_tape.mjs new file mode 100644 index 000000000..35e4cce30 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/_tape.mjs @@ -0,0 +1,35 @@ +import { postcssTape } from '@csstools/postcss-tape'; +import plugin from '@csstools/postcss-content-alt-text'; + +postcssTape(plugin)({ + basic: { + message: 'supports basic usage', + }, + 'basic:preserve-false': { + message: 'supports basic usage with { preserve: false }', + options: { + preserve: false, + }, + }, + 'basic:strip-alt-text': { + message: 'supports basic usage with { stripAltText: true }', + options: { + stripAltText: true, + }, + }, + 'examples/example': { + message: 'minimal example', + }, + 'examples/example:preserve-false': { + message: 'minimal example', + options: { + preserve: false, + }, + }, + 'examples/example:strip-alt-text': { + message: 'minimal example', + options: { + stripAltText: true, + }, + }, +}); diff --git a/plugins/postcss-content-alt-text/test/basic.css b/plugins/postcss-content-alt-text/test/basic.css new file mode 100644 index 000000000..6872ef12d --- /dev/null +++ b/plugins/postcss-content-alt-text/test/basic.css @@ -0,0 +1,43 @@ +.foo { + content: "1" / "0"; +} + +.foo { + content: var(--b) / var(--a); +} + +.foo { + content: "2" "0" / "0"; +} + +.foo { + content: "3" / "0" "0"; +} + +.ignore { + content: "4" "0"; +} + +.ignore { + content: "5" / "0" / "0"; +} + +.ignore { + content: "6" var(--foo, "0" / "0"); +} + +.ignore { + content: "7"; + content: "7" / "0"; +} + +.ignore { + content: "8" "0"; + content: "8" / "0"; +} + +@supports (content: "b" / "c") { + .ignore { + content: "9" / "0"; + } +} diff --git a/plugins/postcss-content-alt-text/test/basic.expect.css b/plugins/postcss-content-alt-text/test/basic.expect.css new file mode 100644 index 000000000..b4cbd27b7 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/basic.expect.css @@ -0,0 +1,52 @@ +.foo { + content: "1" "0"; + content: "1" / "0"; +} + +.foo { + content: var(--b) var(--a); +} + +@supports (content: "a" / "a") { +.foo { + content: var(--b) / var(--a); +} +} + +.foo { + content: "2" "0" "0"; + content: "2" "0" / "0"; +} + +.foo { + content: "3" "0" "0"; + content: "3" / "0" "0"; +} + +.ignore { + content: "4" "0"; +} + +.ignore { + content: "5" / "0" / "0"; +} + +.ignore { + content: "6" var(--foo, "0" / "0"); +} + +.ignore { + content: "7"; + content: "7" / "0"; +} + +.ignore { + content: "8" "0"; + content: "8" / "0"; +} + +@supports (content: "b" / "c") { + .ignore { + content: "9" / "0"; + } +} 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 new file mode 100644 index 000000000..521a80f3d --- /dev/null +++ b/plugins/postcss-content-alt-text/test/basic.preserve-false.expect.css @@ -0,0 +1,43 @@ +.foo { + content: "1" "0"; +} + +.foo { + content: var(--b) var(--a); +} + +.foo { + content: "2" "0" "0"; +} + +.foo { + content: "3" "0" "0"; +} + +.ignore { + content: "4" "0"; +} + +.ignore { + content: "5" / "0" / "0"; +} + +.ignore { + content: "6" var(--foo, "0" / "0"); +} + +.ignore { + content: "7"; + content: "7" / "0"; +} + +.ignore { + content: "8" "0"; + content: "8" / "0"; +} + +@supports (content: "b" / "c") { + .ignore { + content: "9" / "0"; + } +} 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 new file mode 100644 index 000000000..956530d79 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/basic.strip-alt-text.expect.css @@ -0,0 +1,52 @@ +.foo { + content: "1" ; + content: "1" / "0"; +} + +.foo { + content: var(--b) ; +} + +@supports (content: "a" / "a") { +.foo { + content: var(--b) / var(--a); +} +} + +.foo { + content: "2" "0" ; + content: "2" "0" / "0"; +} + +.foo { + content: "3" ; + content: "3" / "0" "0"; +} + +.ignore { + content: "4" "0"; +} + +.ignore { + content: "5" / "0" / "0"; +} + +.ignore { + content: "6" var(--foo, "0" / "0"); +} + +.ignore { + content: "7"; + content: "7" / "0"; +} + +.ignore { + content: "8" "0"; + content: "8" / "0"; +} + +@supports (content: "b" / "c") { + .ignore { + content: "9" / "0"; + } +} diff --git a/plugins/postcss-content-alt-text/test/examples/example.css b/plugins/postcss-content-alt-text/test/examples/example.css new file mode 100644 index 000000000..0bcf7c152 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/examples/example.css @@ -0,0 +1,3 @@ +.foo { + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} diff --git a/plugins/postcss-content-alt-text/test/examples/example.expect.css b/plugins/postcss-content-alt-text/test/examples/example.expect.css new file mode 100644 index 000000000..d2f53dbb2 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/examples/example.expect.css @@ -0,0 +1,4 @@ +.foo { + content: url(tree.jpg) "A beautiful tree in a dark forest"; + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} 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 new file mode 100644 index 000000000..677dc3d6d --- /dev/null +++ b/plugins/postcss-content-alt-text/test/examples/example.preserve-false.expect.css @@ -0,0 +1,3 @@ +.foo { + content: url(tree.jpg) "A beautiful tree in a dark forest"; +} 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 new file mode 100644 index 000000000..99ab75472 --- /dev/null +++ b/plugins/postcss-content-alt-text/test/examples/example.strip-alt-text.expect.css @@ -0,0 +1,4 @@ +.foo { + content: url(tree.jpg) ; + content: url(tree.jpg) / "A beautiful tree in a dark forest"; +} diff --git a/plugins/postcss-content-alt-text/tsconfig.json b/plugins/postcss-content-alt-text/tsconfig.json new file mode 100644 index 000000000..500af6d26 --- /dev/null +++ b/plugins/postcss-content-alt-text/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "strict": true + }, + "include": ["./src/**/*"], + "exclude": ["dist"] +}