From 89b9aafc1ca5d9b09ea35509cf1c1ad1d6580182 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Apr 2025 14:14:00 +0200 Subject: [PATCH 01/21] wip --- plugins/postcss-custom-functions/.gitignore | 6 + plugins/postcss-custom-functions/.nvmrc | 1 + plugins/postcss-custom-functions/CHANGELOG.md | 5 + plugins/postcss-custom-functions/INSTALL.md | 235 ++++++++++++++++++ plugins/postcss-custom-functions/LICENSE.md | 18 ++ plugins/postcss-custom-functions/README.md | 100 ++++++++ .../api-extractor.json | 4 + .../postcss-custom-functions/docs/README.md | 54 ++++ plugins/postcss-custom-functions/package.json | 78 ++++++ plugins/postcss-custom-functions/src/index.ts | 55 ++++ .../postcss-custom-functions/test/_import.mjs | 10 + .../test/_require.cjs | 9 + .../postcss-custom-functions/test/_tape.mjs | 23 ++ .../test/basic.color.expect.css | 7 + .../postcss-custom-functions/test/basic.css | 18 ++ .../test/basic.expect.css | 32 +++ .../test/examples/example.css | 7 + .../test/examples/example.expect.css | 7 + .../examples/example.preserve-true.expect.css | 8 + .../postcss-custom-functions/tsconfig.json | 10 + 20 files changed, 687 insertions(+) create mode 100644 plugins/postcss-custom-functions/.gitignore create mode 100644 plugins/postcss-custom-functions/.nvmrc create mode 100644 plugins/postcss-custom-functions/CHANGELOG.md create mode 100644 plugins/postcss-custom-functions/INSTALL.md create mode 100644 plugins/postcss-custom-functions/LICENSE.md create mode 100644 plugins/postcss-custom-functions/README.md create mode 100644 plugins/postcss-custom-functions/api-extractor.json create mode 100644 plugins/postcss-custom-functions/docs/README.md create mode 100644 plugins/postcss-custom-functions/package.json create mode 100644 plugins/postcss-custom-functions/src/index.ts create mode 100644 plugins/postcss-custom-functions/test/_import.mjs create mode 100644 plugins/postcss-custom-functions/test/_require.cjs create mode 100644 plugins/postcss-custom-functions/test/_tape.mjs create mode 100644 plugins/postcss-custom-functions/test/basic.color.expect.css create mode 100644 plugins/postcss-custom-functions/test/basic.css create mode 100644 plugins/postcss-custom-functions/test/basic.expect.css create mode 100644 plugins/postcss-custom-functions/test/examples/example.css create mode 100644 plugins/postcss-custom-functions/test/examples/example.expect.css create mode 100644 plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css create mode 100644 plugins/postcss-custom-functions/tsconfig.json diff --git a/plugins/postcss-custom-functions/.gitignore b/plugins/postcss-custom-functions/.gitignore new file mode 100644 index 0000000000..e5b28db4ad --- /dev/null +++ b/plugins/postcss-custom-functions/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.html diff --git a/plugins/postcss-custom-functions/.nvmrc b/plugins/postcss-custom-functions/.nvmrc new file mode 100644 index 0000000000..bb8c76c68e --- /dev/null +++ b/plugins/postcss-custom-functions/.nvmrc @@ -0,0 +1 @@ +v22.11.0 diff --git a/plugins/postcss-custom-functions/CHANGELOG.md b/plugins/postcss-custom-functions/CHANGELOG.md new file mode 100644 index 0000000000..e412cd2370 --- /dev/null +++ b/plugins/postcss-custom-functions/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changes to PostCSS Custom Functions + +### Unreleased (major) + +- Initial version diff --git a/plugins/postcss-custom-functions/INSTALL.md b/plugins/postcss-custom-functions/INSTALL.md new file mode 100644 index 0000000000..ddf2455907 --- /dev/null +++ b/plugins/postcss-custom-functions/INSTALL.md @@ -0,0 +1,235 @@ +# Installing PostCSS Custom Functions + +[PostCSS Custom Functions] 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 Custom Functions] to your project: + +```bash +npm install postcss @csstools/postcss-custom-functions --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +// commonjs +const postcss = require('postcss'); +const postcssBasePlugin = require('@csstools/postcss-custom-functions'); + +postcss([ + postcssBasePlugin(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +```js +// esm +import postcss from 'postcss'; +import postcssBasePlugin from '@csstools/postcss-custom-functions'; + +postcss([ + postcssBasePlugin(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +## PostCSS CLI + +Add [PostCSS CLI] to your project: + +```bash +npm install postcss-cli @csstools/postcss-custom-functions --save-dev +``` + +Use [PostCSS Custom Functions] in your `postcss.config.js` configuration file: + +```js +const postcssBasePlugin = require('@csstools/postcss-custom-functions'); + +module.exports = { + plugins: [ + postcssBasePlugin(/* pluginOptions */) + ] +} +``` + +## PostCSS Load Config + +If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config). + +```bash +npm install @csstools/postcss-custom-functions --save-dev +``` + +`package.json`: + +```json +{ + "postcss": { + "plugins": { + "@csstools/postcss-custom-functions": {} + } + } +} +``` + +`.postcssrc.json`: + +```json +{ + "plugins": { + "@csstools/postcss-custom-functions": {} + } +} +``` + +_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-custom-functions --save-dev +``` + +Use [PostCSS Custom Functions] 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-custom-functions", + { + // 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-custom-functions --save-dev +``` + +Use [PostCSS Custom Functions] in your `postcss.config.json` file: + +```json +{ + "plugins": [ + "@csstools/postcss-custom-functions" + ] +} +``` + +```json5 +{ + "plugins": [ + [ + "@csstools/postcss-custom-functions", + { + // Optionally add plugin options + } + ] + ] +} +``` + +## Gulp + +Add [Gulp PostCSS] to your project: + +```bash +npm install gulp-postcss @csstools/postcss-custom-functions --save-dev +``` + +Use [PostCSS Custom Functions] in your Gulpfile: + +```js +const postcss = require('gulp-postcss'); +const postcssBasePlugin = require('@csstools/postcss-custom-functions'); + +gulp.task('css', function () { + var plugins = [ + postcssBasePlugin(/* 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-custom-functions --save-dev +``` + +Use [PostCSS Custom Functions] in your Gruntfile: + +```js +const postcssBasePlugin = require('@csstools/postcss-custom-functions'); + +grunt.loadNpmTasks('grunt-postcss'); + +grunt.initConfig({ + postcss: { + options: { + processors: [ + postcssBasePlugin(/* 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 Custom Functions]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-functions +[Next.js]: https://nextjs.org diff --git a/plugins/postcss-custom-functions/LICENSE.md b/plugins/postcss-custom-functions/LICENSE.md new file mode 100644 index 0000000000..e8ae93b9f9 --- /dev/null +++ b/plugins/postcss-custom-functions/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-custom-functions/README.md b/plugins/postcss-custom-functions/README.md new file mode 100644 index 0000000000..056481f55b --- /dev/null +++ b/plugins/postcss-custom-functions/README.md @@ -0,0 +1,100 @@ +# PostCSS Custom Functions [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-custom-functions --save-dev +``` + +[PostCSS Custom Functions] lets you easily create new plugins following some [CSS Specification]. + +```css +.foo { + color: red; +} + +.baz { + color: green; +} + +/* becomes */ + +.foo { + color: blue; +} + +.baz { + color: green; +} +``` + +## Usage + +Add [PostCSS Custom Functions] to your project: + +```bash +npm install postcss @csstools/postcss-custom-functions --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssBasePlugin = require('@csstools/postcss-custom-functions'); + +postcss([ + postcssBasePlugin(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +[PostCSS Custom Functions] 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 not preserved. + +```js +postcssBasePlugin({ preserve: true }) +``` + +```css +.foo { + color: red; +} + +.baz { + color: green; +} + +/* becomes */ + +.foo { + color: blue; + color: red; +} + +.baz { + color: green; +} +``` + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[css-url]: https://cssdb.org/#TODO +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/postcss-custom-functions + +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Custom Functions]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-functions +[CSS Specification]: #TODO diff --git a/plugins/postcss-custom-functions/api-extractor.json b/plugins/postcss-custom-functions/api-extractor.json new file mode 100644 index 0000000000..42058be517 --- /dev/null +++ b/plugins/postcss-custom-functions/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-custom-functions/docs/README.md b/plugins/postcss-custom-functions/docs/README.md new file mode 100644 index 0000000000..f424146db1 --- /dev/null +++ b/plugins/postcss-custom-functions/docs/README.md @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + +
+ +[] lets you easily create new plugins following some [CSS Specification]. + +```css + + +/* becomes */ + + +``` + + + + + +## Options + +### preserve + +The `preserve` option determines whether the original notation +is preserved. By default, it is not preserved. + +```js +({ preserve: true }) +``` + +```css + + +/* becomes */ + + +``` + + +[CSS Specification]: diff --git a/plugins/postcss-custom-functions/package.json b/plugins/postcss-custom-functions/package.json new file mode 100644 index 0000000000..fb28822f9e --- /dev/null +++ b/plugins/postcss-custom-functions/package.json @@ -0,0 +1,78 @@ +{ + "name": "@csstools/postcss-custom-functions", + "description": "Custom functions allow authors the same power as custom properties, but parameterized", + "version": "0.0.0", + "contributors": [ + { + "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": ">=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" + ], + "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" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/csstools/postcss-plugins.git", + "directory": "plugins/postcss-custom-functions" + }, + "keywords": [ + "postcss-plugin" + ], + "csstools": { + "cssdbId": "TODO", + "exportName": "postcssCustomFunctions", + "humanReadableName": "PostCSS Custom Functions", + "specUrl": "https://drafts.csswg.org/css-mixins-1/#function-rule" + }, + "volta": { + "extends": "../../package.json" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-functions#readme", + "bugs": "https://github.com/csstools/postcss-plugins/issues" +} diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts new file mode 100644 index 0000000000..7839229e26 --- /dev/null +++ b/plugins/postcss-custom-functions/src/index.ts @@ -0,0 +1,55 @@ +import type { PluginCreator } from 'postcss'; + +/** postcss-custom-functions plugin options */ +export type pluginOptions = { + /** Preserve the original notation. default: false */ + preserve?: boolean, +}; + +const creator: PluginCreator = (opts?: pluginOptions) => { + const options: pluginOptions = Object.assign( + // Default options + { + color: null, + preserve: false, + }, + // Provided options + opts, + ); + + return { + postcssPlugin: 'postcss-custom-functions', + Declaration(decl): void { + if (decl.value === 'red') { + // Determine the new value. + let newValue = 'blue'; + if (options.color) { + newValue = options.color; + } + + // Check if it is different from the current value. + if (newValue === decl.value) { + return; + } + + // Insert the new value before the current value. + decl.cloneBefore({ + prop: 'color', + value: newValue, + }); + + // If the current value is preserved we are done and return here. + if (options.preserve) { + return; + } + + // If the current value is not preserved we remove it. + decl.remove(); + } + }, + }; +}; + +creator.postcss = true; + +export default creator; diff --git a/plugins/postcss-custom-functions/test/_import.mjs b/plugins/postcss-custom-functions/test/_import.mjs new file mode 100644 index 0000000000..cba1a21d14 --- /dev/null +++ b/plugins/postcss-custom-functions/test/_import.mjs @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import plugin from '@csstools/postcss-custom-functions'; + +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-custom-functions/test/_require.cjs b/plugins/postcss-custom-functions/test/_require.cjs new file mode 100644 index 0000000000..90dc6a5d2e --- /dev/null +++ b/plugins/postcss-custom-functions/test/_require.cjs @@ -0,0 +1,9 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const plugin = require('@csstools/postcss-custom-functions'); + +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-custom-functions/test/_tape.mjs b/plugins/postcss-custom-functions/test/_tape.mjs new file mode 100644 index 0000000000..7ff5bcd061 --- /dev/null +++ b/plugins/postcss-custom-functions/test/_tape.mjs @@ -0,0 +1,23 @@ +import { postcssTape } from '@csstools/postcss-tape'; +import plugin from '@csstools/postcss-custom-functions'; + +postcssTape(plugin)({ + basic: { + message: 'supports basic usage', + }, + 'basic:color': { + message: 'supports { color: \'\' }', + options: { + color: 'purple', + }, + }, + 'examples/example': { + message: 'minimal example', + }, + 'examples/example:preserve-true': { + message: 'minimal example', + options: { + preserve: true, + }, + }, +}); diff --git a/plugins/postcss-custom-functions/test/basic.color.expect.css b/plugins/postcss-custom-functions/test/basic.color.expect.css new file mode 100644 index 0000000000..041b1dabf3 --- /dev/null +++ b/plugins/postcss-custom-functions/test/basic.color.expect.css @@ -0,0 +1,7 @@ +.foo { + color: purple; +} + +.baz { + color: green; +} diff --git a/plugins/postcss-custom-functions/test/basic.css b/plugins/postcss-custom-functions/test/basic.css new file mode 100644 index 0000000000..9cbf5a4861 --- /dev/null +++ b/plugins/postcss-custom-functions/test/basic.css @@ -0,0 +1,18 @@ +@function --negative(--value) { + result: calc(-1 * var(--value)); +} + +.negative-a { + --gap: 1em; + padding: --negative(var(--gap)); +} + +.negative-b { + padding: --negative(1em); +} + +.negative-c { + --value: 1em; + padding: --negative(); +} + diff --git a/plugins/postcss-custom-functions/test/basic.expect.css b/plugins/postcss-custom-functions/test/basic.expect.css new file mode 100644 index 0000000000..fa050f4f21 --- /dev/null +++ b/plugins/postcss-custom-functions/test/basic.expect.css @@ -0,0 +1,32 @@ +@function --negative(--value) { + result: calc(-1 * var(--value)); +} + +.negative-a { + --csstools--negative--0--value: var(--gap, var(--value)); + --csstools--negative--0--result: calc(-1 * var(--csstools--negative--0--value)); +} + +.negative-a { + --gap: 1em; + padding: var(--csstools--negative--0--result); +} + +.negative-b { + --csstools--negative--1--value: 1em; + --csstools--negative--1--result: calc(-1 * var(--csstools--negative--1--value)); +} + +.negative-b { + padding: var(--csstools--negative--1--result); +} + +.negative-c { + --value: 1em; + --csstools--negative--2--value: var(--value); + --csstools--negative--2--result: calc(-1 * var(--csstools--negative--2--value)); +} + +.negative-c { + padding: var(--csstools--negative--2--result); +} diff --git a/plugins/postcss-custom-functions/test/examples/example.css b/plugins/postcss-custom-functions/test/examples/example.css new file mode 100644 index 0000000000..181f83a541 --- /dev/null +++ b/plugins/postcss-custom-functions/test/examples/example.css @@ -0,0 +1,7 @@ +.foo { + color: red; +} + +.baz { + color: green; +} diff --git a/plugins/postcss-custom-functions/test/examples/example.expect.css b/plugins/postcss-custom-functions/test/examples/example.expect.css new file mode 100644 index 0000000000..9d738d5ac3 --- /dev/null +++ b/plugins/postcss-custom-functions/test/examples/example.expect.css @@ -0,0 +1,7 @@ +.foo { + color: blue; +} + +.baz { + color: green; +} diff --git a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css new file mode 100644 index 0000000000..8b020470a1 --- /dev/null +++ b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css @@ -0,0 +1,8 @@ +.foo { + color: blue; + color: red; +} + +.baz { + color: green; +} diff --git a/plugins/postcss-custom-functions/tsconfig.json b/plugins/postcss-custom-functions/tsconfig.json new file mode 100644 index 0000000000..500af6d266 --- /dev/null +++ b/plugins/postcss-custom-functions/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "strict": true + }, + "include": ["./src/**/*"], + "exclude": ["dist"] +} From 22bbd827e6d99417cf5e1fddafb4af2eb9554629 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Sat, 31 May 2025 11:01:32 +0200 Subject: [PATCH 02/21] wip --- packages/custom-function-parser/.gitignore | 6 + packages/custom-function-parser/.nvmrc | 1 + packages/custom-function-parser/CHANGELOG.md | 5 + packages/custom-function-parser/LICENSE.md | 20 + packages/custom-function-parser/README.md | 28 + .../custom-function-parser/api-extractor.json | 7 + .../custom-function-parser/dist/index.cjs | 1 + .../custom-function-parser/dist/index.d.ts | 73 + .../custom-function-parser/dist/index.mjs | 1 + .../docs/custom-function-parser.api.json | 1888 +++++++++++++++++ packages/custom-function-parser/package.json | 78 + packages/custom-function-parser/src/index.ts | 11 + .../src/nodes/custom-function.ts | 79 + .../src/nodes/function-parameter.ts | 94 + .../src/parser/parse-custom-function.ts | 278 +++ .../src/parser/parse.ts | 29 + .../src/util/node-type.ts | 4 + .../src/util/type-predicates.ts | 10 + .../custom-function-parser/stryker.conf.json | 19 + .../custom-function-parser/test/_import.mjs | 3 + .../custom-function-parser/test/_require.cjs | 3 + .../cases/custom-function/0001.expect.json | 28 + .../test/cases/custom-function/0001.mjs | 13 + .../cases/custom-function/0002.expect.json | 65 + .../test/cases/custom-function/0002.mjs | 13 + .../cases/custom-function/0003.expect.json | 65 + .../test/cases/custom-function/0003.mjs | 13 + .../cases/custom-function/0004.expect.json | 233 ++ .../test/cases/custom-function/0004.mjs | 13 + .../cases/custom-function/0005.expect.json | 1 + .../test/cases/custom-function/0005.mjs | 13 + .../cases/custom-function/0006.expect.json | 1 + .../test/cases/custom-function/0006.mjs | 13 + .../cases/custom-function/0007.expect.json | 1 + .../test/cases/custom-function/0007.mjs | 13 + .../cases/custom-function/0008.expect.json | 229 ++ .../test/cases/custom-function/0008.mjs | 13 + .../cases/custom-function/0009.expect.json | 1 + .../test/cases/custom-function/0009.mjs | 13 + .../cases/custom-function/0010.expect.json | 144 ++ .../test/cases/custom-function/0010.mjs | 13 + .../cases/custom-function/0011.expect.json | 280 +++ .../test/cases/custom-function/0011.mjs | 13 + .../cases/custom-function/0012.expect.json | 294 +++ .../test/cases/custom-function/0012.mjs | 13 + .../cases/custom-function/0013.expect.json | 251 +++ .../test/cases/custom-function/0013.mjs | 13 + .../cases/custom-function/0014.expect.json | 432 ++++ .../test/cases/custom-function/0014.mjs | 13 + .../test/get-argument-type/0001.mjs | 35 + .../test/get-default-value/0001.mjs | 78 + .../test/get-name/0001.mjs | 34 + packages/custom-function-parser/test/test.mjs | 20 + .../test/util/run-test.mjs | 38 + packages/custom-function-parser/tsconfig.json | 10 + 55 files changed, 5060 insertions(+) create mode 100644 packages/custom-function-parser/.gitignore create mode 100644 packages/custom-function-parser/.nvmrc create mode 100644 packages/custom-function-parser/CHANGELOG.md create mode 100644 packages/custom-function-parser/LICENSE.md create mode 100644 packages/custom-function-parser/README.md create mode 100644 packages/custom-function-parser/api-extractor.json create mode 100644 packages/custom-function-parser/dist/index.cjs create mode 100644 packages/custom-function-parser/dist/index.d.ts create mode 100644 packages/custom-function-parser/dist/index.mjs create mode 100644 packages/custom-function-parser/docs/custom-function-parser.api.json create mode 100644 packages/custom-function-parser/package.json create mode 100644 packages/custom-function-parser/src/index.ts create mode 100644 packages/custom-function-parser/src/nodes/custom-function.ts create mode 100644 packages/custom-function-parser/src/nodes/function-parameter.ts create mode 100644 packages/custom-function-parser/src/parser/parse-custom-function.ts create mode 100644 packages/custom-function-parser/src/parser/parse.ts create mode 100644 packages/custom-function-parser/src/util/node-type.ts create mode 100644 packages/custom-function-parser/src/util/type-predicates.ts create mode 100644 packages/custom-function-parser/stryker.conf.json create mode 100644 packages/custom-function-parser/test/_import.mjs create mode 100644 packages/custom-function-parser/test/_require.cjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0001.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0001.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0002.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0002.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0003.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0003.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0004.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0004.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0005.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0005.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0006.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0006.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0007.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0007.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0008.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0008.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0009.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0009.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0010.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0010.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0011.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0011.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0012.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0012.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0013.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0013.mjs create mode 100644 packages/custom-function-parser/test/cases/custom-function/0014.expect.json create mode 100644 packages/custom-function-parser/test/cases/custom-function/0014.mjs create mode 100644 packages/custom-function-parser/test/get-argument-type/0001.mjs create mode 100644 packages/custom-function-parser/test/get-default-value/0001.mjs create mode 100644 packages/custom-function-parser/test/get-name/0001.mjs create mode 100644 packages/custom-function-parser/test/test.mjs create mode 100644 packages/custom-function-parser/test/util/run-test.mjs create mode 100644 packages/custom-function-parser/tsconfig.json diff --git a/packages/custom-function-parser/.gitignore b/packages/custom-function-parser/.gitignore new file mode 100644 index 0000000000..8b84de0347 --- /dev/null +++ b/packages/custom-function-parser/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.json diff --git a/packages/custom-function-parser/.nvmrc b/packages/custom-function-parser/.nvmrc new file mode 100644 index 0000000000..bb8c76c68e --- /dev/null +++ b/packages/custom-function-parser/.nvmrc @@ -0,0 +1 @@ +v22.11.0 diff --git a/packages/custom-function-parser/CHANGELOG.md b/packages/custom-function-parser/CHANGELOG.md new file mode 100644 index 0000000000..1230a0570c --- /dev/null +++ b/packages/custom-function-parser/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changes to Custom Function Parser + +### Unreleased (major) + +- Initial version diff --git a/packages/custom-function-parser/LICENSE.md b/packages/custom-function-parser/LICENSE.md new file mode 100644 index 0000000000..af5411fa24 --- /dev/null +++ b/packages/custom-function-parser/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +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, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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/packages/custom-function-parser/README.md b/packages/custom-function-parser/README.md new file mode 100644 index 0000000000..8c4ea70429 --- /dev/null +++ b/packages/custom-function-parser/README.md @@ -0,0 +1,28 @@ +# Custom Function Parser for CSS + +[npm version][npm-url] +[Build Status][cli-url] +[Discord][discord] + +Implemented from : https://www.w3.org/TR/mediaqueries-5/ + +## Usage + +Add [Custom Function Parser] to your project: + +```bash +npm install @csstools/custom-function-parser @csstools/css-parser-algorithms @csstools/css-tokenizer --save-dev +``` + +[Custom Function Parser] depends on our CSS tokenizer and parser algorithms. +It must be used together with `@csstools/css-tokenizer` and `@csstools/css-parser-algorithms`. + +```ts +// TODO +``` + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/custom-function-parser + +[Custom Function Parser]: https://github.com/csstools/postcss-plugins/tree/main/packages/custom-function-parser diff --git a/packages/custom-function-parser/api-extractor.json b/packages/custom-function-parser/api-extractor.json new file mode 100644 index 0000000000..ccef9beeb0 --- /dev/null +++ b/packages/custom-function-parser/api-extractor.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.json", + "docModel": { + "enabled": true + } +} diff --git a/packages/custom-function-parser/dist/index.cjs b/packages/custom-function-parser/dist/index.cjs new file mode 100644 index 0000000000..bac482a7ed --- /dev/null +++ b/packages/custom-function-parser/dist/index.cjs @@ -0,0 +1 @@ +"use strict";var e,t=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");exports.NodeType=void 0,(e=exports.NodeType||(exports.NodeType={})).CustomFunction="custom-function",e.FunctionParameter="function-parameter";class CustomFunction{type=exports.NodeType.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],i=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=i}getName(){return this.function.getName()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return r.stringify(...this.before)+r.stringify(...this.function.tokens())+r.stringify(...this.returnsKeyword)+r.stringify(...this.returnType)+r.stringify(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((e=>e.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&(e instanceof CustomFunction&&e.type===exports.NodeType.CustomFunction)}}class FunctionParameter{type=exports.NodeType.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],i=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=i}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return r.stringify(...this.argumentType.filter((e=>!r.isTokenComment(e)))).trim()}getDefaultValue(){return r.stringify(...this.defaultValue).trim()}tokens(){return[...this.before,this.name,...this.argumentType,this.colon,...this.defaultValue,...this.after].filter((e=>!!e))}toString(){return r.stringify(...this.before)+r.stringify(this.name)+r.stringify(...this.argumentType)+(this.colon?r.stringify(this.colon):"")+r.stringify(...this.defaultValue)+r.stringify(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&(e instanceof FunctionParameter&&e.type===exports.NodeType.FunctionParameter)}}function parseCustomFunction(e){let n=null,s=-1,i=-1,o=-1,a=e.length;for(let a=0;ao;r--){const n=e[r];if(!t.isWhiteSpaceOrCommentNode(n))break;a=r}const u=parseFunctionParameters(n.value);return!!u&&new CustomFunction(n,u,-1===o?[]:e.slice(s+1,o).flatMap((e=>e.tokens())),-1===o?[]:e.slice(o,a).flatMap((e=>e.tokens())),e.slice(0,s).flatMap((e=>e.tokens())),e.slice(a).flatMap((e=>e.tokens())))}function parseFunctionParameters(e){if(!e.some((e=>!t.isWhiteSpaceOrCommentNode(e))))return[];const r=[];for(let t=0;t0&&t.isTokenNode(l)&&r.isTokenDelim(l.value)&&">"===l.value[4].value)n--;else if(-1===o&&0===n&&t.isTokenNode(l)&&r.isTokenColon(l.value))o=f,a=l.value;else if(-1===o){if(-1!==o)return!1}else u=f}}if(!i)return!1;if(-1!==o&&-1===u)return!1;const l=-1!==o?o-1:f-1;for(let r=e.length-1;r>Math.max(l,u);r--){const n=e[r];if(!t.isWhitespaceNode(n))break;c=r}let m=[];if(-1!==u){const n=e.slice(o+1,c),s=n.filter((e=>!t.isWhiteSpaceOrCommentNode(e)));m=1===s.length&&t.isSimpleBlockNode(s[0])&&r.isTokenOpenCurly(s[0].startToken)?s[0].value.flatMap((e=>e.tokens())):n.flatMap((e=>e.tokens()))}return{advance:f,node:new FunctionParameter(i,e.slice(s+1,l+1).flatMap((e=>e.tokens())),a,m,e.slice(0,s).flatMap((e=>e.tokens())),e.slice(c,f+1).flatMap((e=>e.tokens())))}}function parseFromTokens(e,r){return parseCustomFunction(t.parseListOfComponentValues(e,{onParseError:r?.onParseError}))}exports.CustomFunction=CustomFunction,exports.FunctionParameter=FunctionParameter,exports.isCustomFunction=function isCustomFunction(e){return CustomFunction.isCustomFunction(e)},exports.isFunctionParameter=function isFunctionParameter(e){return FunctionParameter.isFunctionParameter(e)},exports.parse=function parse(e,t){return parseFromTokens(r.tokenize({css:e},{onParseError:t?.onParseError}),t)},exports.parseFromTokens=parseFromTokens; diff --git a/packages/custom-function-parser/dist/index.d.ts b/packages/custom-function-parser/dist/index.d.ts new file mode 100644 index 0000000000..7c3113393e --- /dev/null +++ b/packages/custom-function-parser/dist/index.d.ts @@ -0,0 +1,73 @@ +import type { CSSToken } from '@csstools/css-tokenizer'; +import type { FunctionNode } from '@csstools/css-parser-algorithms'; +import type { ParseError } from '@csstools/css-tokenizer'; +import type { TokenColon } from '@csstools/css-tokenizer'; +import type { TokenIdent } from '@csstools/css-tokenizer'; + +export declare class CustomFunction { + type: NodeType; + function: FunctionNode; + parameters: Array; + returnsKeyword: Array; + returnType: Array; + before: Array; + after: Array; + constructor(fn: FunctionNode, parameters: Array, returnsKeyword: Array, returnType: Array, before?: Array, after?: Array); + getName(): string; + tokens(): Array; + toString(): string; + /** + * @internal + */ + toJSON(): Record; + /** + * @internal + */ + isCustomFunction(): this is CustomFunction; + static isCustomFunction(x: unknown): x is CustomFunction; +} + +export declare class FunctionParameter { + type: NodeType; + name: TokenIdent; + argumentType: Array; + colon: TokenColon | null; + defaultValue: Array; + before: Array; + after: Array; + constructor(name: TokenIdent, argumentType: Array, colon: TokenColon | null, defaultValue: Array, before?: Array, after?: Array); + getName(): string; + getNameToken(): TokenIdent | null; + getArgumentType(): string; + getDefaultValue(): string; + tokens(): Array; + toString(): string; + /** + * @internal + */ + toJSON(): Record; + /** + * @internal + */ + isFunctionParameter(): this is FunctionParameter; + static isFunctionParameter(x: unknown): x is FunctionParameter; +} + +export declare function isCustomFunction(x: unknown): x is CustomFunction; + +export declare function isFunctionParameter(x: unknown): x is FunctionParameter; + +export declare enum NodeType { + CustomFunction = "custom-function", + FunctionParameter = "function-parameter" +} + +export declare function parse(source: string, options?: { + onParseError?: (error: ParseError) => void; +}): CustomFunction | false; + +export declare function parseFromTokens(tokens: Array, options?: { + onParseError?: (error: ParseError) => void; +}): CustomFunction | false; + +export { } diff --git a/packages/custom-function-parser/dist/index.mjs b/packages/custom-function-parser/dist/index.mjs new file mode 100644 index 0000000000..cc3d9a9602 --- /dev/null +++ b/packages/custom-function-parser/dist/index.mjs @@ -0,0 +1 @@ +import{isWhiteSpaceOrCommentNode as t,isFunctionNode as e,isTokenNode as r,isWhitespaceNode as n,isSimpleBlockNode as s,parseListOfComponentValues as i}from"@csstools/css-parser-algorithms";import{stringify as o,isTokenComment as a,isTokenIdent as u,isTokenComma as c,isTokenDelim as f,isTokenColon as l,isTokenOpenCurly as m,tokenize as h}from"@csstools/css-tokenizer";var p;!function(t){t.CustomFunction="custom-function",t.FunctionParameter="function-parameter"}(p||(p={}));class CustomFunction{type=p.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(t,e,r,n,s=[],i=[]){this.function=t,this.parameters=e,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=i}getName(){return this.function.getName()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return o(...this.before)+o(...this.function.tokens())+o(...this.returnsKeyword)+o(...this.returnType)+o(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((t=>t.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(t){return!!t&&(t instanceof CustomFunction&&t.type===p.CustomFunction)}}class FunctionParameter{type=p.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(t,e,r,n,s=[],i=[]){this.name=t,this.argumentType=e,this.colon=r,this.defaultValue=n,this.before=s,this.after=i}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return o(...this.argumentType.filter((t=>!a(t)))).trim()}getDefaultValue(){return o(...this.defaultValue).trim()}tokens(){return[...this.before,this.name,...this.argumentType,this.colon,...this.defaultValue,...this.after].filter((t=>!!t))}toString(){return o(...this.before)+o(this.name)+o(...this.argumentType)+(this.colon?o(this.colon):"")+o(...this.defaultValue)+o(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(t){return!!t&&(t instanceof FunctionParameter&&t.type===p.FunctionParameter)}}function parseCustomFunction(n){let s=null,i=-1,o=-1,a=-1,c=n.length;for(let c=0;ca;e--){const r=n[e];if(!t(r))break;c=e}const f=parseFunctionParameters(s.value);return!!f&&new CustomFunction(s,f,-1===a?[]:n.slice(i+1,a).flatMap((t=>t.tokens())),-1===a?[]:n.slice(a,c).flatMap((t=>t.tokens())),n.slice(0,i).flatMap((t=>t.tokens())),n.slice(c).flatMap((t=>t.tokens())))}function parseFunctionParameters(e){if(!e.some((e=>!t(e))))return[];const r=[];for(let t=0;t0&&r(n)&&f(n.value)&&">"===n.value[4].value)i--;else if(-1===h&&0===i&&r(n)&&l(n.value))h=y,p=n.value;else if(-1===h){if(-1!==h)return!1}else F=y}}if(!a)return!1;if(-1!==h&&-1===F)return!1;const k=-1!==h?h-1:y-1;for(let t=e.length-1;t>Math.max(k,F);t--){const r=e[t];if(!n(r))break;g=t}let P=[];if(-1!==F){const r=e.slice(h+1,g),n=r.filter((e=>!t(e)));P=1===n.length&&s(n[0])&&m(n[0].startToken)?n[0].value.flatMap((t=>t.tokens())):r.flatMap((t=>t.tokens()))}return{advance:y,node:new FunctionParameter(a,e.slice(o+1,k+1).flatMap((t=>t.tokens())),p,P,e.slice(0,o).flatMap((t=>t.tokens())),e.slice(g,y+1).flatMap((t=>t.tokens())))}}function parseFromTokens(t,e){return parseCustomFunction(i(t,{onParseError:e?.onParseError}))}function parse(t,e){return parseFromTokens(h({css:t},{onParseError:e?.onParseError}),e)}function isCustomFunction(t){return CustomFunction.isCustomFunction(t)}function isFunctionParameter(t){return FunctionParameter.isFunctionParameter(t)}export{CustomFunction,FunctionParameter,p as NodeType,isCustomFunction,isFunctionParameter,parse,parseFromTokens}; diff --git a/packages/custom-function-parser/docs/custom-function-parser.api.json b/packages/custom-function-parser/docs/custom-function-parser.api.json new file mode 100644 index 0000000000..d3f9f0c943 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.api.json @@ -0,0 +1,1888 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "schemaVersion": 1011, + "oldestForwardsCompatibleVersion": 1001, + "tsdocConfig": { + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "noStandardTags": true, + "tagDefinitions": [ + { + "tagName": "@alpha", + "syntaxKind": "modifier" + }, + { + "tagName": "@beta", + "syntaxKind": "modifier" + }, + { + "tagName": "@defaultValue", + "syntaxKind": "block" + }, + { + "tagName": "@decorator", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@deprecated", + "syntaxKind": "block" + }, + { + "tagName": "@eventProperty", + "syntaxKind": "modifier" + }, + { + "tagName": "@example", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@experimental", + "syntaxKind": "modifier" + }, + { + "tagName": "@inheritDoc", + "syntaxKind": "inline" + }, + { + "tagName": "@internal", + "syntaxKind": "modifier" + }, + { + "tagName": "@label", + "syntaxKind": "inline" + }, + { + "tagName": "@link", + "syntaxKind": "inline", + "allowMultiple": true + }, + { + "tagName": "@override", + "syntaxKind": "modifier" + }, + { + "tagName": "@packageDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@param", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@privateRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@public", + "syntaxKind": "modifier" + }, + { + "tagName": "@readonly", + "syntaxKind": "modifier" + }, + { + "tagName": "@remarks", + "syntaxKind": "block" + }, + { + "tagName": "@returns", + "syntaxKind": "block" + }, + { + "tagName": "@sealed", + "syntaxKind": "modifier" + }, + { + "tagName": "@see", + "syntaxKind": "block" + }, + { + "tagName": "@throws", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@typeParam", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@virtual", + "syntaxKind": "modifier" + }, + { + "tagName": "@betaDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@internalRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@preapproved", + "syntaxKind": "modifier" + } + ], + "supportForTags": { + "@alpha": true, + "@beta": true, + "@defaultValue": true, + "@decorator": true, + "@deprecated": true, + "@eventProperty": true, + "@example": true, + "@experimental": true, + "@inheritDoc": true, + "@internal": true, + "@label": true, + "@link": true, + "@override": true, + "@packageDocumentation": true, + "@param": true, + "@privateRemarks": true, + "@public": true, + "@readonly": true, + "@remarks": true, + "@returns": true, + "@sealed": true, + "@see": true, + "@throws": true, + "@typeParam": true, + "@virtual": true, + "@betaDocumentation": true, + "@internalRemarks": true, + "@preapproved": true + }, + "reportUnsupportedHtmlElements": false + } + }, + "kind": "Package", + "canonicalReference": "@csstools/custom-function-parser!", + "docComment": "", + "name": "@csstools/custom-function-parser", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "@csstools/custom-function-parser!", + "name": "", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Class", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction:class", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare class CustomFunction " + } + ], + "fileUrlPath": "dist/_types/nodes/custom-function.d.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "CustomFunction", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction:constructor(1)", + "docComment": "/**\n * Constructs a new instance of the `CustomFunction` class\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "constructor(fn: " + }, + { + "kind": "Reference", + "text": "FunctionNode", + "canonicalReference": "@csstools/css-parser-algorithms!FunctionNode:class" + }, + { + "kind": "Content", + "text": ", parameters: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "FunctionParameter", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter:class" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", returnsKeyword: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", returnType: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", before?: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", after?: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ");" + } + ], + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "fn", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "parameters", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 7 + }, + "isOptional": false + }, + { + "parameterName": "returnsKeyword", + "parameterTypeTokenRange": { + "startIndex": 8, + "endIndex": 12 + }, + "isOptional": false + }, + { + "parameterName": "returnType", + "parameterTypeTokenRange": { + "startIndex": 13, + "endIndex": 17 + }, + "isOptional": false + }, + { + "parameterName": "before", + "parameterTypeTokenRange": { + "startIndex": 18, + "endIndex": 22 + }, + "isOptional": true + }, + { + "parameterName": "after", + "parameterTypeTokenRange": { + "startIndex": 23, + "endIndex": 27 + }, + "isOptional": true + } + ] + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#after:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "after: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "after", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#before:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "before: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "before", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#function:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "function: " + }, + { + "kind": "Reference", + "text": "FunctionNode", + "canonicalReference": "@csstools/css-parser-algorithms!FunctionNode:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "function", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#getName:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getName(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getName" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction.isCustomFunction:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "static isCustomFunction(x: " + }, + { + "kind": "Content", + "text": "unknown" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "x", + "canonicalReference": "@csstools/custom-function-parser!~x" + }, + { + "kind": "Content", + "text": " is " + }, + { + "kind": "Reference", + "text": "CustomFunction", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "x", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "isCustomFunction" + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#parameters:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "parameters: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "FunctionParameter", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter:class" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "parameters", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#returnsKeyword:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "returnsKeyword: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "returnsKeyword", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#returnType:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "returnType: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "returnType", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#tokens:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "tokens(): " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "tokens" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#toString:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "toString(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "toString" + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#type:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "type: " + }, + { + "kind": "Reference", + "text": "NodeType", + "canonicalReference": "@csstools/custom-function-parser!NodeType:enum" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "type", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + } + ], + "implementsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter:class", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare class FunctionParameter " + } + ], + "fileUrlPath": "dist/_types/nodes/function-parameter.d.ts", + "releaseTag": "Public", + "isAbstract": false, + "name": "FunctionParameter", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter:constructor(1)", + "docComment": "/**\n * Constructs a new instance of the `FunctionParameter` class\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "constructor(name: " + }, + { + "kind": "Reference", + "text": "TokenIdent", + "canonicalReference": "@csstools/css-tokenizer!TokenIdent:interface" + }, + { + "kind": "Content", + "text": ", argumentType: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", colon: " + }, + { + "kind": "Reference", + "text": "TokenColon", + "canonicalReference": "@csstools/css-tokenizer!TokenColon:interface" + }, + { + "kind": "Content", + "text": " | null" + }, + { + "kind": "Content", + "text": ", defaultValue: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", before?: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", after?: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ");" + } + ], + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "argumentType", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 7 + }, + "isOptional": false + }, + { + "parameterName": "colon", + "parameterTypeTokenRange": { + "startIndex": 8, + "endIndex": 10 + }, + "isOptional": false + }, + { + "parameterName": "defaultValue", + "parameterTypeTokenRange": { + "startIndex": 11, + "endIndex": 15 + }, + "isOptional": false + }, + { + "parameterName": "before", + "parameterTypeTokenRange": { + "startIndex": 16, + "endIndex": 20 + }, + "isOptional": true + }, + { + "parameterName": "after", + "parameterTypeTokenRange": { + "startIndex": 21, + "endIndex": 25 + }, + "isOptional": true + } + ] + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#after:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "after: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "after", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#argumentType:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "argumentType: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "argumentType", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#before:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "before: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "before", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#colon:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "colon: " + }, + { + "kind": "Reference", + "text": "TokenColon", + "canonicalReference": "@csstools/css-tokenizer!TokenColon:interface" + }, + { + "kind": "Content", + "text": " | null" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "colon", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#defaultValue:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "defaultValue: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "defaultValue", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#getArgumentType:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getArgumentType(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getArgumentType" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#getDefaultValue:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getDefaultValue(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getDefaultValue" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#getName:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getName(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getName" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#getNameToken:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getNameToken(): " + }, + { + "kind": "Reference", + "text": "TokenIdent", + "canonicalReference": "@csstools/css-tokenizer!TokenIdent:interface" + }, + { + "kind": "Content", + "text": " | null" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getNameToken" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter.isFunctionParameter:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "static isFunctionParameter(x: " + }, + { + "kind": "Content", + "text": "unknown" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "x", + "canonicalReference": "@csstools/custom-function-parser!~x" + }, + { + "kind": "Content", + "text": " is " + }, + { + "kind": "Reference", + "text": "FunctionParameter", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "x", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "isFunctionParameter" + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#name:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "name: " + }, + { + "kind": "Reference", + "text": "TokenIdent", + "canonicalReference": "@csstools/css-tokenizer!TokenIdent:interface" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "name", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#tokens:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "tokens(): " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "tokens" + }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#toString:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "toString(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "toString" + }, + { + "kind": "Property", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#type:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "type: " + }, + { + "kind": "Reference", + "text": "NodeType", + "canonicalReference": "@csstools/custom-function-parser!NodeType:enum" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "type", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + } + ], + "implementsTokenRanges": [] + }, + { + "kind": "Function", + "canonicalReference": "@csstools/custom-function-parser!isCustomFunction:function(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function isCustomFunction(x: " + }, + { + "kind": "Content", + "text": "unknown" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "x", + "canonicalReference": "@csstools/custom-function-parser!~x" + }, + { + "kind": "Content", + "text": " is " + }, + { + "kind": "Reference", + "text": "CustomFunction", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "dist/_types/util/type-predicates.d.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "x", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "isCustomFunction" + }, + { + "kind": "Function", + "canonicalReference": "@csstools/custom-function-parser!isFunctionParameter:function(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function isFunctionParameter(x: " + }, + { + "kind": "Content", + "text": "unknown" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "x", + "canonicalReference": "@csstools/custom-function-parser!~x" + }, + { + "kind": "Content", + "text": " is " + }, + { + "kind": "Reference", + "text": "FunctionParameter", + "canonicalReference": "@csstools/custom-function-parser!FunctionParameter:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "dist/_types/util/type-predicates.d.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "x", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "isFunctionParameter" + }, + { + "kind": "Enum", + "canonicalReference": "@csstools/custom-function-parser!NodeType:enum", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare enum NodeType " + } + ], + "fileUrlPath": "dist/_types/util/node-type.d.ts", + "releaseTag": "Public", + "name": "NodeType", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EnumMember", + "canonicalReference": "@csstools/custom-function-parser!NodeType.CustomFunction:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "CustomFunction = " + }, + { + "kind": "Content", + "text": "\"custom-function\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "CustomFunction" + }, + { + "kind": "EnumMember", + "canonicalReference": "@csstools/custom-function-parser!NodeType.FunctionParameter:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "FunctionParameter = " + }, + { + "kind": "Content", + "text": "\"function-parameter\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "FunctionParameter" + } + ] + }, + { + "kind": "Function", + "canonicalReference": "@csstools/custom-function-parser!parse:function(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function parse(source: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", options?: " + }, + { + "kind": "Content", + "text": "{\n onParseError?: (error: " + }, + { + "kind": "Reference", + "text": "ParseError", + "canonicalReference": "@csstools/css-tokenizer!ParseError:class" + }, + { + "kind": "Content", + "text": ") => void;\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "CustomFunction", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction:class" + }, + { + "kind": "Content", + "text": " | false" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "dist/_types/parser/parse.d.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "source", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 6 + }, + "isOptional": true + } + ], + "name": "parse" + }, + { + "kind": "Function", + "canonicalReference": "@csstools/custom-function-parser!parseFromTokens:function(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function parseFromTokens(tokens: " + }, + { + "kind": "Reference", + "text": "Array", + "canonicalReference": "!Array:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "CSSToken", + "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ", options?: " + }, + { + "kind": "Content", + "text": "{\n onParseError?: (error: " + }, + { + "kind": "Reference", + "text": "ParseError", + "canonicalReference": "@csstools/css-tokenizer!ParseError:class" + }, + { + "kind": "Content", + "text": ") => void;\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "CustomFunction", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction:class" + }, + { + "kind": "Content", + "text": " | false" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "dist/_types/parser/parse.d.ts", + "returnTypeTokenRange": { + "startIndex": 10, + "endIndex": 12 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "tokens", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + }, + "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 6, + "endIndex": 9 + }, + "isOptional": true + } + ], + "name": "parseFromTokens" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/custom-function-parser/package.json b/packages/custom-function-parser/package.json new file mode 100644 index 0000000000..14fa51097c --- /dev/null +++ b/packages/custom-function-parser/package.json @@ -0,0 +1,78 @@ +{ + "name": "@csstools/custom-function-parser", + "description": "Parse CSS custom function params.", + "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", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=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" + ], + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.mjs", + "docs": "node ../../.github/bin/generate-docs/api-documenter.mjs", + "lint": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run build && npm run test", + "stryker": "stryker run --logLevel error", + "test": "node --test ./test/test.mjs ./test/_import.mjs ./test/_require.cjs", + "test:rewrite-expects": "REWRITE_EXPECTS=true node --test ./test/test.mjs" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/custom-function-parser#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/csstools/postcss-plugins.git", + "directory": "packages/custom-function-parser" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "css", + "custom functions", + "parser" + ], + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/custom-function-parser/src/index.ts b/packages/custom-function-parser/src/index.ts new file mode 100644 index 0000000000..03072e2eab --- /dev/null +++ b/packages/custom-function-parser/src/index.ts @@ -0,0 +1,11 @@ +export { parse, parseFromTokens } from './parser/parse'; + +export { NodeType } from './util/node-type'; + +export { + isCustomFunction, + isFunctionParameter, +} from './util/type-predicates'; + +export { CustomFunction } from './nodes/custom-function'; +export { FunctionParameter } from './nodes/function-parameter'; diff --git a/packages/custom-function-parser/src/nodes/custom-function.ts b/packages/custom-function-parser/src/nodes/custom-function.ts new file mode 100644 index 0000000000..7477981bae --- /dev/null +++ b/packages/custom-function-parser/src/nodes/custom-function.ts @@ -0,0 +1,79 @@ +import type { CSSToken } from '@csstools/css-tokenizer'; +import { stringify } from '@csstools/css-tokenizer'; +import { NodeType } from '../util/node-type'; +import type { FunctionParameter } from './function-parameter'; +import type { FunctionNode } from '@csstools/css-parser-algorithms'; + +export class CustomFunction { + type = NodeType.CustomFunction; + + function: FunctionNode; + parameters: Array; + returnsKeyword: Array; + returnType: Array; + before: Array; + after: Array; + + constructor(fn: FunctionNode, parameters: Array, returnsKeyword: Array, returnType: Array, before: Array = [], after: Array = []) { + this.function = fn; + this.parameters = parameters; + this.returnsKeyword = returnsKeyword; + this.returnType = returnType; + this.before = before; + this.after = after; + } + + getName(): string { + return this.function.getName(); + } + + tokens(): Array { + return [ + ...this.before, + ...this.function.tokens(), + ...this.returnsKeyword, + ...this.returnType, + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + stringify(...this.function.tokens()) + stringify(...this.returnsKeyword) + stringify(...this.returnType) + stringify(...this.after); + } + + /** + * @internal + */ + toJSON(): Record { + return { + type: this.type, + name: this.getName(), + string: this.toString(), + tokens: this.tokens(), + parameters: this.parameters.map((x) => x.toJSON()), + returnsKeyword: this.returnsKeyword, + returnType: this.returnType, + before: this.before, + after: this.after, + }; + } + + /** + * @internal + */ + isCustomFunction(): this is CustomFunction { + return CustomFunction.isCustomFunction(this); + } + + static isCustomFunction(x: unknown): x is CustomFunction { + if (!x) { + return false; + } + + if (!(x instanceof CustomFunction)) { + return false; + } + + return x.type === NodeType.CustomFunction; + } +} diff --git a/packages/custom-function-parser/src/nodes/function-parameter.ts b/packages/custom-function-parser/src/nodes/function-parameter.ts new file mode 100644 index 0000000000..b17c9bfa20 --- /dev/null +++ b/packages/custom-function-parser/src/nodes/function-parameter.ts @@ -0,0 +1,94 @@ +import type { CSSToken, TokenColon, TokenIdent} from '@csstools/css-tokenizer'; +import { isTokenComment, stringify } from '@csstools/css-tokenizer'; +import { NodeType } from '../util/node-type'; + +export class FunctionParameter { + type = NodeType.FunctionParameter; + + name: TokenIdent; + argumentType: Array; + colon: TokenColon | null; + defaultValue: Array; + before: Array; + after: Array; + + constructor(name: TokenIdent, argumentType: Array, colon: TokenColon | null, defaultValue: Array, before: Array = [], after: Array = []) { + this.name = name; + this.argumentType = argumentType; + this.colon = colon; + this.defaultValue = defaultValue; + this.before = before; + this.after = after; + } + + getName(): string { + return this.name[4].value; + } + + getNameToken(): TokenIdent | null { + return this.name; + } + + getArgumentType(): string { + return stringify(...this.argumentType.filter((x) => !isTokenComment(x))).trim(); + } + + getDefaultValue(): string { + return stringify(...this.defaultValue).trim(); + } + + tokens(): Array { + return [ + ...this.before, + this.name, + ...this.argumentType, + this.colon, + ...this.defaultValue, + ...this.after, + ]. filter((x) => !!x); + } + + toString(): string { + return stringify(...this.before) + + stringify(this.name) + + stringify(...this.argumentType) + + (this.colon ? stringify(this.colon) : '') + + stringify(...this.defaultValue) + + stringify(...this.after); + } + + /** + * @internal + */ + toJSON(): Record { + return { + type: this.type, + name: this.getName(), + string: this.toString(), + argumentType: this.argumentType, + colon: this.colon, + defaultValue: this.defaultValue, + before: this.before, + after: this.after, + }; + } + + /** + * @internal + */ + isFunctionParameter(): this is FunctionParameter { + return FunctionParameter.isFunctionParameter(this); + } + + static isFunctionParameter(x: unknown): x is FunctionParameter { + if (!x) { + return false; + } + + if (!(x instanceof FunctionParameter)) { + return false; + } + + return x.type === NodeType.FunctionParameter; + } +} diff --git a/packages/custom-function-parser/src/parser/parse-custom-function.ts b/packages/custom-function-parser/src/parser/parse-custom-function.ts new file mode 100644 index 0000000000..5be8af09dd --- /dev/null +++ b/packages/custom-function-parser/src/parser/parse-custom-function.ts @@ -0,0 +1,278 @@ +import { isFunctionNode, isSimpleBlockNode, isTokenNode, isWhitespaceNode, isWhiteSpaceOrCommentNode } from '@csstools/css-parser-algorithms'; +import type { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms'; +import type { CSSToken, TokenColon, TokenIdent } from '@csstools/css-tokenizer'; +import { isTokenColon, isTokenComma, isTokenDelim, isTokenIdent, isTokenOpenCurly } from '@csstools/css-tokenizer'; +import { CustomFunction } from '../nodes/custom-function'; +import { FunctionParameter } from '../nodes/function-parameter'; + +export function parseCustomFunction(componentValues: Array): CustomFunction | false { + let fnNode: FunctionNode | null = null; + let fnIndex = -1; + let returnsKeywordIndex = -1; + let returnTypeIndex = -1; + let afterIndex = componentValues.length; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (isWhiteSpaceOrCommentNode(componentValue)) { + continue; + } + + if (fnIndex === -1 && isFunctionNode(componentValue)) { + if (!componentValue.getName().startsWith('--')) { + return false; + } + + fnIndex = i; + fnNode = componentValue; + continue; + } + + if (fnIndex !== -1 && returnsKeywordIndex === -1 && isTokenNode(componentValue)) { + const token = componentValue.value; + if (!isTokenIdent(token)) { + return false; + } + + if (token[4].value.toLowerCase() !== 'returns') { + return false; + } + + returnsKeywordIndex = i; + continue; + } + + if (returnsKeywordIndex !== -1) { + returnTypeIndex = i; + break; + } + + return false; + } + + if (returnsKeywordIndex !== -1 && returnTypeIndex === -1) { + return false; + } + + if (!fnNode) { + return false; + } + + for (let i = (componentValues.length - 1); i > returnTypeIndex; i--) { + const componentValue = componentValues[i]; + if (isWhiteSpaceOrCommentNode(componentValue)) { + afterIndex = i; + continue; + } + + break; + } + + const parameters = parseFunctionParameters(fnNode.value); + if (!parameters) { + return false; + } + + return new CustomFunction( + fnNode, + parameters, + returnTypeIndex === -1 ? [] : componentValues.slice(fnIndex + 1, returnTypeIndex).flatMap((x) => { + return x.tokens(); + }), + returnTypeIndex === -1 ? [] : componentValues.slice(returnTypeIndex, afterIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(0, fnIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(afterIndex).flatMap((x) => { + return x.tokens(); + }) + ); +} + +function parseFunctionParameters(componentValues: Array): Array | false { + if (!componentValues.some(x => !isWhiteSpaceOrCommentNode(x))) { + return []; + } + + const parameters: Array = []; + + for (let i = 0; i < componentValues.length; i++) { + const result = parseFunctionParameter( + componentValues.slice(i) + ); + + if (!result) { + return false; + } + + i += result.advance; + parameters.push(result.node); + continue; + } + + return parameters; +} + +function parseFunctionParameter(componentValues: Array): { advance: number, node: FunctionParameter } | false { + let syntaxComponentDepth = 0; + + let nameIndex = -1; + let nameToken: TokenIdent | null = null; + + let colonIndex = -1; + let colonToken: TokenColon | null = null; + + let defaultValueIndex = -1; + + let afterIndex = componentValues.length; + + let i = 0; + for (i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (isWhiteSpaceOrCommentNode(componentValue)) { + continue; + } + + if ( + syntaxComponentDepth === 0 && + isTokenNode(componentValue) && + isTokenComma(componentValue.value) + ) { + afterIndex = i; + break; + } + + if (nameIndex === -1) { + if ( + isTokenNode(componentValue) && + isTokenIdent(componentValue.value) && + componentValue.value[4].value.startsWith('--') + ) { + nameIndex = i; + nameToken = componentValue.value; + continue + } + + return false; + } + + if ( + colonIndex === -1 && + isTokenNode(componentValue) && + isTokenDelim(componentValue.value) && + componentValue.value[4].value === '<' + ) { + syntaxComponentDepth++; + continue; + } + + if ( + colonIndex === -1 && + syntaxComponentDepth > 0 && + isTokenNode(componentValue) && + isTokenDelim(componentValue.value) && + componentValue.value[4].value === '>' + ) { + syntaxComponentDepth--; + continue; + } + + if ( + colonIndex === -1 && + syntaxComponentDepth === 0 && + isTokenNode(componentValue) && + isTokenColon(componentValue.value) + ) { + colonIndex = i; + colonToken = componentValue.value; + continue; + } + + if (colonIndex !== -1) { + defaultValueIndex = i; + continue; + } + + if (colonIndex === -1) { + continue; + } + + return false; + } + + if (!nameToken) { + return false; + } + + if (colonIndex !== -1 && defaultValueIndex === -1) { + return false; + } + + const typeEndIndex = colonIndex !== -1 ? (colonIndex - 1) : (i - 1); + + for (let j = (componentValues.length - 1); j > Math.max(typeEndIndex, defaultValueIndex); j--) { + const componentValue = componentValues[j]; + if (isWhitespaceNode(componentValue)) { + afterIndex = j; + continue; + } + + break; + } + + let argumentType: Array = []; + { + const argumentTypeComponentValues = componentValues.slice(nameIndex + 1, typeEndIndex + 1); + const meaningfulComponentValues = argumentTypeComponentValues.filter((x) => !isWhiteSpaceOrCommentNode(x)); + if ( + meaningfulComponentValues.length === 1 && + isFunctionNode(meaningfulComponentValues[0]) && + meaningfulComponentValues[0].getName().toLowerCase() === 'type' + ) { + argumentType = meaningfulComponentValues[0].value.flatMap((x) => { + return x.tokens(); + }) + } else { + argumentType = argumentTypeComponentValues.flatMap((x) => { + return x.tokens(); + }) + } + } + + let defaultValue: Array = []; + if (defaultValueIndex !== -1) { + const defaultValueComponentValues = componentValues.slice(colonIndex + 1, afterIndex); + const meaningfulComponentValues = defaultValueComponentValues.filter((x) => !isWhiteSpaceOrCommentNode(x)); + if ( + meaningfulComponentValues.length === 1 && + isSimpleBlockNode(meaningfulComponentValues[0]) && + isTokenOpenCurly(meaningfulComponentValues[0].startToken) + ) { + defaultValue = meaningfulComponentValues[0].value.flatMap((x) => { + return x.tokens(); + }) + } else { + defaultValue = defaultValueComponentValues.flatMap((x) => { + return x.tokens(); + }) + } + } + + return { + advance: i, + node: new FunctionParameter( + nameToken, + argumentType, + colonToken, + defaultValue, + componentValues.slice(0, nameIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(afterIndex, i + 1).flatMap((x) => { + return x.tokens(); + }) + ) + }; +} diff --git a/packages/custom-function-parser/src/parser/parse.ts b/packages/custom-function-parser/src/parser/parse.ts new file mode 100644 index 0000000000..0bfb93fd5a --- /dev/null +++ b/packages/custom-function-parser/src/parser/parse.ts @@ -0,0 +1,29 @@ +import { parseListOfComponentValues } from '@csstools/css-parser-algorithms'; +import type { CSSToken, ParseError } from '@csstools/css-tokenizer'; +import { tokenize } from '@csstools/css-tokenizer'; +import { parseCustomFunction } from './parse-custom-function'; +import type { CustomFunction } from '../nodes/custom-function'; + +export function parseFromTokens( + tokens: Array, + options?: { + onParseError?: (error: ParseError) => void + }, +): CustomFunction | false { + const componentValues = parseListOfComponentValues(tokens, { + onParseError: options?.onParseError, + }); + + return parseCustomFunction(componentValues); +} + +export function parse( + source: string, + options?: { + onParseError?: (error: ParseError) => void + }, +): CustomFunction | false { + return parseFromTokens(tokenize({ css: source }, { + onParseError: options?.onParseError, + }), options); +} diff --git a/packages/custom-function-parser/src/util/node-type.ts b/packages/custom-function-parser/src/util/node-type.ts new file mode 100644 index 0000000000..c835af765d --- /dev/null +++ b/packages/custom-function-parser/src/util/node-type.ts @@ -0,0 +1,4 @@ +export enum NodeType { + CustomFunction = 'custom-function', + FunctionParameter = 'function-parameter', +} diff --git a/packages/custom-function-parser/src/util/type-predicates.ts b/packages/custom-function-parser/src/util/type-predicates.ts new file mode 100644 index 0000000000..d6bb8663e7 --- /dev/null +++ b/packages/custom-function-parser/src/util/type-predicates.ts @@ -0,0 +1,10 @@ +import { CustomFunction } from '../nodes/custom-function'; +import { FunctionParameter } from '../nodes/function-parameter'; + +export function isCustomFunction(x: unknown): x is CustomFunction { + return CustomFunction.isCustomFunction(x); +} + +export function isFunctionParameter(x: unknown): x is FunctionParameter { + return FunctionParameter.isFunctionParameter(x); +} diff --git a/packages/custom-function-parser/stryker.conf.json b/packages/custom-function-parser/stryker.conf.json new file mode 100644 index 0000000000..0951af7f6c --- /dev/null +++ b/packages/custom-function-parser/stryker.conf.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "mutate": [ + "src/**/*.ts" + ], + "buildCommand": "rollup -c ../../rollup/default.mjs", + "testRunner": "command", + "coverageAnalysis": "perTest", + "tempDirName": "../../.stryker-tmp", + "commandRunner": { + "command": "node ./test/test.mjs" + }, + "thresholds": { + "high": 100, + "low": 100, + "break": 100 + }, + "inPlace": true +} diff --git a/packages/custom-function-parser/test/_import.mjs b/packages/custom-function-parser/test/_import.mjs new file mode 100644 index 0000000000..3215dfbbbe --- /dev/null +++ b/packages/custom-function-parser/test/_import.mjs @@ -0,0 +1,3 @@ +import { parse } from '@csstools/media-query-list-parser'; + +parse('(min-width: 300px)'); diff --git a/packages/custom-function-parser/test/_require.cjs b/packages/custom-function-parser/test/_require.cjs new file mode 100644 index 0000000000..09406cf784 --- /dev/null +++ b/packages/custom-function-parser/test/_require.cjs @@ -0,0 +1,3 @@ +const { parse } = require('@csstools/media-query-list-parser'); + +parse('(min-width: 300px)'); diff --git a/packages/custom-function-parser/test/cases/custom-function/0001.expect.json b/packages/custom-function-parser/test/cases/custom-function/0001.expect.json new file mode 100644 index 0000000000..b395b0a334 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0001.expect.json @@ -0,0 +1,28 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo()", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "parameters": [], + "returnsKeyword": [], + "returnType": [], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0001.mjs b/packages/custom-function-parser/test/cases/custom-function/0001.mjs new file mode 100644 index 0000000000..60136a0323 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo()', + 'custom-function/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0002.expect.json b/packages/custom-function-parser/test/cases/custom-function/0002.expect.json new file mode 100644 index 0000000000..41e50ba567 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0002.expect.json @@ -0,0 +1,65 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": " --foo( ) ", + "tokens": [ + [ + "whitespace-token", + " ", + 0, + 0, + null + ], + [ + "function-token", + "--foo(", + 1, + 6, + { + "value": "--foo" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + ")-token", + ")", + 8, + 8, + null + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ] + ], + "parameters": [], + "returnsKeyword": [], + "returnType": [], + "before": [ + [ + "whitespace-token", + " ", + 0, + 0, + null + ] + ], + "after": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ] + ] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0002.mjs b/packages/custom-function-parser/test/cases/custom-function/0002.mjs new file mode 100644 index 0000000000..e6b1b6ed5d --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + ' --foo( ) ', + 'custom-function/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0003.expect.json b/packages/custom-function-parser/test/cases/custom-function/0003.expect.json new file mode 100644 index 0000000000..dbd1fd8e39 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0003.expect.json @@ -0,0 +1,65 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "/* a comment */--foo( )/* a comment */", + "tokens": [ + [ + "comment", + "/* a comment */", + 0, + 14, + null + ], + [ + "function-token", + "--foo(", + 15, + 20, + { + "value": "--foo" + } + ], + [ + "whitespace-token", + " ", + 21, + 21, + null + ], + [ + ")-token", + ")", + 22, + 22, + null + ], + [ + "comment", + "/* a comment */", + 23, + 37, + null + ] + ], + "parameters": [], + "returnsKeyword": [], + "returnType": [], + "before": [ + [ + "comment", + "/* a comment */", + 0, + 14, + null + ] + ], + "after": [ + [ + "comment", + "/* a comment */", + 23, + 37, + null + ] + ] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0003.mjs b/packages/custom-function-parser/test/cases/custom-function/0003.mjs new file mode 100644 index 0000000000..0ce45d1f96 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '/* a comment */--foo( )/* a comment */', + 'custom-function/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0004.expect.json b/packages/custom-function-parser/test/cases/custom-function/0004.expect.json new file mode 100644 index 0000000000..2d83c0857b --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0004.expect.json @@ -0,0 +1,233 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo(--x ) returns ", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + "ident-token", + "--x", + 6, + 8, + { + "value": "--x" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ], + [ + ")-token", + ")", + 18, + 18, + null + ], + [ + "whitespace-token", + " ", + 19, + 19, + null + ], + [ + "ident-token", + "returns", + 20, + 26, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ], + [ + "delim-token", + "<", + 28, + 28, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 29, + 34, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 35, + 35, + { + "value": ">" + } + ] + ], + "parameters": [ + { + "type": "function-parameter", + "name": "--x", + "string": "--x ", + "argumentType": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ] + ], + "colon": null, + "defaultValue": [], + "before": [], + "after": [] + } + ], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 19, + 19, + null + ], + [ + "ident-token", + "returns", + 20, + 26, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 28, + 28, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 29, + 34, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 35, + 35, + { + "value": ">" + } + ] + ], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0004.mjs b/packages/custom-function-parser/test/cases/custom-function/0004.mjs new file mode 100644 index 0000000000..29cc1ab991 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo(--x ) returns ', + 'custom-function/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0005.expect.json b/packages/custom-function-parser/test/cases/custom-function/0005.expect.json new file mode 100644 index 0000000000..02e4a84d62 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0005.expect.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0005.mjs b/packages/custom-function-parser/test/cases/custom-function/0005.mjs new file mode 100644 index 0000000000..99272aa0a2 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo() returns', + 'custom-function/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0006.expect.json b/packages/custom-function-parser/test/cases/custom-function/0006.expect.json new file mode 100644 index 0000000000..02e4a84d62 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0006.expect.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0006.mjs b/packages/custom-function-parser/test/cases/custom-function/0006.mjs new file mode 100644 index 0000000000..62c3390757 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0006.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo() ', + 'custom-function/0006', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0007.expect.json b/packages/custom-function-parser/test/cases/custom-function/0007.expect.json new file mode 100644 index 0000000000..02e4a84d62 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0007.expect.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0007.mjs b/packages/custom-function-parser/test/cases/custom-function/0007.mjs new file mode 100644 index 0000000000..d4b5c7a3d7 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0007.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'returns --foo()', + 'custom-function/0007', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0008.expect.json b/packages/custom-function-parser/test/cases/custom-function/0008.expect.json new file mode 100644 index 0000000000..fcb2394f19 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0008.expect.json @@ -0,0 +1,229 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo() /* a comment */ returns /* a comment */ /* a comment */ ", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "comment", + "/* a comment */", + 8, + 22, + null + ], + [ + "whitespace-token", + " ", + 23, + 23, + null + ], + [ + "ident-token", + "returns", + 24, + 30, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 31, + 31, + null + ], + [ + "comment", + "/* a comment */", + 32, + 46, + null + ], + [ + "whitespace-token", + " ", + 47, + 47, + null + ], + [ + "delim-token", + "<", + 48, + 48, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 49, + 54, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 55, + 55, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 56, + 56, + null + ], + [ + "comment", + "/* a comment */", + 57, + 71, + null + ], + [ + "whitespace-token", + " ", + 72, + 72, + null + ] + ], + "parameters": [], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "comment", + "/* a comment */", + 8, + 22, + null + ], + [ + "whitespace-token", + " ", + 23, + 23, + null + ], + [ + "ident-token", + "returns", + 24, + 30, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 31, + 31, + null + ], + [ + "comment", + "/* a comment */", + 32, + 46, + null + ], + [ + "whitespace-token", + " ", + 47, + 47, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 48, + 48, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 49, + 54, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 55, + 55, + { + "value": ">" + } + ] + ], + "before": [], + "after": [ + [ + "whitespace-token", + " ", + 56, + 56, + null + ], + [ + "comment", + "/* a comment */", + 57, + 71, + null + ], + [ + "whitespace-token", + " ", + 72, + 72, + null + ] + ] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0008.mjs b/packages/custom-function-parser/test/cases/custom-function/0008.mjs new file mode 100644 index 0000000000..26b9b6e243 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0008.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo() /* a comment */ returns /* a comment */ /* a comment */ ', + 'custom-function/0008', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0009.expect.json b/packages/custom-function-parser/test/cases/custom-function/0009.expect.json new file mode 100644 index 0000000000..02e4a84d62 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0009.expect.json @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0009.mjs b/packages/custom-function-parser/test/cases/custom-function/0009.mjs new file mode 100644 index 0000000000..b313901435 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0009.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo() --bar()', + 'custom-function/0009', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0010.expect.json b/packages/custom-function-parser/test/cases/custom-function/0010.expect.json new file mode 100644 index 0000000000..5084801020 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0010.expect.json @@ -0,0 +1,144 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo() returns <--bar()>", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "returns", + 8, + 14, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 15, + 15, + null + ], + [ + "delim-token", + "<", + 16, + 16, + { + "value": "<" + } + ], + [ + "function-token", + "--bar(", + 17, + 22, + { + "value": "--bar" + } + ], + [ + ")-token", + ")", + 23, + 23, + null + ], + [ + "delim-token", + ">", + 24, + 24, + { + "value": ">" + } + ] + ], + "parameters": [], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "returns", + 8, + 14, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 15, + 15, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 16, + 16, + { + "value": "<" + } + ], + [ + "function-token", + "--bar(", + 17, + 22, + { + "value": "--bar" + } + ], + [ + ")-token", + ")", + 23, + 23, + null + ], + [ + "delim-token", + ">", + 24, + 24, + { + "value": ">" + } + ] + ], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0010.mjs b/packages/custom-function-parser/test/cases/custom-function/0010.mjs new file mode 100644 index 0000000000..2c8714a7a7 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0010.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo() returns <--bar()>', + 'custom-function/0010', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0011.expect.json b/packages/custom-function-parser/test/cases/custom-function/0011.expect.json new file mode 100644 index 0000000000..6790c98f1d --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0011.expect.json @@ -0,0 +1,280 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo(--x : red ) returns ", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + "ident-token", + "--x", + 6, + 8, + { + "value": "--x" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "colon-token", + ":", + 17, + 17, + null + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "ident-token", + "red", + 19, + 21, + { + "value": "red" + } + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + ")-token", + ")", + 23, + 23, + null + ], + [ + "whitespace-token", + " ", + 24, + 24, + null + ], + [ + "ident-token", + "returns", + 25, + 31, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 32, + 32, + null + ], + [ + "delim-token", + "<", + 33, + 33, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 34, + 39, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 40, + 40, + { + "value": ">" + } + ] + ], + "parameters": [ + { + "type": "function-parameter", + "name": "--x", + "string": "--x : red ", + "argumentType": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ] + ], + "colon": [ + "colon-token", + ":", + 17, + 17, + null + ], + "defaultValue": [ + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "ident-token", + "red", + 19, + 21, + { + "value": "red" + } + ] + ], + "before": [], + "after": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + } + ], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ], + [ + "ident-token", + "returns", + 25, + 31, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 32, + 32, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 33, + 33, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 34, + 39, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 40, + 40, + { + "value": ">" + } + ] + ], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0011.mjs b/packages/custom-function-parser/test/cases/custom-function/0011.mjs new file mode 100644 index 0000000000..2c67a7e9da --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0011.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo(--x : red ) returns ', + 'custom-function/0011', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0012.expect.json b/packages/custom-function-parser/test/cases/custom-function/0012.expect.json new file mode 100644 index 0000000000..b961a6b18f --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0012.expect.json @@ -0,0 +1,294 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo(--x : red ) returns ", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + "ident-token", + "--x", + 6, + 8, + { + "value": "--x" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ], + [ + "colon-token", + ":", + 18, + 18, + null + ], + [ + "whitespace-token", + " ", + 19, + 19, + null + ], + [ + "ident-token", + "red", + 20, + 22, + { + "value": "red" + } + ], + [ + "whitespace-token", + " ", + 23, + 23, + null + ], + [ + ")-token", + ")", + 24, + 24, + null + ], + [ + "whitespace-token", + " ", + 25, + 25, + null + ], + [ + "ident-token", + "returns", + 26, + 32, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 33, + 33, + null + ], + [ + "delim-token", + "<", + 34, + 34, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 35, + 40, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 41, + 41, + { + "value": ">" + } + ] + ], + "parameters": [ + { + "type": "function-parameter", + "name": "--x", + "string": "--x : red ", + "argumentType": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ] + ], + "colon": [ + "colon-token", + ":", + 18, + 18, + null + ], + "defaultValue": [ + [ + "whitespace-token", + " ", + 19, + 19, + null + ], + [ + "ident-token", + "red", + 20, + 22, + { + "value": "red" + } + ] + ], + "before": [], + "after": [ + [ + "whitespace-token", + " ", + 23, + 23, + null + ] + ] + } + ], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 25, + 25, + null + ], + [ + "ident-token", + "returns", + 26, + 32, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 33, + 33, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 34, + 34, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 35, + 40, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 41, + 41, + { + "value": ">" + } + ] + ], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0012.mjs b/packages/custom-function-parser/test/cases/custom-function/0012.mjs new file mode 100644 index 0000000000..d4ec11efcc --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0012.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo(--x : red ) returns ', + 'custom-function/0012', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0013.expect.json b/packages/custom-function-parser/test/cases/custom-function/0013.expect.json new file mode 100644 index 0000000000..d2362c3a5c --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0013.expect.json @@ -0,0 +1,251 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo(--x :red) returns ", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + "ident-token", + "--x", + 6, + 8, + { + "value": "--x" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "colon-token", + ":", + 17, + 17, + null + ], + [ + "ident-token", + "red", + 18, + 20, + { + "value": "red" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + "ident-token", + "returns", + 23, + 29, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 30, + 30, + null + ], + [ + "delim-token", + "<", + 31, + 31, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 32, + 37, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 38, + 38, + { + "value": ">" + } + ] + ], + "parameters": [ + { + "type": "function-parameter", + "name": "--x", + "string": "--x :red", + "argumentType": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ] + ], + "colon": [ + "colon-token", + ":", + 17, + 17, + null + ], + "defaultValue": [ + [ + "ident-token", + "red", + 18, + 20, + { + "value": "red" + } + ] + ], + "before": [], + "after": [] + } + ], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + "ident-token", + "returns", + 23, + 29, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 30, + 30, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 31, + 31, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 32, + 37, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 38, + 38, + { + "value": ">" + } + ] + ], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0013.mjs b/packages/custom-function-parser/test/cases/custom-function/0013.mjs new file mode 100644 index 0000000000..bc97c32ff9 --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0013.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo(--x :red) returns ', + 'custom-function/0013', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/cases/custom-function/0014.expect.json b/packages/custom-function-parser/test/cases/custom-function/0014.expect.json new file mode 100644 index 0000000000..e53edf59cb --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0014.expect.json @@ -0,0 +1,432 @@ +{ + "type": "custom-function", + "name": "--foo", + "string": "--foo(--x : red, --y, --z ) returns ", + "tokens": [ + [ + "function-token", + "--foo(", + 0, + 5, + { + "value": "--foo" + } + ], + [ + "ident-token", + "--x", + 6, + 8, + { + "value": "--x" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ], + [ + "colon-token", + ":", + 17, + 17, + null + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "ident-token", + "red", + 19, + 21, + { + "value": "red" + } + ], + [ + "comma-token", + ",", + 22, + 22, + null + ], + [ + "whitespace-token", + " ", + 23, + 23, + null + ], + [ + "ident-token", + "--y", + 24, + 26, + { + "value": "--y" + } + ], + [ + "comma-token", + ",", + 27, + 27, + null + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "ident-token", + "--z", + 29, + 31, + { + "value": "--z" + } + ], + [ + "whitespace-token", + " ", + 32, + 32, + null + ], + [ + "delim-token", + "<", + 33, + 33, + { + "value": "<" + } + ], + [ + "ident-token", + "length", + 34, + 39, + { + "value": "length" + } + ], + [ + "delim-token", + ">", + 40, + 40, + { + "value": ">" + } + ], + [ + ")-token", + ")", + 41, + 41, + null + ], + [ + "whitespace-token", + " ", + 42, + 42, + null + ], + [ + "ident-token", + "returns", + 43, + 49, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 50, + 50, + null + ], + [ + "delim-token", + "<", + 51, + 51, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 52, + 57, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 58, + 58, + { + "value": ">" + } + ] + ], + "parameters": [ + { + "type": "function-parameter", + "name": "--x", + "string": "--x : red,", + "argumentType": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "<", + 10, + 10, + { + "value": "<" + } + ], + [ + "ident-token", + "color", + 11, + 15, + { + "value": "color" + } + ], + [ + "delim-token", + ">", + 16, + 16, + { + "value": ">" + } + ] + ], + "colon": [ + "colon-token", + ":", + 17, + 17, + null + ], + "defaultValue": [ + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "ident-token", + "red", + 19, + 21, + { + "value": "red" + } + ] + ], + "before": [], + "after": [ + [ + "comma-token", + ",", + 22, + 22, + null + ] + ] + }, + { + "type": "function-parameter", + "name": "--y", + "string": " --y,", + "argumentType": [], + "colon": null, + "defaultValue": [], + "before": [ + [ + "whitespace-token", + " ", + 23, + 23, + null + ] + ], + "after": [ + [ + "comma-token", + ",", + 27, + 27, + null + ] + ] + }, + { + "type": "function-parameter", + "name": "--z", + "string": " --z ", + "argumentType": [ + [ + "whitespace-token", + " ", + 32, + 32, + null + ], + [ + "delim-token", + "<", + 33, + 33, + { + "value": "<" + } + ], + [ + "ident-token", + "length", + 34, + 39, + { + "value": "length" + } + ], + [ + "delim-token", + ">", + 40, + 40, + { + "value": ">" + } + ] + ], + "colon": null, + "defaultValue": [], + "before": [ + [ + "whitespace-token", + " ", + 28, + 28, + null + ] + ], + "after": [] + } + ], + "returnsKeyword": [ + [ + "whitespace-token", + " ", + 42, + 42, + null + ], + [ + "ident-token", + "returns", + 43, + 49, + { + "value": "returns" + } + ], + [ + "whitespace-token", + " ", + 50, + 50, + null + ] + ], + "returnType": [ + [ + "delim-token", + "<", + 51, + 51, + { + "value": "<" + } + ], + [ + "ident-token", + "string", + 52, + 57, + { + "value": "string" + } + ], + [ + "delim-token", + ">", + 58, + 58, + { + "value": ">" + } + ] + ], + "before": [], + "after": [] +} \ No newline at end of file diff --git a/packages/custom-function-parser/test/cases/custom-function/0014.mjs b/packages/custom-function-parser/test/cases/custom-function/0014.mjs new file mode 100644 index 0000000000..c2e1df664d --- /dev/null +++ b/packages/custom-function-parser/test/cases/custom-function/0014.mjs @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '--foo(--x : red, --y, --z ) returns ', + 'custom-function/0014', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/custom-function-parser/test/get-argument-type/0001.mjs b/packages/custom-function-parser/test/get-argument-type/0001.mjs new file mode 100644 index 0000000000..1d4e676901 --- /dev/null +++ b/packages/custom-function-parser/test/get-argument-type/0001.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert'; +import { parse } from '@csstools/custom-function-parser'; + + +{ + const fn = parse('--foo(--bar, --baz : "foo")'); + + assert.strictEqual( + fn.parameters[0].getArgumentType(), + '', + ); + + assert.strictEqual( + fn.parameters[1].getArgumentType(), + '', + ); +} + +{ + const fn = parse('--foo(--bar /* a comment */ /* a comment */ : /* a comment */ "foo")'); + + assert.strictEqual( + fn.parameters[0].getArgumentType(), + '', + ); +} + +{ + const fn = parse('--foo(--bar [ /* a comment */ /* a comment */ | /* a comment */ /* a comment */ ] : /* a comment */ "foo")'); + + assert.strictEqual( + fn.parameters[0].getArgumentType(), + '[ | ]', + ); +} diff --git a/packages/custom-function-parser/test/get-default-value/0001.mjs b/packages/custom-function-parser/test/get-default-value/0001.mjs new file mode 100644 index 0000000000..0cbc7c615f --- /dev/null +++ b/packages/custom-function-parser/test/get-default-value/0001.mjs @@ -0,0 +1,78 @@ +import assert from 'node:assert'; +import { parse } from '@csstools/custom-function-parser'; + + +{ + const fn = parse('--foo(--bar, --baz : "foo")'); + + assert.strictEqual( + fn.parameters[0].getDefaultValue(), + '', + ); + + assert.strictEqual( + fn.parameters[1].getDefaultValue(), + '"foo"', + ); +} + +{ + const fn = parse('--foo(--bar : /* a comment */ "foo" /* a comment */)'); + + assert.strictEqual( + fn.parameters[0].getDefaultValue(), + '/* a comment */ "foo" /* a comment */', + ); +} + +{ + const fn = parse('--foo(--bar : /* a comment */ "foo" /* a comment */)'); + + assert.strictEqual( + fn.parameters[0].getDefaultValue(), + '/* a comment */ "foo" /* a comment */', + ); +} + +{ + const fn = parse('--foo(--bar: { 1, 2 }, --baz: 1)'); + + assert.strictEqual( + fn.parameters[0].getDefaultValue(), + '1, 2', + ); + + assert.strictEqual( + fn.parameters[1].getDefaultValue(), + '1', + ); +} + +{ + const fn = parse('--foo(--bar: { 1, 2 } "something", --baz: 1)'); + + assert.strictEqual( + fn.parameters[0].getDefaultValue(), + '{ 1, 2 } "something"', + ); + + assert.strictEqual( + fn.parameters[1].getDefaultValue(), + '1', + ); +} + + +{ + const fn = parse('--foo(--bar: ( 1, 2 ), --baz: 1)'); + + assert.strictEqual( + fn.parameters[0].getDefaultValue(), + '( 1, 2 )', + ); + + assert.strictEqual( + fn.parameters[1].getDefaultValue(), + '1', + ); +} diff --git a/packages/custom-function-parser/test/get-name/0001.mjs b/packages/custom-function-parser/test/get-name/0001.mjs new file mode 100644 index 0000000000..25257035df --- /dev/null +++ b/packages/custom-function-parser/test/get-name/0001.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert'; +import { parse } from '@csstools/custom-function-parser'; + +{ + const fn = parse('--foo()'); + + assert.strictEqual( + fn.getName(), + '--foo', + ); +} + +{ + const fn = parse('--foo(--bar)'); + + assert.strictEqual( + fn.parameters[0].getName(), + '--bar', + ); +} + +{ + const fn = parse('--foo(--bar, --baz : "foo")'); + + assert.strictEqual( + fn.parameters[0].getName(), + '--bar', + ); + + assert.strictEqual( + fn.parameters[1].getName(), + '--baz', + ); +} diff --git a/packages/custom-function-parser/test/test.mjs b/packages/custom-function-parser/test/test.mjs new file mode 100644 index 0000000000..5035026cfd --- /dev/null +++ b/packages/custom-function-parser/test/test.mjs @@ -0,0 +1,20 @@ +import './cases/custom-function/0001.mjs'; +import './cases/custom-function/0002.mjs'; +import './cases/custom-function/0003.mjs'; +import './cases/custom-function/0004.mjs'; +import './cases/custom-function/0005.mjs'; +import './cases/custom-function/0006.mjs'; +import './cases/custom-function/0007.mjs'; +import './cases/custom-function/0008.mjs'; +import './cases/custom-function/0009.mjs'; +import './cases/custom-function/0010.mjs'; +import './cases/custom-function/0011.mjs'; +import './cases/custom-function/0012.mjs'; +import './cases/custom-function/0013.mjs'; +import './cases/custom-function/0014.mjs'; + +import './get-name/0001.mjs'; + +import './get-argument-type/0001.mjs'; + +import './get-default-value/0001.mjs'; diff --git a/packages/custom-function-parser/test/util/run-test.mjs b/packages/custom-function-parser/test/util/run-test.mjs new file mode 100644 index 0000000000..3df55ebfb6 --- /dev/null +++ b/packages/custom-function-parser/test/util/run-test.mjs @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { parse } from '@csstools/custom-function-parser'; + +export function runTest(source, testPath, assertEqual) { + const resultAST = parse(source); + + const resultAST_JSON = JSON.stringify(resultAST, null, '\t'); + + if (process.env['REWRITE_EXPECTS'] === 'true') { + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.expect.json`), resultAST_JSON); + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.result.json`), resultAST_JSON); + } else { + const expectData = JSON.parse(fs.readFileSync(path.join(process.cwd(), `./test/cases/${testPath}.expect.json`), 'utf-8').toString()); + + if (expectData === false) { + assertEqual( + resultAST, + expectData, + ); + } else { + assertEqual( + resultAST.toString(), + expectData.string, + ); + + assertEqual( + resultAST.toString(), + source, + ); + } + + assertEqual( + JSON.parse(resultAST_JSON), + expectData, + ); + } +} diff --git a/packages/custom-function-parser/tsconfig.json b/packages/custom-function-parser/tsconfig.json new file mode 100644 index 0000000000..c4bc02ba44 --- /dev/null +++ b/packages/custom-function-parser/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "strict": true, + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} From 73d25a199063833c16e6a191d470029583684e3b Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Mon, 2 Jun 2025 13:06:28 +0200 Subject: [PATCH 03/21] wip --- package-lock.json | 61 ++++ .../custom-function-parser/dist/index.cjs | 2 +- .../custom-function-parser/dist/index.d.ts | 5 +- .../custom-function-parser/dist/index.mjs | 2 +- .../docs/custom-function-parser.api.json | 107 ++----- .../src/nodes/custom-function.ts | 21 +- .../src/nodes/function-parameter.ts | 21 -- .../cases/custom-function/0004.expect.json | 1 - .../cases/custom-function/0011.expect.json | 1 - .../cases/custom-function/0012.expect.json | 1 - .../cases/custom-function/0013.expect.json | 1 - .../cases/custom-function/0014.expect.json | 3 - .../test/get-argument-type/0001.mjs | 32 ++ .../test/get-return-type/0001.mjs | 66 ++++ packages/custom-function-parser/test/test.mjs | 2 + packages/media-query-list-parser/CHANGELOG.md | 4 + .../src/util/type-predicates.ts | 2 +- .../postcss-custom-functions/dist/index.cjs | 1 + .../postcss-custom-functions/dist/index.d.ts | 12 + .../postcss-custom-functions/dist/index.mjs | 1 + plugins/postcss-custom-functions/package.json | 7 + .../src/cascade-layers.ts | 129 ++++++++ .../src/custom-functions-from-root.ts | 77 +++++ plugins/postcss-custom-functions/src/index.ts | 296 ++++++++++++++++-- .../src/is-processable-rule.ts | 30 ++ .../test/basic.color.expect.css | 110 ++++++- .../postcss-custom-functions/test/basic.css | 10 + .../test/basic.expect.css | 101 +++++- .../test/examples/example.expect.css | 2 +- .../examples/example.preserve-true.expect.css | 1 - 30 files changed, 954 insertions(+), 155 deletions(-) create mode 100644 packages/custom-function-parser/test/get-return-type/0001.mjs create mode 100644 plugins/postcss-custom-functions/dist/index.cjs create mode 100644 plugins/postcss-custom-functions/dist/index.d.ts create mode 100644 plugins/postcss-custom-functions/dist/index.mjs create mode 100644 plugins/postcss-custom-functions/src/cascade-layers.ts create mode 100644 plugins/postcss-custom-functions/src/custom-functions-from-root.ts create mode 100644 plugins/postcss-custom-functions/src/is-processable-rule.ts diff --git a/package-lock.json b/package-lock.json index 8cfc6b7270..5a2d38af89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1898,6 +1898,10 @@ "resolved": "cli/csstools-cli", "link": true }, + "node_modules/@csstools/custom-function-parser": { + "resolved": "packages/custom-function-parser", + "link": true + }, "node_modules/@csstools/generate-test-cases": { "resolved": "packages/generate-test-cases", "link": true @@ -1942,6 +1946,10 @@ "resolved": "plugins/postcss-contrast-color-function", "link": true }, + "node_modules/@csstools/postcss-custom-functions": { + "resolved": "plugins/postcss-custom-functions", + "link": true + }, "node_modules/@csstools/postcss-debug-logger": { "resolved": "plugins/postcss-debug-logger", "link": true @@ -9256,6 +9264,28 @@ "node": ">=18" } }, + "packages/custom-function-parser": { + "name": "@csstools/custom-function-parser", + "version": "0.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, "packages/generate-test-cases": { "name": "@csstools/generate-test-cases", "version": "1.0.0", @@ -10064,6 +10094,37 @@ "postcss": "^8.4" } }, + "plugins/postcss-custom-functions": { + "name": "@csstools/postcss-custom-functions", + "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/cascade-layer-name-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/custom-function-parser": "^0.0.0", + "@csstools/utilities": "^2.0.0" + }, + "devDependencies": { + "@csstools/postcss-tape": "*" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "plugins/postcss-custom-media": { "version": "11.0.5", "funding": [ diff --git a/packages/custom-function-parser/dist/index.cjs b/packages/custom-function-parser/dist/index.cjs index bac482a7ed..2c750baead 100644 --- a/packages/custom-function-parser/dist/index.cjs +++ b/packages/custom-function-parser/dist/index.cjs @@ -1 +1 @@ -"use strict";var e,t=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");exports.NodeType=void 0,(e=exports.NodeType||(exports.NodeType={})).CustomFunction="custom-function",e.FunctionParameter="function-parameter";class CustomFunction{type=exports.NodeType.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],i=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=i}getName(){return this.function.getName()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return r.stringify(...this.before)+r.stringify(...this.function.tokens())+r.stringify(...this.returnsKeyword)+r.stringify(...this.returnType)+r.stringify(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((e=>e.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&(e instanceof CustomFunction&&e.type===exports.NodeType.CustomFunction)}}class FunctionParameter{type=exports.NodeType.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],i=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=i}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return r.stringify(...this.argumentType.filter((e=>!r.isTokenComment(e)))).trim()}getDefaultValue(){return r.stringify(...this.defaultValue).trim()}tokens(){return[...this.before,this.name,...this.argumentType,this.colon,...this.defaultValue,...this.after].filter((e=>!!e))}toString(){return r.stringify(...this.before)+r.stringify(this.name)+r.stringify(...this.argumentType)+(this.colon?r.stringify(this.colon):"")+r.stringify(...this.defaultValue)+r.stringify(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&(e instanceof FunctionParameter&&e.type===exports.NodeType.FunctionParameter)}}function parseCustomFunction(e){let n=null,s=-1,i=-1,o=-1,a=e.length;for(let a=0;ao;r--){const n=e[r];if(!t.isWhiteSpaceOrCommentNode(n))break;a=r}const u=parseFunctionParameters(n.value);return!!u&&new CustomFunction(n,u,-1===o?[]:e.slice(s+1,o).flatMap((e=>e.tokens())),-1===o?[]:e.slice(o,a).flatMap((e=>e.tokens())),e.slice(0,s).flatMap((e=>e.tokens())),e.slice(a).flatMap((e=>e.tokens())))}function parseFunctionParameters(e){if(!e.some((e=>!t.isWhiteSpaceOrCommentNode(e))))return[];const r=[];for(let t=0;t0&&t.isTokenNode(l)&&r.isTokenDelim(l.value)&&">"===l.value[4].value)n--;else if(-1===o&&0===n&&t.isTokenNode(l)&&r.isTokenColon(l.value))o=f,a=l.value;else if(-1===o){if(-1!==o)return!1}else u=f}}if(!i)return!1;if(-1!==o&&-1===u)return!1;const l=-1!==o?o-1:f-1;for(let r=e.length-1;r>Math.max(l,u);r--){const n=e[r];if(!t.isWhitespaceNode(n))break;c=r}let m=[];if(-1!==u){const n=e.slice(o+1,c),s=n.filter((e=>!t.isWhiteSpaceOrCommentNode(e)));m=1===s.length&&t.isSimpleBlockNode(s[0])&&r.isTokenOpenCurly(s[0].startToken)?s[0].value.flatMap((e=>e.tokens())):n.flatMap((e=>e.tokens()))}return{advance:f,node:new FunctionParameter(i,e.slice(s+1,l+1).flatMap((e=>e.tokens())),a,m,e.slice(0,s).flatMap((e=>e.tokens())),e.slice(c,f+1).flatMap((e=>e.tokens())))}}function parseFromTokens(e,r){return parseCustomFunction(t.parseListOfComponentValues(e,{onParseError:r?.onParseError}))}exports.CustomFunction=CustomFunction,exports.FunctionParameter=FunctionParameter,exports.isCustomFunction=function isCustomFunction(e){return CustomFunction.isCustomFunction(e)},exports.isFunctionParameter=function isFunctionParameter(e){return FunctionParameter.isFunctionParameter(e)},exports.parse=function parse(e,t){return parseFromTokens(r.tokenize({css:e},{onParseError:t?.onParseError}),t)},exports.parseFromTokens=parseFromTokens; +"use strict";var e,t=require("@csstools/css-parser-algorithms"),n=require("@csstools/css-tokenizer");exports.NodeType=void 0,(e=exports.NodeType||(exports.NodeType={})).CustomFunction="custom-function",e.FunctionParameter="function-parameter";class CustomFunction{type=exports.NodeType.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,n,r,o=[],s=[]){this.function=e,this.parameters=t,this.returnsKeyword=n,this.returnType=r,this.before=o,this.after=s}getName(){return this.function.getName()}getReturnType(){const e=t.parseListOfComponentValues(this.returnType).filter((e=>!t.isWhiteSpaceOrCommentNode(e)));return 1===e.length&&t.isFunctionNode(e[0])&&"type"===e[0].getName().toLowerCase()?n.stringify(...e[0].value.flatMap((e=>e.tokens())).filter((e=>!n.isTokenComment(e)))).trim():n.stringify(...this.returnType.filter((e=>!n.isTokenComment(e)))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return n.stringify(...this.before)+n.stringify(...this.function.tokens())+n.stringify(...this.returnsKeyword)+n.stringify(...this.returnType)+n.stringify(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((e=>e.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&(e instanceof CustomFunction&&e.type===exports.NodeType.CustomFunction)}}class FunctionParameter{type=exports.NodeType.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,n,r,o=[],s=[]){this.name=e,this.argumentType=t,this.colon=n,this.defaultValue=r,this.before=o,this.after=s}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return n.stringify(...this.argumentType.filter((e=>!n.isTokenComment(e)))).trim()}getDefaultValue(){return n.stringify(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&(e instanceof FunctionParameter&&e.type===exports.NodeType.FunctionParameter)}}function parseCustomFunction(e){let r=null,o=-1,s=-1,i=-1,a=e.length;for(let a=0;ai;n--){const r=e[n];if(!t.isWhiteSpaceOrCommentNode(r))break;a=n}const u=parseFunctionParameters(r.value);return!!u&&new CustomFunction(r,u,-1===i?[]:e.slice(o+1,i).flatMap((e=>e.tokens())),-1===i?[]:e.slice(i,a).flatMap((e=>e.tokens())),e.slice(0,o).flatMap((e=>e.tokens())),e.slice(a).flatMap((e=>e.tokens())))}function parseFunctionParameters(e){if(!e.some((e=>!t.isWhiteSpaceOrCommentNode(e))))return[];const n=[];for(let t=0;t0&&t.isTokenNode(m)&&n.isTokenDelim(m.value)&&">"===m.value[4].value)r--;else if(-1===i&&0===r&&t.isTokenNode(m)&&n.isTokenColon(m.value))i=l,a=m.value;else if(-1===i){if(-1!==i)return!1}else u=l}}if(!s)return!1;if(-1!==i&&-1===u)return!1;const m=-1!==i?i-1:l-1;for(let n=e.length-1;n>Math.max(m,u);n--){const r=e[n];if(!t.isWhitespaceNode(r))break;c=n}let f=[];{const n=e.slice(o+1,m+1),r=n.filter((e=>!t.isWhiteSpaceOrCommentNode(e)));f=1===r.length&&t.isFunctionNode(r[0])&&"type"===r[0].getName().toLowerCase()?r[0].value.flatMap((e=>e.tokens())):n.flatMap((e=>e.tokens()))}let p=[];if(-1!==u){const r=e.slice(i+1,c),o=r.filter((e=>!t.isWhiteSpaceOrCommentNode(e)));p=1===o.length&&t.isSimpleBlockNode(o[0])&&n.isTokenOpenCurly(o[0].startToken)?o[0].value.flatMap((e=>e.tokens())):r.flatMap((e=>e.tokens()))}return{advance:l,node:new FunctionParameter(s,f,a,p,e.slice(0,o).flatMap((e=>e.tokens())),e.slice(c,l+1).flatMap((e=>e.tokens())))}}function parseFromTokens(e,n){return parseCustomFunction(t.parseListOfComponentValues(e,{onParseError:n?.onParseError}))}exports.CustomFunction=CustomFunction,exports.FunctionParameter=FunctionParameter,exports.isCustomFunction=function isCustomFunction(e){return CustomFunction.isCustomFunction(e)},exports.isFunctionParameter=function isFunctionParameter(e){return FunctionParameter.isFunctionParameter(e)},exports.parse=function parse(e,t){return parseFromTokens(n.tokenize({css:e},{onParseError:t?.onParseError}),t)},exports.parseFromTokens=parseFromTokens; diff --git a/packages/custom-function-parser/dist/index.d.ts b/packages/custom-function-parser/dist/index.d.ts index 7c3113393e..bc0198b30b 100644 --- a/packages/custom-function-parser/dist/index.d.ts +++ b/packages/custom-function-parser/dist/index.d.ts @@ -1,5 +1,5 @@ import type { CSSToken } from '@csstools/css-tokenizer'; -import type { FunctionNode } from '@csstools/css-parser-algorithms'; +import { FunctionNode } from '@csstools/css-parser-algorithms'; import type { ParseError } from '@csstools/css-tokenizer'; import type { TokenColon } from '@csstools/css-tokenizer'; import type { TokenIdent } from '@csstools/css-tokenizer'; @@ -14,6 +14,7 @@ export declare class CustomFunction { after: Array; constructor(fn: FunctionNode, parameters: Array, returnsKeyword: Array, returnType: Array, before?: Array, after?: Array); getName(): string; + getReturnType(): string; tokens(): Array; toString(): string; /** @@ -40,8 +41,6 @@ export declare class FunctionParameter { getNameToken(): TokenIdent | null; getArgumentType(): string; getDefaultValue(): string; - tokens(): Array; - toString(): string; /** * @internal */ diff --git a/packages/custom-function-parser/dist/index.mjs b/packages/custom-function-parser/dist/index.mjs index cc3d9a9602..e5aacd84b8 100644 --- a/packages/custom-function-parser/dist/index.mjs +++ b/packages/custom-function-parser/dist/index.mjs @@ -1 +1 @@ -import{isWhiteSpaceOrCommentNode as t,isFunctionNode as e,isTokenNode as r,isWhitespaceNode as n,isSimpleBlockNode as s,parseListOfComponentValues as i}from"@csstools/css-parser-algorithms";import{stringify as o,isTokenComment as a,isTokenIdent as u,isTokenComma as c,isTokenDelim as f,isTokenColon as l,isTokenOpenCurly as m,tokenize as h}from"@csstools/css-tokenizer";var p;!function(t){t.CustomFunction="custom-function",t.FunctionParameter="function-parameter"}(p||(p={}));class CustomFunction{type=p.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(t,e,r,n,s=[],i=[]){this.function=t,this.parameters=e,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=i}getName(){return this.function.getName()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return o(...this.before)+o(...this.function.tokens())+o(...this.returnsKeyword)+o(...this.returnType)+o(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((t=>t.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(t){return!!t&&(t instanceof CustomFunction&&t.type===p.CustomFunction)}}class FunctionParameter{type=p.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(t,e,r,n,s=[],i=[]){this.name=t,this.argumentType=e,this.colon=r,this.defaultValue=n,this.before=s,this.after=i}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return o(...this.argumentType.filter((t=>!a(t)))).trim()}getDefaultValue(){return o(...this.defaultValue).trim()}tokens(){return[...this.before,this.name,...this.argumentType,this.colon,...this.defaultValue,...this.after].filter((t=>!!t))}toString(){return o(...this.before)+o(this.name)+o(...this.argumentType)+(this.colon?o(this.colon):"")+o(...this.defaultValue)+o(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(t){return!!t&&(t instanceof FunctionParameter&&t.type===p.FunctionParameter)}}function parseCustomFunction(n){let s=null,i=-1,o=-1,a=-1,c=n.length;for(let c=0;ca;e--){const r=n[e];if(!t(r))break;c=e}const f=parseFunctionParameters(s.value);return!!f&&new CustomFunction(s,f,-1===a?[]:n.slice(i+1,a).flatMap((t=>t.tokens())),-1===a?[]:n.slice(a,c).flatMap((t=>t.tokens())),n.slice(0,i).flatMap((t=>t.tokens())),n.slice(c).flatMap((t=>t.tokens())))}function parseFunctionParameters(e){if(!e.some((e=>!t(e))))return[];const r=[];for(let t=0;t0&&r(n)&&f(n.value)&&">"===n.value[4].value)i--;else if(-1===h&&0===i&&r(n)&&l(n.value))h=y,p=n.value;else if(-1===h){if(-1!==h)return!1}else F=y}}if(!a)return!1;if(-1!==h&&-1===F)return!1;const k=-1!==h?h-1:y-1;for(let t=e.length-1;t>Math.max(k,F);t--){const r=e[t];if(!n(r))break;g=t}let P=[];if(-1!==F){const r=e.slice(h+1,g),n=r.filter((e=>!t(e)));P=1===n.length&&s(n[0])&&m(n[0].startToken)?n[0].value.flatMap((t=>t.tokens())):r.flatMap((t=>t.tokens()))}return{advance:y,node:new FunctionParameter(a,e.slice(o+1,k+1).flatMap((t=>t.tokens())),p,P,e.slice(0,o).flatMap((t=>t.tokens())),e.slice(g,y+1).flatMap((t=>t.tokens())))}}function parseFromTokens(t,e){return parseCustomFunction(i(t,{onParseError:e?.onParseError}))}function parse(t,e){return parseFromTokens(h({css:t},{onParseError:e?.onParseError}),e)}function isCustomFunction(t){return CustomFunction.isCustomFunction(t)}function isFunctionParameter(t){return FunctionParameter.isFunctionParameter(t)}export{CustomFunction,FunctionParameter,p as NodeType,isCustomFunction,isFunctionParameter,parse,parseFromTokens}; +import{parseListOfComponentValues as t,isWhiteSpaceOrCommentNode as e,isFunctionNode as r,isTokenNode as n,isWhitespaceNode as s,isSimpleBlockNode as o}from"@csstools/css-parser-algorithms";import{stringify as a,isTokenComment as i,isTokenIdent as u,isTokenComma as l,isTokenDelim as c,isTokenColon as f,isTokenOpenCurly as m,tokenize as h}from"@csstools/css-tokenizer";var p;!function(t){t.CustomFunction="custom-function",t.FunctionParameter="function-parameter"}(p||(p={}));class CustomFunction{type=p.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(t,e,r,n,s=[],o=[]){this.function=t,this.parameters=e,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const n=t(this.returnType).filter((t=>!e(t)));return 1===n.length&&r(n[0])&&"type"===n[0].getName().toLowerCase()?a(...n[0].value.flatMap((t=>t.tokens())).filter((t=>!i(t)))).trim():a(...this.returnType.filter((t=>!i(t)))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return a(...this.before)+a(...this.function.tokens())+a(...this.returnsKeyword)+a(...this.returnType)+a(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((t=>t.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(t){return!!t&&(t instanceof CustomFunction&&t.type===p.CustomFunction)}}class FunctionParameter{type=p.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(t,e,r,n,s=[],o=[]){this.name=t,this.argumentType=e,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return a(...this.argumentType.filter((t=>!i(t)))).trim()}getDefaultValue(){return a(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(t){return!!t&&(t instanceof FunctionParameter&&t.type===p.FunctionParameter)}}function parseCustomFunction(t){let s=null,o=-1,a=-1,i=-1,l=t.length;for(let l=0;li;r--){const n=t[r];if(!e(n))break;l=r}const c=parseFunctionParameters(s.value);return!!c&&new CustomFunction(s,c,-1===i?[]:t.slice(o+1,i).flatMap((t=>t.tokens())),-1===i?[]:t.slice(i,l).flatMap((t=>t.tokens())),t.slice(0,o).flatMap((t=>t.tokens())),t.slice(l).flatMap((t=>t.tokens())))}function parseFunctionParameters(t){if(!t.some((t=>!e(t))))return[];const r=[];for(let e=0;e0&&n(r)&&c(r.value)&&">"===r.value[4].value)a--;else if(-1===p&&0===a&&n(r)&&f(r.value))p=k,F=r.value;else if(-1===p){if(-1!==p)return!1}else g=k}}if(!h)return!1;if(-1!==p&&-1===g)return!1;const v=-1!==p?p-1:k-1;for(let e=t.length-1;e>Math.max(v,g);e--){const r=t[e];if(!s(r))break;y=e}let P=[];{const n=t.slice(i+1,v+1),s=n.filter((t=>!e(t)));P=1===s.length&&r(s[0])&&"type"===s[0].getName().toLowerCase()?s[0].value.flatMap((t=>t.tokens())):n.flatMap((t=>t.tokens()))}let C=[];if(-1!==g){const r=t.slice(p+1,y),n=r.filter((t=>!e(t)));C=1===n.length&&o(n[0])&&m(n[0].startToken)?n[0].value.flatMap((t=>t.tokens())):r.flatMap((t=>t.tokens()))}return{advance:k,node:new FunctionParameter(h,P,F,C,t.slice(0,i).flatMap((t=>t.tokens())),t.slice(y,k+1).flatMap((t=>t.tokens())))}}function parseFromTokens(e,r){return parseCustomFunction(t(e,{onParseError:r?.onParseError}))}function parse(t,e){return parseFromTokens(h({css:t},{onParseError:e?.onParseError}),e)}function isCustomFunction(t){return CustomFunction.isCustomFunction(t)}function isFunctionParameter(t){return FunctionParameter.isFunctionParameter(t)}export{CustomFunction,FunctionParameter,p as NodeType,isCustomFunction,isFunctionParameter,parse,parseFromTokens}; diff --git a/packages/custom-function-parser/docs/custom-function-parser.api.json b/packages/custom-function-parser/docs/custom-function-parser.api.json index d3f9f0c943..078502edd3 100644 --- a/packages/custom-function-parser/docs/custom-function-parser.api.json +++ b/packages/custom-function-parser/docs/custom-function-parser.api.json @@ -520,6 +520,37 @@ "isAbstract": false, "name": "getName" }, + { + "kind": "Method", + "canonicalReference": "@csstools/custom-function-parser!CustomFunction#getReturnType:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getReturnType(): " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getReturnType" + }, { "kind": "Method", "canonicalReference": "@csstools/custom-function-parser!CustomFunction.isCustomFunction:member(1)", @@ -1439,82 +1470,6 @@ "isProtected": false, "isAbstract": false }, - { - "kind": "Method", - "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#tokens:member(1)", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "tokens(): " - }, - { - "kind": "Reference", - "text": "Array", - "canonicalReference": "!Array:interface" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "CSSToken", - "canonicalReference": "@csstools/css-tokenizer!CSSToken:type" - }, - { - "kind": "Content", - "text": ">" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 5 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "tokens" - }, - { - "kind": "Method", - "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#toString:member(1)", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "toString(): " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "toString" - }, { "kind": "Property", "canonicalReference": "@csstools/custom-function-parser!FunctionParameter#type:member", diff --git a/packages/custom-function-parser/src/nodes/custom-function.ts b/packages/custom-function-parser/src/nodes/custom-function.ts index 7477981bae..cf086c1ab0 100644 --- a/packages/custom-function-parser/src/nodes/custom-function.ts +++ b/packages/custom-function-parser/src/nodes/custom-function.ts @@ -1,8 +1,8 @@ import type { CSSToken } from '@csstools/css-tokenizer'; -import { stringify } from '@csstools/css-tokenizer'; +import { isTokenComment, stringify } from '@csstools/css-tokenizer'; import { NodeType } from '../util/node-type'; import type { FunctionParameter } from './function-parameter'; -import type { FunctionNode } from '@csstools/css-parser-algorithms'; +import { isFunctionNode, isWhiteSpaceOrCommentNode, parseListOfComponentValues, type FunctionNode } from '@csstools/css-parser-algorithms'; export class CustomFunction { type = NodeType.CustomFunction; @@ -27,6 +27,23 @@ export class CustomFunction { return this.function.getName(); } + getReturnType(): string { + const meaningfulComponentValues = parseListOfComponentValues(this.returnType).filter((x) => !isWhiteSpaceOrCommentNode(x)); + if ( + meaningfulComponentValues.length === 1 && + isFunctionNode(meaningfulComponentValues[0]) && + meaningfulComponentValues[0].getName().toLowerCase() === 'type' + ) { + return stringify( + ...meaningfulComponentValues[0].value + .flatMap((x) => x.tokens()) + .filter((x) => !isTokenComment(x)) + ).trim(); + } + + return stringify(...this.returnType.filter((x) => !isTokenComment(x))).trim(); + } + tokens(): Array { return [ ...this.before, diff --git a/packages/custom-function-parser/src/nodes/function-parameter.ts b/packages/custom-function-parser/src/nodes/function-parameter.ts index b17c9bfa20..8d97781797 100644 --- a/packages/custom-function-parser/src/nodes/function-parameter.ts +++ b/packages/custom-function-parser/src/nodes/function-parameter.ts @@ -37,26 +37,6 @@ export class FunctionParameter { return stringify(...this.defaultValue).trim(); } - tokens(): Array { - return [ - ...this.before, - this.name, - ...this.argumentType, - this.colon, - ...this.defaultValue, - ...this.after, - ]. filter((x) => !!x); - } - - toString(): string { - return stringify(...this.before) + - stringify(this.name) + - stringify(...this.argumentType) + - (this.colon ? stringify(this.colon) : '') + - stringify(...this.defaultValue) + - stringify(...this.after); - } - /** * @internal */ @@ -64,7 +44,6 @@ export class FunctionParameter { return { type: this.type, name: this.getName(), - string: this.toString(), argumentType: this.argumentType, colon: this.colon, defaultValue: this.defaultValue, diff --git a/packages/custom-function-parser/test/cases/custom-function/0004.expect.json b/packages/custom-function-parser/test/cases/custom-function/0004.expect.json index 2d83c0857b..f23fcadcc3 100644 --- a/packages/custom-function-parser/test/cases/custom-function/0004.expect.json +++ b/packages/custom-function-parser/test/cases/custom-function/0004.expect.json @@ -124,7 +124,6 @@ { "type": "function-parameter", "name": "--x", - "string": "--x ", "argumentType": [ [ "whitespace-token", diff --git a/packages/custom-function-parser/test/cases/custom-function/0011.expect.json b/packages/custom-function-parser/test/cases/custom-function/0011.expect.json index 6790c98f1d..371322e160 100644 --- a/packages/custom-function-parser/test/cases/custom-function/0011.expect.json +++ b/packages/custom-function-parser/test/cases/custom-function/0011.expect.json @@ -147,7 +147,6 @@ { "type": "function-parameter", "name": "--x", - "string": "--x : red ", "argumentType": [ [ "whitespace-token", diff --git a/packages/custom-function-parser/test/cases/custom-function/0012.expect.json b/packages/custom-function-parser/test/cases/custom-function/0012.expect.json index b961a6b18f..429e7f146f 100644 --- a/packages/custom-function-parser/test/cases/custom-function/0012.expect.json +++ b/packages/custom-function-parser/test/cases/custom-function/0012.expect.json @@ -154,7 +154,6 @@ { "type": "function-parameter", "name": "--x", - "string": "--x : red ", "argumentType": [ [ "whitespace-token", diff --git a/packages/custom-function-parser/test/cases/custom-function/0013.expect.json b/packages/custom-function-parser/test/cases/custom-function/0013.expect.json index d2362c3a5c..c33cd72351 100644 --- a/packages/custom-function-parser/test/cases/custom-function/0013.expect.json +++ b/packages/custom-function-parser/test/cases/custom-function/0013.expect.json @@ -133,7 +133,6 @@ { "type": "function-parameter", "name": "--x", - "string": "--x :red", "argumentType": [ [ "whitespace-token", diff --git a/packages/custom-function-parser/test/cases/custom-function/0014.expect.json b/packages/custom-function-parser/test/cases/custom-function/0014.expect.json index e53edf59cb..f130f34fc1 100644 --- a/packages/custom-function-parser/test/cases/custom-function/0014.expect.json +++ b/packages/custom-function-parser/test/cases/custom-function/0014.expect.json @@ -220,7 +220,6 @@ { "type": "function-parameter", "name": "--x", - "string": "--x : red,", "argumentType": [ [ "whitespace-token", @@ -296,7 +295,6 @@ { "type": "function-parameter", "name": "--y", - "string": " --y,", "argumentType": [], "colon": null, "defaultValue": [], @@ -322,7 +320,6 @@ { "type": "function-parameter", "name": "--z", - "string": " --z ", "argumentType": [ [ "whitespace-token", diff --git a/packages/custom-function-parser/test/get-argument-type/0001.mjs b/packages/custom-function-parser/test/get-argument-type/0001.mjs index 1d4e676901..1fc6625a46 100644 --- a/packages/custom-function-parser/test/get-argument-type/0001.mjs +++ b/packages/custom-function-parser/test/get-argument-type/0001.mjs @@ -16,6 +16,20 @@ import { parse } from '@csstools/custom-function-parser'; ); } +{ + const fn = parse('--foo(--bar, --baz type(): "foo")'); + + assert.strictEqual( + fn.parameters[0].getArgumentType(), + '', + ); + + assert.strictEqual( + fn.parameters[1].getArgumentType(), + '', + ); +} + { const fn = parse('--foo(--bar /* a comment */ /* a comment */ : /* a comment */ "foo")'); @@ -25,6 +39,15 @@ import { parse } from '@csstools/custom-function-parser'; ); } +{ + const fn = parse('--foo(--bar type(/* a comment */ /* a comment */) : /* a comment */ "foo")'); + + assert.strictEqual( + fn.parameters[0].getArgumentType(), + '', + ); +} + { const fn = parse('--foo(--bar [ /* a comment */ /* a comment */ | /* a comment */ /* a comment */ ] : /* a comment */ "foo")'); @@ -33,3 +56,12 @@ import { parse } from '@csstools/custom-function-parser'; '[ | ]', ); } + +{ + const fn = parse('--foo(--bar type([ /* a comment */ /* a comment */ | /* a comment */ /* a comment */ ]) : /* a comment */ "foo")'); + + assert.strictEqual( + fn.parameters[0].getArgumentType(), + '[ | ]', + ); +} diff --git a/packages/custom-function-parser/test/get-return-type/0001.mjs b/packages/custom-function-parser/test/get-return-type/0001.mjs new file mode 100644 index 0000000000..17b92a6223 --- /dev/null +++ b/packages/custom-function-parser/test/get-return-type/0001.mjs @@ -0,0 +1,66 @@ +import assert from 'node:assert'; +import { parse } from '@csstools/custom-function-parser'; + + +{ + const fn = parse('--foo()'); + + assert.strictEqual( + fn.getReturnType(), + '', + ); +} + +{ + const fn = parse('--foo() returns '); + + assert.strictEqual( + fn.getReturnType(), + '', + ); +} + +{ + const fn = parse('--foo() returns type()'); + + assert.strictEqual( + fn.getReturnType(), + '', + ); +} + +{ + const fn = parse('--foo(--bar: /* a comment */ "foo") returns /* a comment */ /* a comment */'); + + assert.strictEqual( + fn.getReturnType(), + '', + ); +} + +{ + const fn = parse('--foo(--bar: /* a comment */ "foo") returns type(/* a comment */ /* a comment */)'); + + assert.strictEqual( + fn.getReturnType(), + '', + ); +} + +{ + const fn = parse('--foo(--bar: /* a comment */ "foo") returns [ /* a comment */ /* a comment */ | /* a comment */ /* a comment */ ]'); + + assert.strictEqual( + fn.getReturnType(), + '[ | ]', + ); +} + +{ + const fn = parse('--foo(--bar: /* a comment */ "foo") returns type([ /* a comment */ /* a comment */ | /* a comment */ /* a comment */ ])'); + + assert.strictEqual( + fn.getReturnType(), + '[ | ]', + ); +} diff --git a/packages/custom-function-parser/test/test.mjs b/packages/custom-function-parser/test/test.mjs index 5035026cfd..e01af6572f 100644 --- a/packages/custom-function-parser/test/test.mjs +++ b/packages/custom-function-parser/test/test.mjs @@ -17,4 +17,6 @@ import './get-name/0001.mjs'; import './get-argument-type/0001.mjs'; +import './get-return-type/0001.mjs'; + import './get-default-value/0001.mjs'; diff --git a/packages/media-query-list-parser/CHANGELOG.md b/packages/media-query-list-parser/CHANGELOG.md index 7fc66e56ba..2c313d07c9 100644 --- a/packages/media-query-list-parser/CHANGELOG.md +++ b/packages/media-query-list-parser/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes to Media Query List Parser +### Unreleased (patch) + +- Fix type predicate for `isCustomMedia` + ### 4.0.2 _November 1, 2024_ diff --git a/packages/media-query-list-parser/src/util/type-predicates.ts b/packages/media-query-list-parser/src/util/type-predicates.ts index 924715ff77..ef005591b3 100644 --- a/packages/media-query-list-parser/src/util/type-predicates.ts +++ b/packages/media-query-list-parser/src/util/type-predicates.ts @@ -17,7 +17,7 @@ import { MediaOr } from '../nodes/media-or'; import type { MediaQuery} from '../nodes/media-query'; import { MediaQueryInvalid, MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query'; -export function isCustomMedia(x: unknown): x is GeneralEnclosed { +export function isCustomMedia(x: unknown): x is CustomMedia { return CustomMedia.isCustomMedia(x); } diff --git a/plugins/postcss-custom-functions/dist/index.cjs b/plugins/postcss-custom-functions/dist/index.cjs new file mode 100644 index 0000000000..700e25e6c6 --- /dev/null +++ b/plugins/postcss-custom-functions/dist/index.cjs @@ -0,0 +1 @@ +"use strict";var e=require("@csstools/cascade-layer-name-parser"),t=require("@csstools/custom-function-parser"),n=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");const s=e.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(t){const n=new Map,r=new Map,o=[];t.walkAtRules((t=>{if("layer"!==t.name.toLowerCase())return;{let e=t.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===t.root())break;return}e=e.parent}}let a;if(t.nodes)a=normalizeLayerName(t.params,1);else{if(!t.params.trim())return;a=t.params}let i=e.parse(a);if(i?.length){{let e=t.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=r.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(e.addLayerToModel(o,i),t.nodes){const e=i[0].concat(s);n.set(t,e),r.set(t,i[0])}}}));for(const t of n.values())e.addLayerToModel(o,[t]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const o=new Set(["scope","container","layer"]),a=/^function$/i;function isProcessableRule(e){if(!a.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!o.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,n,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=t.parse(i,{onParseError:t=>{e.warn(n,`Failed to parse custom function : "${e.params}" with message: "${t instanceof Error?t.message:t}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const d=o.get(c)??-1;if(l&&l>=d&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const i=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const s=[];return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){s.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(o,{atRule:a}){if(!i.test(o.value))return;const u=o.parent;if(!u)return;const c=r.tokenize({css:o.value});if(!c.some((e=>r.isTokenFunction(e)&&e[4].value.startsWith("--"))))return;const l=n.replaceComponentValues([n.parseListOfComponentValues(c)],(t=>{if(!n.isFunctionNode(t))return;const i=t.getName();if(!i.startsWith("--"))return;const c=e.get(i);if(!c)return;const l=new CallingContext(u,u,o,t,c.function,c.node,a);return l.invalid?new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[]):(u.before(l.argumentRule),u.before(l.bodyRule),s.push(l),new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[new n.TokenNode([r.TokenType.Ident,l.resultIdent,-1,-1,{value:l.resultIdent}])]))})),p=n.stringify(l);p!==o.value&&(o.cloneBefore({value:p}),t?.preserve||o.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,r,s,o,a,i,u=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=r,this.function=s,this.customFunction=o,this.customFunctionBody=a,this.parent=u;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=n.parseCommaSeparatedListOfComponentValues(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=i({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=i({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const r=this.customFunction.getReturnType();if(r){const e=i({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,r)=>{const s=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:e.getDefaultValue()}));const o=t[r];o&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:n.stringify([o])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}module.exports=creator; diff --git a/plugins/postcss-custom-functions/dist/index.d.ts b/plugins/postcss-custom-functions/dist/index.d.ts new file mode 100644 index 0000000000..d7d3f8d43f --- /dev/null +++ b/plugins/postcss-custom-functions/dist/index.d.ts @@ -0,0 +1,12 @@ +import type { PluginCreator } from 'postcss'; + +declare const creator: PluginCreator; +export default creator; + +/** postcss-custom-functions plugin options */ +export declare type pluginOptions = { + /** Preserve the original notation. default: false */ + preserve?: boolean; +}; + +export { } diff --git a/plugins/postcss-custom-functions/dist/index.mjs b/plugins/postcss-custom-functions/dist/index.mjs new file mode 100644 index 0000000000..7b52273020 --- /dev/null +++ b/plugins/postcss-custom-functions/dist/index.mjs @@ -0,0 +1 @@ +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parse as n}from"@csstools/custom-function-parser";import{replaceComponentValues as r,parseListOfComponentValues as s,isFunctionNode as o,FunctionNode as a,TokenNode as i,stringify as u,parseCommaSeparatedListOfComponentValues as c}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenFunction as p,TokenType as m}from"@csstools/css-tokenizer";const h=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,o=[];n.walkAtRules((n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}e=e.parent}}let a;if(n.nodes)a=normalizeLayerName(n.params,1);else{if(!n.params.trim())return;a=n.params}let i=e(a);if(i?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(t(o,i),n.nodes){const e=i[0].concat(h);r.set(n,e),s.set(n,i[0])}}}));for(const e of r.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of r)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const f=new Set(["scope","container","layer"]),d=/^function$/i;function isProcessableRule(e){if(!d.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!f.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,t,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=n(i,{onParseError:n=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${n instanceof Error?n.message:n}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const h=o.get(c)??-1;if(l&&l>=h&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const g=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const n=[];return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){n.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(c,{atRule:h}){if(!g.test(c.value))return;const f=c.parent;if(!f)return;const d=l({css:c.value});if(!d.some((e=>p(e)&&e[4].value.startsWith("--"))))return;const y=r([s(d)],(t=>{if(!o(t))return;const r=t.getName();if(!r.startsWith("--"))return;const s=e.get(r);if(!s)return;const u=new CallingContext(f,f,c,t,s.function,s.node,h);return u.invalid?new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[]):(f.before(u.argumentRule),f.before(u.bodyRule),n.push(u),new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[new i([m.Ident,u.resultIdent,-1,-1,{value:u.resultIdent}])]))})),v=u(y);v!==c.value&&(c.cloneBefore({value:v}),t?.preserve||c.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,n,r,s,o,a,i=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=n,this.function=r,this.customFunction=s,this.customFunctionBody=o,this.parent=i;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=c(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=a({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=a({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const n=this.customFunction.getReturnType();if(n){const e=a({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,n)=>{const r=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:e.getDefaultValue()}));const s=t[n];s&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:u([s])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}export{creator as default}; diff --git a/plugins/postcss-custom-functions/package.json b/plugins/postcss-custom-functions/package.json index fb28822f9e..d5e66fc1ca 100644 --- a/plugins/postcss-custom-functions/package.json +++ b/plugins/postcss-custom-functions/package.json @@ -42,6 +42,13 @@ "README.md", "dist" ], + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/custom-function-parser": "^0.0.0", + "@csstools/utilities": "^2.0.0" + }, "peerDependencies": { "postcss": "^8.4" }, diff --git a/plugins/postcss-custom-functions/src/cascade-layers.ts b/plugins/postcss-custom-functions/src/cascade-layers.ts new file mode 100644 index 0000000000..5d042e7850 --- /dev/null +++ b/plugins/postcss-custom-functions/src/cascade-layers.ts @@ -0,0 +1,129 @@ +import type { AtRule, Container, Document, Node, Root } from 'postcss'; +import type { LayerName} from '@csstools/cascade-layer-name-parser'; +import { parse as parseCascadeLayerNames, addLayerToModel } from '@csstools/cascade-layer-name-parser'; + +const implicitLayerNameForCloning = parseCascadeLayerNames('csstools-implicit-layer')[0]; + +export function collectCascadeLayerOrder(root: Root): WeakMap { + const references: Map = new Map(); + const referencesForLayerNames: Map = new Map(); + + const layers: Array = []; + const anonLayerCounter = 1; + + root.walkAtRules((node) => { + if (node.name.toLowerCase() !== 'layer') { + return; + } + + { + // We do not want to process anything except for `@layer` rules + // and maybe `@layer` rules inside other `@later` rules. + // + // Traverse up the tree and abort when we find something unexpected + let parent: Container | Document | undefined = node.parent; + while (parent) { + if (parent.type === 'atrule' && (parent as AtRule).name.toLowerCase() === 'layer') { + parent = parent.parent; + continue; + } + + if (parent === node.root()) { + break; + } + + return; + } + } + + let layerParams; + if (node.nodes) { // @layer { .foo {} } + layerParams = normalizeLayerName(node.params, anonLayerCounter); + } else if (node.params.trim()) { // @layer a, b; + layerParams = node.params; + } else { // @layer; + return; + } + + let currentLayerNames = parseCascadeLayerNames(layerParams); + if (!currentLayerNames?.length) { + return; + } + + { + // Stitch the layer names of the current node together with those of ancestors. + // @layer foo { @layer bar { .any {} } } + // -> "foo.bar" + let parent: Container | Document | undefined = node.parent; + while (parent && parent.type === 'atrule' && (parent as AtRule).name.toLowerCase() === 'layer') { + const parentLayerName = referencesForLayerNames.get(parent); + if (!parentLayerName) { + parent = parent.parent; + continue; + } + + currentLayerNames = currentLayerNames.map((layerName) => { + return parentLayerName.concat(layerName); + }); + + parent = parent.parent; + } + } + + // Add the new layer names to "layers". + addLayerToModel(layers, currentLayerNames); + + if (node.nodes) { + // Implicit layers have higher priority than nested layers. + // This requires some trickery. + // + // 1. connect the node to the real layer + // 2. connect the node to an implicit layer + // 3. use the real layer to resolve other real layer names + // 4. use the implicit layer later + + const implicitLayerName = currentLayerNames[0].concat(implicitLayerNameForCloning); + references.set(node, implicitLayerName); + referencesForLayerNames.set(node, currentLayerNames[0]); + } + }); + + for (const layerName of references.values()) { + // Add the implicit layer names to "layers". + // By doing this after all the real layers we are sure that the implicit layers have the right order in "layers". + addLayerToModel(layers, [layerName]); + } + + const out: WeakMap = new WeakMap(); + for (const [node, layerName] of references) { + out.set(node, layers.findIndex((x) => { + return layerName.equal(x); + })); + } + + return out; +} + +// 0 : node was not found +// any number : node was found, higher numbers have higher priority +// a very large number : node wasn't layered, highest priority +export function cascadeLayerNumberForNode(node: Node, layers: WeakMap): number { + if (node.parent && node.parent.type === 'atrule' && (node.parent as AtRule).name.toLowerCase() === 'layer') { + if (!layers.has(node.parent)) { + return 0; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return layers.get(node.parent)! + 1; + } + + return 10_000_000; +} + +function normalizeLayerName(layerName: string, counter: number): string { + if (layerName.trim()) { + return layerName; + } + + return `csstools-anon-layer--${counter++}`; +} diff --git a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts new file mode 100644 index 0000000000..3210463bb8 --- /dev/null +++ b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts @@ -0,0 +1,77 @@ +import type { ChildNode, Container, Document, Result, Root as PostCSSRoot, AtRule } from 'postcss'; +import { cascadeLayerNumberForNode, collectCascadeLayerOrder } from './cascade-layers'; +import { isProcessableRule } from './is-processable-rule'; +import type { CustomFunction} from '@csstools/custom-function-parser'; +import { parse } from '@csstools/custom-function-parser'; + +export type CustomFunctionAndNode = { + function: CustomFunction; + node: AtRule; +} + +// return custom functions from the css root, conditionally removing them +export function getCustomFunctions(root: PostCSSRoot, result: Result, opts: { preserve?: boolean }): Map { + // initialize custom selectors + const customFunctions = new Map(); + const customFunctionsCascadeLayerMapping: Map = new Map(); + + const cascadeLayersOrder = collectCascadeLayerOrder(root); + + root.walkAtRules((atRule) => { + if (!isProcessableRule(atRule)) { + return; + } + + const source = atRule.params.trim(); + + const customFunction = parse(source, { + onParseError: (err) => { + atRule.warn(result, `Failed to parse custom function : "${atRule.params}" with message: "${(err instanceof Error) ? err.message : err}"`); + } + }); + if (!customFunction) { + return; + } + + const name = customFunction.getName(); + + const thisCascadeLayer = cascadeLayerNumberForNode(atRule, cascadeLayersOrder); + const existingCascadeLayer = customFunctionsCascadeLayerMapping.get(name) ?? -1; + + if (thisCascadeLayer && thisCascadeLayer >= existingCascadeLayer) { + customFunctionsCascadeLayerMapping.set(name, thisCascadeLayer); + // re-parsing is important to obtain the correct AST shape + customFunctions.set(name, { + node: atRule, + function: customFunction, + }); + } + + if (!opts.preserve) { + const parent = atRule.parent; + atRule.remove(); + + removeEmptyAncestorBlocks(parent); + } + }); + + return customFunctions; +} + +function removeEmptyAncestorBlocks(block: Container | undefined): void { + if (!block) { + return; + } + + let currentNode: Document | Container | undefined = block; + + while (currentNode) { + if (currentNode.nodes && currentNode.nodes.length > 0) { + return; + } + + const parent: Document | Container | undefined = currentNode.parent; + currentNode.remove(); + currentNode = parent; + } +} diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts index 7839229e26..5190c85c7c 100644 --- a/plugins/postcss-custom-functions/src/index.ts +++ b/plugins/postcss-custom-functions/src/index.ts @@ -1,4 +1,10 @@ -import type { PluginCreator } from 'postcss'; +import type { AtRule, AtRuleProps, Declaration, Plugin, PluginCreator } from 'postcss'; +import type { CustomFunctionAndNode } from './custom-functions-from-root'; +import { getCustomFunctions } from './custom-functions-from-root'; +import { FunctionNode, isFunctionNode, parseCommaSeparatedListOfComponentValues, parseListOfComponentValues, replaceComponentValues, stringify, TokenNode } from '@csstools/css-parser-algorithms'; +import { isTokenFunction, tokenize, TokenType } from '@csstools/css-tokenizer'; +import type { ContainerWithChildren } from 'postcss/lib/container'; +import type { CustomFunction } from '@csstools/custom-function-parser'; /** postcss-custom-functions plugin options */ export type pluginOptions = { @@ -6,11 +12,12 @@ export type pluginOptions = { preserve?: boolean, }; +const HAS_CUSTOM_FUNCTION = /--.*?\(/i; + const creator: PluginCreator = (opts?: pluginOptions) => { const options: pluginOptions = Object.assign( // Default options { - color: null, preserve: false, }, // Provided options @@ -19,37 +26,276 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return { postcssPlugin: 'postcss-custom-functions', - Declaration(decl): void { - if (decl.value === 'red') { - // Determine the new value. - let newValue = 'blue'; - if (options.color) { - newValue = options.color; - } + prepare(): Plugin { + // const transformedNodes = new WeakSet(); + let customFunctions: Map = new Map(); + const callingContexts: Array = []; - // Check if it is different from the current value. - if (newValue === decl.value) { - return; - } + return { + postcssPlugin: 'postcss-custom-functions', + Once(root, { result }): void { + customFunctions = getCustomFunctions(root, result, { preserve: options.preserve }); + }, + OnceExit(root): void { + callingContexts.forEach((callingContext) => { + callingContext.registrations.forEach((registration) => { + root.append(registration); + }); + }); + }, + Declaration(decl, { atRule }): void { + if (!HAS_CUSTOM_FUNCTION.test(decl.value)) { + return; + } - // Insert the new value before the current value. - decl.cloneBefore({ - prop: 'color', - value: newValue, - }); + const parent = decl.parent; + if (!parent) { + return; + } - // If the current value is preserved we are done and return here. - if (options.preserve) { - return; - } + const tokens = tokenize({ css: decl.value }); + if (!tokens.some((x) => isTokenFunction(x) && x[4].value.startsWith('--'))) { + return; + } - // If the current value is not preserved we remove it. - decl.remove(); - } + const transformed = replaceComponentValues( + [parseListOfComponentValues(tokens)], + (componentValue) => { + if (!isFunctionNode(componentValue)) { + return; + } + + const name = componentValue.getName(); + if (!name.startsWith('--')) { + return; + } + + const fn = customFunctions.get(name); + if (!fn) { + return; + } + + const callingContext = new CallingContext( + parent, + parent, + decl, + componentValue, + fn.function, + fn.node, + atRule, + ); + + if (callingContext.invalid) { + // guaranteed invalid + return new FunctionNode( + [TokenType.Function, 'var(', -1, -1, { value: 'var' }], + [TokenType.CloseParen, ')', -1, -1, undefined], + [], + ); + } + + parent.before(callingContext.argumentRule); + parent.before(callingContext.bodyRule); + callingContexts.push(callingContext); + + return new FunctionNode( + [TokenType.Function, 'var(', -1, -1, { value: 'var' }], + [TokenType.CloseParen, ')', -1, -1, undefined], + [ + new TokenNode( + [TokenType.Ident, callingContext.resultIdent, -1, -1, { value: callingContext.resultIdent }], + ) + ], + ); + } + ); + + const modified = stringify(transformed); + + if (modified === decl.value) { + return; + } + + decl.cloneBefore({ value: modified }); + + if (!options?.preserve) { + decl.remove(); + } + }, + }; }, }; }; +function generateDashedIdent(type: string, name: string, id: string): string { + const parts = [ + type, + id, + name, + ].filter((x) => !!x); + + return `--csstools-${parts.join('-')}`; +} + creator.postcss = true; export default creator; + +class CallingContext { + static counter = 0; + + id: string; + + rootElement: ContainerWithChildren; + element: ContainerWithChildren; + property: Declaration; + parent: CallingContext | null = null; + + function: FunctionNode; + customFunction: CustomFunction; + customFunctionBody: AtRule; + + registrations: Array = []; + + argumentRule: ContainerWithChildren; + bodyRule: ContainerWithChildren; + + resultIdent: string; + + varCounter: number = 0; + + invalid: boolean = false; + + // TODO: some ident mapping + + constructor( + rootElement: ContainerWithChildren, + element: ContainerWithChildren, + property: Declaration, + + fn: FunctionNode, + customFunction: CustomFunction, + customFunctionBody: AtRule, + + atRule: (defaults?: AtRuleProps) => AtRule, + + parent: CallingContext | null = null, + ) { + this.id = (CallingContext.counter++).toString(36); + + this.rootElement = rootElement; + this.element = element; + this.property = property; + + this.function = fn; + this.customFunction = customFunction; + this.customFunctionBody = customFunctionBody; + + this.parent = parent; + + { + const varCounterValue = (++this.varCounter).toString(36); + this.resultIdent = generateDashedIdent('custom-function-call', varCounterValue, this.id); + + const args = parseCommaSeparatedListOfComponentValues(this.function.value.flatMap(x => x.tokens())); + if (args.length > this.customFunction.parameters.length) { + this.invalid = true; + } + + this.customFunction.parameters.forEach((parameter) => { + const parameterName = parameter.getName(); + const parameterType = parameter.getArgumentType() || '*'; + + const argumentCustomProperty = atRule({ + name: 'property', + params: generateDashedIdent('function-argument', parameterName, this.id), + nodes: [ + this.property.clone({ + prop: 'syntax', + value: `"${parameterType}"`, + }), + this.property.clone({ + prop: 'inherit', + value: "false", + }) + ], + source: this.property.source + }); + + this.registrations.push(argumentCustomProperty); + + const bodyCustomProperty = atRule({ + name: 'property', + params: generateDashedIdent('function-body', parameterName, this.id), + nodes: [ + this.property.clone({ + prop: 'syntax', + value: '"*"', + }), + this.property.clone({ + prop: 'inherit', + value: "false", + }) + ], + source: this.property.source + }); + + this.registrations.push(bodyCustomProperty); + }); + + const returnType = this.customFunction.getReturnType(); + if (returnType) { + const customProperty = atRule({ + name: 'property', + params: generateDashedIdent('return-value', '', this.id), + nodes: [ + this.property.clone({ + prop: 'syntax', + value: `"${returnType}"`, + }), + this.property.clone({ + prop: 'inherit', + value: "false", + }) + ], + source: this.property.source + }); + + this.registrations.push(customProperty); + } + + this.argumentRule = this.element.clone({ nodes: [] }); + + this.customFunction.parameters.forEach((parameter, index) => { + const parameterName = parameter.getName(); + const defaultValue = parameter.getDefaultValue(); + + if (defaultValue) { + this.argumentRule.append(this.property.clone({ + prop: generateDashedIdent('function-argument', parameterName, this.id), + value: parameter.getDefaultValue(), + })); + } + + const argumentValue = args[index]; + // TODO: `{1, 2}` -> `1, 2` + if (argumentValue) { + this.argumentRule.append(this.property.clone({ + prop: generateDashedIdent('function-argument', parameterName, this.id), + value: stringify([argumentValue]), + })); + } + }); + + this.bodyRule = this.element.clone({ + nodes: [ + ...(this.customFunctionBody.nodes?.map((x) => x.clone()) ?? []) + ] + }); + + this.bodyRule.walkDecls(/^result$/i, (resultDecl) => { + resultDecl.prop = this.resultIdent; + }); + } + } +} diff --git a/plugins/postcss-custom-functions/src/is-processable-rule.ts b/plugins/postcss-custom-functions/src/is-processable-rule.ts new file mode 100644 index 0000000000..881407f26a --- /dev/null +++ b/plugins/postcss-custom-functions/src/is-processable-rule.ts @@ -0,0 +1,30 @@ +import type { AtRule, ChildNode, Container, Document } from 'postcss'; + +const allowedParentAtRules = new Set(['scope', 'container', 'layer']); + +export const IS_FUNCTION_REGEX = /^function$/i; + +export function isProcessableRule(atRule: AtRule): boolean { + if (!IS_FUNCTION_REGEX.test(atRule.name)) { + return false; + } + + if (!atRule.params || !atRule.params.includes('--')) { + return false; + } + + if (!atRule.nodes?.length) { + return false; + } + + let parent: Container | Document | undefined = atRule.parent; + while (parent) { + if (parent.type === 'atrule' && !allowedParentAtRules.has((parent as AtRule).name.toLowerCase())) { + return false; + } + + parent = parent.parent; + } + + return true; +} diff --git a/plugins/postcss-custom-functions/test/basic.color.expect.css b/plugins/postcss-custom-functions/test/basic.color.expect.css index 041b1dabf3..0dd8840ac4 100644 --- a/plugins/postcss-custom-functions/test/basic.color.expect.css +++ b/plugins/postcss-custom-functions/test/basic.color.expect.css @@ -1,7 +1,109 @@ -.foo { - color: purple; +.negative-a { + --csstools-function-argument-8---value: var(--gap); } -.baz { - color: green; +.negative-a { + --csstools-custom-function-call-8-1: calc(-1 * var(--value)); +} + +.negative-a { + --gap: 1em; + padding: var(--csstools-custom-function-call-8-1); +} + +.negative-b { + --csstools-function-argument-9---value: 1em; +} + +.negative-b { + --csstools-custom-function-call-9-1: calc(-1 * var(--value)); +} + +.negative-b { + padding: var(--csstools-custom-function-call-9-1); +} + +.negative-c { +} + +.negative-c { + --csstools-custom-function-call-a-1: calc(-1 * var(--value)); +} + +.negative-c { + --value: 1em; + padding: var(--csstools-custom-function-call-a-1); +} + +.color-a { + --csstools-function-argument-b---color: black; + --csstools-function-argument-b---color: var(--a-color); + --csstools-function-argument-b---alpha: 1; + --csstools-function-argument-b---alpha: 0.5; +} + +.color-a { + --a: clamp(0, var(--alpha), 1); + + --csstools-custom-function-call-b-1: oklch(from var(--color) l c h / var(--a)); +} + +.color-a { + --a-color: red; + color: var(--csstools-custom-function-call-b-1); +} + +@property --csstools-function-argument-8---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-body-8---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-9---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-body-9---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-a---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-body-a---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-b---color { + syntax: ""; + inherit: false; +} + +@property --csstools-function-body-b---color { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-b---alpha { + syntax: ""; + inherit: false; +} + +@property --csstools-function-body-b---alpha { + syntax: "*"; + inherit: false; +} + +@property --csstools-return-value-b { + syntax: ""; + inherit: false; } diff --git a/plugins/postcss-custom-functions/test/basic.css b/plugins/postcss-custom-functions/test/basic.css index 9cbf5a4861..5b4c7fc42d 100644 --- a/plugins/postcss-custom-functions/test/basic.css +++ b/plugins/postcss-custom-functions/test/basic.css @@ -16,3 +16,13 @@ padding: --negative(); } +@function --alpha(--color : black, --alpha : 1) returns { + --a: clamp(0, var(--alpha), 1); + + result: oklch(from var(--color) l c h / var(--a)); +} + +.color-a { + --a-color: red; + color: --alpha(var(--a-color), 0.5); +} diff --git a/plugins/postcss-custom-functions/test/basic.expect.css b/plugins/postcss-custom-functions/test/basic.expect.css index fa050f4f21..e84c0b9265 100644 --- a/plugins/postcss-custom-functions/test/basic.expect.css +++ b/plugins/postcss-custom-functions/test/basic.expect.css @@ -1,32 +1,109 @@ -@function --negative(--value) { - result: calc(-1 * var(--value)); +.negative-a { + --csstools-function-argument-0---value: var(--gap); } .negative-a { - --csstools--negative--0--value: var(--gap, var(--value)); - --csstools--negative--0--result: calc(-1 * var(--csstools--negative--0--value)); + --csstools-custom-function-call-0-1: calc(-1 * var(--value)); } .negative-a { --gap: 1em; - padding: var(--csstools--negative--0--result); + padding: var(--csstools-custom-function-call-0-1); } .negative-b { - --csstools--negative--1--value: 1em; - --csstools--negative--1--result: calc(-1 * var(--csstools--negative--1--value)); + --csstools-function-argument-1---value: 1em; } .negative-b { - padding: var(--csstools--negative--1--result); + --csstools-custom-function-call-1-1: calc(-1 * var(--value)); +} + +.negative-b { + padding: var(--csstools-custom-function-call-1-1); } .negative-c { - --value: 1em; - --csstools--negative--2--value: var(--value); - --csstools--negative--2--result: calc(-1 * var(--csstools--negative--2--value)); } .negative-c { - padding: var(--csstools--negative--2--result); + --csstools-custom-function-call-2-1: calc(-1 * var(--value)); +} + +.negative-c { + --value: 1em; + padding: var(--csstools-custom-function-call-2-1); +} + +.color-a { + --csstools-function-argument-3---color: black; + --csstools-function-argument-3---color: var(--a-color); + --csstools-function-argument-3---alpha: 1; + --csstools-function-argument-3---alpha: 0.5; +} + +.color-a { + --a: clamp(0, var(--alpha), 1); + + --csstools-custom-function-call-3-1: oklch(from var(--color) l c h / var(--a)); +} + +.color-a { + --a-color: red; + color: var(--csstools-custom-function-call-3-1); +} + +@property --csstools-function-argument-0---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-body-0---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-1---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-body-1---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-2---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-body-2---value { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-3---color { + syntax: ""; + inherit: false; +} + +@property --csstools-function-body-3---color { + syntax: "*"; + inherit: false; +} + +@property --csstools-function-argument-3---alpha { + syntax: ""; + inherit: false; +} + +@property --csstools-function-body-3---alpha { + syntax: "*"; + inherit: false; +} + +@property --csstools-return-value-3 { + syntax: ""; + inherit: false; } diff --git a/plugins/postcss-custom-functions/test/examples/example.expect.css b/plugins/postcss-custom-functions/test/examples/example.expect.css index 9d738d5ac3..181f83a541 100644 --- a/plugins/postcss-custom-functions/test/examples/example.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.expect.css @@ -1,5 +1,5 @@ .foo { - color: blue; + color: red; } .baz { diff --git a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css index 8b020470a1..181f83a541 100644 --- a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css @@ -1,5 +1,4 @@ .foo { - color: blue; color: red; } From 2ddd14a521faa09b1bba993210e9ab5512b44756 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Tue, 3 Jun 2025 08:20:43 +0200 Subject: [PATCH 04/21] wip --- package-lock.json | 160 ++++----- .../postcss-custom-functions/dist/index.cjs | 2 +- .../postcss-custom-functions/dist/index.mjs | 2 +- plugins/postcss-custom-functions/src/index.ts | 318 +++++++++--------- .../test/basic.color.expect.css | 50 +-- 5 files changed, 269 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dd47d3151..007aa42435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4105,17 +4105,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", - "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", + "integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.33.0", - "@typescript-eslint/type-utils": "8.33.0", - "@typescript-eslint/utils": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/type-utils": "8.33.1", + "@typescript-eslint/utils": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -4129,7 +4129,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.33.0", + "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -4145,16 +4145,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", - "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", + "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.33.0", - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/typescript-estree": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "engines": { @@ -4170,14 +4170,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", - "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.33.0", - "@typescript-eslint/types": "^8.33.0", + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "engines": { @@ -4186,17 +4186,20 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", - "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0" + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4207,9 +4210,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", - "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", "dev": true, "license": "MIT", "engines": { @@ -4224,14 +4227,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", - "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz", + "integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.33.0", - "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4248,9 +4251,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", - "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", "dev": true, "license": "MIT", "engines": { @@ -4262,16 +4265,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", - "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.33.0", - "@typescript-eslint/tsconfig-utils": "8.33.0", - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/visitor-keys": "8.33.0", + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4330,16 +4333,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", - "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.33.0", - "@typescript-eslint/types": "8.33.0", - "@typescript-eslint/typescript-estree": "8.33.0" + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4354,13 +4357,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", - "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5336,9 +5339,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.161", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz", - "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==", + "version": "1.5.162", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.162.tgz", + "integrity": "sha512-hQA+Zb5QQwoSaXJWEAGEw1zhk//O7qDzib05Z4qTqZfNju/FAkrm5ZInp0JbTp4Z18A6bilopdZWEYrFSsfllA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -5873,13 +5876,13 @@ } }, "node_modules/fd-package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-1.2.0.tgz", - "integrity": "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", "dev": true, "license": "MIT", "dependencies": { - "walk-up-path": "^3.0.1" + "walk-up-path": "^4.0.0" } }, "node_modules/fd-slicer": { @@ -6001,13 +6004,13 @@ "license": "ISC" }, "node_modules/formatly": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.2.3.tgz", - "integrity": "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.2.4.tgz", + "integrity": "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==", "dev": true, "license": "MIT", "dependencies": { - "fd-package-json": "^1.2.0" + "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" @@ -8963,15 +8966,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", - "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz", + "integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.33.0", - "@typescript-eslint/parser": "8.33.0", - "@typescript-eslint/utils": "8.33.0" + "@typescript-eslint/eslint-plugin": "8.33.1", + "@typescript-eslint/parser": "8.33.1", + "@typescript-eslint/utils": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9113,11 +9116,14 @@ "license": "MIT" }, "node_modules/walk-up-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", - "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/weapon-regex": { "version": "1.3.2", @@ -9307,9 +9313,9 @@ } }, "node_modules/zod": { - "version": "3.25.48", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.48.tgz", - "integrity": "sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==", + "version": "3.25.49", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.49.tgz", + "integrity": "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==", "dev": true, "license": "MIT", "funding": { diff --git a/plugins/postcss-custom-functions/dist/index.cjs b/plugins/postcss-custom-functions/dist/index.cjs index 700e25e6c6..a046eb7abe 100644 --- a/plugins/postcss-custom-functions/dist/index.cjs +++ b/plugins/postcss-custom-functions/dist/index.cjs @@ -1 +1 @@ -"use strict";var e=require("@csstools/cascade-layer-name-parser"),t=require("@csstools/custom-function-parser"),n=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");const s=e.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(t){const n=new Map,r=new Map,o=[];t.walkAtRules((t=>{if("layer"!==t.name.toLowerCase())return;{let e=t.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===t.root())break;return}e=e.parent}}let a;if(t.nodes)a=normalizeLayerName(t.params,1);else{if(!t.params.trim())return;a=t.params}let i=e.parse(a);if(i?.length){{let e=t.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=r.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(e.addLayerToModel(o,i),t.nodes){const e=i[0].concat(s);n.set(t,e),r.set(t,i[0])}}}));for(const t of n.values())e.addLayerToModel(o,[t]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const o=new Set(["scope","container","layer"]),a=/^function$/i;function isProcessableRule(e){if(!a.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!o.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,n,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=t.parse(i,{onParseError:t=>{e.warn(n,`Failed to parse custom function : "${e.params}" with message: "${t instanceof Error?t.message:t}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const d=o.get(c)??-1;if(l&&l>=d&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const i=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const s=[];return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){s.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(o,{atRule:a}){if(!i.test(o.value))return;const u=o.parent;if(!u)return;const c=r.tokenize({css:o.value});if(!c.some((e=>r.isTokenFunction(e)&&e[4].value.startsWith("--"))))return;const l=n.replaceComponentValues([n.parseListOfComponentValues(c)],(t=>{if(!n.isFunctionNode(t))return;const i=t.getName();if(!i.startsWith("--"))return;const c=e.get(i);if(!c)return;const l=new CallingContext(u,u,o,t,c.function,c.node,a);return l.invalid?new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[]):(u.before(l.argumentRule),u.before(l.bodyRule),s.push(l),new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[new n.TokenNode([r.TokenType.Ident,l.resultIdent,-1,-1,{value:l.resultIdent}])]))})),p=n.stringify(l);p!==o.value&&(o.cloneBefore({value:p}),t?.preserve||o.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,r,s,o,a,i,u=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=r,this.function=s,this.customFunction=o,this.customFunctionBody=a,this.parent=u;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=n.parseCommaSeparatedListOfComponentValues(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=i({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=i({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const r=this.customFunction.getReturnType();if(r){const e=i({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,r)=>{const s=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:e.getDefaultValue()}));const o=t[r];o&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:n.stringify([o])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}module.exports=creator; +"use strict";var e=require("@csstools/cascade-layer-name-parser"),t=require("@csstools/custom-function-parser"),n=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");const s=e.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(t){const n=new Map,r=new Map,o=[];t.walkAtRules((t=>{if("layer"!==t.name.toLowerCase())return;{let e=t.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===t.root())break;return}e=e.parent}}let a;if(t.nodes)a=normalizeLayerName(t.params,1);else{if(!t.params.trim())return;a=t.params}let i=e.parse(a);if(i?.length){{let e=t.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=r.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(e.addLayerToModel(o,i),t.nodes){const e=i[0].concat(s);n.set(t,e),r.set(t,i[0])}}}));for(const t of n.values())e.addLayerToModel(o,[t]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const o=new Set(["scope","container","layer"]),a=/^function$/i;function isProcessableRule(e){if(!a.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!o.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,n,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=t.parse(i,{onParseError:t=>{e.warn(n,`Failed to parse custom function : "${e.params}" with message: "${t instanceof Error?t.message:t}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const d=o.get(c)??-1;if(l&&l>=d&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const i=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const s=[];class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,r,s,o,a,i,u=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=r,this.function=s,this.customFunction=o,this.customFunctionBody=a,this.parent=u;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=n.parseCommaSeparatedListOfComponentValues(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=i({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=i({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const r=this.customFunction.getReturnType();if(r){const e=i({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,r)=>{const s=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:e.getDefaultValue()}));const o=t[r];o&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:n.stringify([o])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){s.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(o,{atRule:a}){if(!i.test(o.value))return;const u=o.parent;if(!u)return;const c=r.tokenize({css:o.value});if(!c.some((e=>r.isTokenFunction(e)&&e[4].value.startsWith("--"))))return;const l=n.replaceComponentValues([n.parseListOfComponentValues(c)],(t=>{if(!n.isFunctionNode(t))return;const i=t.getName();if(!i.startsWith("--"))return;const c=e.get(i);if(!c)return;const l=new CallingContext(u,u,o,t,c.function,c.node,a);return l.invalid?new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[]):(u.before(l.argumentRule),u.before(l.bodyRule),s.push(l),new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[new n.TokenNode([r.TokenType.Ident,l.resultIdent,-1,-1,{value:l.resultIdent}])]))})),p=n.stringify(l);p!==o.value&&(o.cloneBefore({value:p}),t?.preserve||o.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0,module.exports=creator; diff --git a/plugins/postcss-custom-functions/dist/index.mjs b/plugins/postcss-custom-functions/dist/index.mjs index 7b52273020..17da43e1ce 100644 --- a/plugins/postcss-custom-functions/dist/index.mjs +++ b/plugins/postcss-custom-functions/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parse as n}from"@csstools/custom-function-parser";import{replaceComponentValues as r,parseListOfComponentValues as s,isFunctionNode as o,FunctionNode as a,TokenNode as i,stringify as u,parseCommaSeparatedListOfComponentValues as c}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenFunction as p,TokenType as m}from"@csstools/css-tokenizer";const h=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,o=[];n.walkAtRules((n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}e=e.parent}}let a;if(n.nodes)a=normalizeLayerName(n.params,1);else{if(!n.params.trim())return;a=n.params}let i=e(a);if(i?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(t(o,i),n.nodes){const e=i[0].concat(h);r.set(n,e),s.set(n,i[0])}}}));for(const e of r.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of r)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const f=new Set(["scope","container","layer"]),d=/^function$/i;function isProcessableRule(e){if(!d.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!f.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,t,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=n(i,{onParseError:n=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${n instanceof Error?n.message:n}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const h=o.get(c)??-1;if(l&&l>=h&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const g=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const n=[];return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){n.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(c,{atRule:h}){if(!g.test(c.value))return;const f=c.parent;if(!f)return;const d=l({css:c.value});if(!d.some((e=>p(e)&&e[4].value.startsWith("--"))))return;const y=r([s(d)],(t=>{if(!o(t))return;const r=t.getName();if(!r.startsWith("--"))return;const s=e.get(r);if(!s)return;const u=new CallingContext(f,f,c,t,s.function,s.node,h);return u.invalid?new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[]):(f.before(u.argumentRule),f.before(u.bodyRule),n.push(u),new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[new i([m.Ident,u.resultIdent,-1,-1,{value:u.resultIdent}])]))})),v=u(y);v!==c.value&&(c.cloneBefore({value:v}),t?.preserve||c.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,n,r,s,o,a,i=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=n,this.function=r,this.customFunction=s,this.customFunctionBody=o,this.parent=i;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=c(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=a({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=a({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const n=this.customFunction.getReturnType();if(n){const e=a({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,n)=>{const r=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:e.getDefaultValue()}));const s=t[n];s&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:u([s])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}export{creator as default}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parse as n}from"@csstools/custom-function-parser";import{replaceComponentValues as r,parseListOfComponentValues as s,isFunctionNode as o,FunctionNode as a,TokenNode as i,stringify as u,parseCommaSeparatedListOfComponentValues as c}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenFunction as p,TokenType as m}from"@csstools/css-tokenizer";const h=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,o=[];n.walkAtRules((n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}e=e.parent}}let a;if(n.nodes)a=normalizeLayerName(n.params,1);else{if(!n.params.trim())return;a=n.params}let i=e(a);if(i?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(t(o,i),n.nodes){const e=i[0].concat(h);r.set(n,e),s.set(n,i[0])}}}));for(const e of r.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of r)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const f=new Set(["scope","container","layer"]),d=/^function$/i;function isProcessableRule(e){if(!d.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!f.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,t,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=n(i,{onParseError:n=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${n instanceof Error?n.message:n}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const h=o.get(c)??-1;if(l&&l>=h&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const g=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const n=[];class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,n,r,s,o,a,i=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=n,this.function=r,this.customFunction=s,this.customFunctionBody=o,this.parent=i;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=c(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=a({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=a({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const n=this.customFunction.getReturnType();if(n){const e=a({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,n)=>{const r=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:e.getDefaultValue()}));const s=t[n];s&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:u([s])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){n.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(c,{atRule:h}){if(!g.test(c.value))return;const f=c.parent;if(!f)return;const d=l({css:c.value});if(!d.some((e=>p(e)&&e[4].value.startsWith("--"))))return;const y=r([s(d)],(t=>{if(!o(t))return;const r=t.getName();if(!r.startsWith("--"))return;const s=e.get(r);if(!s)return;const u=new CallingContext(f,f,c,t,s.function,s.node,h);return u.invalid?new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[]):(f.before(u.argumentRule),f.before(u.bodyRule),n.push(u),new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[new i([m.Ident,u.resultIdent,-1,-1,{value:u.resultIdent}])]))})),v=u(y);v!==c.value&&(c.cloneBefore({value:v}),t?.preserve||c.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;export{creator as default}; diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts index 5190c85c7c..e871340811 100644 --- a/plugins/postcss-custom-functions/src/index.ts +++ b/plugins/postcss-custom-functions/src/index.ts @@ -31,6 +31,165 @@ const creator: PluginCreator = (opts?: pluginOptions) => { let customFunctions: Map = new Map(); const callingContexts: Array = []; + class CallingContext { + static counter = 0; + + id: string; + + rootElement: ContainerWithChildren; + element: ContainerWithChildren; + property: Declaration; + parent: CallingContext | null = null; + + function: FunctionNode; + customFunction: CustomFunction; + customFunctionBody: AtRule; + + registrations: Array = []; + + argumentRule: ContainerWithChildren; + bodyRule: ContainerWithChildren; + + resultIdent: string; + + varCounter: number = 0; + + invalid: boolean = false; + + // TODO: some ident mapping + + constructor( + rootElement: ContainerWithChildren, + element: ContainerWithChildren, + property: Declaration, + + fn: FunctionNode, + customFunction: CustomFunction, + customFunctionBody: AtRule, + + atRule: (defaults?: AtRuleProps) => AtRule, + + parent: CallingContext | null = null, + ) { + this.id = (CallingContext.counter++).toString(36); + + this.rootElement = rootElement; + this.element = element; + this.property = property; + + this.function = fn; + this.customFunction = customFunction; + this.customFunctionBody = customFunctionBody; + + this.parent = parent; + + { + const varCounterValue = (++this.varCounter).toString(36); + this.resultIdent = generateDashedIdent('custom-function-call', varCounterValue, this.id); + + const args = parseCommaSeparatedListOfComponentValues(this.function.value.flatMap(x => x.tokens())); + if (args.length > this.customFunction.parameters.length) { + this.invalid = true; + } + + this.customFunction.parameters.forEach((parameter) => { + const parameterName = parameter.getName(); + const parameterType = parameter.getArgumentType() || '*'; + + const argumentCustomProperty = atRule({ + name: 'property', + params: generateDashedIdent('function-argument', parameterName, this.id), + nodes: [ + this.property.clone({ + prop: 'syntax', + value: `"${parameterType}"`, + }), + this.property.clone({ + prop: 'inherit', + value: "false", + }) + ], + source: this.property.source + }); + + this.registrations.push(argumentCustomProperty); + + const bodyCustomProperty = atRule({ + name: 'property', + params: generateDashedIdent('function-body', parameterName, this.id), + nodes: [ + this.property.clone({ + prop: 'syntax', + value: '"*"', + }), + this.property.clone({ + prop: 'inherit', + value: "false", + }) + ], + source: this.property.source + }); + + this.registrations.push(bodyCustomProperty); + }); + + const returnType = this.customFunction.getReturnType(); + if (returnType) { + const customProperty = atRule({ + name: 'property', + params: generateDashedIdent('return-value', '', this.id), + nodes: [ + this.property.clone({ + prop: 'syntax', + value: `"${returnType}"`, + }), + this.property.clone({ + prop: 'inherit', + value: "false", + }) + ], + source: this.property.source + }); + + this.registrations.push(customProperty); + } + + this.argumentRule = this.element.clone({ nodes: [] }); + + this.customFunction.parameters.forEach((parameter, index) => { + const parameterName = parameter.getName(); + const defaultValue = parameter.getDefaultValue(); + + if (defaultValue) { + this.argumentRule.append(this.property.clone({ + prop: generateDashedIdent('function-argument', parameterName, this.id), + value: parameter.getDefaultValue(), + })); + } + + const argumentValue = args[index]; + // TODO: `{1, 2}` -> `1, 2` + if (argumentValue) { + this.argumentRule.append(this.property.clone({ + prop: generateDashedIdent('function-argument', parameterName, this.id), + value: stringify([argumentValue]), + })); + } + }); + + this.bodyRule = this.element.clone({ + nodes: [ + ...(this.customFunctionBody.nodes?.map((x) => x.clone()) ?? []) + ] + }); + + this.bodyRule.walkDecls(/^result$/i, (resultDecl) => { + resultDecl.prop = this.resultIdent; + }); + } + } + } + return { postcssPlugin: 'postcss-custom-functions', Once(root, { result }): void { @@ -140,162 +299,3 @@ function generateDashedIdent(type: string, name: string, id: string): string { creator.postcss = true; export default creator; - -class CallingContext { - static counter = 0; - - id: string; - - rootElement: ContainerWithChildren; - element: ContainerWithChildren; - property: Declaration; - parent: CallingContext | null = null; - - function: FunctionNode; - customFunction: CustomFunction; - customFunctionBody: AtRule; - - registrations: Array = []; - - argumentRule: ContainerWithChildren; - bodyRule: ContainerWithChildren; - - resultIdent: string; - - varCounter: number = 0; - - invalid: boolean = false; - - // TODO: some ident mapping - - constructor( - rootElement: ContainerWithChildren, - element: ContainerWithChildren, - property: Declaration, - - fn: FunctionNode, - customFunction: CustomFunction, - customFunctionBody: AtRule, - - atRule: (defaults?: AtRuleProps) => AtRule, - - parent: CallingContext | null = null, - ) { - this.id = (CallingContext.counter++).toString(36); - - this.rootElement = rootElement; - this.element = element; - this.property = property; - - this.function = fn; - this.customFunction = customFunction; - this.customFunctionBody = customFunctionBody; - - this.parent = parent; - - { - const varCounterValue = (++this.varCounter).toString(36); - this.resultIdent = generateDashedIdent('custom-function-call', varCounterValue, this.id); - - const args = parseCommaSeparatedListOfComponentValues(this.function.value.flatMap(x => x.tokens())); - if (args.length > this.customFunction.parameters.length) { - this.invalid = true; - } - - this.customFunction.parameters.forEach((parameter) => { - const parameterName = parameter.getName(); - const parameterType = parameter.getArgumentType() || '*'; - - const argumentCustomProperty = atRule({ - name: 'property', - params: generateDashedIdent('function-argument', parameterName, this.id), - nodes: [ - this.property.clone({ - prop: 'syntax', - value: `"${parameterType}"`, - }), - this.property.clone({ - prop: 'inherit', - value: "false", - }) - ], - source: this.property.source - }); - - this.registrations.push(argumentCustomProperty); - - const bodyCustomProperty = atRule({ - name: 'property', - params: generateDashedIdent('function-body', parameterName, this.id), - nodes: [ - this.property.clone({ - prop: 'syntax', - value: '"*"', - }), - this.property.clone({ - prop: 'inherit', - value: "false", - }) - ], - source: this.property.source - }); - - this.registrations.push(bodyCustomProperty); - }); - - const returnType = this.customFunction.getReturnType(); - if (returnType) { - const customProperty = atRule({ - name: 'property', - params: generateDashedIdent('return-value', '', this.id), - nodes: [ - this.property.clone({ - prop: 'syntax', - value: `"${returnType}"`, - }), - this.property.clone({ - prop: 'inherit', - value: "false", - }) - ], - source: this.property.source - }); - - this.registrations.push(customProperty); - } - - this.argumentRule = this.element.clone({ nodes: [] }); - - this.customFunction.parameters.forEach((parameter, index) => { - const parameterName = parameter.getName(); - const defaultValue = parameter.getDefaultValue(); - - if (defaultValue) { - this.argumentRule.append(this.property.clone({ - prop: generateDashedIdent('function-argument', parameterName, this.id), - value: parameter.getDefaultValue(), - })); - } - - const argumentValue = args[index]; - // TODO: `{1, 2}` -> `1, 2` - if (argumentValue) { - this.argumentRule.append(this.property.clone({ - prop: generateDashedIdent('function-argument', parameterName, this.id), - value: stringify([argumentValue]), - })); - } - }); - - this.bodyRule = this.element.clone({ - nodes: [ - ...(this.customFunctionBody.nodes?.map((x) => x.clone()) ?? []) - ] - }); - - this.bodyRule.walkDecls(/^result$/i, (resultDecl) => { - resultDecl.prop = this.resultIdent; - }); - } - } -} diff --git a/plugins/postcss-custom-functions/test/basic.color.expect.css b/plugins/postcss-custom-functions/test/basic.color.expect.css index 0dd8840ac4..e84c0b9265 100644 --- a/plugins/postcss-custom-functions/test/basic.color.expect.css +++ b/plugins/postcss-custom-functions/test/basic.color.expect.css @@ -1,109 +1,109 @@ .negative-a { - --csstools-function-argument-8---value: var(--gap); + --csstools-function-argument-0---value: var(--gap); } .negative-a { - --csstools-custom-function-call-8-1: calc(-1 * var(--value)); + --csstools-custom-function-call-0-1: calc(-1 * var(--value)); } .negative-a { --gap: 1em; - padding: var(--csstools-custom-function-call-8-1); + padding: var(--csstools-custom-function-call-0-1); } .negative-b { - --csstools-function-argument-9---value: 1em; + --csstools-function-argument-1---value: 1em; } .negative-b { - --csstools-custom-function-call-9-1: calc(-1 * var(--value)); + --csstools-custom-function-call-1-1: calc(-1 * var(--value)); } .negative-b { - padding: var(--csstools-custom-function-call-9-1); + padding: var(--csstools-custom-function-call-1-1); } .negative-c { } .negative-c { - --csstools-custom-function-call-a-1: calc(-1 * var(--value)); + --csstools-custom-function-call-2-1: calc(-1 * var(--value)); } .negative-c { --value: 1em; - padding: var(--csstools-custom-function-call-a-1); + padding: var(--csstools-custom-function-call-2-1); } .color-a { - --csstools-function-argument-b---color: black; - --csstools-function-argument-b---color: var(--a-color); - --csstools-function-argument-b---alpha: 1; - --csstools-function-argument-b---alpha: 0.5; + --csstools-function-argument-3---color: black; + --csstools-function-argument-3---color: var(--a-color); + --csstools-function-argument-3---alpha: 1; + --csstools-function-argument-3---alpha: 0.5; } .color-a { --a: clamp(0, var(--alpha), 1); - --csstools-custom-function-call-b-1: oklch(from var(--color) l c h / var(--a)); + --csstools-custom-function-call-3-1: oklch(from var(--color) l c h / var(--a)); } .color-a { --a-color: red; - color: var(--csstools-custom-function-call-b-1); + color: var(--csstools-custom-function-call-3-1); } -@property --csstools-function-argument-8---value { +@property --csstools-function-argument-0---value { syntax: "*"; inherit: false; } -@property --csstools-function-body-8---value { +@property --csstools-function-body-0---value { syntax: "*"; inherit: false; } -@property --csstools-function-argument-9---value { +@property --csstools-function-argument-1---value { syntax: "*"; inherit: false; } -@property --csstools-function-body-9---value { +@property --csstools-function-body-1---value { syntax: "*"; inherit: false; } -@property --csstools-function-argument-a---value { +@property --csstools-function-argument-2---value { syntax: "*"; inherit: false; } -@property --csstools-function-body-a---value { +@property --csstools-function-body-2---value { syntax: "*"; inherit: false; } -@property --csstools-function-argument-b---color { +@property --csstools-function-argument-3---color { syntax: ""; inherit: false; } -@property --csstools-function-body-b---color { +@property --csstools-function-body-3---color { syntax: "*"; inherit: false; } -@property --csstools-function-argument-b---alpha { +@property --csstools-function-argument-3---alpha { syntax: ""; inherit: false; } -@property --csstools-function-body-b---alpha { +@property --csstools-function-body-3---alpha { syntax: "*"; inherit: false; } -@property --csstools-return-value-b { +@property --csstools-return-value-3 { syntax: ""; inherit: false; } From 5439be73b16aebe16702f8139893ea6e1a4e3a1a Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Thu, 5 Jun 2025 13:43:26 +0200 Subject: [PATCH 05/21] wip --- plugins/postcss-custom-functions/dist/index.cjs | 2 +- plugins/postcss-custom-functions/dist/index.mjs | 2 +- plugins/postcss-custom-functions/src/index.ts | 15 +++++++++++---- .../src/is-processable-declaration.ts | 16 ++++++++++++++++ .../test/basic.color.expect.css | 5 +++++ .../test/basic.expect.css | 5 +++++ 6 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 plugins/postcss-custom-functions/src/is-processable-declaration.ts diff --git a/plugins/postcss-custom-functions/dist/index.cjs b/plugins/postcss-custom-functions/dist/index.cjs index a046eb7abe..d29aa2b696 100644 --- a/plugins/postcss-custom-functions/dist/index.cjs +++ b/plugins/postcss-custom-functions/dist/index.cjs @@ -1 +1 @@ -"use strict";var e=require("@csstools/cascade-layer-name-parser"),t=require("@csstools/custom-function-parser"),n=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");const s=e.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(t){const n=new Map,r=new Map,o=[];t.walkAtRules((t=>{if("layer"!==t.name.toLowerCase())return;{let e=t.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===t.root())break;return}e=e.parent}}let a;if(t.nodes)a=normalizeLayerName(t.params,1);else{if(!t.params.trim())return;a=t.params}let i=e.parse(a);if(i?.length){{let e=t.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=r.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(e.addLayerToModel(o,i),t.nodes){const e=i[0].concat(s);n.set(t,e),r.set(t,i[0])}}}));for(const t of n.values())e.addLayerToModel(o,[t]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const o=new Set(["scope","container","layer"]),a=/^function$/i;function isProcessableRule(e){if(!a.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!o.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,n,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=t.parse(i,{onParseError:t=>{e.warn(n,`Failed to parse custom function : "${e.params}" with message: "${t instanceof Error?t.message:t}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const d=o.get(c)??-1;if(l&&l>=d&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const i=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const s=[];class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,r,s,o,a,i,u=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=r,this.function=s,this.customFunction=o,this.customFunctionBody=a,this.parent=u;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=n.parseCommaSeparatedListOfComponentValues(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=i({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=i({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const r=this.customFunction.getReturnType();if(r){const e=i({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,r)=>{const s=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:e.getDefaultValue()}));const o=t[r];o&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:n.stringify([o])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){s.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(o,{atRule:a}){if(!i.test(o.value))return;const u=o.parent;if(!u)return;const c=r.tokenize({css:o.value});if(!c.some((e=>r.isTokenFunction(e)&&e[4].value.startsWith("--"))))return;const l=n.replaceComponentValues([n.parseListOfComponentValues(c)],(t=>{if(!n.isFunctionNode(t))return;const i=t.getName();if(!i.startsWith("--"))return;const c=e.get(i);if(!c)return;const l=new CallingContext(u,u,o,t,c.function,c.node,a);return l.invalid?new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[]):(u.before(l.argumentRule),u.before(l.bodyRule),s.push(l),new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[new n.TokenNode([r.TokenType.Ident,l.resultIdent,-1,-1,{value:l.resultIdent}])]))})),p=n.stringify(l);p!==o.value&&(o.cloneBefore({value:p}),t?.preserve||o.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0,module.exports=creator; +"use strict";var e=require("@csstools/cascade-layer-name-parser"),t=require("@csstools/custom-function-parser"),n=require("@csstools/css-parser-algorithms"),r=require("@csstools/css-tokenizer");const s=e.parse("csstools-implicit-layer")[0];function collectCascadeLayerOrder(t){const n=new Map,r=new Map,o=[];t.walkAtRules((t=>{if("layer"!==t.name.toLowerCase())return;{let e=t.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===t.root())break;return}e=e.parent}}let a;if(t.nodes)a=normalizeLayerName(t.params,1);else{if(!t.params.trim())return;a=t.params}let i=e.parse(a);if(i?.length){{let e=t.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=r.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(e.addLayerToModel(o,i),t.nodes){const e=i[0].concat(s);n.set(t,e),r.set(t,i[0])}}}));for(const t of n.values())e.addLayerToModel(o,[t]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const o=new Set(["scope","container","layer"]),a=/^function$/i;function isProcessableRule(e){if(!a.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!o.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,n,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=t.parse(i,{onParseError:t=>{e.warn(n,`Failed to parse custom function : "${e.params}" with message: "${t instanceof Error?t.message:t}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const d=o.get(c)??-1;if(l&&l>=d&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const i=new Set(["keyframes"]);function isProcessableDeclaration(e){let t=e.parent;for(;t;){if("atrule"===t.type&&i.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const u=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const s=[];class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,r,s,o,a,i,u=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=r,this.function=s,this.customFunction=o,this.customFunctionBody=a,this.parent=u;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=n.parseCommaSeparatedListOfComponentValues(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*";this.registrations.push(i({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"}),this.property.clone({prop:"initial-value",value:e.getDefaultValue()})],source:this.property.source}));const r=i({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r)}));const r=this.customFunction.getReturnType();if(r){const e=i({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,r)=>{const s=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:e.getDefaultValue()}));const o=t[r];o&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",s,this.id),value:n.stringify([o])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){s.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(o,{atRule:a}){if(!u.test(o.value))return;const i=o.parent;if(!i)return;if(!isProcessableDeclaration(o))return;const c=r.tokenize({css:o.value});if(!c.some((e=>r.isTokenFunction(e)&&e[4].value.startsWith("--"))))return;const l=n.replaceComponentValues([n.parseListOfComponentValues(c)],(t=>{if(!n.isFunctionNode(t))return;const u=t.getName();if(!u.startsWith("--"))return;const c=e.get(u);if(!c)return;const l=new CallingContext(i,i,o,t,c.function,c.node,a);return l.invalid?new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[]):(i.before(l.argumentRule),i.before(l.bodyRule),s.push(l),new n.FunctionNode([r.TokenType.Function,"var(",-1,-1,{value:"var"}],[r.TokenType.CloseParen,")",-1,-1,void 0],[new n.TokenNode([r.TokenType.Ident,l.resultIdent,-1,-1,{value:l.resultIdent}])]))})),p=n.stringify(l);p!==o.value&&(o.cloneBefore({value:p}),t?.preserve||o.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0,module.exports=creator; diff --git a/plugins/postcss-custom-functions/dist/index.mjs b/plugins/postcss-custom-functions/dist/index.mjs index 17da43e1ce..e73615f223 100644 --- a/plugins/postcss-custom-functions/dist/index.mjs +++ b/plugins/postcss-custom-functions/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parse as n}from"@csstools/custom-function-parser";import{replaceComponentValues as r,parseListOfComponentValues as s,isFunctionNode as o,FunctionNode as a,TokenNode as i,stringify as u,parseCommaSeparatedListOfComponentValues as c}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenFunction as p,TokenType as m}from"@csstools/css-tokenizer";const h=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(n){const r=new Map,s=new Map,o=[];n.walkAtRules((n=>{if("layer"!==n.name.toLowerCase())return;{let e=n.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===n.root())break;return}e=e.parent}}let a;if(n.nodes)a=normalizeLayerName(n.params,1);else{if(!n.params.trim())return;a=n.params}let i=e(a);if(i?.length){{let e=n.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(t(o,i),n.nodes){const e=i[0].concat(h);r.set(n,e),s.set(n,i[0])}}}));for(const e of r.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of r)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const f=new Set(["scope","container","layer"]),d=/^function$/i;function isProcessableRule(e){if(!d.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!f.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,t,r){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=n(i,{onParseError:n=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${n instanceof Error?n.message:n}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const h=o.get(c)??-1;if(l&&l>=h&&(o.set(c,l),s.set(c,{node:e,function:u})),!r.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const g=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const n=[];class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,n,r,s,o,a,i=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=n,this.function=r,this.customFunction=s,this.customFunctionBody=o,this.parent=i;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=c(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),n=e.getArgumentType()||"*",r=a({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(r);const s=a({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(s)}));const n=this.customFunction.getReturnType();if(n){const e=a({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${n}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,n)=>{const r=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:e.getDefaultValue()}));const s=t[n];s&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",r,this.id),value:u([s])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}return{postcssPlugin:"postcss-custom-functions",Once(n,{result:r}){e=getCustomFunctions(n,r,{preserve:t.preserve})},OnceExit(e){n.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(c,{atRule:h}){if(!g.test(c.value))return;const f=c.parent;if(!f)return;const d=l({css:c.value});if(!d.some((e=>p(e)&&e[4].value.startsWith("--"))))return;const y=r([s(d)],(t=>{if(!o(t))return;const r=t.getName();if(!r.startsWith("--"))return;const s=e.get(r);if(!s)return;const u=new CallingContext(f,f,c,t,s.function,s.node,h);return u.invalid?new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[]):(f.before(u.argumentRule),f.before(u.bodyRule),n.push(u),new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[new i([m.Ident,u.resultIdent,-1,-1,{value:u.resultIdent}])]))})),v=u(y);v!==c.value&&(c.cloneBefore({value:v}),t?.preserve||c.remove())}}}}};function generateDashedIdent(e,t,n){return`--csstools-${[e,n,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;export{creator as default}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parse as r}from"@csstools/custom-function-parser";import{replaceComponentValues as n,parseListOfComponentValues as s,isFunctionNode as o,FunctionNode as a,TokenNode as i,stringify as u,parseCommaSeparatedListOfComponentValues as c}from"@csstools/css-parser-algorithms";import{tokenize as l,isTokenFunction as p,TokenType as m}from"@csstools/css-tokenizer";const f=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules((r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let i=e(a);if(i?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(i=i.map((e=>t.concat(e))),e=e.parent):e=e.parent}}if(t(o,i),r.nodes){const e=i[0].concat(f);n.set(r,e),s.set(r,i[0])}}}));for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex((e=>t.equal(e))));return a}function normalizeLayerName(e,t){return e.trim()?e:"csstools-anon-layer--"+t++}const h=new Set(["scope","container","layer"]),d=/^function$/i;function isProcessableRule(e){if(!d.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!h.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}function getCustomFunctions(e,t,n){const s=new Map,o=new Map,a=collectCascadeLayerOrder(e);return e.walkAtRules((e=>{if(!isProcessableRule(e))return;const i=e.params.trim(),u=r(i,{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!u)return;const c=u.getName(),l=(m=a,(p=e).parent&&"atrule"===p.parent.type&&"layer"===p.parent.name.toLowerCase()?m.has(p.parent)?m.get(p.parent)+1:0:1e7);var p,m;const f=o.get(c)??-1;if(l&&l>=f&&(o.set(c,l),s.set(c,{node:e,function:u})),!n.preserve){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}})),s}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const g=new Set(["keyframes"]);function isProcessableDeclaration(e){let t=e.parent;for(;t;){if("atrule"===t.type&&g.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const y=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=[];class CallingContext{static counter=0;id;rootElement;element;property;parent=null;function;customFunction;customFunctionBody;registrations=[];argumentRule;bodyRule;resultIdent;varCounter=0;invalid=!1;constructor(e,t,r,n,s,o,a,i=null){this.id=(CallingContext.counter++).toString(36),this.rootElement=e,this.element=t,this.property=r,this.function=n,this.customFunction=s,this.customFunctionBody=o,this.parent=i;{const e=(++this.varCounter).toString(36);this.resultIdent=generateDashedIdent("custom-function-call",e,this.id);const t=c(this.function.value.flatMap((e=>e.tokens())));t.length>this.customFunction.parameters.length&&(this.invalid=!0),this.customFunction.parameters.forEach((e=>{const t=e.getName(),r=e.getArgumentType()||"*";this.registrations.push(a({name:"property",params:generateDashedIdent("function-argument",t,this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"}),this.property.clone({prop:"initial-value",value:e.getDefaultValue()})],source:this.property.source}));const n=a({name:"property",params:generateDashedIdent("function-body",t,this.id),nodes:[this.property.clone({prop:"syntax",value:'"*"'}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(n)}));const r=this.customFunction.getReturnType();if(r){const e=a({name:"property",params:generateDashedIdent("return-value","",this.id),nodes:[this.property.clone({prop:"syntax",value:`"${r}"`}),this.property.clone({prop:"inherit",value:"false"})],source:this.property.source});this.registrations.push(e)}this.argumentRule=this.element.clone({nodes:[]}),this.customFunction.parameters.forEach(((e,r)=>{const n=e.getName();e.getDefaultValue()&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",n,this.id),value:e.getDefaultValue()}));const s=t[r];s&&this.argumentRule.append(this.property.clone({prop:generateDashedIdent("function-argument",n,this.id),value:u([s])}))})),this.bodyRule=this.element.clone({nodes:[...this.customFunctionBody.nodes?.map((e=>e.clone()))??[]]}),this.bodyRule.walkDecls(/^result$/i,(e=>{e.prop=this.resultIdent}))}}}return{postcssPlugin:"postcss-custom-functions",Once(r,{result:n}){e=getCustomFunctions(r,n,{preserve:t.preserve})},OnceExit(e){r.forEach((t=>{t.registrations.forEach((t=>{e.append(t)}))}))},Declaration(c,{atRule:f}){if(!y.test(c.value))return;const h=c.parent;if(!h)return;if(!isProcessableDeclaration(c))return;const d=l({css:c.value});if(!d.some((e=>p(e)&&e[4].value.startsWith("--"))))return;const g=n([s(d)],(t=>{if(!o(t))return;const n=t.getName();if(!n.startsWith("--"))return;const s=e.get(n);if(!s)return;const u=new CallingContext(h,h,c,t,s.function,s.node,f);return u.invalid?new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[]):(h.before(u.argumentRule),h.before(u.bodyRule),r.push(u),new a([m.Function,"var(",-1,-1,{value:"var"}],[m.CloseParen,")",-1,-1,void 0],[new i([m.Ident,u.resultIdent,-1,-1,{value:u.resultIdent}])]))})),v=u(g);v!==c.value&&(c.cloneBefore({value:v}),t?.preserve||c.remove())}}}}};function generateDashedIdent(e,t,r){return`--csstools-${[e,r,t].filter((e=>!!e)).join("-")}`}creator.postcss=!0;export{creator as default}; diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts index e871340811..80b513623e 100644 --- a/plugins/postcss-custom-functions/src/index.ts +++ b/plugins/postcss-custom-functions/src/index.ts @@ -5,6 +5,7 @@ import { FunctionNode, isFunctionNode, parseCommaSeparatedListOfComponentValues, import { isTokenFunction, tokenize, TokenType } from '@csstools/css-tokenizer'; import type { ContainerWithChildren } from 'postcss/lib/container'; import type { CustomFunction } from '@csstools/custom-function-parser'; +import { isProcessableDeclaration } from './is-processable-declaration'; /** postcss-custom-functions plugin options */ export type pluginOptions = { @@ -96,7 +97,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { const parameterName = parameter.getName(); const parameterType = parameter.getArgumentType() || '*'; - const argumentCustomProperty = atRule({ + this.registrations.push(atRule({ name: 'property', params: generateDashedIdent('function-argument', parameterName, this.id), nodes: [ @@ -107,12 +108,14 @@ const creator: PluginCreator = (opts?: pluginOptions) => { this.property.clone({ prop: 'inherit', value: "false", + }), + this.property.clone({ + prop: 'initial-value', + value: parameter.getDefaultValue(), }) ], source: this.property.source - }); - - this.registrations.push(argumentCustomProperty); + })); const bodyCustomProperty = atRule({ name: 'property', @@ -212,6 +215,10 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } + if (!isProcessableDeclaration(decl)) { + return; + } + const tokens = tokenize({ css: decl.value }); if (!tokens.some((x) => isTokenFunction(x) && x[4].value.startsWith('--'))) { return; diff --git a/plugins/postcss-custom-functions/src/is-processable-declaration.ts b/plugins/postcss-custom-functions/src/is-processable-declaration.ts new file mode 100644 index 0000000000..aa9a3ae4ff --- /dev/null +++ b/plugins/postcss-custom-functions/src/is-processable-declaration.ts @@ -0,0 +1,16 @@ +import type { AtRule, ChildNode, Container, Declaration, Document } from 'postcss'; + +const blockedParentAtRules = new Set(['keyframes']); + +export function isProcessableDeclaration(decl: Declaration): boolean { + let parent: Container | Document | undefined = decl.parent; + while (parent) { + if (parent.type === 'atrule' && blockedParentAtRules.has((parent as AtRule).name.toLowerCase())) { + return false; + } + + parent = parent.parent; + } + + return true; +} diff --git a/plugins/postcss-custom-functions/test/basic.color.expect.css b/plugins/postcss-custom-functions/test/basic.color.expect.css index e84c0b9265..8fa829300d 100644 --- a/plugins/postcss-custom-functions/test/basic.color.expect.css +++ b/plugins/postcss-custom-functions/test/basic.color.expect.css @@ -56,6 +56,7 @@ @property --csstools-function-argument-0---value { syntax: "*"; inherit: false; + initial-value: ; } @property --csstools-function-body-0---value { @@ -66,6 +67,7 @@ @property --csstools-function-argument-1---value { syntax: "*"; inherit: false; + initial-value: ; } @property --csstools-function-body-1---value { @@ -76,6 +78,7 @@ @property --csstools-function-argument-2---value { syntax: "*"; inherit: false; + initial-value: ; } @property --csstools-function-body-2---value { @@ -86,6 +89,7 @@ @property --csstools-function-argument-3---color { syntax: ""; inherit: false; + initial-value: black; } @property --csstools-function-body-3---color { @@ -96,6 +100,7 @@ @property --csstools-function-argument-3---alpha { syntax: ""; inherit: false; + initial-value: 1; } @property --csstools-function-body-3---alpha { diff --git a/plugins/postcss-custom-functions/test/basic.expect.css b/plugins/postcss-custom-functions/test/basic.expect.css index e84c0b9265..8fa829300d 100644 --- a/plugins/postcss-custom-functions/test/basic.expect.css +++ b/plugins/postcss-custom-functions/test/basic.expect.css @@ -56,6 +56,7 @@ @property --csstools-function-argument-0---value { syntax: "*"; inherit: false; + initial-value: ; } @property --csstools-function-body-0---value { @@ -66,6 +67,7 @@ @property --csstools-function-argument-1---value { syntax: "*"; inherit: false; + initial-value: ; } @property --csstools-function-body-1---value { @@ -76,6 +78,7 @@ @property --csstools-function-argument-2---value { syntax: "*"; inherit: false; + initial-value: ; } @property --csstools-function-body-2---value { @@ -86,6 +89,7 @@ @property --csstools-function-argument-3---color { syntax: ""; inherit: false; + initial-value: black; } @property --csstools-function-body-3---color { @@ -96,6 +100,7 @@ @property --csstools-function-argument-3---alpha { syntax: ""; inherit: false; + initial-value: 1; } @property --csstools-function-body-3---alpha { From fc3fb3a4821fe7d4a7e5006ecdf612c839272371 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 11:04:07 +0200 Subject: [PATCH 06/21] wip --- package-lock.json | 10 +- .../custom-function-parser/dist/index.cjs | 1 - .../custom-function-parser/dist/index.mjs | 2 +- .../docs/custom-function-parser.api.json | 16 + ...ion-parser.customfunction._constructor_.md | 122 +++ ...om-function-parser.customfunction.after.md | 11 + ...m-function-parser.customfunction.before.md | 11 + ...function-parser.customfunction.function.md | 11 + ...-function-parser.customfunction.getname.md | 15 + ...ion-parser.customfunction.getreturntype.md | 15 + ...-parser.customfunction.iscustomfunction.md | 50 ++ .../custom-function-parser.customfunction.md | 272 +++++++ ...nction-parser.customfunction.parameters.md | 11 + ...on-parser.customfunction.returnskeyword.md | 11 + ...nction-parser.customfunction.returntype.md | 11 + ...m-function-parser.customfunction.tokens.md | 15 + ...function-parser.customfunction.tostring.md | 15 + ...tom-function-parser.customfunction.type.md | 11 + ...-parser.functionparameter._constructor_.md | 122 +++ ...function-parser.functionparameter.after.md | 11 + ...n-parser.functionparameter.argumenttype.md | 11 + ...unction-parser.functionparameter.before.md | 11 + ...function-parser.functionparameter.colon.md | 11 + ...n-parser.functionparameter.defaultvalue.md | 11 + ...arser.functionparameter.getargumenttype.md | 15 + ...arser.functionparameter.getdefaultvalue.md | 15 + ...nction-parser.functionparameter.getname.md | 15 + ...n-parser.functionparameter.getnametoken.md | 15 + ...r.functionparameter.isfunctionparameter.md | 50 ++ ...ustom-function-parser.functionparameter.md | 272 +++++++ ...-function-parser.functionparameter.name.md | 11 + ...-function-parser.functionparameter.type.md | 11 + ...custom-function-parser.iscustomfunction.md | 50 ++ ...tom-function-parser.isfunctionparameter.md | 50 ++ .../docs/custom-function-parser.md | 114 +++ .../docs/custom-function-parser.nodetype.md | 60 ++ .../docs/custom-function-parser.parse.md | 68 ++ .../custom-function-parser.parsefromtokens.md | 68 ++ packages/custom-function-parser/docs/index.md | 30 + packages/custom-function-parser/package.json | 13 +- .../src/parser/parse-custom-function.ts | 60 +- .../src/parser/parse.ts | 11 +- .../custom-function-parser/test/_import.mjs | 4 +- .../custom-function-parser/test/_require.cjs | 4 +- .../test/parse-prelude/0001.mjs | 104 +++ packages/custom-function-parser/test/test.mjs | 2 + packages/custom-function-parser/tsconfig.json | 1 + plugins/postcss-custom-functions/INSTALL.md | 20 +- plugins/postcss-custom-functions/README.md | 98 ++- .../postcss-custom-functions/dist/index.cjs | 1 - .../postcss-custom-functions/dist/index.d.ts | 1 + .../postcss-custom-functions/dist/index.mjs | 2 +- .../postcss-custom-functions/docs/README.md | 11 +- plugins/postcss-custom-functions/package.json | 26 +- .../src/cascade-layers.ts | 2 +- .../src/custom-functions-from-root.ts | 37 +- plugins/postcss-custom-functions/src/index.ts | 261 +------ .../src/is-processable-declaration.ts | 11 +- .../src/is-processable-rule.ts | 2 +- .../postcss-custom-functions/src/transpile.ts | 418 +++++++++++ .../test/_browser.mjs | 134 ++++ .../postcss-custom-functions/test/_tape.mjs | 30 +- .../postcss-custom-functions/test/basic.css | 17 +- .../test/basic.expect.css | 104 +-- .../test/basic.preserve-true.expect.css | 46 ++ .../test/conditionals.css | 22 + .../test/conditionals.expect.css | 29 + .../test/examples/example.css | 14 +- .../test/examples/example.expect.css | 18 +- .../examples/example.preserve-true.expect.css | 30 +- .../postcss-custom-functions/test/ignore.css | 23 + .../postcss-custom-functions/test/invalid.css | 23 + .../test/invalid.expect.css | 15 + .../postcss-custom-functions/test/nested.css | 31 + .../test/nested.expect.css | 42 ++ .../test/wpt/dashed-function-cycles.html | 368 +++++++++ .../test/wpt/dashed-function-eval.html | 708 ++++++++++++++++++ .../dashed-function-standard-property.html | 51 ++ .../test/wpt/function-conditionals.html | 451 +++++++++++ .../test/wpt/local-var-substitution.html | 103 +++ .../postcss-custom-functions/tsconfig.json | 1 + 81 files changed, 4512 insertions(+), 468 deletions(-) delete mode 100644 packages/custom-function-parser/dist/index.cjs create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction._constructor_.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.after.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.before.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.function.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.getname.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.getreturntype.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.iscustomfunction.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.parameters.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.returnskeyword.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.returntype.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.tokens.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.tostring.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.customfunction.type.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter._constructor_.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.after.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.argumenttype.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.before.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.colon.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.defaultvalue.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.getargumenttype.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.getdefaultvalue.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.getname.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.getnametoken.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.isfunctionparameter.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.name.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.functionparameter.type.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.iscustomfunction.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.isfunctionparameter.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.nodetype.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.parse.md create mode 100644 packages/custom-function-parser/docs/custom-function-parser.parsefromtokens.md create mode 100644 packages/custom-function-parser/docs/index.md create mode 100644 packages/custom-function-parser/test/parse-prelude/0001.mjs delete mode 100644 plugins/postcss-custom-functions/dist/index.cjs create mode 100644 plugins/postcss-custom-functions/src/transpile.ts create mode 100644 plugins/postcss-custom-functions/test/_browser.mjs create mode 100644 plugins/postcss-custom-functions/test/basic.preserve-true.expect.css create mode 100644 plugins/postcss-custom-functions/test/conditionals.css create mode 100644 plugins/postcss-custom-functions/test/conditionals.expect.css create mode 100644 plugins/postcss-custom-functions/test/ignore.css create mode 100644 plugins/postcss-custom-functions/test/invalid.css create mode 100644 plugins/postcss-custom-functions/test/invalid.expect.css create mode 100644 plugins/postcss-custom-functions/test/nested.css create mode 100644 plugins/postcss-custom-functions/test/nested.expect.css create mode 100644 plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html create mode 100644 plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html create mode 100644 plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html create mode 100644 plugins/postcss-custom-functions/test/wpt/function-conditionals.html create mode 100644 plugins/postcss-custom-functions/test/wpt/local-var-substitution.html diff --git a/package-lock.json b/package-lock.json index 41d195ff99..0e11c54341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7607,7 +7607,7 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.1", @@ -8578,14 +8578,14 @@ "@csstools/cascade-layer-name-parser": "^3.0.1", "@csstools/css-parser-algorithms": "^4.0.1", "@csstools/css-tokenizer": "^4.0.2", - "@csstools/custom-function-parser": "^0.0.0", - "@csstools/utilities": "^3.0.0" + "@csstools/custom-function-parser": "^0.0.0" }, "devDependencies": { - "@csstools/postcss-tape": "*" + "@csstools/postcss-tape": "*", + "puppeteer": "^25.11.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" diff --git a/packages/custom-function-parser/dist/index.cjs b/packages/custom-function-parser/dist/index.cjs deleted file mode 100644 index 2c750baead..0000000000 --- a/packages/custom-function-parser/dist/index.cjs +++ /dev/null @@ -1 +0,0 @@ -"use strict";var e,t=require("@csstools/css-parser-algorithms"),n=require("@csstools/css-tokenizer");exports.NodeType=void 0,(e=exports.NodeType||(exports.NodeType={})).CustomFunction="custom-function",e.FunctionParameter="function-parameter";class CustomFunction{type=exports.NodeType.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,n,r,o=[],s=[]){this.function=e,this.parameters=t,this.returnsKeyword=n,this.returnType=r,this.before=o,this.after=s}getName(){return this.function.getName()}getReturnType(){const e=t.parseListOfComponentValues(this.returnType).filter((e=>!t.isWhiteSpaceOrCommentNode(e)));return 1===e.length&&t.isFunctionNode(e[0])&&"type"===e[0].getName().toLowerCase()?n.stringify(...e[0].value.flatMap((e=>e.tokens())).filter((e=>!n.isTokenComment(e)))).trim():n.stringify(...this.returnType.filter((e=>!n.isTokenComment(e)))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return n.stringify(...this.before)+n.stringify(...this.function.tokens())+n.stringify(...this.returnsKeyword)+n.stringify(...this.returnType)+n.stringify(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((e=>e.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&(e instanceof CustomFunction&&e.type===exports.NodeType.CustomFunction)}}class FunctionParameter{type=exports.NodeType.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,n,r,o=[],s=[]){this.name=e,this.argumentType=t,this.colon=n,this.defaultValue=r,this.before=o,this.after=s}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return n.stringify(...this.argumentType.filter((e=>!n.isTokenComment(e)))).trim()}getDefaultValue(){return n.stringify(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&(e instanceof FunctionParameter&&e.type===exports.NodeType.FunctionParameter)}}function parseCustomFunction(e){let r=null,o=-1,s=-1,i=-1,a=e.length;for(let a=0;ai;n--){const r=e[n];if(!t.isWhiteSpaceOrCommentNode(r))break;a=n}const u=parseFunctionParameters(r.value);return!!u&&new CustomFunction(r,u,-1===i?[]:e.slice(o+1,i).flatMap((e=>e.tokens())),-1===i?[]:e.slice(i,a).flatMap((e=>e.tokens())),e.slice(0,o).flatMap((e=>e.tokens())),e.slice(a).flatMap((e=>e.tokens())))}function parseFunctionParameters(e){if(!e.some((e=>!t.isWhiteSpaceOrCommentNode(e))))return[];const n=[];for(let t=0;t0&&t.isTokenNode(m)&&n.isTokenDelim(m.value)&&">"===m.value[4].value)r--;else if(-1===i&&0===r&&t.isTokenNode(m)&&n.isTokenColon(m.value))i=l,a=m.value;else if(-1===i){if(-1!==i)return!1}else u=l}}if(!s)return!1;if(-1!==i&&-1===u)return!1;const m=-1!==i?i-1:l-1;for(let n=e.length-1;n>Math.max(m,u);n--){const r=e[n];if(!t.isWhitespaceNode(r))break;c=n}let f=[];{const n=e.slice(o+1,m+1),r=n.filter((e=>!t.isWhiteSpaceOrCommentNode(e)));f=1===r.length&&t.isFunctionNode(r[0])&&"type"===r[0].getName().toLowerCase()?r[0].value.flatMap((e=>e.tokens())):n.flatMap((e=>e.tokens()))}let p=[];if(-1!==u){const r=e.slice(i+1,c),o=r.filter((e=>!t.isWhiteSpaceOrCommentNode(e)));p=1===o.length&&t.isSimpleBlockNode(o[0])&&n.isTokenOpenCurly(o[0].startToken)?o[0].value.flatMap((e=>e.tokens())):r.flatMap((e=>e.tokens()))}return{advance:l,node:new FunctionParameter(s,f,a,p,e.slice(0,o).flatMap((e=>e.tokens())),e.slice(c,l+1).flatMap((e=>e.tokens())))}}function parseFromTokens(e,n){return parseCustomFunction(t.parseListOfComponentValues(e,{onParseError:n?.onParseError}))}exports.CustomFunction=CustomFunction,exports.FunctionParameter=FunctionParameter,exports.isCustomFunction=function isCustomFunction(e){return CustomFunction.isCustomFunction(e)},exports.isFunctionParameter=function isFunctionParameter(e){return FunctionParameter.isFunctionParameter(e)},exports.parse=function parse(e,t){return parseFromTokens(n.tokenize({css:e},{onParseError:t?.onParseError}),t)},exports.parseFromTokens=parseFromTokens; diff --git a/packages/custom-function-parser/dist/index.mjs b/packages/custom-function-parser/dist/index.mjs index e5aacd84b8..4a0221b0bb 100644 --- a/packages/custom-function-parser/dist/index.mjs +++ b/packages/custom-function-parser/dist/index.mjs @@ -1 +1 @@ -import{parseListOfComponentValues as t,isWhiteSpaceOrCommentNode as e,isFunctionNode as r,isTokenNode as n,isWhitespaceNode as s,isSimpleBlockNode as o}from"@csstools/css-parser-algorithms";import{stringify as a,isTokenComment as i,isTokenIdent as u,isTokenComma as l,isTokenDelim as c,isTokenColon as f,isTokenOpenCurly as m,tokenize as h}from"@csstools/css-tokenizer";var p;!function(t){t.CustomFunction="custom-function",t.FunctionParameter="function-parameter"}(p||(p={}));class CustomFunction{type=p.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(t,e,r,n,s=[],o=[]){this.function=t,this.parameters=e,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const n=t(this.returnType).filter((t=>!e(t)));return 1===n.length&&r(n[0])&&"type"===n[0].getName().toLowerCase()?a(...n[0].value.flatMap((t=>t.tokens())).filter((t=>!i(t)))).trim():a(...this.returnType.filter((t=>!i(t)))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return a(...this.before)+a(...this.function.tokens())+a(...this.returnsKeyword)+a(...this.returnType)+a(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map((t=>t.toJSON())),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(t){return!!t&&(t instanceof CustomFunction&&t.type===p.CustomFunction)}}class FunctionParameter{type=p.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(t,e,r,n,s=[],o=[]){this.name=t,this.argumentType=e,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return a(...this.argumentType.filter((t=>!i(t)))).trim()}getDefaultValue(){return a(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(t){return!!t&&(t instanceof FunctionParameter&&t.type===p.FunctionParameter)}}function parseCustomFunction(t){let s=null,o=-1,a=-1,i=-1,l=t.length;for(let l=0;li;r--){const n=t[r];if(!e(n))break;l=r}const c=parseFunctionParameters(s.value);return!!c&&new CustomFunction(s,c,-1===i?[]:t.slice(o+1,i).flatMap((t=>t.tokens())),-1===i?[]:t.slice(i,l).flatMap((t=>t.tokens())),t.slice(0,o).flatMap((t=>t.tokens())),t.slice(l).flatMap((t=>t.tokens())))}function parseFunctionParameters(t){if(!t.some((t=>!e(t))))return[];const r=[];for(let e=0;e0&&n(r)&&c(r.value)&&">"===r.value[4].value)a--;else if(-1===p&&0===a&&n(r)&&f(r.value))p=k,F=r.value;else if(-1===p){if(-1!==p)return!1}else g=k}}if(!h)return!1;if(-1!==p&&-1===g)return!1;const v=-1!==p?p-1:k-1;for(let e=t.length-1;e>Math.max(v,g);e--){const r=t[e];if(!s(r))break;y=e}let P=[];{const n=t.slice(i+1,v+1),s=n.filter((t=>!e(t)));P=1===s.length&&r(s[0])&&"type"===s[0].getName().toLowerCase()?s[0].value.flatMap((t=>t.tokens())):n.flatMap((t=>t.tokens()))}let C=[];if(-1!==g){const r=t.slice(p+1,y),n=r.filter((t=>!e(t)));C=1===n.length&&o(n[0])&&m(n[0].startToken)?n[0].value.flatMap((t=>t.tokens())):r.flatMap((t=>t.tokens()))}return{advance:k,node:new FunctionParameter(h,P,F,C,t.slice(0,i).flatMap((t=>t.tokens())),t.slice(y,k+1).flatMap((t=>t.tokens())))}}function parseFromTokens(e,r){return parseCustomFunction(t(e,{onParseError:r?.onParseError}))}function parse(t,e){return parseFromTokens(h({css:t},{onParseError:e?.onParseError}),e)}function isCustomFunction(t){return CustomFunction.isCustomFunction(t)}function isFunctionParameter(t){return FunctionParameter.isFunctionParameter(t)}export{CustomFunction,FunctionParameter,p as NodeType,isCustomFunction,isFunctionParameter,parse,parseFromTokens}; +import{parseListOfComponentValues as t,isWhiteSpaceOrCommentNode as e,isFunctionNode as r,isTokenNode as n,isWhitespaceNode as s,isSimpleBlockNode as o}from"@csstools/css-parser-algorithms";import{stringify as a,isTokenComment as i,isTokenIdent as u,isTokenComma as l,isTokenDelim as c,isTokenColon as f,isTokenOpenCurly as m,tokenize as h}from"@csstools/css-tokenizer";var p;!function(t){t.CustomFunction="custom-function",t.FunctionParameter="function-parameter"}(p||(p={}));class CustomFunction{type=p.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(t,e,r,n,s=[],o=[]){this.function=t,this.parameters=e,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const n=t(this.returnType).filter(t=>!e(t));return 1===n.length&&r(n[0])&&"type"===n[0].getName().toLowerCase()?a(...n[0].value.flatMap(t=>t.tokens()).filter(t=>!i(t))).trim():a(...this.returnType.filter(t=>!i(t))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return a(...this.before)+a(...this.function.tokens())+a(...this.returnsKeyword)+a(...this.returnType)+a(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(t=>t.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(t){return!!t&&(t instanceof CustomFunction&&t.type===p.CustomFunction)}}class FunctionParameter{type=p.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(t,e,r,n,s=[],o=[]){this.name=t,this.argumentType=e,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return a(...this.argumentType.filter(t=>!i(t))).trim()}getDefaultValue(){return a(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(t){return!!t&&(t instanceof FunctionParameter&&t.type===p.FunctionParameter)}}function parseCustomFunction(t){let s=null,o=-1,a=-1,i=-1,l=t.length;for(let l=0;li;r--){const n=t[r];if(!e(n))break;l=r}const c=parseFunctionParameters(s.value);return!!c&&new CustomFunction(s,c,-1===i?[]:t.slice(o+1,i).flatMap(t=>t.tokens()),-1===i?[]:t.slice(i,l).flatMap(t=>t.tokens()),t.slice(0,o).flatMap(t=>t.tokens()),t.slice(l).flatMap(t=>t.tokens()))}function parseFunctionParameters(t){if(!t.some(t=>!e(t)))return[];const r=[],n=new Set;for(let e=0;e0&&n(r)&&c(r.value)&&">"===r.value[4].value)a--;else if(-1===p&&0===a&&n(r)&&f(r.value))p=y,F=r.value;else if(-1===p){if(-1!==p)return!1}else g=y}}if(!h)return!1;if(-1!==p&&-1===g)return!1;const k=-1!==p?p-1:y-1;for(let e=t.length-1;e>Math.max(k,g);e--){const r=t[e];if(!s(r))break;v=e}let P;{const n=t.slice(i+1,k+1),s=n.filter(t=>!e(t));P=1===s.length&&r(s[0])&&"type"===s[0].getName().toLowerCase()?s[0].value.flatMap(t=>t.tokens()):n.flatMap(t=>t.tokens())}let T=[];if(-1!==g){const r=t.slice(p+1,v);if(hasTopLevelImportant(r))return!1;const n=r.filter(t=>!e(t));T=1===n.length&&o(n[0])&&m(n[0].startToken)?n[0].value.flatMap(t=>t.tokens()):r.flatMap(t=>t.tokens())}return{advance:y,node:new FunctionParameter(h,P,F,T,t.slice(0,i).flatMap(t=>t.tokens()),t.slice(v,y+1).flatMap(t=>t.tokens()))}}function hasTopLevelImportant(t){for(let r=0;r{n=!0,r?.onParseError?.(t)}});return!n&&parseCustomFunction(s)}function parse(t,e){return parseFromTokens(h({css:t},{onParseError:e?.onParseError}),e)}function isCustomFunction(t){return CustomFunction.isCustomFunction(t)}function isFunctionParameter(t){return FunctionParameter.isFunctionParameter(t)}export{CustomFunction,FunctionParameter,p as NodeType,isCustomFunction,isFunctionParameter,parse,parseFromTokens}; diff --git a/packages/custom-function-parser/docs/custom-function-parser.api.json b/packages/custom-function-parser/docs/custom-function-parser.api.json index 078502edd3..f8cbd831d0 100644 --- a/packages/custom-function-parser/docs/custom-function-parser.api.json +++ b/packages/custom-function-parser/docs/custom-function-parser.api.json @@ -113,6 +113,22 @@ "tagName": "@virtual", "syntaxKind": "modifier" }, + { + "tagName": "@jsx", + "syntaxKind": "block" + }, + { + "tagName": "@jsxRuntime", + "syntaxKind": "block" + }, + { + "tagName": "@jsxFrag", + "syntaxKind": "block" + }, + { + "tagName": "@jsxImportSource", + "syntaxKind": "block" + }, { "tagName": "@betaDocumentation", "syntaxKind": "modifier" diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction._constructor_.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction._constructor_.md new file mode 100644 index 0000000000..8ebf12afe3 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction._constructor_.md @@ -0,0 +1,122 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [(constructor)](./custom-function-parser.customfunction._constructor_.md) + +## CustomFunction.(constructor) + +Constructs a new instance of the `CustomFunction` class + +**Signature:** + +```typescript +constructor(fn: FunctionNode, parameters: Array, returnsKeyword: Array, returnType: Array, before?: Array, after?: Array); +``` + +## Parameters + + + + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +fn + + + + +FunctionNode + + + + + +
+ +parameters + + + + +Array<[FunctionParameter](./custom-function-parser.functionparameter.md)> + + + + + +
+ +returnsKeyword + + + + +Array<CSSToken> + + + + + +
+ +returnType + + + + +Array<CSSToken> + + + + + +
+ +before + + + + +Array<CSSToken> + + + + +_(Optional)_ + + +
+ +after + + + + +Array<CSSToken> + + + + +_(Optional)_ + + +
+ diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.after.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.after.md new file mode 100644 index 0000000000..b4a8560637 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.after.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [after](./custom-function-parser.customfunction.after.md) + +## CustomFunction.after property + +**Signature:** + +```typescript +after: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.before.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.before.md new file mode 100644 index 0000000000..6833cca8a9 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.before.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [before](./custom-function-parser.customfunction.before.md) + +## CustomFunction.before property + +**Signature:** + +```typescript +before: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.function.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.function.md new file mode 100644 index 0000000000..0f0d92f5d4 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.function.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [function](./custom-function-parser.customfunction.function.md) + +## CustomFunction.function property + +**Signature:** + +```typescript +function: FunctionNode; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.getname.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.getname.md new file mode 100644 index 0000000000..33eac18b13 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.getname.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [getName](./custom-function-parser.customfunction.getname.md) + +## CustomFunction.getName() method + +**Signature:** + +```typescript +getName(): string; +``` +**Returns:** + +string + diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.getreturntype.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.getreturntype.md new file mode 100644 index 0000000000..941629802b --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.getreturntype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [getReturnType](./custom-function-parser.customfunction.getreturntype.md) + +## CustomFunction.getReturnType() method + +**Signature:** + +```typescript +getReturnType(): string; +``` +**Returns:** + +string + diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.iscustomfunction.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.iscustomfunction.md new file mode 100644 index 0000000000..83c83f3adc --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.iscustomfunction.md @@ -0,0 +1,50 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [isCustomFunction](./custom-function-parser.customfunction.iscustomfunction.md) + +## CustomFunction.isCustomFunction() method + +**Signature:** + +```typescript +static isCustomFunction(x: unknown): x is CustomFunction; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +x + + + + +unknown + + + + + +
+ +**Returns:** + +x is [CustomFunction](./custom-function-parser.customfunction.md) + diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.md new file mode 100644 index 0000000000..fe94f0b56a --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.md @@ -0,0 +1,272 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) + +## CustomFunction class + +**Signature:** + +```typescript +export declare class CustomFunction +``` + +## Constructors + + + +
+ +Constructor + + + + +Modifiers + + + + +Description + + +
+ +[(constructor)(fn, parameters, returnsKeyword, returnType, before, after)](./custom-function-parser.customfunction._constructor_.md) + + + + + + + +Constructs a new instance of the `CustomFunction` class + + +
+ +## Properties + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[after](./custom-function-parser.customfunction.after.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[before](./custom-function-parser.customfunction.before.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[function](./custom-function-parser.customfunction.function.md) + + + + + + + +FunctionNode + + + + + +
+ +[parameters](./custom-function-parser.customfunction.parameters.md) + + + + + + + +Array<[FunctionParameter](./custom-function-parser.functionparameter.md)> + + + + + +
+ +[returnsKeyword](./custom-function-parser.customfunction.returnskeyword.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[returnType](./custom-function-parser.customfunction.returntype.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[type](./custom-function-parser.customfunction.type.md) + + + + + + + +[NodeType](./custom-function-parser.nodetype.md) + + + + + +
+ +## Methods + + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[getName()](./custom-function-parser.customfunction.getname.md) + + + + + + + + +
+ +[getReturnType()](./custom-function-parser.customfunction.getreturntype.md) + + + + + + + + +
+ +[isCustomFunction(x)](./custom-function-parser.customfunction.iscustomfunction.md) + + + + +`static` + + + + + +
+ +[tokens()](./custom-function-parser.customfunction.tokens.md) + + + + + + + + +
+ +[toString()](./custom-function-parser.customfunction.tostring.md) + + + + + + + + +
+ diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.parameters.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.parameters.md new file mode 100644 index 0000000000..0514a6bd4f --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.parameters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [parameters](./custom-function-parser.customfunction.parameters.md) + +## CustomFunction.parameters property + +**Signature:** + +```typescript +parameters: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.returnskeyword.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.returnskeyword.md new file mode 100644 index 0000000000..ca45873c36 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.returnskeyword.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [returnsKeyword](./custom-function-parser.customfunction.returnskeyword.md) + +## CustomFunction.returnsKeyword property + +**Signature:** + +```typescript +returnsKeyword: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.returntype.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.returntype.md new file mode 100644 index 0000000000..d182ee4c09 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.returntype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [returnType](./custom-function-parser.customfunction.returntype.md) + +## CustomFunction.returnType property + +**Signature:** + +```typescript +returnType: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.tokens.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.tokens.md new file mode 100644 index 0000000000..f2724c1f23 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.tokens.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [tokens](./custom-function-parser.customfunction.tokens.md) + +## CustomFunction.tokens() method + +**Signature:** + +```typescript +tokens(): Array; +``` +**Returns:** + +Array<CSSToken> + diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.tostring.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.tostring.md new file mode 100644 index 0000000000..a55569c78f --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.tostring.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [toString](./custom-function-parser.customfunction.tostring.md) + +## CustomFunction.toString() method + +**Signature:** + +```typescript +toString(): string; +``` +**Returns:** + +string + diff --git a/packages/custom-function-parser/docs/custom-function-parser.customfunction.type.md b/packages/custom-function-parser/docs/custom-function-parser.customfunction.type.md new file mode 100644 index 0000000000..0fec80e0a6 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.customfunction.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [CustomFunction](./custom-function-parser.customfunction.md) > [type](./custom-function-parser.customfunction.type.md) + +## CustomFunction.type property + +**Signature:** + +```typescript +type: NodeType; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter._constructor_.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter._constructor_.md new file mode 100644 index 0000000000..8cf69f0606 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter._constructor_.md @@ -0,0 +1,122 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [(constructor)](./custom-function-parser.functionparameter._constructor_.md) + +## FunctionParameter.(constructor) + +Constructs a new instance of the `FunctionParameter` class + +**Signature:** + +```typescript +constructor(name: TokenIdent, argumentType: Array, colon: TokenColon | null, defaultValue: Array, before?: Array, after?: Array); +``` + +## Parameters + + + + + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +name + + + + +TokenIdent + + + + + +
+ +argumentType + + + + +Array<CSSToken> + + + + + +
+ +colon + + + + +TokenColon \| null + + + + + +
+ +defaultValue + + + + +Array<CSSToken> + + + + + +
+ +before + + + + +Array<CSSToken> + + + + +_(Optional)_ + + +
+ +after + + + + +Array<CSSToken> + + + + +_(Optional)_ + + +
+ diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.after.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.after.md new file mode 100644 index 0000000000..cf92b817df --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.after.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [after](./custom-function-parser.functionparameter.after.md) + +## FunctionParameter.after property + +**Signature:** + +```typescript +after: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.argumenttype.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.argumenttype.md new file mode 100644 index 0000000000..f88fae04de --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.argumenttype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [argumentType](./custom-function-parser.functionparameter.argumenttype.md) + +## FunctionParameter.argumentType property + +**Signature:** + +```typescript +argumentType: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.before.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.before.md new file mode 100644 index 0000000000..13491a768a --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.before.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [before](./custom-function-parser.functionparameter.before.md) + +## FunctionParameter.before property + +**Signature:** + +```typescript +before: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.colon.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.colon.md new file mode 100644 index 0000000000..36490c65b4 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.colon.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [colon](./custom-function-parser.functionparameter.colon.md) + +## FunctionParameter.colon property + +**Signature:** + +```typescript +colon: TokenColon | null; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.defaultvalue.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.defaultvalue.md new file mode 100644 index 0000000000..f0c780b6e1 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.defaultvalue.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [defaultValue](./custom-function-parser.functionparameter.defaultvalue.md) + +## FunctionParameter.defaultValue property + +**Signature:** + +```typescript +defaultValue: Array; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getargumenttype.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getargumenttype.md new file mode 100644 index 0000000000..9560ffed54 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getargumenttype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [getArgumentType](./custom-function-parser.functionparameter.getargumenttype.md) + +## FunctionParameter.getArgumentType() method + +**Signature:** + +```typescript +getArgumentType(): string; +``` +**Returns:** + +string + diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getdefaultvalue.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getdefaultvalue.md new file mode 100644 index 0000000000..4e44b65697 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getdefaultvalue.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [getDefaultValue](./custom-function-parser.functionparameter.getdefaultvalue.md) + +## FunctionParameter.getDefaultValue() method + +**Signature:** + +```typescript +getDefaultValue(): string; +``` +**Returns:** + +string + diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getname.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getname.md new file mode 100644 index 0000000000..d14560841e --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getname.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [getName](./custom-function-parser.functionparameter.getname.md) + +## FunctionParameter.getName() method + +**Signature:** + +```typescript +getName(): string; +``` +**Returns:** + +string + diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getnametoken.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getnametoken.md new file mode 100644 index 0000000000..2d5d5bb350 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.getnametoken.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [getNameToken](./custom-function-parser.functionparameter.getnametoken.md) + +## FunctionParameter.getNameToken() method + +**Signature:** + +```typescript +getNameToken(): TokenIdent | null; +``` +**Returns:** + +TokenIdent \| null + diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.isfunctionparameter.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.isfunctionparameter.md new file mode 100644 index 0000000000..745342d235 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.isfunctionparameter.md @@ -0,0 +1,50 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [isFunctionParameter](./custom-function-parser.functionparameter.isfunctionparameter.md) + +## FunctionParameter.isFunctionParameter() method + +**Signature:** + +```typescript +static isFunctionParameter(x: unknown): x is FunctionParameter; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +x + + + + +unknown + + + + + +
+ +**Returns:** + +x is [FunctionParameter](./custom-function-parser.functionparameter.md) + diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.md new file mode 100644 index 0000000000..6c48fff0e9 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.md @@ -0,0 +1,272 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) + +## FunctionParameter class + +**Signature:** + +```typescript +export declare class FunctionParameter +``` + +## Constructors + + + +
+ +Constructor + + + + +Modifiers + + + + +Description + + +
+ +[(constructor)(name, argumentType, colon, defaultValue, before, after)](./custom-function-parser.functionparameter._constructor_.md) + + + + + + + +Constructs a new instance of the `FunctionParameter` class + + +
+ +## Properties + + + + + + + + + +
+ +Property + + + + +Modifiers + + + + +Type + + + + +Description + + +
+ +[after](./custom-function-parser.functionparameter.after.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[argumentType](./custom-function-parser.functionparameter.argumenttype.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[before](./custom-function-parser.functionparameter.before.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[colon](./custom-function-parser.functionparameter.colon.md) + + + + + + + +TokenColon \| null + + + + + +
+ +[defaultValue](./custom-function-parser.functionparameter.defaultvalue.md) + + + + + + + +Array<CSSToken> + + + + + +
+ +[name](./custom-function-parser.functionparameter.name.md) + + + + + + + +TokenIdent + + + + + +
+ +[type](./custom-function-parser.functionparameter.type.md) + + + + + + + +[NodeType](./custom-function-parser.nodetype.md) + + + + + +
+ +## Methods + + + + + + + +
+ +Method + + + + +Modifiers + + + + +Description + + +
+ +[getArgumentType()](./custom-function-parser.functionparameter.getargumenttype.md) + + + + + + + + +
+ +[getDefaultValue()](./custom-function-parser.functionparameter.getdefaultvalue.md) + + + + + + + + +
+ +[getName()](./custom-function-parser.functionparameter.getname.md) + + + + + + + + +
+ +[getNameToken()](./custom-function-parser.functionparameter.getnametoken.md) + + + + + + + + +
+ +[isFunctionParameter(x)](./custom-function-parser.functionparameter.isfunctionparameter.md) + + + + +`static` + + + + + +
+ diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.name.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.name.md new file mode 100644 index 0000000000..3140600220 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [name](./custom-function-parser.functionparameter.name.md) + +## FunctionParameter.name property + +**Signature:** + +```typescript +name: TokenIdent; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.functionparameter.type.md b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.type.md new file mode 100644 index 0000000000..2cfb3d496a --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.functionparameter.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [FunctionParameter](./custom-function-parser.functionparameter.md) > [type](./custom-function-parser.functionparameter.type.md) + +## FunctionParameter.type property + +**Signature:** + +```typescript +type: NodeType; +``` diff --git a/packages/custom-function-parser/docs/custom-function-parser.iscustomfunction.md b/packages/custom-function-parser/docs/custom-function-parser.iscustomfunction.md new file mode 100644 index 0000000000..3885c62b7a --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.iscustomfunction.md @@ -0,0 +1,50 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [isCustomFunction](./custom-function-parser.iscustomfunction.md) + +## isCustomFunction() function + +**Signature:** + +```typescript +export declare function isCustomFunction(x: unknown): x is CustomFunction; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +x + + + + +unknown + + + + + +
+ +**Returns:** + +x is [CustomFunction](./custom-function-parser.customfunction.md) + diff --git a/packages/custom-function-parser/docs/custom-function-parser.isfunctionparameter.md b/packages/custom-function-parser/docs/custom-function-parser.isfunctionparameter.md new file mode 100644 index 0000000000..5e7b2beab5 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.isfunctionparameter.md @@ -0,0 +1,50 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [isFunctionParameter](./custom-function-parser.isfunctionparameter.md) + +## isFunctionParameter() function + +**Signature:** + +```typescript +export declare function isFunctionParameter(x: unknown): x is FunctionParameter; +``` + +## Parameters + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +x + + + + +unknown + + + + + +
+ +**Returns:** + +x is [FunctionParameter](./custom-function-parser.functionparameter.md) + diff --git a/packages/custom-function-parser/docs/custom-function-parser.md b/packages/custom-function-parser/docs/custom-function-parser.md new file mode 100644 index 0000000000..1219811293 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.md @@ -0,0 +1,114 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) + +## custom-function-parser package + +## Classes + + + + +
+ +Class + + + + +Description + + +
+ +[CustomFunction](./custom-function-parser.customfunction.md) + + + + + +
+ +[FunctionParameter](./custom-function-parser.functionparameter.md) + + + + + +
+ +## Enumerations + + + +
+ +Enumeration + + + + +Description + + +
+ +[NodeType](./custom-function-parser.nodetype.md) + + + + + +
+ +## Functions + + + + + + +
+ +Function + + + + +Description + + +
+ +[isCustomFunction(x)](./custom-function-parser.iscustomfunction.md) + + + + + +
+ +[isFunctionParameter(x)](./custom-function-parser.isfunctionparameter.md) + + + + + +
+ +[parse(source, options)](./custom-function-parser.parse.md) + + + + + +
+ +[parseFromTokens(tokens, options)](./custom-function-parser.parsefromtokens.md) + + + + + +
+ diff --git a/packages/custom-function-parser/docs/custom-function-parser.nodetype.md b/packages/custom-function-parser/docs/custom-function-parser.nodetype.md new file mode 100644 index 0000000000..e6ada40558 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.nodetype.md @@ -0,0 +1,60 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [NodeType](./custom-function-parser.nodetype.md) + +## NodeType enum + +**Signature:** + +```typescript +export declare enum NodeType +``` + +## Enumeration Members + + + + +
+ +Member + + + + +Value + + + + +Description + + +
+ +CustomFunction + + + + +`"custom-function"` + + + + + +
+ +FunctionParameter + + + + +`"function-parameter"` + + + + + +
+ diff --git a/packages/custom-function-parser/docs/custom-function-parser.parse.md b/packages/custom-function-parser/docs/custom-function-parser.parse.md new file mode 100644 index 0000000000..9fa52388a0 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.parse.md @@ -0,0 +1,68 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [parse](./custom-function-parser.parse.md) + +## parse() function + +**Signature:** + +```typescript +export declare function parse(source: string, options?: { + onParseError?: (error: ParseError) => void; +}): CustomFunction | false; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +source + + + + +string + + + + + +
+ +options + + + + +{ onParseError?: (error: ParseError) => void; } + + + + +_(Optional)_ + + +
+ +**Returns:** + +[CustomFunction](./custom-function-parser.customfunction.md) \| false + diff --git a/packages/custom-function-parser/docs/custom-function-parser.parsefromtokens.md b/packages/custom-function-parser/docs/custom-function-parser.parsefromtokens.md new file mode 100644 index 0000000000..f51e42ef37 --- /dev/null +++ b/packages/custom-function-parser/docs/custom-function-parser.parsefromtokens.md @@ -0,0 +1,68 @@ + + +[Home](./index.md) > [@csstools/custom-function-parser](./custom-function-parser.md) > [parseFromTokens](./custom-function-parser.parsefromtokens.md) + +## parseFromTokens() function + +**Signature:** + +```typescript +export declare function parseFromTokens(tokens: Array, options?: { + onParseError?: (error: ParseError) => void; +}): CustomFunction | false; +``` + +## Parameters + + + + +
+ +Parameter + + + + +Type + + + + +Description + + +
+ +tokens + + + + +Array<CSSToken> + + + + + +
+ +options + + + + +{ onParseError?: (error: ParseError) => void; } + + + + +_(Optional)_ + + +
+ +**Returns:** + +[CustomFunction](./custom-function-parser.customfunction.md) \| false + diff --git a/packages/custom-function-parser/docs/index.md b/packages/custom-function-parser/docs/index.md new file mode 100644 index 0000000000..28cbb9d646 --- /dev/null +++ b/packages/custom-function-parser/docs/index.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + + + +
+ +Package + + + + +Description + + +
+ +[@csstools/custom-function-parser](./custom-function-parser.md) + + + + + +
+ diff --git a/packages/custom-function-parser/package.json b/packages/custom-function-parser/package.json index 8db875d13f..36aa089773 100644 --- a/packages/custom-function-parser/package.json +++ b/packages/custom-function-parser/package.json @@ -25,20 +25,13 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "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" - } + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" } }, "files": [ diff --git a/packages/custom-function-parser/src/parser/parse-custom-function.ts b/packages/custom-function-parser/src/parser/parse-custom-function.ts index 5be8af09dd..79419cb47a 100644 --- a/packages/custom-function-parser/src/parser/parse-custom-function.ts +++ b/packages/custom-function-parser/src/parser/parse-custom-function.ts @@ -97,6 +97,7 @@ function parseFunctionParameters(componentValues: Array): Array< } const parameters: Array = []; + const seenNames = new Set(); for (let i = 0; i < componentValues.length; i++) { const result = parseFunctionParameter( @@ -107,6 +108,14 @@ function parseFunctionParameters(componentValues: Array): Array< return false; } + const name = result.node.getName(); + if (seenNames.has(name)) { + // The same custom property name may not appear more than once. + return false; + } + + seenNames.add(name); + i += result.advance; parameters.push(result.node); continue; @@ -129,7 +138,7 @@ function parseFunctionParameter(componentValues: Array): { advan let afterIndex = componentValues.length; let i = 0; - for (i = 0; i < componentValues.length; i++) { + for (; i < componentValues.length; i++) { const componentValue = componentValues[i]; if (isWhiteSpaceOrCommentNode(componentValue)) { continue; @@ -152,7 +161,7 @@ function parseFunctionParameter(componentValues: Array): { advan ) { nameIndex = i; nameToken = componentValue.value; - continue + continue; } return false; @@ -222,7 +231,7 @@ function parseFunctionParameter(componentValues: Array): { advan break; } - let argumentType: Array = []; + let argumentType: Array; { const argumentTypeComponentValues = componentValues.slice(nameIndex + 1, typeEndIndex + 1); const meaningfulComponentValues = argumentTypeComponentValues.filter((x) => !isWhiteSpaceOrCommentNode(x)); @@ -233,17 +242,22 @@ function parseFunctionParameter(componentValues: Array): { advan ) { argumentType = meaningfulComponentValues[0].value.flatMap((x) => { return x.tokens(); - }) + }); } else { argumentType = argumentTypeComponentValues.flatMap((x) => { return x.tokens(); - }) + }); } } let defaultValue: Array = []; if (defaultValueIndex !== -1) { const defaultValueComponentValues = componentValues.slice(colonIndex + 1, afterIndex); + if (hasTopLevelImportant(defaultValueComponentValues)) { + // `!important` is not part of a . + return false; + } + const meaningfulComponentValues = defaultValueComponentValues.filter((x) => !isWhiteSpaceOrCommentNode(x)); if ( meaningfulComponentValues.length === 1 && @@ -252,11 +266,11 @@ function parseFunctionParameter(componentValues: Array): { advan ) { defaultValue = meaningfulComponentValues[0].value.flatMap((x) => { return x.tokens(); - }) + }); } else { defaultValue = defaultValueComponentValues.flatMap((x) => { return x.tokens(); - }) + }); } } @@ -276,3 +290,35 @@ function parseFunctionParameter(componentValues: Array): { advan ) }; } + +function hasTopLevelImportant(componentValues: Array): boolean { + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if ( + !isTokenNode(componentValue) || + !isTokenDelim(componentValue.value) || + componentValue.value[4].value !== '!' + ) { + continue; + } + + for (let j = i + 1; j < componentValues.length; j++) { + const next = componentValues[j]; + if (isWhiteSpaceOrCommentNode(next)) { + continue; + } + + if ( + isTokenNode(next) && + isTokenIdent(next.value) && + next.value[4].value.toLowerCase() === 'important' + ) { + return true; + } + + break; + } + } + + return false; +} diff --git a/packages/custom-function-parser/src/parser/parse.ts b/packages/custom-function-parser/src/parser/parse.ts index 0bfb93fd5a..d3c11fda29 100644 --- a/packages/custom-function-parser/src/parser/parse.ts +++ b/packages/custom-function-parser/src/parser/parse.ts @@ -10,10 +10,19 @@ export function parseFromTokens( onParseError?: (error: ParseError) => void }, ): CustomFunction | false { + let errored = false; + const componentValues = parseListOfComponentValues(tokens, { - onParseError: options?.onParseError, + onParseError: (error) => { + errored = true; + options?.onParseError?.(error); + }, }); + if (errored) { + return false; + } + return parseCustomFunction(componentValues); } diff --git a/packages/custom-function-parser/test/_import.mjs b/packages/custom-function-parser/test/_import.mjs index 3215dfbbbe..643f950dfe 100644 --- a/packages/custom-function-parser/test/_import.mjs +++ b/packages/custom-function-parser/test/_import.mjs @@ -1,3 +1,3 @@ -import { parse } from '@csstools/media-query-list-parser'; +import { parse } from '@csstools/custom-function-parser'; -parse('(min-width: 300px)'); +parse('--foo(--bar)'); diff --git a/packages/custom-function-parser/test/_require.cjs b/packages/custom-function-parser/test/_require.cjs index 09406cf784..43b8ffa196 100644 --- a/packages/custom-function-parser/test/_require.cjs +++ b/packages/custom-function-parser/test/_require.cjs @@ -1,3 +1,3 @@ -const { parse } = require('@csstools/media-query-list-parser'); +const { parse } = require('@csstools/custom-function-parser'); -parse('(min-width: 300px)'); +parse('--foo(--bar)'); diff --git a/packages/custom-function-parser/test/parse-prelude/0001.mjs b/packages/custom-function-parser/test/parse-prelude/0001.mjs new file mode 100644 index 0000000000..2562066923 --- /dev/null +++ b/packages/custom-function-parser/test/parse-prelude/0001.mjs @@ -0,0 +1,104 @@ +import assert from 'node:assert'; +import { parse } from '@csstools/custom-function-parser'; + +// Cases derived from: +// https://github.com/web-platform-tests/wpt/blob/master/css/css-mixins/functions/at-function-parsing.html +// +// Only the cases that do not require full `` syntax validation are +// asserted here. Type validation is part of the typed parameters iteration. + +const valid = [ + '--foo()', + '--foo( )', + '--foo(--x)', + '--foo( --x )', + '--foo(--x auto)', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x )', + '--foo(--x
-[] lets you easily create new plugins following some [CSS Specification]. +[] lets you use `@function` custom functions following [CSS Custom Functions and Mixins 1]. + +Custom functions are still an early draft. +This plugin is only a partial implementation to avoid conflicts with the final specification. + +Unsupported: +- typed parameters and return types +- default parameter values ```css @@ -51,4 +58,4 @@ is preserved. By default, it is not preserved. ``` -[CSS Specification]: +[CSS Custom Functions and Mixins 1]: diff --git a/plugins/postcss-custom-functions/package.json b/plugins/postcss-custom-functions/package.json index a797fd2157..ced258c15d 100644 --- a/plugins/postcss-custom-functions/package.json +++ b/plugins/postcss-custom-functions/package.json @@ -20,20 +20,13 @@ } ], "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "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" - } + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" } }, "files": [ @@ -46,14 +39,14 @@ "@csstools/cascade-layer-name-parser": "^3.0.1", "@csstools/css-parser-algorithms": "^4.0.1", "@csstools/css-tokenizer": "^4.0.2", - "@csstools/custom-function-parser": "^0.0.0", - "@csstools/utilities": "^3.0.0" + "@csstools/custom-function-parser": "^0.0.0" }, "peerDependencies": { "postcss": "^8.4" }, "devDependencies": { - "@csstools/postcss-tape": "*" + "@csstools/postcss-tape": "*", + "puppeteer": "^25.11.0" }, "scripts": { "build": "rollup -c ../../rollup/default.mjs", @@ -63,23 +56,22 @@ "test": "node --test", "test:rewrite-expects": "REWRITE_EXPECTS=true node --test" }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-functions#readme", "repository": { "type": "git", "url": "git+https://github.com/csstools/postcss-plugins.git", "directory": "plugins/postcss-custom-functions" }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", "keywords": [ "postcss-plugin" ], "csstools": { - "cssdbId": "TODO", "exportName": "postcssCustomFunctions", "humanReadableName": "PostCSS Custom Functions", "specUrl": "https://drafts.csswg.org/css-mixins-1/#function-rule" }, "volta": { "extends": "../../package.json" - }, - "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-functions#readme", - "bugs": "https://github.com/csstools/postcss-plugins/issues" + } } diff --git a/plugins/postcss-custom-functions/src/cascade-layers.ts b/plugins/postcss-custom-functions/src/cascade-layers.ts index 5d042e7850..ee81c1b221 100644 --- a/plugins/postcss-custom-functions/src/cascade-layers.ts +++ b/plugins/postcss-custom-functions/src/cascade-layers.ts @@ -125,5 +125,5 @@ function normalizeLayerName(layerName: string, counter: number): string { return layerName; } - return `csstools-anon-layer--${counter++}`; + return `csstools-anon-layer--${counter}`; } diff --git a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts index 3210463bb8..94db45dca5 100644 --- a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts +++ b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts @@ -1,22 +1,38 @@ import type { ChildNode, Container, Document, Result, Root as PostCSSRoot, AtRule } from 'postcss'; import { cascadeLayerNumberForNode, collectCascadeLayerOrder } from './cascade-layers'; import { isProcessableRule } from './is-processable-rule'; -import type { CustomFunction} from '@csstools/custom-function-parser'; +import type { CustomFunction } from '@csstools/custom-function-parser'; import { parse } from '@csstools/custom-function-parser'; export type CustomFunctionAndNode = { function: CustomFunction; node: AtRule; + supported: boolean; +}; + +/** + * A function is supported by this first iteration when it has no typed + * parameters, no default values and no return type. + */ +function isSupportedCustomFunction(customFunction: CustomFunction): boolean { + if (customFunction.getReturnType()) { + return false; + } + + return customFunction.parameters.every((parameter) => { + return !parameter.getArgumentType() && !parameter.getDefaultValue(); + }); } -// return custom functions from the css root, conditionally removing them +// Return custom functions from the css root, conditionally removing them. export function getCustomFunctions(root: PostCSSRoot, result: Result, opts: { preserve?: boolean }): Map { - // initialize custom selectors const customFunctions = new Map(); const customFunctionsCascadeLayerMapping: Map = new Map(); const cascadeLayersOrder = collectCascadeLayerOrder(root); + const removable: Array = []; + root.walkAtRules((atRule) => { if (!isProcessableRule(atRule)) { return; @@ -34,26 +50,33 @@ export function getCustomFunctions(root: PostCSSRoot, result: Result, opts: { pr } const name = customFunction.getName(); + const supported = isSupportedCustomFunction(customFunction); const thisCascadeLayer = cascadeLayerNumberForNode(atRule, cascadeLayersOrder); const existingCascadeLayer = customFunctionsCascadeLayerMapping.get(name) ?? -1; - if (thisCascadeLayer && thisCascadeLayer >= existingCascadeLayer) { + if (thisCascadeLayer >= existingCascadeLayer) { customFunctionsCascadeLayerMapping.set(name, thisCascadeLayer); - // re-parsing is important to obtain the correct AST shape customFunctions.set(name, { node: atRule, function: customFunction, + supported, }); } - if (!opts.preserve) { + if (supported) { + removable.push(atRule); + } + }); + + if (!opts.preserve) { + for (const atRule of removable) { const parent = atRule.parent; atRule.remove(); removeEmptyAncestorBlocks(parent); } - }); + } return customFunctions; } diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts index 80b513623e..16dabc08a4 100644 --- a/plugins/postcss-custom-functions/src/index.ts +++ b/plugins/postcss-custom-functions/src/index.ts @@ -1,11 +1,8 @@ -import type { AtRule, AtRuleProps, Declaration, Plugin, PluginCreator } from 'postcss'; +import type { Plugin, PluginCreator } from 'postcss'; import type { CustomFunctionAndNode } from './custom-functions-from-root'; import { getCustomFunctions } from './custom-functions-from-root'; -import { FunctionNode, isFunctionNode, parseCommaSeparatedListOfComponentValues, parseListOfComponentValues, replaceComponentValues, stringify, TokenNode } from '@csstools/css-parser-algorithms'; -import { isTokenFunction, tokenize, TokenType } from '@csstools/css-tokenizer'; -import type { ContainerWithChildren } from 'postcss/lib/container'; -import type { CustomFunction } from '@csstools/custom-function-parser'; import { isProcessableDeclaration } from './is-processable-declaration'; +import { CustomFunctionTranspiler } from './transpile'; /** postcss-custom-functions plugin options */ export type pluginOptions = { @@ -28,263 +25,32 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return { postcssPlugin: 'postcss-custom-functions', prepare(): Plugin { - // const transformedNodes = new WeakSet(); let customFunctions: Map = new Map(); - const callingContexts: Array = []; - - class CallingContext { - static counter = 0; - - id: string; - - rootElement: ContainerWithChildren; - element: ContainerWithChildren; - property: Declaration; - parent: CallingContext | null = null; - - function: FunctionNode; - customFunction: CustomFunction; - customFunctionBody: AtRule; - - registrations: Array = []; - - argumentRule: ContainerWithChildren; - bodyRule: ContainerWithChildren; - - resultIdent: string; - - varCounter: number = 0; - - invalid: boolean = false; - - // TODO: some ident mapping - - constructor( - rootElement: ContainerWithChildren, - element: ContainerWithChildren, - property: Declaration, - - fn: FunctionNode, - customFunction: CustomFunction, - customFunctionBody: AtRule, - - atRule: (defaults?: AtRuleProps) => AtRule, - - parent: CallingContext | null = null, - ) { - this.id = (CallingContext.counter++).toString(36); - - this.rootElement = rootElement; - this.element = element; - this.property = property; - - this.function = fn; - this.customFunction = customFunction; - this.customFunctionBody = customFunctionBody; - - this.parent = parent; - - { - const varCounterValue = (++this.varCounter).toString(36); - this.resultIdent = generateDashedIdent('custom-function-call', varCounterValue, this.id); - - const args = parseCommaSeparatedListOfComponentValues(this.function.value.flatMap(x => x.tokens())); - if (args.length > this.customFunction.parameters.length) { - this.invalid = true; - } - - this.customFunction.parameters.forEach((parameter) => { - const parameterName = parameter.getName(); - const parameterType = parameter.getArgumentType() || '*'; - - this.registrations.push(atRule({ - name: 'property', - params: generateDashedIdent('function-argument', parameterName, this.id), - nodes: [ - this.property.clone({ - prop: 'syntax', - value: `"${parameterType}"`, - }), - this.property.clone({ - prop: 'inherit', - value: "false", - }), - this.property.clone({ - prop: 'initial-value', - value: parameter.getDefaultValue(), - }) - ], - source: this.property.source - })); - - const bodyCustomProperty = atRule({ - name: 'property', - params: generateDashedIdent('function-body', parameterName, this.id), - nodes: [ - this.property.clone({ - prop: 'syntax', - value: '"*"', - }), - this.property.clone({ - prop: 'inherit', - value: "false", - }) - ], - source: this.property.source - }); - - this.registrations.push(bodyCustomProperty); - }); - - const returnType = this.customFunction.getReturnType(); - if (returnType) { - const customProperty = atRule({ - name: 'property', - params: generateDashedIdent('return-value', '', this.id), - nodes: [ - this.property.clone({ - prop: 'syntax', - value: `"${returnType}"`, - }), - this.property.clone({ - prop: 'inherit', - value: "false", - }) - ], - source: this.property.source - }); - - this.registrations.push(customProperty); - } - - this.argumentRule = this.element.clone({ nodes: [] }); - - this.customFunction.parameters.forEach((parameter, index) => { - const parameterName = parameter.getName(); - const defaultValue = parameter.getDefaultValue(); - - if (defaultValue) { - this.argumentRule.append(this.property.clone({ - prop: generateDashedIdent('function-argument', parameterName, this.id), - value: parameter.getDefaultValue(), - })); - } - - const argumentValue = args[index]; - // TODO: `{1, 2}` -> `1, 2` - if (argumentValue) { - this.argumentRule.append(this.property.clone({ - prop: generateDashedIdent('function-argument', parameterName, this.id), - value: stringify([argumentValue]), - })); - } - }); - - this.bodyRule = this.element.clone({ - nodes: [ - ...(this.customFunctionBody.nodes?.map((x) => x.clone()) ?? []) - ] - }); - - this.bodyRule.walkDecls(/^result$/i, (resultDecl) => { - resultDecl.prop = this.resultIdent; - }); - } - } - } + const transpiler = new CustomFunctionTranspiler(); return { postcssPlugin: 'postcss-custom-functions', Once(root, { result }): void { customFunctions = getCustomFunctions(root, result, { preserve: options.preserve }); + transpiler.setCustomFunctions(customFunctions); }, - OnceExit(root): void { - callingContexts.forEach((callingContext) => { - callingContext.registrations.forEach((registration) => { - root.append(registration); - }); - }); - }, - Declaration(decl, { atRule }): void { + Declaration(decl): void { if (!HAS_CUSTOM_FUNCTION.test(decl.value)) { return; } - const parent = decl.parent; - if (!parent) { - return; - } - if (!isProcessableDeclaration(decl)) { return; } - const tokens = tokenize({ css: decl.value }); - if (!tokens.some((x) => isTokenFunction(x) && x[4].value.startsWith('--'))) { - return; - } - - const transformed = replaceComponentValues( - [parseListOfComponentValues(tokens)], - (componentValue) => { - if (!isFunctionNode(componentValue)) { - return; - } - - const name = componentValue.getName(); - if (!name.startsWith('--')) { - return; - } - - const fn = customFunctions.get(name); - if (!fn) { - return; - } - - const callingContext = new CallingContext( - parent, - parent, - decl, - componentValue, - fn.function, - fn.node, - atRule, - ); - - if (callingContext.invalid) { - // guaranteed invalid - return new FunctionNode( - [TokenType.Function, 'var(', -1, -1, { value: 'var' }], - [TokenType.CloseParen, ')', -1, -1, undefined], - [], - ); - } - - parent.before(callingContext.argumentRule); - parent.before(callingContext.bodyRule); - callingContexts.push(callingContext); - - return new FunctionNode( - [TokenType.Function, 'var(', -1, -1, { value: 'var' }], - [TokenType.CloseParen, ')', -1, -1, undefined], - [ - new TokenNode( - [TokenType.Ident, callingContext.resultIdent, -1, -1, { value: callingContext.resultIdent }], - ) - ], - ); - } - ); - - const modified = stringify(transformed); - - if (modified === decl.value) { + const modified = transpiler.processDeclaration(decl); + if (modified === null) { return; } decl.cloneBefore({ value: modified }); - if (!options?.preserve) { + if (!options.preserve) { decl.remove(); } }, @@ -293,16 +59,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { }; }; -function generateDashedIdent(type: string, name: string, id: string): string { - const parts = [ - type, - id, - name, - ].filter((x) => !!x); - - return `--csstools-${parts.join('-')}`; -} - creator.postcss = true; export default creator; +export { creator as 'module.exports' }; diff --git a/plugins/postcss-custom-functions/src/is-processable-declaration.ts b/plugins/postcss-custom-functions/src/is-processable-declaration.ts index aa9a3ae4ff..ebdd42c5f4 100644 --- a/plugins/postcss-custom-functions/src/is-processable-declaration.ts +++ b/plugins/postcss-custom-functions/src/is-processable-declaration.ts @@ -1,8 +1,17 @@ import type { AtRule, ChildNode, Container, Declaration, Document } from 'postcss'; -const blockedParentAtRules = new Set(['keyframes']); +const blockedParentAtRules = new Set(['function', 'keyframes']); +const GENERATED_PREFIX = '--csstools-custom-function'; export function isProcessableDeclaration(decl: Declaration): boolean { + if (decl.prop.startsWith(GENERATED_PREFIX)) { + return false; + } + + if (!decl.parent || decl.parent.type !== 'rule') { + return false; + } + let parent: Container | Document | undefined = decl.parent; while (parent) { if (parent.type === 'atrule' && blockedParentAtRules.has((parent as AtRule).name.toLowerCase())) { diff --git a/plugins/postcss-custom-functions/src/is-processable-rule.ts b/plugins/postcss-custom-functions/src/is-processable-rule.ts index 881407f26a..06d5ea2752 100644 --- a/plugins/postcss-custom-functions/src/is-processable-rule.ts +++ b/plugins/postcss-custom-functions/src/is-processable-rule.ts @@ -2,7 +2,7 @@ import type { AtRule, ChildNode, Container, Document } from 'postcss'; const allowedParentAtRules = new Set(['scope', 'container', 'layer']); -export const IS_FUNCTION_REGEX = /^function$/i; +const IS_FUNCTION_REGEX = /^function$/i; export function isProcessableRule(atRule: AtRule): boolean { if (!IS_FUNCTION_REGEX.test(atRule.name)) { diff --git a/plugins/postcss-custom-functions/src/transpile.ts b/plugins/postcss-custom-functions/src/transpile.ts new file mode 100644 index 0000000000..86be579108 --- /dev/null +++ b/plugins/postcss-custom-functions/src/transpile.ts @@ -0,0 +1,418 @@ +import type { AtRule, ChildNode, Declaration, Rule } from 'postcss'; +import { Declaration as PostCSSDeclaration } from 'postcss'; +import type { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms'; +import { + FunctionNode as CSSAFunctionNode, + TokenNode as CSSATokenNode, + isFunctionNode, + isSimpleBlockNode, + isTokenNode, + isWhiteSpaceOrCommentNode, + parseCommaSeparatedListOfComponentValues, + parseListOfComponentValues, + replaceComponentValues, + stringify, +} from '@csstools/css-parser-algorithms'; +import { TokenType, isTokenFunction, isTokenIdent, isTokenOpenCurly, mutateIdent, tokenize } from '@csstools/css-tokenizer'; +import type { CustomFunctionAndNode } from './custom-functions-from-root'; + +const GENERATED_PREFIX = '--csstools-custom-function'; +const INVALID_IDENT = `${GENERATED_PREFIX}-invalid`; +const CSS_WIDE_KEYWORDS = new Set(['initial', 'inherit', 'unset', 'revert', 'revert-layer']); + +/** + * A single lexical scope while evaluating a custom function. + * + * Maps the authored custom property names (`--x`) that are visible in this + * scope to the generated, collision free names that are emitted into the CSS. + */ +class Scope { + names: Map = new Map(); + parent: Scope | null; + + constructor(parent: Scope | null) { + this.parent = parent; + } + + get(name: string): string | undefined { + const value = this.names.get(name); + if (value !== undefined) { + return value; + } + + return this.parent?.get(name); + } +} + +/** + * Transpiles `` calls into a form that works without native + * custom function support. + * + * The evaluation model follows the spec closely: the function body is emitted + * as custom property declarations on the element where the function is called, + * with a generated name per parameter, local variable and result. The browser + * then performs the actual `var()` substitution, cascade and conditional + * handling. + */ +export class CustomFunctionTranspiler { + private customFunctions: Map = new Map(); + private counter = 0; + private expanding: Array = []; + + setCustomFunctions(customFunctions: Map): void { + this.customFunctions = customFunctions; + } + + /** + * Replace all known custom function calls in a declaration value. + * Returns the new value, or `null` when nothing changed. + */ + processDeclaration(decl: Declaration): string | null { + const tokens = tokenize({ css: decl.value }); + if (!tokens.some((token) => isTokenFunction(token) && token[4].value.startsWith('--'))) { + return null; + } + + const parent = decl.parent; + if (!parent || parent.type !== 'rule') { + return null; + } + + const element = parent; + const componentValues = parseListOfComponentValues(tokens); + + replaceComponentValues([componentValues], (node) => { + if (!isFunctionNode(node)) { + return; + } + + const name = node.getName(); + if (!name.startsWith('--')) { + return; + } + + const entry = this.customFunctions.get(name); + if (!entry || !entry.supported) { + return; + } + + return this.processCall(node, element, new Scope(null)); + }); + + const modified = stringify([componentValues]); + if (modified === decl.value) { + return null; + } + + return modified; + } + + private processCall(fn: FunctionNode, element: Rule, parentScope: Scope): Array { + const name = fn.getName(); + const entry = this.customFunctions.get(name); + + if (!entry || !entry.supported) { + return [this.varReference(INVALID_IDENT)]; + } + + // A function that (indirectly) calls itself is cyclic and evaluates to + // the guaranteed-invalid value. + if (this.expanding.includes(name)) { + return [this.varReference(INVALID_IDENT)]; + } + + const args = this.parseArguments(fn); + if (!args) { + return [this.varReference(INVALID_IDENT)]; + } + + const parameters = entry.function.parameters; + + // Without default values every parameter must be provided. + if (args.length !== parameters.length) { + return [this.varReference(INVALID_IDENT)]; + } + + // CSS-wide keywords in `result` are left unresolved by the spec, so they + // must be substituted directly instead of going through `var()`. + const keywordResult = staticResultKeyword(entry.node); + if (keywordResult) { + return [new CSSATokenNode([TokenType.Ident, keywordResult, -1, -1, { value: keywordResult }])]; + } + + const id = (this.counter++).toString(36); + const scope = new Scope(parentScope); + + for (let i = 0; i < parameters.length; i++) { + scope.names.set(parameters[i].getName(), this.argName(id, i)); + } + + for (const localName of collectLocalNames(entry.node)) { + scope.names.set(localName, this.localName(id, localName)); + } + + this.expanding.push(name); + + // Arguments are resolved in the scope of the caller, not the callee. + const argDecls: Array = []; + for (let i = 0; i < parameters.length; i++) { + argDecls.push(new PostCSSDeclaration({ + prop: this.argName(id, i), + value: this.rewriteValue(stringify([args[i]]), element, parentScope), + source: element.source, + })); + } + + if (argDecls.length) { + element.before(element.clone({ nodes: argDecls })); + } + + const bodyNodes = this.emitBody(entry.node.nodes || [], element, scope, id); + for (const node of bodyNodes) { + element.before(node); + } + + this.expanding.pop(); + + return [this.varReference(this.resultName(id))]; + } + + private emitBody(containerNodes: Array, element: Rule, scope: Scope, id: string): Array { + const out: Array = []; + let pendingDecls: Array = []; + + const flush = (): void => { + if (!pendingDecls.length) { + return; + } + + out.push(element.clone({ nodes: pendingDecls })); + pendingDecls = []; + }; + + for (const node of containerNodes) { + if (node.type === 'decl') { + const processed = this.processBodyDeclaration(node, element, scope, id); + if (processed) { + pendingDecls.push(processed); + } + + continue; + } + + if (node.type === 'atrule') { + flush(); + + const children = this.emitBody(node.nodes || [], element, scope, id); + if (children.length) { + out.push(node.clone({ nodes: children })); + } + + continue; + } + } + + flush(); + + return out; + } + + private processBodyDeclaration(decl: Declaration, element: Rule, scope: Scope, id: string): Declaration | null { + let prop: string; + + if (decl.prop.toLowerCase() === 'result') { + prop = this.resultName(id); + } else if (decl.prop.startsWith('--')) { + const mapped = scope.get(decl.prop); + if (!mapped) { + return null; + } + + prop = mapped; + } else { + // Unknown descriptors are invalid and ignored. + return null; + } + + return decl.clone({ + prop, + value: this.rewriteValue(decl.value, element, scope), + }); + } + + private rewriteValue(value: string, element: Rule, scope: Scope): string { + const tokens = tokenize({ css: value }); + if (!tokens.some((token) => isTokenFunction(token) && (token[4].value.toLowerCase() === 'var' || token[4].value.startsWith('--')))) { + return value; + } + + const componentValues = parseListOfComponentValues(tokens); + + replaceComponentValues([componentValues], (node) => { + if (!isFunctionNode(node)) { + return; + } + + const name = node.getName(); + + if (name.toLowerCase() === 'var') { + this.rewriteVarReference(node, scope); + return; + } + + if (!name.startsWith('--')) { + return; + } + + const entry = this.customFunctions.get(name); + if (!entry || !entry.supported) { + return; + } + + return this.processCall(node, element, scope); + }); + + return stringify([componentValues]); + } + + private rewriteVarReference(node: FunctionNode, scope: Scope): void { + for (const child of node.value) { + if (isWhiteSpaceOrCommentNode(child)) { + continue; + } + + if (!isTokenNode(child) || !isTokenIdent(child.value)) { + return; + } + + const original = child.value[4].value; + if (!original.startsWith('--')) { + return; + } + + const mapped = scope.get(original); + if (!mapped) { + return; + } + + mutateIdent(child.value, mapped); + return; + } + } + + private parseArguments(fn: FunctionNode): Array> | false { + const meaningful = fn.value.filter((componentValue) => !isWhiteSpaceOrCommentNode(componentValue)); + if (meaningful.length === 0) { + return []; + } + + const lists = parseCommaSeparatedListOfComponentValues(fn.value.flatMap((componentValue) => componentValue.tokens())); + + const args: Array> = []; + for (const list of lists) { + const arg = unwrapArgument(list); + if (!arg) { + return false; + } + + args.push(arg); + } + + return args; + } + + private varReference(name: string): FunctionNode { + return new CSSAFunctionNode( + [TokenType.Function, 'var(', -1, -1, { value: 'var' }], + [TokenType.CloseParen, ')', -1, -1, undefined], + [ + new CSSATokenNode([TokenType.Ident, name, -1, -1, { value: name }]), + ], + ); + } + + private argName(id: string, index: number): string { + return `${GENERATED_PREFIX}-${id}-arg-${index}`; + } + + private localName(id: string, name: string): string { + return `${GENERATED_PREFIX}-${id}-local-${name.slice(2)}`; + } + + private resultName(id: string): string { + return `${GENERATED_PREFIX}-${id}-result`; + } +} + +/** + * Strip a `{}` wrapper from a single argument. + * + * https://drafts.csswg.org/css-values-5/#component-function-commas + */ +function unwrapArgument(list: Array): Array | false { + const meaningful = list.filter((componentValue) => !isWhiteSpaceOrCommentNode(componentValue)); + if (meaningful.length === 0) { + return false; + } + + if ( + meaningful.length === 1 && + isSimpleBlockNode(meaningful[0]) && + isTokenOpenCurly(meaningful[0].startToken) + ) { + return meaningful[0].value; + } + + return list; +} + +/** + * Collect every local custom property declared anywhere in a function body. + */ +function collectLocalNames(atRule: AtRule): Set { + const names = new Set(); + + atRule.walkDecls((decl) => { + if (decl.prop.startsWith('--')) { + names.add(decl.prop); + } + }); + + return names; +} + +/** + * When the winning `result` descriptor is a CSS-wide keyword and no conditional + * rule can change it, the call evaluates to that keyword directly. + */ +function staticResultKeyword(atRule: AtRule): string | null { + let conditionalResult = false; + atRule.walkAtRules((child) => { + child.walkDecls((decl) => { + if (decl.prop.toLowerCase() === 'result') { + conditionalResult = true; + } + }); + }); + + if (conditionalResult) { + return null; + } + + let value: string | null = null; + for (const node of atRule.nodes || []) { + if (node.type === 'decl' && node.prop.toLowerCase() === 'result') { + value = node.value.trim(); + } + } + + if (!value) { + return null; + } + + if (CSS_WIDE_KEYWORDS.has(value.toLowerCase())) { + return value; + } + + return null; +} diff --git a/plugins/postcss-custom-functions/test/_browser.mjs b/plugins/postcss-custom-functions/test/_browser.mjs new file mode 100644 index 0000000000..737f6148c0 --- /dev/null +++ b/plugins/postcss-custom-functions/test/_browser.mjs @@ -0,0 +1,134 @@ +import puppeteer from 'puppeteer'; +import http from 'node:http'; +import fs from 'node:fs/promises'; +import plugin from '@csstools/postcss-custom-functions'; +import postcss from 'postcss'; +import test from 'node:test'; +import process from 'node:process'; + +// The transformed cases run the source through this plugin. +// +// The untransformed cases are handed to the browser as-is and only pass when +// the browser itself implements custom functions. Point Puppeteer at such a +// browser via `PUPPETEER_EXECUTABLE_PATH`. + +const PAGES = [ + 'wpt/dashed-function-eval.html', + 'wpt/function-conditionals.html', + 'wpt/dashed-function-standard-property.html', + 'wpt/local-var-substitution.html', + 'wpt/dashed-function-cycles.html', +]; + +const requestListener = async function (req, res) { + + const parsedUrl = new URL(req.url, 'http://localhost:8080'); + const pathname = parsedUrl.pathname; + + if (PAGES.includes(pathname.replace(/^\//, ''))) { + res.setHeader('Content-type', 'text/html'); + res.writeHead(200); + res.end(await fs.readFile('test' + pathname, 'utf8')); + return; + } + + if (pathname === '' || pathname === '/') { + res.setHeader('Content-type', 'text/html'); + res.writeHead(200); + + res.end(` + + + Custom Functions Test + + +

Custom Functions Test

+
+ + + `); + return; + } + + if (pathname === '/test/styles.css' && req.method === 'POST') { + const data = await new Promise((resolve, reject) => { + let buf = []; + req.on('data', (chunk) => { + buf.push(chunk); + }); + + req.on('end', () => { + resolve(Buffer.concat(buf).toString()); + }); + + req.on('error', (err) => { + reject(err); + }); + }); + + const css = parsedUrl.searchParams.get('transform') === 'false' + ? data + : (await postcss([plugin()]).process(data, { from: 'test/styles.css', to: 'test/styles.css' })).css; + + res.setHeader('Content-type', 'text/css'); + res.writeHead(200); + res.end(css); + return; + } + + res.setHeader('Content-type', 'text/plain'); + res.writeHead(404); + res.end('Not found'); +}; + +function startServers() { + const server = http.createServer(requestListener); + server.listen(8080); + + return () => { + server.close(); + }; +} + +if (!process.env.DEBUG) { + test('browser', { skip: process.env.GITHUB_ACTIONS && !process.env.BROWSER_TESTS }, async () => { + const cleanup = startServers(); + + let browser; + + try { + browser = await puppeteer.launch({ + headless: 'new', + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, + args: ['--enable-experimental-web-platform-features'], + }); + + const page = await browser.newPage(); + page.on('pageerror', (msg) => { + throw msg; + }); + + for (const url of PAGES) { + await page.goto('http://localhost:8080/' + url); + const result = await page.evaluate(async () => { + // eslint-disable-next-line no-undef + return await window.runTest(); + }); + if (!result) { + throw new Error('Test failed, expected "window.runTest()" to return true'); + } + } + } finally { + await browser?.close(); + + await cleanup(); + } + }); +} else { + startServers(); + + // eslint-disable-next-line no-console + console.log('visit : http://localhost:8080'); +} diff --git a/plugins/postcss-custom-functions/test/_tape.mjs b/plugins/postcss-custom-functions/test/_tape.mjs index 7ff5bcd061..d6098109b8 100644 --- a/plugins/postcss-custom-functions/test/_tape.mjs +++ b/plugins/postcss-custom-functions/test/_tape.mjs @@ -5,17 +5,39 @@ postcssTape(plugin)({ basic: { message: 'supports basic usage', }, - 'basic:color': { - message: 'supports { color: \'\' }', + 'basic:preserve-true': { + message: 'supports basic usage with { preserve: true }', options: { - color: 'purple', + preserve: true, + }, + }, + nested: { + message: 'supports nested calls and dynamic scoping', + }, + 'conditionals': { + message: 'supports conditional rules', + }, + 'invalid': { + message: 'handles invalid invocations', + }, + 'ignore': { + message: 'ignores unsupported functions', + expect: 'ignore.css', + result: 'ignore.css', + }, + 'ignore:preserve-true': { + message: 'ignores unsupported functions with { preserve: true }', + expect: 'ignore.css', + result: 'ignore.css', + options: { + preserve: true, }, }, 'examples/example': { message: 'minimal example', }, 'examples/example:preserve-true': { - message: 'minimal example', + message: 'minimal example with { preserve: true }', options: { preserve: true, }, diff --git a/plugins/postcss-custom-functions/test/basic.css b/plugins/postcss-custom-functions/test/basic.css index 5b4c7fc42d..d7058db4dc 100644 --- a/plugins/postcss-custom-functions/test/basic.css +++ b/plugins/postcss-custom-functions/test/basic.css @@ -11,18 +11,11 @@ padding: --negative(1em); } -.negative-c { - --value: 1em; - padding: --negative(); +@function --circle-area(--r) { + --r2: calc(var(--r) * var(--r)); + result: calc(3.14159 * var(--r2)); } -@function --alpha(--color : black, --alpha : 1) returns { - --a: clamp(0, var(--alpha), 1); - - result: oklch(from var(--color) l c h / var(--a)); -} - -.color-a { - --a-color: red; - color: --alpha(var(--a-color), 0.5); +.circle { + width: --circle-area(2px); } diff --git a/plugins/postcss-custom-functions/test/basic.expect.css b/plugins/postcss-custom-functions/test/basic.expect.css index 8fa829300d..557d7cd39d 100644 --- a/plugins/postcss-custom-functions/test/basic.expect.css +++ b/plugins/postcss-custom-functions/test/basic.expect.css @@ -1,114 +1,34 @@ -.negative-a { - --csstools-function-argument-0---value: var(--gap); +.negative-a {--csstools-custom-function-0-arg-0: var(--gap); } .negative-a { - --csstools-custom-function-call-0-1: calc(-1 * var(--value)); + --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); } .negative-a { --gap: 1em; - padding: var(--csstools-custom-function-call-0-1); + padding: var(--csstools-custom-function-0-result); } -.negative-b { - --csstools-function-argument-1---value: 1em; +.negative-b {--csstools-custom-function-1-arg-0: 1em; } .negative-b { - --csstools-custom-function-call-1-1: calc(-1 * var(--value)); + --csstools-custom-function-1-result: calc(-1 * var(--csstools-custom-function-1-arg-0)); } .negative-b { - padding: var(--csstools-custom-function-call-1-1); -} - -.negative-c { -} - -.negative-c { - --csstools-custom-function-call-2-1: calc(-1 * var(--value)); -} - -.negative-c { - --value: 1em; - padding: var(--csstools-custom-function-call-2-1); -} - -.color-a { - --csstools-function-argument-3---color: black; - --csstools-function-argument-3---color: var(--a-color); - --csstools-function-argument-3---alpha: 1; - --csstools-function-argument-3---alpha: 0.5; -} - -.color-a { - --a: clamp(0, var(--alpha), 1); - - --csstools-custom-function-call-3-1: oklch(from var(--color) l c h / var(--a)); -} - -.color-a { - --a-color: red; - color: var(--csstools-custom-function-call-3-1); -} - -@property --csstools-function-argument-0---value { - syntax: "*"; - inherit: false; - initial-value: ; -} - -@property --csstools-function-body-0---value { - syntax: "*"; - inherit: false; -} - -@property --csstools-function-argument-1---value { - syntax: "*"; - inherit: false; - initial-value: ; -} - -@property --csstools-function-body-1---value { - syntax: "*"; - inherit: false; -} - -@property --csstools-function-argument-2---value { - syntax: "*"; - inherit: false; - initial-value: ; -} - -@property --csstools-function-body-2---value { - syntax: "*"; - inherit: false; -} - -@property --csstools-function-argument-3---color { - syntax: ""; - inherit: false; - initial-value: black; -} - -@property --csstools-function-body-3---color { - syntax: "*"; - inherit: false; + padding: var(--csstools-custom-function-1-result); } -@property --csstools-function-argument-3---alpha { - syntax: ""; - inherit: false; - initial-value: 1; +.circle {--csstools-custom-function-2-arg-0: 2px; } -@property --csstools-function-body-3---alpha { - syntax: "*"; - inherit: false; +.circle { + --csstools-custom-function-2-local-r2: calc(var(--csstools-custom-function-2-arg-0) * var(--csstools-custom-function-2-arg-0)); + --csstools-custom-function-2-result: calc(3.14159 * var(--csstools-custom-function-2-local-r2)); } -@property --csstools-return-value-3 { - syntax: ""; - inherit: false; +.circle { + width: var(--csstools-custom-function-2-result); } diff --git a/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css b/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css new file mode 100644 index 0000000000..634b8fc269 --- /dev/null +++ b/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css @@ -0,0 +1,46 @@ +@function --negative(--value) { + result: calc(-1 * var(--value)); +} + +.negative-a {--csstools-custom-function-0-arg-0: var(--gap); +} + +.negative-a { + --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); +} + +.negative-a { + --gap: 1em; + padding: var(--csstools-custom-function-0-result); + padding: --negative(var(--gap)); +} + +.negative-b {--csstools-custom-function-1-arg-0: 1em; +} + +.negative-b { + --csstools-custom-function-1-result: calc(-1 * var(--csstools-custom-function-1-arg-0)); +} + +.negative-b { + padding: var(--csstools-custom-function-1-result); + padding: --negative(1em); +} + +@function --circle-area(--r) { + --r2: calc(var(--r) * var(--r)); + result: calc(3.14159 * var(--r2)); +} + +.circle {--csstools-custom-function-2-arg-0: 2px; +} + +.circle { + --csstools-custom-function-2-local-r2: calc(var(--csstools-custom-function-2-arg-0) * var(--csstools-custom-function-2-arg-0)); + --csstools-custom-function-2-result: calc(3.14159 * var(--csstools-custom-function-2-local-r2)); +} + +.circle { + width: var(--csstools-custom-function-2-result); + width: --circle-area(2px); +} diff --git a/plugins/postcss-custom-functions/test/conditionals.css b/plugins/postcss-custom-functions/test/conditionals.css new file mode 100644 index 0000000000..dcfe0b2522 --- /dev/null +++ b/plugins/postcss-custom-functions/test/conditionals.css @@ -0,0 +1,22 @@ +@function --f() { + result: 16px; + @media (width > 1000px) { + result: 20px; + } +} + +.conditional { + width: --f(); +} + +@function --locals() { + --x: FAIL; + @media (width > 0px) { + --x: PASS; + } + result: var(--x); +} + +.conditional-locals { + width: --locals(); +} diff --git a/plugins/postcss-custom-functions/test/conditionals.expect.css b/plugins/postcss-custom-functions/test/conditionals.expect.css new file mode 100644 index 0000000000..c6eded946f --- /dev/null +++ b/plugins/postcss-custom-functions/test/conditionals.expect.css @@ -0,0 +1,29 @@ +.conditional { + --csstools-custom-function-0-result: 16px; +} + @media (width > 1000px) {.conditional { + --csstools-custom-function-0-result: 20px; +} + } + +.conditional { + width: var(--csstools-custom-function-0-result); +} + +.conditional-locals { + --csstools-custom-function-1-local-x: FAIL; +} + @media (width > 0px) { + +.conditional-locals { + --csstools-custom-function-1-local-x: PASS; +} + } + +.conditional-locals { + --csstools-custom-function-1-result: var(--csstools-custom-function-1-local-x); +} + +.conditional-locals { + width: var(--csstools-custom-function-1-result); +} diff --git a/plugins/postcss-custom-functions/test/examples/example.css b/plugins/postcss-custom-functions/test/examples/example.css index 181f83a541..69e0f2abc1 100644 --- a/plugins/postcss-custom-functions/test/examples/example.css +++ b/plugins/postcss-custom-functions/test/examples/example.css @@ -1,7 +1,13 @@ -.foo { - color: red; +@function --negative(--value) { + result: calc(-1 * var(--value)); } -.baz { - color: green; +@function --double(--value) { + result: calc(2 * var(--value)); +} + +html { + --gap: 1em; + padding: --negative(var(--gap)); + margin: --double(4px); } diff --git a/plugins/postcss-custom-functions/test/examples/example.expect.css b/plugins/postcss-custom-functions/test/examples/example.expect.css index 181f83a541..b1f80c329b 100644 --- a/plugins/postcss-custom-functions/test/examples/example.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.expect.css @@ -1,7 +1,15 @@ -.foo { - color: red; +html {--csstools-custom-function-0-arg-0: var(--gap); } - -.baz { - color: green; +html { + --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); +} +html {--csstools-custom-function-1-arg-0: 4px; +} +html { + --csstools-custom-function-1-result: calc(2 * var(--csstools-custom-function-1-arg-0)); +} +html { + --gap: 1em; + padding: var(--csstools-custom-function-0-result); + margin: var(--csstools-custom-function-1-result); } diff --git a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css index 181f83a541..c4ae25cad3 100644 --- a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css @@ -1,7 +1,29 @@ -.foo { - color: red; +@function --negative(--value) { + result: calc(-1 * var(--value)); } -.baz { - color: green; +@function --double(--value) { + result: calc(2 * var(--value)); +} + +html {--csstools-custom-function-0-arg-0: var(--gap); +} + +html { + --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); +} + +html {--csstools-custom-function-1-arg-0: 4px; +} + +html { + --csstools-custom-function-1-result: calc(2 * var(--csstools-custom-function-1-arg-0)); +} + +html { + --gap: 1em; + padding: var(--csstools-custom-function-0-result); + padding: --negative(var(--gap)); + margin: var(--csstools-custom-function-1-result); + margin: --double(4px); } diff --git a/plugins/postcss-custom-functions/test/ignore.css b/plugins/postcss-custom-functions/test/ignore.css new file mode 100644 index 0000000000..b971ae2e08 --- /dev/null +++ b/plugins/postcss-custom-functions/test/ignore.css @@ -0,0 +1,23 @@ +@function --typed(--x ) { + result: var(--x); +} + +.typed { + width: --typed(10px); +} + +@function --defaulted(--x: 1px) { + result: var(--x); +} + +.defaulted { + width: --defaulted(); +} + +@function --typed-return() returns { + result: 10px; +} + +.typed-return { + width: --typed-return(); +} diff --git a/plugins/postcss-custom-functions/test/invalid.css b/plugins/postcss-custom-functions/test/invalid.css new file mode 100644 index 0000000000..820978f3fb --- /dev/null +++ b/plugins/postcss-custom-functions/test/invalid.css @@ -0,0 +1,23 @@ +@function --f(--x) { + result: 10px; +} + +.missing { + width: --f(); +} + +.extra { + width: --f(1, 2); +} + +.unknown { + width: --unknown(1); +} + +@function --empty-args(--x) { + result: 10px; +} + +.empty-args { + width: --empty-args( ); +} diff --git a/plugins/postcss-custom-functions/test/invalid.expect.css b/plugins/postcss-custom-functions/test/invalid.expect.css new file mode 100644 index 0000000000..c82d87b51f --- /dev/null +++ b/plugins/postcss-custom-functions/test/invalid.expect.css @@ -0,0 +1,15 @@ +.missing { + width: var(--csstools-custom-function-invalid); +} + +.extra { + width: var(--csstools-custom-function-invalid); +} + +.unknown { + width: --unknown(1); +} + +.empty-args { + width: var(--csstools-custom-function-invalid); +} diff --git a/plugins/postcss-custom-functions/test/nested.css b/plugins/postcss-custom-functions/test/nested.css new file mode 100644 index 0000000000..88518d5dda --- /dev/null +++ b/plugins/postcss-custom-functions/test/nested.css @@ -0,0 +1,31 @@ +@function --f(--x) { + result: --g(); +} + +@function --g() { + result: var(--x); +} + +.scope { + --x: FAIL; + width: --f(PASS); +} + +@function --one() { + --x: 1; + result: --read(); +} + +@function --two() { + --x: 2; + result: --read(); +} + +@function --read() { + result: var(--x); +} + +.scopes { + --x: 0; + width: --one() --two() --read(); +} diff --git a/plugins/postcss-custom-functions/test/nested.expect.css b/plugins/postcss-custom-functions/test/nested.expect.css new file mode 100644 index 0000000000..c5251a3ad8 --- /dev/null +++ b/plugins/postcss-custom-functions/test/nested.expect.css @@ -0,0 +1,42 @@ +.scope {--csstools-custom-function-0-arg-0: PASS; +} + +.scope { + --csstools-custom-function-1-result: var(--csstools-custom-function-0-arg-0); +} + +.scope { + --csstools-custom-function-0-result: var(--csstools-custom-function-1-result); +} + +.scope { + --x: FAIL; + width: var(--csstools-custom-function-0-result); +} + +.scopes { + --csstools-custom-function-3-result: var(--csstools-custom-function-2-local-x); +} + +.scopes { + --csstools-custom-function-2-local-x: 1; + --csstools-custom-function-2-result: var(--csstools-custom-function-3-result); +} + +.scopes { + --csstools-custom-function-5-result: var(--csstools-custom-function-4-local-x); +} + +.scopes { + --csstools-custom-function-4-local-x: 2; + --csstools-custom-function-4-result: var(--csstools-custom-function-5-result); +} + +.scopes { + --csstools-custom-function-6-result: var(--x); +} + +.scopes { + --x: 0; + width: var(--csstools-custom-function-2-result) var(--csstools-custom-function-4-result) var(--csstools-custom-function-6-result); +} diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html new file mode 100644 index 0000000000..1352339702 --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html @@ -0,0 +1,368 @@ + +Custom Functions: Handling cycles + + +
+
+
+
+ + diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html new file mode 100644 index 0000000000..5b17fee58e --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html @@ -0,0 +1,708 @@ + +Custom Functions: Evaluating a <dashed-function> + + +
+
+
+
+ + diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html new file mode 100644 index 0000000000..a9514ff518 --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html @@ -0,0 +1,51 @@ + +Custom Functions: Evaluating a <dashed-function> in standard properties + + +
+ + diff --git a/plugins/postcss-custom-functions/test/wpt/function-conditionals.html b/plugins/postcss-custom-functions/test/wpt/function-conditionals.html new file mode 100644 index 0000000000..6ecdf1f247 --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/function-conditionals.html @@ -0,0 +1,451 @@ + +Custom Functions: Conditionals within @function + + + + +
+
+
+
+
+
+ + diff --git a/plugins/postcss-custom-functions/test/wpt/local-var-substitution.html b/plugins/postcss-custom-functions/test/wpt/local-var-substitution.html new file mode 100644 index 0000000000..cb56db72f9 --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/local-var-substitution.html @@ -0,0 +1,103 @@ + +Custom Functions: Local var() substitution + + +
+
+
+
+ + diff --git a/plugins/postcss-custom-functions/tsconfig.json b/plugins/postcss-custom-functions/tsconfig.json index 500af6d266..24dab9cc3d 100644 --- a/plugins/postcss-custom-functions/tsconfig.json +++ b/plugins/postcss-custom-functions/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "dist", "declarationDir": ".", + "rootDir": "./src", "strict": true }, "include": ["./src/**/*"], From 47dfbc747876a107f377e9a03b0f29bdb8a7af18 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 11:27:09 +0200 Subject: [PATCH 07/21] fix --- plugins/postcss-custom-functions/README.md | 51 ++-- .../postcss-custom-functions/dist/index.mjs | 2 +- .../postcss-custom-functions/docs/README.md | 10 +- .../src/custom-functions-from-root.ts | 13 +- plugins/postcss-custom-functions/src/index.ts | 5 + .../src/is-processable-declaration.ts | 2 +- .../src/is-processable-rule.ts | 5 +- .../postcss-custom-functions/src/transpile.ts | 194 ++++++++++++-- .../test/_browser.mjs | 2 + .../postcss-custom-functions/test/_tape.mjs | 15 ++ .../test/basic.expect.css | 20 +- .../test/basic.preserve-true.expect.css | 20 +- .../test/conditionals.expect.css | 14 +- .../postcss-custom-functions/test/cycles.css | 31 +++ .../test/cycles.expect.css | 40 +++ .../test/examples/example.css | 2 +- .../test/examples/example.expect.css | 17 +- .../examples/example.preserve-true.expect.css | 20 +- .../test/invalid.expect.css | 6 +- .../postcss-custom-functions/test/locals.css | 34 +++ .../test/locals.expect.css | 43 +++ .../test/nested.expect.css | 24 +- .../postcss-custom-functions/test/typed.css | 27 ++ .../test/typed.expect.css | 73 +++++ .../test/typed.preserve-true.expect.css | 89 +++++++ .../test/wpt/dashed-function-eval.html | 12 + .../test/wpt/function-layer.html | 149 +++++++++++ .../test/wpt/function-parameter-types.html | 249 ++++++++++++++++++ 28 files changed, 1063 insertions(+), 106 deletions(-) create mode 100644 plugins/postcss-custom-functions/test/cycles.css create mode 100644 plugins/postcss-custom-functions/test/cycles.expect.css create mode 100644 plugins/postcss-custom-functions/test/locals.css create mode 100644 plugins/postcss-custom-functions/test/locals.expect.css create mode 100644 plugins/postcss-custom-functions/test/typed.css create mode 100644 plugins/postcss-custom-functions/test/typed.expect.css create mode 100644 plugins/postcss-custom-functions/test/typed.preserve-true.expect.css create mode 100644 plugins/postcss-custom-functions/test/wpt/function-layer.html create mode 100644 plugins/postcss-custom-functions/test/wpt/function-parameter-types.html diff --git a/plugins/postcss-custom-functions/README.md b/plugins/postcss-custom-functions/README.md index e4be67e924..03a24757d2 100644 --- a/plugins/postcss-custom-functions/README.md +++ b/plugins/postcss-custom-functions/README.md @@ -12,15 +12,21 @@ Custom functions are still an early draft. This plugin is only a partial implementation to avoid conflicts with the final specification. Unsupported: -- typed parameters and return types -- default parameter values +- typed parameters without default values +- untyped parameters with default values +- return types + +Typed parameters with default values are checked by the browser through a +generated `@property` registration. A default value that is not +computationally independent (for example `1em` or `var(--x)`) can not be used +as an `initial-value` and disables type checking for that parameter. ```css @function --negative(--value) { result: calc(-1 * var(--value)); } -@function --double(--value) { +@function --double(--value : 1px) { result: calc(2 * var(--value)); } @@ -32,20 +38,25 @@ html { /* becomes */ -html {--csstools-custom-function-0-arg-0: var(--gap); +html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); } html { - --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); } -html {--csstools-custom-function-1-arg-0: 4px; +html {--_csstools-cf-1agle9gi-1-arg-0: 4px; } html { - --csstools-custom-function-1-result: calc(2 * var(--csstools-custom-function-1-arg-0)); + --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); } html { --gap: 1em; - padding: var(--csstools-custom-function-0-result); - margin: var(--csstools-custom-function-1-result); + padding: var(--_csstools-cf-1agle9gi-0-result); + margin: var(--_csstools-cf-1agle9gi-1-result); +} +@property --_csstools-cf-1agle9gi-1-arg-0 { + syntax: ""; + inherits: false; + initial-value: 1px; } ``` @@ -95,7 +106,7 @@ postcssCustomFunctions({ preserve: true }) result: calc(-1 * var(--value)); } -@function --double(--value) { +@function --double(--value : 1px) { result: calc(2 * var(--value)); } @@ -111,31 +122,37 @@ html { result: calc(-1 * var(--value)); } -@function --double(--value) { +@function --double(--value : 1px) { result: calc(2 * var(--value)); } -html {--csstools-custom-function-0-arg-0: var(--gap); +html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); } html { - --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); } -html {--csstools-custom-function-1-arg-0: 4px; +html {--_csstools-cf-1agle9gi-1-arg-0: 4px; } html { - --csstools-custom-function-1-result: calc(2 * var(--csstools-custom-function-1-arg-0)); + --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); } html { --gap: 1em; - padding: var(--csstools-custom-function-0-result); + padding: var(--_csstools-cf-1agle9gi-0-result); padding: --negative(var(--gap)); - margin: var(--csstools-custom-function-1-result); + margin: var(--_csstools-cf-1agle9gi-1-result); margin: --double(4px); } + +@property --_csstools-cf-1agle9gi-1-arg-0 { + syntax: ""; + inherits: false; + initial-value: 1px; +} ``` [cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test diff --git a/plugins/postcss-custom-functions/dist/index.mjs b/plugins/postcss-custom-functions/dist/index.mjs index b661234f32..d36c2ce1f9 100644 --- a/plugins/postcss-custom-functions/dist/index.mjs +++ b/plugins/postcss-custom-functions/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parseListOfComponentValues as r,isWhiteSpaceOrCommentNode as n,isFunctionNode as s,isTokenNode as o,isWhitespaceNode as a,isSimpleBlockNode as u,replaceComponentValues as i,stringify as l,TokenNode as c,parseCommaSeparatedListOfComponentValues as f,FunctionNode as p}from"@csstools/css-parser-algorithms";import{tokenize as m,stringify as h,isTokenComment as g,isTokenComma as v,isTokenIdent as d,isTokenDelim as y,isTokenColon as w,isTokenOpenCurly as F,isTokenFunction as C,TokenType as k,mutateIdent as N}from"@csstools/css-tokenizer";import{Declaration as P}from"postcss";const T=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let u=e(a);if(u?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(u=u.map(e=>t.concat(e)),e=e.parent):e=e.parent}}if(t(o,u),r.nodes){const e=u[0].concat(T);n.set(r,e),s.set(r,u[0])}}});for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex(e=>t.equal(e)));return a}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const L=new Set(["scope","container","layer"]),b=/^function$/i;function isProcessableRule(e){if(!b.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!L.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}var M,R;(R=M||(M={})).CustomFunction="custom-function",R.FunctionParameter="function-parameter";class CustomFunction{type=M.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],o=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const e=r(this.returnType).filter(e=>!n(e));return 1===e.length&&s(e[0])&&"type"===e[0].getName().toLowerCase()?h(...e[0].value.flatMap(e=>e.tokens()).filter(e=>!g(e))).trim():h(...this.returnType.filter(e=>!g(e))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return h(...this.before)+h(...this.function.tokens())+h(...this.returnsKeyword)+h(...this.returnType)+h(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(e=>e.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&e instanceof CustomFunction&&e.type===M.CustomFunction}}class FunctionParameter{type=M.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],o=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return h(...this.argumentType.filter(e=>!g(e))).trim()}getDefaultValue(){return h(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&e instanceof FunctionParameter&&e.type===M.FunctionParameter}}function parseCustomFunction(e){let t=null,r=-1,a=-1,u=-1,i=e.length;for(let i=0;iu;t--){const r=e[t];if(!n(r))break;i=t}const l=parseFunctionParameters(t.value);return!!l&&new CustomFunction(t,l,-1===u?[]:e.slice(r+1,u).flatMap(e=>e.tokens()),-1===u?[]:e.slice(u,i).flatMap(e=>e.tokens()),e.slice(0,r).flatMap(e=>e.tokens()),e.slice(i).flatMap(e=>e.tokens()))}function parseFunctionParameters(e){if(!e.some(e=>!n(e)))return[];const t=[],r=new Set;for(let n=0;n0&&o(s)&&y(s.value)&&">"===s.value[4].value)t--;else if(-1===l&&0===t&&o(s)&&w(s.value))l=m,c=s.value;else if(-1===l){if(-1!==l)return!1}else f=m}}if(!i)return!1;if(-1!==l&&-1===f)return!1;const h=-1!==l?l-1:m-1;for(let t=e.length-1;t>Math.max(h,f);t--){const r=e[t];if(!a(r))break;p=t}let g;{const t=e.slice(r+1,h+1),o=t.filter(e=>!n(e));g=1===o.length&&s(o[0])&&"type"===o[0].getName().toLowerCase()?o[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}let C=[];if(-1!==f){const t=e.slice(l+1,p);if(hasTopLevelImportant(t))return!1;const r=t.filter(e=>!n(e));C=1===r.length&&u(r[0])&&F(r[0].startToken)?r[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}return{advance:m,node:new FunctionParameter(i,g,c,C,e.slice(0,r).flatMap(e=>e.tokens()),e.slice(p,m+1).flatMap(e=>e.tokens()))}}function hasTopLevelImportant(e){for(let t=0;t{n=!0,t?.onParseError?.(e)}});return!n&&parseCustomFunction(s)}function parse(e,t){return parseFromTokens(m({css:e},{onParseError:t?.onParseError}),t)}function isSupportedCustomFunction(e){return!e.getReturnType()&&e.parameters.every(e=>!e.getArgumentType()&&!e.getDefaultValue())}function getCustomFunctions(e,t,r){const n=new Map,s=new Map,o=collectCascadeLayerOrder(e),a=[];if(e.walkAtRules(e=>{if(!isProcessableRule(e))return;const r=parse(e.params.trim(),{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!r)return;const u=r.getName(),i=isSupportedCustomFunction(r),l=(f=o,(c=e).parent&&"atrule"===c.parent.type&&"layer"===c.parent.name.toLowerCase()?f.has(c.parent)?f.get(c.parent)+1:0:1e7);var c,f;l>=(s.get(u)??-1)&&(s.set(u,l),n.set(u,{node:e,function:r,supported:i})),i&&a.push(e)}),!r.preserve)for(const e of a){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const S=new Set(["function","keyframes"]);function isProcessableDeclaration(e){if(e.prop.startsWith("--csstools-custom-function"))return!1;if(!e.parent||"rule"!==e.parent.type)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&S.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const $="--csstools-custom-function",V=`${$}-invalid`,A=new Set(["initial","inherit","unset","revert","revert-layer"]);class Scope{names=new Map;parent;constructor(e){this.parent=e}get(e){const t=this.names.get(e);return void 0!==t?t:this.parent?.get(e)}}class CustomFunctionTranspiler{customFunctions=new Map;counter=0;expanding=[];setCustomFunctions(e){this.customFunctions=e}processDeclaration(e){const t=m({css:e.value});if(!t.some(e=>C(e)&&e[4].value.startsWith("--")))return null;const n=e.parent;if(!n||"rule"!==n.type)return null;const o=n,a=r(t);i([a],e=>{if(!s(e))return;const t=e.getName();if(!t.startsWith("--"))return;const r=this.customFunctions.get(t);return r&&r.supported?this.processCall(e,o,new Scope(null)):void 0});const u=l([a]);return u===e.value?null:u}processCall(e,t,r){const n=e.getName(),s=this.customFunctions.get(n);if(!s||!s.supported)return[this.varReference(V)];if(this.expanding.includes(n))return[this.varReference(V)];const o=this.parseArguments(e);if(!o)return[this.varReference(V)];const a=s.function.parameters;if(o.length!==a.length)return[this.varReference(V)];const u=staticResultKeyword(s.node);if(u)return[new c([k.Ident,u,-1,-1,{value:u}])];const i=(this.counter++).toString(36),f=new Scope(r);for(let e=0;e{o.length&&(s.push(t.clone({nodes:o})),o=[])};for(const a of e){if("decl"===a.type){const e=this.processBodyDeclaration(a,t,r,n);e&&o.push(e);continue}if("atrule"===a.type){flush();const e=this.emitBody(a.nodes||[],t,r,n);e.length&&s.push(a.clone({nodes:e}));continue}}return flush(),s}processBodyDeclaration(e,t,r,n){let s;if("result"===e.prop.toLowerCase())s=this.resultName(n);else{if(!e.prop.startsWith("--"))return null;{const t=r.get(e.prop);if(!t)return null;s=t}}return e.clone({prop:s,value:this.rewriteValue(e.value,t,r)})}rewriteValue(e,t,n){const o=m({css:e});if(!o.some(e=>C(e)&&("var"===e[4].value.toLowerCase()||e[4].value.startsWith("--"))))return e;const a=r(o);return i([a],e=>{if(!s(e))return;const r=e.getName();if("var"===r.toLowerCase())return void this.rewriteVarReference(e,n);if(!r.startsWith("--"))return;const o=this.customFunctions.get(r);return o&&o.supported?this.processCall(e,t,n):void 0}),l([a])}rewriteVarReference(e,t){for(const r of e.value){if(n(r))continue;if(!o(r)||!d(r.value))return;const e=r.value[4].value;if(!e.startsWith("--"))return;const s=t.get(e);if(!s)return;return void N(r.value,s)}}parseArguments(e){if(0===e.value.filter(e=>!n(e)).length)return[];const t=f(e.value.flatMap(e=>e.tokens())),r=[];for(const e of t){const t=unwrapArgument(e);if(!t)return!1;r.push(t)}return r}varReference(e){return new p([k.Function,"var(",-1,-1,{value:"var"}],[k.CloseParen,")",-1,-1,void 0],[new c([k.Ident,e,-1,-1,{value:e}])])}argName(e,t){return`${$}-${e}-arg-${t}`}localName(e,t){return`${$}-${e}-local-${t.slice(2)}`}resultName(e){return`${$}-${e}-result`}}function unwrapArgument(e){const t=e.filter(e=>!n(e));return 0!==t.length&&(1===t.length&&u(t[0])&&F(t[0].startToken)?t[0].value:e)}function collectLocalNames(e){const t=new Set;return e.walkDecls(e=>{e.prop.startsWith("--")&&t.add(e.prop)}),t}function staticResultKeyword(e){let t=!1;if(e.walkAtRules(e=>{e.walkDecls(e=>{"result"===e.prop.toLowerCase()&&(t=!0)})}),t)return null;let r=null;for(const t of e.nodes||[])"decl"===t.type&&"result"===t.prop.toLowerCase()&&(r=t.value.trim());return r&&A.has(r.toLowerCase())?r:null}const D=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=new CustomFunctionTranspiler;return{postcssPlugin:"postcss-custom-functions",Once(n,{result:s}){e=getCustomFunctions(n,s,{preserve:t.preserve}),r.setCustomFunctions(e)},Declaration(e){if(!D.test(e.value))return;if(!isProcessableDeclaration(e))return;const n=r.processDeclaration(e);null!==n&&(e.cloneBefore({value:n}),t.preserve||e.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parseListOfComponentValues as r,isWhiteSpaceOrCommentNode as n,isFunctionNode as s,isTokenNode as o,isWhitespaceNode as a,isSimpleBlockNode as u,replaceComponentValues as i,stringify as c,TokenNode as l,parseCommaSeparatedListOfComponentValues as p,FunctionNode as f}from"@csstools/css-parser-algorithms";import{tokenize as m,stringify as h,isTokenComment as g,isTokenComma as v,isTokenIdent as d,isTokenDelim as y,isTokenColon as w,isTokenOpenCurly as F,isTokenFunction as C,TokenType as k,mutateIdent as N}from"@csstools/css-tokenizer";import{Declaration as T,AtRule as b}from"postcss";import L from"node:crypto";import P from"node:path";const M=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let u=e(a);if(u?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(u=u.map(e=>t.concat(e)),e=e.parent):e=e.parent}}if(t(o,u),r.nodes){const e=u[0].concat(M);n.set(r,e),s.set(r,u[0])}}});for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex(e=>t.equal(e)));return a}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const R=new Set(["layer"]),$=/^function$/i;function isProcessableRule(e){if(!$.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!R.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}var S,V;(V=S||(S={})).CustomFunction="custom-function",V.FunctionParameter="function-parameter";class CustomFunction{type=S.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],o=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const e=r(this.returnType).filter(e=>!n(e));return 1===e.length&&s(e[0])&&"type"===e[0].getName().toLowerCase()?h(...e[0].value.flatMap(e=>e.tokens()).filter(e=>!g(e))).trim():h(...this.returnType.filter(e=>!g(e))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return h(...this.before)+h(...this.function.tokens())+h(...this.returnsKeyword)+h(...this.returnType)+h(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(e=>e.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&e instanceof CustomFunction&&e.type===S.CustomFunction}}class FunctionParameter{type=S.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],o=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return h(...this.argumentType.filter(e=>!g(e))).trim()}getDefaultValue(){return h(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&e instanceof FunctionParameter&&e.type===S.FunctionParameter}}function parseCustomFunction(e){let t=null,r=-1,a=-1,u=-1,i=e.length;for(let i=0;iu;t--){const r=e[t];if(!n(r))break;i=t}const c=parseFunctionParameters(t.value);return!!c&&new CustomFunction(t,c,-1===u?[]:e.slice(r+1,u).flatMap(e=>e.tokens()),-1===u?[]:e.slice(u,i).flatMap(e=>e.tokens()),e.slice(0,r).flatMap(e=>e.tokens()),e.slice(i).flatMap(e=>e.tokens()))}function parseFunctionParameters(e){if(!e.some(e=>!n(e)))return[];const t=[],r=new Set;for(let n=0;n0&&o(s)&&y(s.value)&&">"===s.value[4].value)t--;else if(-1===c&&0===t&&o(s)&&w(s.value))c=m,l=s.value;else if(-1===c){if(-1!==c)return!1}else p=m}}if(!i)return!1;if(-1!==c&&-1===p)return!1;const h=-1!==c?c-1:m-1;for(let t=e.length-1;t>Math.max(h,p);t--){const r=e[t];if(!a(r))break;f=t}let g;{const t=e.slice(r+1,h+1),o=t.filter(e=>!n(e));g=1===o.length&&s(o[0])&&"type"===o[0].getName().toLowerCase()?o[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}let C=[];if(-1!==p){const t=e.slice(c+1,f);if(hasTopLevelImportant(t))return!1;const r=t.filter(e=>!n(e));C=1===r.length&&u(r[0])&&F(r[0].startToken)?r[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}return{advance:m,node:new FunctionParameter(i,g,l,C,e.slice(0,r).flatMap(e=>e.tokens()),e.slice(f,m+1).flatMap(e=>e.tokens()))}}function hasTopLevelImportant(e){for(let t=0;t{n=!0,t?.onParseError?.(e)}});return!n&&parseCustomFunction(s)}function parse(e,t){return parseFromTokens(m({css:e},{onParseError:t?.onParseError}),t)}function isSupportedCustomFunction(e){return!e.getReturnType()&&e.parameters.every(e=>{const t=e.getArgumentType(),r=!!t&&"*"!==t,n=!!e.getDefaultValue();return r&&n||!r&&!n})}function getCustomFunctions(e,t,r){const n=new Map,s=new Map,o=collectCascadeLayerOrder(e),a=[];if(e.walkAtRules(e=>{if(!isProcessableRule(e))return;const r=parse(e.params.trim(),{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!r)return;const u=r.getName(),i=isSupportedCustomFunction(r),c=(p=o,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?p.has(l.parent)?p.get(l.parent)+1:0:1e7);var l,p;c>=(s.get(u)??-1)&&(s.set(u,c),n.set(u,{node:e,function:r,supported:i})),i&&a.push(e)}),!r.preserve)for(const e of a){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const D=new Set(["function","keyframes"]);function isProcessableDeclaration(e){if(e.prop.startsWith("--_csstools-cf"))return!1;if(!e.parent||"rule"!==e.parent.type)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&D.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const A="--_csstools-cf",W=`${A}-invalid`,E=new Set(["initial","inherit","unset","revert","revert-layer"]),B=new Map;class Scope{names=new Map;parent;constructor(e){this.parent=e}get(e){const t=this.names.get(e);return void 0!==t?t:this.parent?.get(e)}}class CustomFunctionTranspiler{customFunctions=new Map;counter=0;frames=[];registrations=[];sourceHash="0";setCustomFunctions(e){this.customFunctions=e}getRegistrations(){return this.registrations}processDeclaration(e){const t=m({css:e.value});if(!t.some(e=>C(e)&&e[4].value.startsWith("--")))return null;const n=e.parent;if(!n||"rule"!==n.type)return null;this.sourceHash=sourceHashFor(e.source?.input.from);const o=n,a=r(t);i([a],e=>{if(!s(e))return;const t=e.getName();if(!t.startsWith("--"))return;const r=this.customFunctions.get(t);return r&&r.supported?this.processCall(e,o,new Scope(null)):void 0});const u=c([a]);return u===e.value?null:u}processCall(e,t,r){const n=e.getName(),s=this.customFunctions.get(n);if(!s||!s.supported)return[this.varReference(W)];const o=this.frames.find(e=>e.name===n);if(o)return o.cyclic=!0,[this.varReference(W)];const a=this.parseArguments(e);if(!a)return[this.varReference(W)];const u=s.function.parameters;if(a.length>u.length)return[this.varReference(W)];for(let e=a.length;ethis.rewriteValue(c([e]),t,r)),f=(this.counter++).toString(36),m=new Scope(r),h=new Map;for(let e=0;e{a.length&&(o.push(t.clone({nodes:a})),a=[])};for(const u of e){if("decl"===u.type){const e=this.processBodyDeclaration(u,t,r,n,s);e&&a.push(e);continue}if("atrule"===u.type){flush();const e=this.emitBody(u.nodes||[],t,r,n,s);e.length&&o.push(u.clone({nodes:e}));continue}}return flush(),o}processBodyDeclaration(e,t,r,n,s){let o;if("result"===e.prop.toLowerCase())o=this.resultName(s);else{if(!e.prop.startsWith("--"))return null;{const t=r.get(e.prop);if(!t)return null;o=t;const s=e.value.trim(),a=n.get(e.prop);if(a&&"initial"===s.toLowerCase())return e.clone({prop:o,value:`var(${a})`});if("inherit"===s.toLowerCase()){const t=r.parent?.get(e.prop)??e.prop;return e.clone({prop:o,value:`var(${t})`})}}}return e.clone({prop:o,value:this.rewriteValue(e.value,t,r)})}rewriteValue(e,t,n){const o=m({css:e});if(!o.some(e=>C(e)&&("var"===e[4].value.toLowerCase()||e[4].value.startsWith("--"))))return e;const a=r(o);return i([a],e=>{if(!s(e))return;const r=e.getName();if("var"===r.toLowerCase())return void this.rewriteVarReference(e,n);if(!r.startsWith("--"))return;const o=this.customFunctions.get(r);return o&&o.supported?this.processCall(e,t,n):void 0}),c([a])}rewriteVarReference(e,t){for(const r of e.value){if(n(r))continue;if(!o(r)||!d(r.value))return;const e=r.value[4].value;if(!e.startsWith("--"))return;const s=t.get(e);if(!s)return;return void N(r.value,s)}}parseArguments(e){if(0===e.value.filter(e=>!n(e)).length)return[];const t=p(e.value.flatMap(e=>e.tokens())),r=[];for(const e of t){const t=unwrapArgument(e);if(!t)return!1;r.push(t)}return r}varReference(e){return new f([k.Function,"var(",-1,-1,{value:"var"}],[k.CloseParen,")",-1,-1,void 0],[new l([k.Ident,e,-1,-1,{value:e}])])}argName(e,t){return`${A}-${this.sourceHash}-${e}-arg-${t}`}localName(e,t){return`${A}-${this.sourceHash}-${e}-local-${t.slice(2)}`}resultName(e){return`${A}-${this.sourceHash}-${e}-result`}}function unwrapArgument(e){const t=e.filter(e=>!n(e));return 0!==t.length&&(1===t.length&&u(t[0])&&F(t[0].startToken)?t[0].value:e)}function collectLocalNames(e){const t=new Set;return e.walkDecls(e=>{e.prop.startsWith("--")&&t.add(e.prop)}),t}function parameterType(e){const t=e.getArgumentType();return t&&"*"!==t?t:null}function sourceHashFor(e){if(!e)return"0";const t=B.get(e);if(t)return t;const r=L.createHash("md5");r.update(P.basename(P.dirname(e))+"/"+P.basename(e),"utf8");const n=parseInt(r.digest("hex"),16).toString(36).slice(0,8);return B.set(e,n),n}function staticResultKeyword(e){let t=!1;if(e.walkAtRules(e=>{e.walkDecls(e=>{"result"===e.prop.toLowerCase()&&(t=!0)})}),t)return null;let r=null;for(const t of e.nodes||[])"decl"===t.type&&"result"===t.prop.toLowerCase()&&(r=t.value.trim());return r&&E.has(r.toLowerCase())?r:null}const H=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=new CustomFunctionTranspiler;return{postcssPlugin:"postcss-custom-functions",Once(n,{result:s}){e=getCustomFunctions(n,s,{preserve:t.preserve}),r.setCustomFunctions(e)},OnceExit(e){for(const t of r.getRegistrations())e.append(t)},Declaration(e){if(!H.test(e.value))return;if(!isProcessableDeclaration(e))return;const n=r.processDeclaration(e);null!==n&&(e.cloneBefore({value:n}),t.preserve||e.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-functions/docs/README.md b/plugins/postcss-custom-functions/docs/README.md index 23d97123a8..0013c2483f 100644 --- a/plugins/postcss-custom-functions/docs/README.md +++ b/plugins/postcss-custom-functions/docs/README.md @@ -23,8 +23,14 @@ Custom functions are still an early draft. This plugin is only a partial implementation to avoid conflicts with the final specification. Unsupported: -- typed parameters and return types -- default parameter values +- typed parameters without default values +- untyped parameters with default values +- return types + +Typed parameters with default values are checked by the browser through a +generated `@property` registration. A default value that is not +computationally independent (for example `1em` or `var(--x)`) can not be used +as an `initial-value` and disables type checking for that parameter. ```css diff --git a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts index 94db45dca5..573fc9ba77 100644 --- a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts +++ b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts @@ -11,8 +11,11 @@ export type CustomFunctionAndNode = { }; /** - * A function is supported by this first iteration when it has no typed - * parameters, no default values and no return type. + * A function is supported when it has no return type and every parameter is + * either untyped without a default, or typed with a default value. + * + * Typed parameters without defaults and untyped parameters with defaults are + * not supported yet. */ function isSupportedCustomFunction(customFunction: CustomFunction): boolean { if (customFunction.getReturnType()) { @@ -20,7 +23,11 @@ function isSupportedCustomFunction(customFunction: CustomFunction): boolean { } return customFunction.parameters.every((parameter) => { - return !parameter.getArgumentType() && !parameter.getDefaultValue(); + const type = parameter.getArgumentType(); + const typed = !!type && type !== '*'; + const hasDefault = !!parameter.getDefaultValue(); + + return (typed && hasDefault) || (!typed && !hasDefault); }); } diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts index 16dabc08a4..1f3e68baec 100644 --- a/plugins/postcss-custom-functions/src/index.ts +++ b/plugins/postcss-custom-functions/src/index.ts @@ -34,6 +34,11 @@ const creator: PluginCreator = (opts?: pluginOptions) => { customFunctions = getCustomFunctions(root, result, { preserve: options.preserve }); transpiler.setCustomFunctions(customFunctions); }, + OnceExit(root): void { + for (const registration of transpiler.getRegistrations()) { + root.append(registration); + } + }, Declaration(decl): void { if (!HAS_CUSTOM_FUNCTION.test(decl.value)) { return; diff --git a/plugins/postcss-custom-functions/src/is-processable-declaration.ts b/plugins/postcss-custom-functions/src/is-processable-declaration.ts index ebdd42c5f4..8b2cefb05d 100644 --- a/plugins/postcss-custom-functions/src/is-processable-declaration.ts +++ b/plugins/postcss-custom-functions/src/is-processable-declaration.ts @@ -1,7 +1,7 @@ import type { AtRule, ChildNode, Container, Declaration, Document } from 'postcss'; const blockedParentAtRules = new Set(['function', 'keyframes']); -const GENERATED_PREFIX = '--csstools-custom-function'; +const GENERATED_PREFIX = '--_csstools-cf'; export function isProcessableDeclaration(decl: Declaration): boolean { if (decl.prop.startsWith(GENERATED_PREFIX)) { diff --git a/plugins/postcss-custom-functions/src/is-processable-rule.ts b/plugins/postcss-custom-functions/src/is-processable-rule.ts index 06d5ea2752..133d7ccbef 100644 --- a/plugins/postcss-custom-functions/src/is-processable-rule.ts +++ b/plugins/postcss-custom-functions/src/is-processable-rule.ts @@ -1,6 +1,9 @@ import type { AtRule, ChildNode, Container, Document } from 'postcss'; -const allowedParentAtRules = new Set(['scope', 'container', 'layer']); +// `@function` definitions inside dynamic conditional rules (`@media`, +// `@supports`, `@container`, `@scope`, ...) are conditionally available and +// can not be resolved statically. Only cascade layers are supported. +const allowedParentAtRules = new Set(['layer']); const IS_FUNCTION_REGEX = /^function$/i; diff --git a/plugins/postcss-custom-functions/src/transpile.ts b/plugins/postcss-custom-functions/src/transpile.ts index 86be579108..31661c3131 100644 --- a/plugins/postcss-custom-functions/src/transpile.ts +++ b/plugins/postcss-custom-functions/src/transpile.ts @@ -1,5 +1,7 @@ import type { AtRule, ChildNode, Declaration, Rule } from 'postcss'; -import { Declaration as PostCSSDeclaration } from 'postcss'; +import { AtRule as PostCSSAtRule, Declaration as PostCSSDeclaration } from 'postcss'; +import crypto from 'node:crypto'; +import path from 'node:path'; import type { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms'; import { FunctionNode as CSSAFunctionNode, @@ -15,11 +17,14 @@ import { } from '@csstools/css-parser-algorithms'; import { TokenType, isTokenFunction, isTokenIdent, isTokenOpenCurly, mutateIdent, tokenize } from '@csstools/css-tokenizer'; import type { CustomFunctionAndNode } from './custom-functions-from-root'; +import type { FunctionParameter } from '@csstools/custom-function-parser'; -const GENERATED_PREFIX = '--csstools-custom-function'; +const GENERATED_PREFIX = '--_csstools-cf'; const INVALID_IDENT = `${GENERATED_PREFIX}-invalid`; const CSS_WIDE_KEYWORDS = new Set(['initial', 'inherit', 'unset', 'revert', 'revert-layer']); +const sourceHashes = new Map(); + /** * A single lexical scope while evaluating a custom function. * @@ -44,6 +49,11 @@ class Scope { } } +type CallFrame = { + name: string, + cyclic: boolean, +}; + /** * Transpiles `` calls into a form that works without native * custom function support. @@ -51,18 +61,28 @@ class Scope { * The evaluation model follows the spec closely: the function body is emitted * as custom property declarations on the element where the function is called, * with a generated name per parameter, local variable and result. The browser - * then performs the actual `var()` substitution, cascade and conditional - * handling. + * then performs the actual `var()` substitution, type checking, cascade and + * conditional handling. */ export class CustomFunctionTranspiler { private customFunctions: Map = new Map(); private counter = 0; - private expanding: Array = []; + private frames: Array = []; + private registrations: Array = []; + private sourceHash = '0'; setCustomFunctions(customFunctions: Map): void { this.customFunctions = customFunctions; } + /** + * `@property` registrations generated while transpiling. + * These are top level rules and must be appended to the root. + */ + getRegistrations(): Array { + return this.registrations; + } + /** * Replace all known custom function calls in a declaration value. * Returns the new value, or `null` when nothing changed. @@ -78,6 +98,8 @@ export class CustomFunctionTranspiler { return null; } + this.sourceHash = sourceHashFor(decl.source?.input.from); + const element = parent; const componentValues = parseListOfComponentValues(tokens); @@ -117,7 +139,9 @@ export class CustomFunctionTranspiler { // A function that (indirectly) calls itself is cyclic and evaluates to // the guaranteed-invalid value. - if (this.expanding.includes(name)) { + const existingFrame = this.frames.find((frame) => frame.name === name); + if (existingFrame) { + existingFrame.cyclic = true; return [this.varReference(INVALID_IDENT)]; } @@ -128,11 +152,18 @@ export class CustomFunctionTranspiler { const parameters = entry.function.parameters; - // Without default values every parameter must be provided. - if (args.length !== parameters.length) { + // More arguments than parameters is invalid. + if (args.length > parameters.length) { return [this.varReference(INVALID_IDENT)]; } + // A parameter without a default value must be provided. + for (let i = args.length; i < parameters.length; i++) { + if (!parameters[i].getDefaultValue()) { + return [this.varReference(INVALID_IDENT)]; + } + } + // CSS-wide keywords in `result` are left unresolved by the spec, so they // must be substituted directly instead of going through `var()`. const keywordResult = staticResultKeyword(entry.node); @@ -140,44 +171,99 @@ export class CustomFunctionTranspiler { return [new CSSATokenNode([TokenType.Ident, keywordResult, -1, -1, { value: keywordResult }])]; } + // Arguments are resolved in the scope of the caller, before the function + // itself is evaluated. + const evaluatedArgs = args.map((arg) => { + return this.rewriteValue(stringify([arg]), element, parentScope); + }); + const id = (this.counter++).toString(36); const scope = new Scope(parentScope); + const parameterArgs = new Map(); for (let i = 0; i < parameters.length; i++) { - scope.names.set(parameters[i].getName(), this.argName(id, i)); + const argName = this.argName(id, i); + scope.names.set(parameters[i].getName(), argName); + parameterArgs.set(parameters[i].getName(), argName); } for (const localName of collectLocalNames(entry.node)) { scope.names.set(localName, this.localName(id, localName)); } - this.expanding.push(name); + const frame: CallFrame = { name, cyclic: false }; + this.frames.push(frame); - // Arguments are resolved in the scope of the caller, not the callee. const argDecls: Array = []; for (let i = 0; i < parameters.length; i++) { - argDecls.push(new PostCSSDeclaration({ - prop: this.argName(id, i), - value: this.rewriteValue(stringify([args[i]]), element, parentScope), - source: element.source, - })); + if (i < evaluatedArgs.length) { + argDecls.push(new PostCSSDeclaration({ + prop: this.argName(id, i), + value: evaluatedArgs[i], + source: element.source, + })); + + continue; + } + + // A missing argument resolves to the default value in the callee frame. + const defaultValue = parameters[i].getDefaultValue(); + if (defaultValue) { + argDecls.push(new PostCSSDeclaration({ + prop: this.argName(id, i), + value: this.rewriteValue(defaultValue, element, scope), + source: element.source, + })); + } } if (argDecls.length) { element.before(element.clone({ nodes: argDecls })); } - const bodyNodes = this.emitBody(entry.node.nodes || [], element, scope, id); + for (let i = 0; i < parameters.length; i++) { + const type = parameterType(parameters[i]); + if (!type) { + continue; + } + + this.registrations.push(new PostCSSAtRule({ + name: 'property', + params: this.argName(id, i), + nodes: [ + new PostCSSDeclaration({ prop: 'syntax', value: `"${type}"`, source: element.source }), + new PostCSSDeclaration({ prop: 'inherits', value: 'false', source: element.source }), + new PostCSSDeclaration({ prop: 'initial-value', value: parameters[i].getDefaultValue(), source: element.source }), + ], + source: element.source, + })); + } + + const bodyNodes = this.emitBody(entry.node.nodes || [], element, scope, parameterArgs, id); for (const node of bodyNodes) { element.before(node); } - this.expanding.pop(); + // Once a substitution context is marked as cyclic, the whole evaluation + // returns the guaranteed-invalid value. + if (frame.cyclic) { + element.before(element.clone({ + nodes: [ + new PostCSSDeclaration({ + prop: this.resultName(id), + value: `var(${INVALID_IDENT})`, + source: element.source, + }), + ], + })); + } + + this.frames.pop(); return [this.varReference(this.resultName(id))]; } - private emitBody(containerNodes: Array, element: Rule, scope: Scope, id: string): Array { + private emitBody(containerNodes: Array, element: Rule, scope: Scope, parameterArgs: Map, id: string): Array { const out: Array = []; let pendingDecls: Array = []; @@ -192,7 +278,7 @@ export class CustomFunctionTranspiler { for (const node of containerNodes) { if (node.type === 'decl') { - const processed = this.processBodyDeclaration(node, element, scope, id); + const processed = this.processBodyDeclaration(node, element, scope, parameterArgs, id); if (processed) { pendingDecls.push(processed); } @@ -203,7 +289,7 @@ export class CustomFunctionTranspiler { if (node.type === 'atrule') { flush(); - const children = this.emitBody(node.nodes || [], element, scope, id); + const children = this.emitBody(node.nodes || [], element, scope, parameterArgs, id); if (children.length) { out.push(node.clone({ nodes: children })); } @@ -217,7 +303,7 @@ export class CustomFunctionTranspiler { return out; } - private processBodyDeclaration(decl: Declaration, element: Rule, scope: Scope, id: string): Declaration | null { + private processBodyDeclaration(decl: Declaration, element: Rule, scope: Scope, parameterArgs: Map, id: string): Declaration | null { let prop: string; if (decl.prop.toLowerCase() === 'result') { @@ -229,6 +315,27 @@ export class CustomFunctionTranspiler { } prop = mapped; + + const trimmedValue = decl.value.trim(); + + // `initial` resolves to the parameter's own value (argument or default). + const parameterArg = parameterArgs.get(decl.prop); + if (parameterArg && trimmedValue.toLowerCase() === 'initial') { + return decl.clone({ + prop, + value: `var(${parameterArg})`, + }); + } + + // `inherit` resolves to the custom property of the same name in the + // calling context, which is the outer function scope or the element. + if (trimmedValue.toLowerCase() === 'inherit') { + const inherited = scope.parent?.get(decl.prop) ?? decl.prop; + return decl.clone({ + prop, + value: `var(${inherited})`, + }); + } } else { // Unknown descriptors are invalid and ignored. return null; @@ -332,15 +439,15 @@ export class CustomFunctionTranspiler { } private argName(id: string, index: number): string { - return `${GENERATED_PREFIX}-${id}-arg-${index}`; + return `${GENERATED_PREFIX}-${this.sourceHash}-${id}-arg-${index}`; } private localName(id: string, name: string): string { - return `${GENERATED_PREFIX}-${id}-local-${name.slice(2)}`; + return `${GENERATED_PREFIX}-${this.sourceHash}-${id}-local-${name.slice(2)}`; } private resultName(id: string): string { - return `${GENERATED_PREFIX}-${id}-result`; + return `${GENERATED_PREFIX}-${this.sourceHash}-${id}-result`; } } @@ -381,6 +488,43 @@ function collectLocalNames(atRule: AtRule): Set { return names; } +/** + * The type of a parameter, or `null` when it is untyped. + * `type(*)` is the universal syntax and is treated as untyped. + */ +function parameterType(parameter: FunctionParameter): string | null { + const type = parameter.getArgumentType(); + if (!type || type === '*') { + return null; + } + + return type; +} + +/** + * A short, stable hash of the source file, used to avoid collisions between + * generated names from different stylesheets. + * + * This mirrors the naming approach of `postcss-private-rule`. + */ +function sourceHashFor(from: string | undefined): string { + if (!from) { + return '0'; + } + + const existing = sourceHashes.get(from); + if (existing) { + return existing; + } + + const hash = crypto.createHash('md5'); + hash.update(path.basename(path.dirname(from)) + '/' + path.basename(from), 'utf8'); + const value = parseInt(hash.digest('hex'), 16).toString(36).slice(0, 8); + sourceHashes.set(from, value); + + return value; +} + /** * When the winning `result` descriptor is a CSS-wide keyword and no conditional * rule can change it, the call evaluates to that keyword directly. diff --git a/plugins/postcss-custom-functions/test/_browser.mjs b/plugins/postcss-custom-functions/test/_browser.mjs index 737f6148c0..b19f21f50f 100644 --- a/plugins/postcss-custom-functions/test/_browser.mjs +++ b/plugins/postcss-custom-functions/test/_browser.mjs @@ -18,6 +18,8 @@ const PAGES = [ 'wpt/dashed-function-standard-property.html', 'wpt/local-var-substitution.html', 'wpt/dashed-function-cycles.html', + 'wpt/function-parameter-types.html', + 'wpt/function-layer.html', ]; const requestListener = async function (req, res) { diff --git a/plugins/postcss-custom-functions/test/_tape.mjs b/plugins/postcss-custom-functions/test/_tape.mjs index d6098109b8..3c2740495e 100644 --- a/plugins/postcss-custom-functions/test/_tape.mjs +++ b/plugins/postcss-custom-functions/test/_tape.mjs @@ -14,6 +14,21 @@ postcssTape(plugin)({ nested: { message: 'supports nested calls and dynamic scoping', }, + typed: { + message: 'supports typed parameters with defaults', + }, + 'typed:preserve-true': { + message: 'supports typed parameters with defaults with { preserve: true }', + options: { + preserve: true, + }, + }, + locals: { + message: 'supports initial and inherit in local variables', + }, + cycles: { + message: 'handles cyclic functions', + }, 'conditionals': { message: 'supports conditional rules', }, diff --git a/plugins/postcss-custom-functions/test/basic.expect.css b/plugins/postcss-custom-functions/test/basic.expect.css index 557d7cd39d..0b38eed39b 100644 --- a/plugins/postcss-custom-functions/test/basic.expect.css +++ b/plugins/postcss-custom-functions/test/basic.expect.css @@ -1,34 +1,34 @@ -.negative-a {--csstools-custom-function-0-arg-0: var(--gap); +.negative-a {--_csstools-cf-86hq2t3p-0-arg-0: var(--gap); } .negative-a { - --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); + --_csstools-cf-86hq2t3p-0-result: calc(-1 * var(--_csstools-cf-86hq2t3p-0-arg-0)); } .negative-a { --gap: 1em; - padding: var(--csstools-custom-function-0-result); + padding: var(--_csstools-cf-86hq2t3p-0-result); } -.negative-b {--csstools-custom-function-1-arg-0: 1em; +.negative-b {--_csstools-cf-86hq2t3p-1-arg-0: 1em; } .negative-b { - --csstools-custom-function-1-result: calc(-1 * var(--csstools-custom-function-1-arg-0)); + --_csstools-cf-86hq2t3p-1-result: calc(-1 * var(--_csstools-cf-86hq2t3p-1-arg-0)); } .negative-b { - padding: var(--csstools-custom-function-1-result); + padding: var(--_csstools-cf-86hq2t3p-1-result); } -.circle {--csstools-custom-function-2-arg-0: 2px; +.circle {--_csstools-cf-86hq2t3p-2-arg-0: 2px; } .circle { - --csstools-custom-function-2-local-r2: calc(var(--csstools-custom-function-2-arg-0) * var(--csstools-custom-function-2-arg-0)); - --csstools-custom-function-2-result: calc(3.14159 * var(--csstools-custom-function-2-local-r2)); + --_csstools-cf-86hq2t3p-2-local-r2: calc(var(--_csstools-cf-86hq2t3p-2-arg-0) * var(--_csstools-cf-86hq2t3p-2-arg-0)); + --_csstools-cf-86hq2t3p-2-result: calc(3.14159 * var(--_csstools-cf-86hq2t3p-2-local-r2)); } .circle { - width: var(--csstools-custom-function-2-result); + width: var(--_csstools-cf-86hq2t3p-2-result); } diff --git a/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css b/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css index 634b8fc269..39723cc57f 100644 --- a/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css @@ -2,28 +2,28 @@ result: calc(-1 * var(--value)); } -.negative-a {--csstools-custom-function-0-arg-0: var(--gap); +.negative-a {--_csstools-cf-86hq2t3p-0-arg-0: var(--gap); } .negative-a { - --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); + --_csstools-cf-86hq2t3p-0-result: calc(-1 * var(--_csstools-cf-86hq2t3p-0-arg-0)); } .negative-a { --gap: 1em; - padding: var(--csstools-custom-function-0-result); + padding: var(--_csstools-cf-86hq2t3p-0-result); padding: --negative(var(--gap)); } -.negative-b {--csstools-custom-function-1-arg-0: 1em; +.negative-b {--_csstools-cf-86hq2t3p-1-arg-0: 1em; } .negative-b { - --csstools-custom-function-1-result: calc(-1 * var(--csstools-custom-function-1-arg-0)); + --_csstools-cf-86hq2t3p-1-result: calc(-1 * var(--_csstools-cf-86hq2t3p-1-arg-0)); } .negative-b { - padding: var(--csstools-custom-function-1-result); + padding: var(--_csstools-cf-86hq2t3p-1-result); padding: --negative(1em); } @@ -32,15 +32,15 @@ result: calc(3.14159 * var(--r2)); } -.circle {--csstools-custom-function-2-arg-0: 2px; +.circle {--_csstools-cf-86hq2t3p-2-arg-0: 2px; } .circle { - --csstools-custom-function-2-local-r2: calc(var(--csstools-custom-function-2-arg-0) * var(--csstools-custom-function-2-arg-0)); - --csstools-custom-function-2-result: calc(3.14159 * var(--csstools-custom-function-2-local-r2)); + --_csstools-cf-86hq2t3p-2-local-r2: calc(var(--_csstools-cf-86hq2t3p-2-arg-0) * var(--_csstools-cf-86hq2t3p-2-arg-0)); + --_csstools-cf-86hq2t3p-2-result: calc(3.14159 * var(--_csstools-cf-86hq2t3p-2-local-r2)); } .circle { - width: var(--csstools-custom-function-2-result); + width: var(--_csstools-cf-86hq2t3p-2-result); width: --circle-area(2px); } diff --git a/plugins/postcss-custom-functions/test/conditionals.expect.css b/plugins/postcss-custom-functions/test/conditionals.expect.css index c6eded946f..ac2b8ce978 100644 --- a/plugins/postcss-custom-functions/test/conditionals.expect.css +++ b/plugins/postcss-custom-functions/test/conditionals.expect.css @@ -1,29 +1,29 @@ .conditional { - --csstools-custom-function-0-result: 16px; + --_csstools-cf-c7hag91p-0-result: 16px; } @media (width > 1000px) {.conditional { - --csstools-custom-function-0-result: 20px; + --_csstools-cf-c7hag91p-0-result: 20px; } } .conditional { - width: var(--csstools-custom-function-0-result); + width: var(--_csstools-cf-c7hag91p-0-result); } .conditional-locals { - --csstools-custom-function-1-local-x: FAIL; + --_csstools-cf-c7hag91p-1-local-x: FAIL; } @media (width > 0px) { .conditional-locals { - --csstools-custom-function-1-local-x: PASS; + --_csstools-cf-c7hag91p-1-local-x: PASS; } } .conditional-locals { - --csstools-custom-function-1-result: var(--csstools-custom-function-1-local-x); + --_csstools-cf-c7hag91p-1-result: var(--_csstools-cf-c7hag91p-1-local-x); } .conditional-locals { - width: var(--csstools-custom-function-1-result); + width: var(--_csstools-cf-c7hag91p-1-result); } diff --git a/plugins/postcss-custom-functions/test/cycles.css b/plugins/postcss-custom-functions/test/cycles.css new file mode 100644 index 0000000000..9d476c8a7e --- /dev/null +++ b/plugins/postcss-custom-functions/test/cycles.css @@ -0,0 +1,31 @@ +@function --cycle-through-unused-local() { + --unused: --cycle-through-unused-local(); + result: FAIL-result; +} + +.cycle-through-unused-local { + --tmp: --cycle-through-unused-local(); + --actual: var(--tmp, PASS); +} + +@function --self-cycle() { + result: --self-cycle(); +} + +.self-cycle { + --tmp: --self-cycle(); + --actual: var(--tmp, PASS); +} + +@function --ping() { + result: --pong(); +} + +@function --pong() { + result: --ping(); +} + +.ping-pong { + --tmp: --ping(); + --actual: var(--tmp, PASS); +} diff --git a/plugins/postcss-custom-functions/test/cycles.expect.css b/plugins/postcss-custom-functions/test/cycles.expect.css new file mode 100644 index 0000000000..448e023940 --- /dev/null +++ b/plugins/postcss-custom-functions/test/cycles.expect.css @@ -0,0 +1,40 @@ +.cycle-through-unused-local { + --_csstools-cf-63y219qj-0-local-unused: var(--_csstools-cf-invalid); + --_csstools-cf-63y219qj-0-result: FAIL-result; +} + +.cycle-through-unused-local {--_csstools-cf-63y219qj-0-result: var(--_csstools-cf-invalid); +} + +.cycle-through-unused-local { + --tmp: var(--_csstools-cf-63y219qj-0-result); + --actual: var(--tmp, PASS); +} + +.self-cycle { + --_csstools-cf-63y219qj-1-result: var(--_csstools-cf-invalid); +} + +.self-cycle {--_csstools-cf-63y219qj-1-result: var(--_csstools-cf-invalid); +} + +.self-cycle { + --tmp: var(--_csstools-cf-63y219qj-1-result); + --actual: var(--tmp, PASS); +} + +.ping-pong { + --_csstools-cf-63y219qj-3-result: var(--_csstools-cf-invalid); +} + +.ping-pong { + --_csstools-cf-63y219qj-2-result: var(--_csstools-cf-63y219qj-3-result); +} + +.ping-pong {--_csstools-cf-63y219qj-2-result: var(--_csstools-cf-invalid); +} + +.ping-pong { + --tmp: var(--_csstools-cf-63y219qj-2-result); + --actual: var(--tmp, PASS); +} diff --git a/plugins/postcss-custom-functions/test/examples/example.css b/plugins/postcss-custom-functions/test/examples/example.css index 69e0f2abc1..7f0a128445 100644 --- a/plugins/postcss-custom-functions/test/examples/example.css +++ b/plugins/postcss-custom-functions/test/examples/example.css @@ -2,7 +2,7 @@ result: calc(-1 * var(--value)); } -@function --double(--value) { +@function --double(--value : 1px) { result: calc(2 * var(--value)); } diff --git a/plugins/postcss-custom-functions/test/examples/example.expect.css b/plugins/postcss-custom-functions/test/examples/example.expect.css index b1f80c329b..389a422b65 100644 --- a/plugins/postcss-custom-functions/test/examples/example.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.expect.css @@ -1,15 +1,20 @@ -html {--csstools-custom-function-0-arg-0: var(--gap); +html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); } html { - --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); } -html {--csstools-custom-function-1-arg-0: 4px; +html {--_csstools-cf-1agle9gi-1-arg-0: 4px; } html { - --csstools-custom-function-1-result: calc(2 * var(--csstools-custom-function-1-arg-0)); + --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); } html { --gap: 1em; - padding: var(--csstools-custom-function-0-result); - margin: var(--csstools-custom-function-1-result); + padding: var(--_csstools-cf-1agle9gi-0-result); + margin: var(--_csstools-cf-1agle9gi-1-result); +} +@property --_csstools-cf-1agle9gi-1-arg-0 { + syntax: ""; + inherits: false; + initial-value: 1px; } diff --git a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css index c4ae25cad3..7fd089ca5c 100644 --- a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css @@ -2,28 +2,34 @@ result: calc(-1 * var(--value)); } -@function --double(--value) { +@function --double(--value : 1px) { result: calc(2 * var(--value)); } -html {--csstools-custom-function-0-arg-0: var(--gap); +html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); } html { - --csstools-custom-function-0-result: calc(-1 * var(--csstools-custom-function-0-arg-0)); + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); } -html {--csstools-custom-function-1-arg-0: 4px; +html {--_csstools-cf-1agle9gi-1-arg-0: 4px; } html { - --csstools-custom-function-1-result: calc(2 * var(--csstools-custom-function-1-arg-0)); + --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); } html { --gap: 1em; - padding: var(--csstools-custom-function-0-result); + padding: var(--_csstools-cf-1agle9gi-0-result); padding: --negative(var(--gap)); - margin: var(--csstools-custom-function-1-result); + margin: var(--_csstools-cf-1agle9gi-1-result); margin: --double(4px); } + +@property --_csstools-cf-1agle9gi-1-arg-0 { + syntax: ""; + inherits: false; + initial-value: 1px; +} diff --git a/plugins/postcss-custom-functions/test/invalid.expect.css b/plugins/postcss-custom-functions/test/invalid.expect.css index c82d87b51f..b92a2385f7 100644 --- a/plugins/postcss-custom-functions/test/invalid.expect.css +++ b/plugins/postcss-custom-functions/test/invalid.expect.css @@ -1,9 +1,9 @@ .missing { - width: var(--csstools-custom-function-invalid); + width: var(--_csstools-cf-invalid); } .extra { - width: var(--csstools-custom-function-invalid); + width: var(--_csstools-cf-invalid); } .unknown { @@ -11,5 +11,5 @@ } .empty-args { - width: var(--csstools-custom-function-invalid); + width: var(--_csstools-cf-invalid); } diff --git a/plugins/postcss-custom-functions/test/locals.css b/plugins/postcss-custom-functions/test/locals.css new file mode 100644 index 0000000000..4742db67a3 --- /dev/null +++ b/plugins/postcss-custom-functions/test/locals.css @@ -0,0 +1,34 @@ +@function --initial-local(--x : 10px) { + --x: 20px; + --x: initial; + result: var(--x); +} + +.initial-local { + width: --initial-local(5px); +} + +@function --inherit-local() { + --x: inherit; + result: var(--x); +} + +.inherit-local { + --x: 20px; + width: --inherit-local(); +} + +@function --outer() { + --x: 20px; + result: --inherit-outer(); +} + +@function --inherit-outer() { + --x: inherit; + result: var(--x); +} + +.inherit-outer { + --x: 1px; + width: --outer(); +} diff --git a/plugins/postcss-custom-functions/test/locals.expect.css b/plugins/postcss-custom-functions/test/locals.expect.css new file mode 100644 index 0000000000..d9e45d1dbf --- /dev/null +++ b/plugins/postcss-custom-functions/test/locals.expect.css @@ -0,0 +1,43 @@ +.initial-local {--_csstools-cf-e2576d6c-0-arg-0: 5px; +} + +.initial-local { + --_csstools-cf-e2576d6c-0-local-x: 20px; + --_csstools-cf-e2576d6c-0-local-x: var(--_csstools-cf-e2576d6c-0-arg-0); + --_csstools-cf-e2576d6c-0-result: var(--_csstools-cf-e2576d6c-0-local-x); +} + +.initial-local { + width: var(--_csstools-cf-e2576d6c-0-result); +} + +.inherit-local { + --_csstools-cf-e2576d6c-1-local-x: var(--x); + --_csstools-cf-e2576d6c-1-result: var(--_csstools-cf-e2576d6c-1-local-x); +} + +.inherit-local { + --x: 20px; + width: var(--_csstools-cf-e2576d6c-1-result); +} + +.inherit-outer { + --_csstools-cf-e2576d6c-3-local-x: var(--_csstools-cf-e2576d6c-2-local-x); + --_csstools-cf-e2576d6c-3-result: var(--_csstools-cf-e2576d6c-3-local-x); +} + +.inherit-outer { + --_csstools-cf-e2576d6c-2-local-x: 20px; + --_csstools-cf-e2576d6c-2-result: var(--_csstools-cf-e2576d6c-3-result); +} + +.inherit-outer { + --x: 1px; + width: var(--_csstools-cf-e2576d6c-2-result); +} + +@property --_csstools-cf-e2576d6c-0-arg-0 { + syntax: ""; + inherits: false; + initial-value: 10px; +} diff --git a/plugins/postcss-custom-functions/test/nested.expect.css b/plugins/postcss-custom-functions/test/nested.expect.css index c5251a3ad8..e441f8d6ff 100644 --- a/plugins/postcss-custom-functions/test/nested.expect.css +++ b/plugins/postcss-custom-functions/test/nested.expect.css @@ -1,42 +1,42 @@ -.scope {--csstools-custom-function-0-arg-0: PASS; +.scope {--_csstools-cf-ln0emwu0-0-arg-0: PASS; } .scope { - --csstools-custom-function-1-result: var(--csstools-custom-function-0-arg-0); + --_csstools-cf-ln0emwu0-1-result: var(--_csstools-cf-ln0emwu0-0-arg-0); } .scope { - --csstools-custom-function-0-result: var(--csstools-custom-function-1-result); + --_csstools-cf-ln0emwu0-0-result: var(--_csstools-cf-ln0emwu0-1-result); } .scope { --x: FAIL; - width: var(--csstools-custom-function-0-result); + width: var(--_csstools-cf-ln0emwu0-0-result); } .scopes { - --csstools-custom-function-3-result: var(--csstools-custom-function-2-local-x); + --_csstools-cf-ln0emwu0-3-result: var(--_csstools-cf-ln0emwu0-2-local-x); } .scopes { - --csstools-custom-function-2-local-x: 1; - --csstools-custom-function-2-result: var(--csstools-custom-function-3-result); + --_csstools-cf-ln0emwu0-2-local-x: 1; + --_csstools-cf-ln0emwu0-2-result: var(--_csstools-cf-ln0emwu0-3-result); } .scopes { - --csstools-custom-function-5-result: var(--csstools-custom-function-4-local-x); + --_csstools-cf-ln0emwu0-5-result: var(--_csstools-cf-ln0emwu0-4-local-x); } .scopes { - --csstools-custom-function-4-local-x: 2; - --csstools-custom-function-4-result: var(--csstools-custom-function-5-result); + --_csstools-cf-ln0emwu0-4-local-x: 2; + --_csstools-cf-ln0emwu0-4-result: var(--_csstools-cf-ln0emwu0-5-result); } .scopes { - --csstools-custom-function-6-result: var(--x); + --_csstools-cf-ln0emwu0-6-result: var(--x); } .scopes { --x: 0; - width: var(--csstools-custom-function-2-result) var(--csstools-custom-function-4-result) var(--csstools-custom-function-6-result); + width: var(--_csstools-cf-ln0emwu0-2-result) var(--_csstools-cf-ln0emwu0-4-result) var(--_csstools-cf-ln0emwu0-6-result); } diff --git a/plugins/postcss-custom-functions/test/typed.css b/plugins/postcss-custom-functions/test/typed.css new file mode 100644 index 0000000000..1049714c2e --- /dev/null +++ b/plugins/postcss-custom-functions/test/typed.css @@ -0,0 +1,27 @@ +@function --sized(--x : 10px) { + result: var(--x); +} + +.sized-a { + width: --sized(20px); +} + +.sized-b { + width: --sized(); +} + +@function --colorized(--c : red) { + result: var(--c); +} + +.colorized { + color: --colorized(blue); +} + +@function --mixed(--x, --y : 2px, --z : 3px) { + result: var(--x) var(--y) var(--z); +} + +.mixed { + width: --mixed(1px); +} diff --git a/plugins/postcss-custom-functions/test/typed.expect.css b/plugins/postcss-custom-functions/test/typed.expect.css new file mode 100644 index 0000000000..44b56b1f93 --- /dev/null +++ b/plugins/postcss-custom-functions/test/typed.expect.css @@ -0,0 +1,73 @@ +.sized-a {--_csstools-cf-3ammwpqj-0-arg-0: 20px; +} + +.sized-a { + --_csstools-cf-3ammwpqj-0-result: var(--_csstools-cf-3ammwpqj-0-arg-0); +} + +.sized-a { + width: var(--_csstools-cf-3ammwpqj-0-result); +} + +.sized-b {--_csstools-cf-3ammwpqj-1-arg-0: 10px; +} + +.sized-b { + --_csstools-cf-3ammwpqj-1-result: var(--_csstools-cf-3ammwpqj-1-arg-0); +} + +.sized-b { + width: var(--_csstools-cf-3ammwpqj-1-result); +} + +.colorized {--_csstools-cf-3ammwpqj-2-arg-0: blue; +} + +.colorized { + --_csstools-cf-3ammwpqj-2-result: var(--_csstools-cf-3ammwpqj-2-arg-0); +} + +.colorized { + color: var(--_csstools-cf-3ammwpqj-2-result); +} + +.mixed {--_csstools-cf-3ammwpqj-3-arg-0: 1px;--_csstools-cf-3ammwpqj-3-arg-1: 2px;--_csstools-cf-3ammwpqj-3-arg-2: 3px; +} + +.mixed { + --_csstools-cf-3ammwpqj-3-result: var(--_csstools-cf-3ammwpqj-3-arg-0) var(--_csstools-cf-3ammwpqj-3-arg-1) var(--_csstools-cf-3ammwpqj-3-arg-2); +} + +.mixed { + width: var(--_csstools-cf-3ammwpqj-3-result); +} + +@property --_csstools-cf-3ammwpqj-0-arg-0 { + syntax: ""; + inherits: false; + initial-value: 10px; +} + +@property --_csstools-cf-3ammwpqj-1-arg-0 { + syntax: ""; + inherits: false; + initial-value: 10px; +} + +@property --_csstools-cf-3ammwpqj-2-arg-0 { + syntax: ""; + inherits: false; + initial-value: red; +} + +@property --_csstools-cf-3ammwpqj-3-arg-1 { + syntax: ""; + inherits: false; + initial-value: 2px; +} + +@property --_csstools-cf-3ammwpqj-3-arg-2 { + syntax: ""; + inherits: false; + initial-value: 3px; +} diff --git a/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css b/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css new file mode 100644 index 0000000000..2fbf589f29 --- /dev/null +++ b/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css @@ -0,0 +1,89 @@ +@function --sized(--x : 10px) { + result: var(--x); +} + +.sized-a {--_csstools-cf-3ammwpqj-0-arg-0: 20px; +} + +.sized-a { + --_csstools-cf-3ammwpqj-0-result: var(--_csstools-cf-3ammwpqj-0-arg-0); +} + +.sized-a { + width: var(--_csstools-cf-3ammwpqj-0-result); + width: --sized(20px); +} + +.sized-b {--_csstools-cf-3ammwpqj-1-arg-0: 10px; +} + +.sized-b { + --_csstools-cf-3ammwpqj-1-result: var(--_csstools-cf-3ammwpqj-1-arg-0); +} + +.sized-b { + width: var(--_csstools-cf-3ammwpqj-1-result); + width: --sized(); +} + +@function --colorized(--c : red) { + result: var(--c); +} + +.colorized {--_csstools-cf-3ammwpqj-2-arg-0: blue; +} + +.colorized { + --_csstools-cf-3ammwpqj-2-result: var(--_csstools-cf-3ammwpqj-2-arg-0); +} + +.colorized { + color: var(--_csstools-cf-3ammwpqj-2-result); + color: --colorized(blue); +} + +@function --mixed(--x, --y : 2px, --z : 3px) { + result: var(--x) var(--y) var(--z); +} + +.mixed {--_csstools-cf-3ammwpqj-3-arg-0: 1px;--_csstools-cf-3ammwpqj-3-arg-1: 2px;--_csstools-cf-3ammwpqj-3-arg-2: 3px; +} + +.mixed { + --_csstools-cf-3ammwpqj-3-result: var(--_csstools-cf-3ammwpqj-3-arg-0) var(--_csstools-cf-3ammwpqj-3-arg-1) var(--_csstools-cf-3ammwpqj-3-arg-2); +} + +.mixed { + width: var(--_csstools-cf-3ammwpqj-3-result); + width: --mixed(1px); +} + +@property --_csstools-cf-3ammwpqj-0-arg-0 { + syntax: ""; + inherits: false; + initial-value: 10px; +} + +@property --_csstools-cf-3ammwpqj-1-arg-0 { + syntax: ""; + inherits: false; + initial-value: 10px; +} + +@property --_csstools-cf-3ammwpqj-2-arg-0 { + syntax: ""; + inherits: false; + initial-value: red; +} + +@property --_csstools-cf-3ammwpqj-3-arg-1 { + syntax: ""; + inherits: false; + initial-value: 2px; +} + +@property --_csstools-cf-3ammwpqj-3-arg-2 { + syntax: ""; + inherits: false; + initial-value: 3px; +} diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html index 5b17fee58e..81b57860ab 100644 --- a/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html @@ -110,6 +110,18 @@ } `, }, + { + name: 'Untyped parameter with calc()', + style: ` + @function --f(--x type(*)) { + result: var(--x); + } + #target { + --actual: --f(calc(100px + 1px)); + --expected: calc(100px + 1px); + } + `, + }, { name: 'Passing argument to inner function', style: ` diff --git a/plugins/postcss-custom-functions/test/wpt/function-layer.html b/plugins/postcss-custom-functions/test/wpt/function-layer.html new file mode 100644 index 0000000000..81da78d1e8 --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/function-layer.html @@ -0,0 +1,149 @@ + +Custom Functions: @function within @layer + + +
+
+ + diff --git a/plugins/postcss-custom-functions/test/wpt/function-parameter-types.html b/plugins/postcss-custom-functions/test/wpt/function-parameter-types.html new file mode 100644 index 0000000000..e7274f600d --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/function-parameter-types.html @@ -0,0 +1,249 @@ + +Custom Functions: Typed parameters with defaults + + +
+
+
+
+ + From 4fd660c167d6508eeb374d33bb0d537acaab22bc Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 11:36:26 +0200 Subject: [PATCH 08/21] fix --- .../postcss-custom-functions/dist/index.mjs | 2 +- .../src/custom-functions-from-root.ts | 9 +- .../src/is-computationally-independent.ts | 107 ++++++++++++++++++ .../postcss-custom-functions/src/transpile.ts | 14 ++- .../postcss-custom-functions/test/cycles.css | 10 ++ .../test/cycles.expect.css | 19 +++- .../postcss-custom-functions/test/ignore.css | 16 +++ .../test/wpt/dashed-function-cycles.html | 14 +++ 8 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 plugins/postcss-custom-functions/src/is-computationally-independent.ts diff --git a/plugins/postcss-custom-functions/dist/index.mjs b/plugins/postcss-custom-functions/dist/index.mjs index d36c2ce1f9..f0cdca5531 100644 --- a/plugins/postcss-custom-functions/dist/index.mjs +++ b/plugins/postcss-custom-functions/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parseListOfComponentValues as r,isWhiteSpaceOrCommentNode as n,isFunctionNode as s,isTokenNode as o,isWhitespaceNode as a,isSimpleBlockNode as u,replaceComponentValues as i,stringify as c,TokenNode as l,parseCommaSeparatedListOfComponentValues as p,FunctionNode as f}from"@csstools/css-parser-algorithms";import{tokenize as m,stringify as h,isTokenComment as g,isTokenComma as v,isTokenIdent as d,isTokenDelim as y,isTokenColon as w,isTokenOpenCurly as F,isTokenFunction as C,TokenType as k,mutateIdent as N}from"@csstools/css-tokenizer";import{Declaration as T,AtRule as b}from"postcss";import L from"node:crypto";import P from"node:path";const M=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let u=e(a);if(u?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(u=u.map(e=>t.concat(e)),e=e.parent):e=e.parent}}if(t(o,u),r.nodes){const e=u[0].concat(M);n.set(r,e),s.set(r,u[0])}}});for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex(e=>t.equal(e)));return a}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const R=new Set(["layer"]),$=/^function$/i;function isProcessableRule(e){if(!$.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!R.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}var S,V;(V=S||(S={})).CustomFunction="custom-function",V.FunctionParameter="function-parameter";class CustomFunction{type=S.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],o=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const e=r(this.returnType).filter(e=>!n(e));return 1===e.length&&s(e[0])&&"type"===e[0].getName().toLowerCase()?h(...e[0].value.flatMap(e=>e.tokens()).filter(e=>!g(e))).trim():h(...this.returnType.filter(e=>!g(e))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return h(...this.before)+h(...this.function.tokens())+h(...this.returnsKeyword)+h(...this.returnType)+h(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(e=>e.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&e instanceof CustomFunction&&e.type===S.CustomFunction}}class FunctionParameter{type=S.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],o=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return h(...this.argumentType.filter(e=>!g(e))).trim()}getDefaultValue(){return h(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&e instanceof FunctionParameter&&e.type===S.FunctionParameter}}function parseCustomFunction(e){let t=null,r=-1,a=-1,u=-1,i=e.length;for(let i=0;iu;t--){const r=e[t];if(!n(r))break;i=t}const c=parseFunctionParameters(t.value);return!!c&&new CustomFunction(t,c,-1===u?[]:e.slice(r+1,u).flatMap(e=>e.tokens()),-1===u?[]:e.slice(u,i).flatMap(e=>e.tokens()),e.slice(0,r).flatMap(e=>e.tokens()),e.slice(i).flatMap(e=>e.tokens()))}function parseFunctionParameters(e){if(!e.some(e=>!n(e)))return[];const t=[],r=new Set;for(let n=0;n0&&o(s)&&y(s.value)&&">"===s.value[4].value)t--;else if(-1===c&&0===t&&o(s)&&w(s.value))c=m,l=s.value;else if(-1===c){if(-1!==c)return!1}else p=m}}if(!i)return!1;if(-1!==c&&-1===p)return!1;const h=-1!==c?c-1:m-1;for(let t=e.length-1;t>Math.max(h,p);t--){const r=e[t];if(!a(r))break;f=t}let g;{const t=e.slice(r+1,h+1),o=t.filter(e=>!n(e));g=1===o.length&&s(o[0])&&"type"===o[0].getName().toLowerCase()?o[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}let C=[];if(-1!==p){const t=e.slice(c+1,f);if(hasTopLevelImportant(t))return!1;const r=t.filter(e=>!n(e));C=1===r.length&&u(r[0])&&F(r[0].startToken)?r[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}return{advance:m,node:new FunctionParameter(i,g,l,C,e.slice(0,r).flatMap(e=>e.tokens()),e.slice(f,m+1).flatMap(e=>e.tokens()))}}function hasTopLevelImportant(e){for(let t=0;t{n=!0,t?.onParseError?.(e)}});return!n&&parseCustomFunction(s)}function parse(e,t){return parseFromTokens(m({css:e},{onParseError:t?.onParseError}),t)}function isSupportedCustomFunction(e){return!e.getReturnType()&&e.parameters.every(e=>{const t=e.getArgumentType(),r=!!t&&"*"!==t,n=!!e.getDefaultValue();return r&&n||!r&&!n})}function getCustomFunctions(e,t,r){const n=new Map,s=new Map,o=collectCascadeLayerOrder(e),a=[];if(e.walkAtRules(e=>{if(!isProcessableRule(e))return;const r=parse(e.params.trim(),{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!r)return;const u=r.getName(),i=isSupportedCustomFunction(r),c=(p=o,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?p.has(l.parent)?p.get(l.parent)+1:0:1e7);var l,p;c>=(s.get(u)??-1)&&(s.set(u,c),n.set(u,{node:e,function:r,supported:i})),i&&a.push(e)}),!r.preserve)for(const e of a){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const D=new Set(["function","keyframes"]);function isProcessableDeclaration(e){if(e.prop.startsWith("--_csstools-cf"))return!1;if(!e.parent||"rule"!==e.parent.type)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&D.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const A="--_csstools-cf",W=`${A}-invalid`,E=new Set(["initial","inherit","unset","revert","revert-layer"]),B=new Map;class Scope{names=new Map;parent;constructor(e){this.parent=e}get(e){const t=this.names.get(e);return void 0!==t?t:this.parent?.get(e)}}class CustomFunctionTranspiler{customFunctions=new Map;counter=0;frames=[];registrations=[];sourceHash="0";setCustomFunctions(e){this.customFunctions=e}getRegistrations(){return this.registrations}processDeclaration(e){const t=m({css:e.value});if(!t.some(e=>C(e)&&e[4].value.startsWith("--")))return null;const n=e.parent;if(!n||"rule"!==n.type)return null;this.sourceHash=sourceHashFor(e.source?.input.from);const o=n,a=r(t);i([a],e=>{if(!s(e))return;const t=e.getName();if(!t.startsWith("--"))return;const r=this.customFunctions.get(t);return r&&r.supported?this.processCall(e,o,new Scope(null)):void 0});const u=c([a]);return u===e.value?null:u}processCall(e,t,r){const n=e.getName(),s=this.customFunctions.get(n);if(!s||!s.supported)return[this.varReference(W)];const o=this.frames.find(e=>e.name===n);if(o)return o.cyclic=!0,[this.varReference(W)];const a=this.parseArguments(e);if(!a)return[this.varReference(W)];const u=s.function.parameters;if(a.length>u.length)return[this.varReference(W)];for(let e=a.length;ethis.rewriteValue(c([e]),t,r)),f=(this.counter++).toString(36),m=new Scope(r),h=new Map;for(let e=0;e{a.length&&(o.push(t.clone({nodes:a})),a=[])};for(const u of e){if("decl"===u.type){const e=this.processBodyDeclaration(u,t,r,n,s);e&&a.push(e);continue}if("atrule"===u.type){flush();const e=this.emitBody(u.nodes||[],t,r,n,s);e.length&&o.push(u.clone({nodes:e}));continue}}return flush(),o}processBodyDeclaration(e,t,r,n,s){let o;if("result"===e.prop.toLowerCase())o=this.resultName(s);else{if(!e.prop.startsWith("--"))return null;{const t=r.get(e.prop);if(!t)return null;o=t;const s=e.value.trim(),a=n.get(e.prop);if(a&&"initial"===s.toLowerCase())return e.clone({prop:o,value:`var(${a})`});if("inherit"===s.toLowerCase()){const t=r.parent?.get(e.prop)??e.prop;return e.clone({prop:o,value:`var(${t})`})}}}return e.clone({prop:o,value:this.rewriteValue(e.value,t,r)})}rewriteValue(e,t,n){const o=m({css:e});if(!o.some(e=>C(e)&&("var"===e[4].value.toLowerCase()||e[4].value.startsWith("--"))))return e;const a=r(o);return i([a],e=>{if(!s(e))return;const r=e.getName();if("var"===r.toLowerCase())return void this.rewriteVarReference(e,n);if(!r.startsWith("--"))return;const o=this.customFunctions.get(r);return o&&o.supported?this.processCall(e,t,n):void 0}),c([a])}rewriteVarReference(e,t){for(const r of e.value){if(n(r))continue;if(!o(r)||!d(r.value))return;const e=r.value[4].value;if(!e.startsWith("--"))return;const s=t.get(e);if(!s)return;return void N(r.value,s)}}parseArguments(e){if(0===e.value.filter(e=>!n(e)).length)return[];const t=p(e.value.flatMap(e=>e.tokens())),r=[];for(const e of t){const t=unwrapArgument(e);if(!t)return!1;r.push(t)}return r}varReference(e){return new f([k.Function,"var(",-1,-1,{value:"var"}],[k.CloseParen,")",-1,-1,void 0],[new l([k.Ident,e,-1,-1,{value:e}])])}argName(e,t){return`${A}-${this.sourceHash}-${e}-arg-${t}`}localName(e,t){return`${A}-${this.sourceHash}-${e}-local-${t.slice(2)}`}resultName(e){return`${A}-${this.sourceHash}-${e}-result`}}function unwrapArgument(e){const t=e.filter(e=>!n(e));return 0!==t.length&&(1===t.length&&u(t[0])&&F(t[0].startToken)?t[0].value:e)}function collectLocalNames(e){const t=new Set;return e.walkDecls(e=>{e.prop.startsWith("--")&&t.add(e.prop)}),t}function parameterType(e){const t=e.getArgumentType();return t&&"*"!==t?t:null}function sourceHashFor(e){if(!e)return"0";const t=B.get(e);if(t)return t;const r=L.createHash("md5");r.update(P.basename(P.dirname(e))+"/"+P.basename(e),"utf8");const n=parseInt(r.digest("hex"),16).toString(36).slice(0,8);return B.set(e,n),n}function staticResultKeyword(e){let t=!1;if(e.walkAtRules(e=>{e.walkDecls(e=>{"result"===e.prop.toLowerCase()&&(t=!0)})}),t)return null;let r=null;for(const t of e.nodes||[])"decl"===t.type&&"result"===t.prop.toLowerCase()&&(r=t.value.trim());return r&&E.has(r.toLowerCase())?r:null}const H=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=new CustomFunctionTranspiler;return{postcssPlugin:"postcss-custom-functions",Once(n,{result:s}){e=getCustomFunctions(n,s,{preserve:t.preserve}),r.setCustomFunctions(e)},OnceExit(e){for(const t of r.getRegistrations())e.append(t)},Declaration(e){if(!H.test(e.value))return;if(!isProcessableDeclaration(e))return;const n=r.processDeclaration(e);null!==n&&(e.cloneBefore({value:n}),t.preserve||e.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parseListOfComponentValues as r,isWhiteSpaceOrCommentNode as n,isFunctionNode as s,isTokenNode as o,isWhitespaceNode as a,isSimpleBlockNode as u,replaceComponentValues as i,stringify as c,TokenNode as l,parseCommaSeparatedListOfComponentValues as f,FunctionNode as p}from"@csstools/css-parser-algorithms";import{tokenize as m,stringify as h,isTokenComment as v,isTokenComma as g,isTokenIdent as d,isTokenDelim as w,isTokenColon as y,isTokenOpenCurly as C,isTokenDimension as F,isTokenPercentage as k,isTokenFunction as b,TokenType as N,mutateIdent as L}from"@csstools/css-tokenizer";import{Declaration as T,AtRule as P}from"postcss";import M from"node:crypto";import R from"node:path";const S=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let u=e(a);if(u?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(u=u.map(e=>t.concat(e)),e=e.parent):e=e.parent}}if(t(o,u),r.nodes){const e=u[0].concat(S);n.set(r,e),s.set(r,u[0])}}});for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex(e=>t.equal(e)));return a}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const $=new Set(["layer"]),V=/^function$/i;function isProcessableRule(e){if(!V.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!$.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}var x,D;(D=x||(x={})).CustomFunction="custom-function",D.FunctionParameter="function-parameter";class CustomFunction{type=x.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],o=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const e=r(this.returnType).filter(e=>!n(e));return 1===e.length&&s(e[0])&&"type"===e[0].getName().toLowerCase()?h(...e[0].value.flatMap(e=>e.tokens()).filter(e=>!v(e))).trim():h(...this.returnType.filter(e=>!v(e))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return h(...this.before)+h(...this.function.tokens())+h(...this.returnsKeyword)+h(...this.returnType)+h(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(e=>e.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&e instanceof CustomFunction&&e.type===x.CustomFunction}}class FunctionParameter{type=x.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],o=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return h(...this.argumentType.filter(e=>!v(e))).trim()}getDefaultValue(){return h(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&e instanceof FunctionParameter&&e.type===x.FunctionParameter}}function parseCustomFunction(e){let t=null,r=-1,a=-1,u=-1,i=e.length;for(let i=0;iu;t--){const r=e[t];if(!n(r))break;i=t}const c=parseFunctionParameters(t.value);return!!c&&new CustomFunction(t,c,-1===u?[]:e.slice(r+1,u).flatMap(e=>e.tokens()),-1===u?[]:e.slice(u,i).flatMap(e=>e.tokens()),e.slice(0,r).flatMap(e=>e.tokens()),e.slice(i).flatMap(e=>e.tokens()))}function parseFunctionParameters(e){if(!e.some(e=>!n(e)))return[];const t=[],r=new Set;for(let n=0;n0&&o(s)&&w(s.value)&&">"===s.value[4].value)t--;else if(-1===c&&0===t&&o(s)&&y(s.value))c=m,l=s.value;else if(-1===c){if(-1!==c)return!1}else f=m}}if(!i)return!1;if(-1!==c&&-1===f)return!1;const h=-1!==c?c-1:m-1;for(let t=e.length-1;t>Math.max(h,f);t--){const r=e[t];if(!a(r))break;p=t}let v;{const t=e.slice(r+1,h+1),o=t.filter(e=>!n(e));v=1===o.length&&s(o[0])&&"type"===o[0].getName().toLowerCase()?o[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}let F=[];if(-1!==f){const t=e.slice(c+1,p);if(hasTopLevelImportant(t))return!1;const r=t.filter(e=>!n(e));F=1===r.length&&u(r[0])&&C(r[0].startToken)?r[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}return{advance:m,node:new FunctionParameter(i,v,l,F,e.slice(0,r).flatMap(e=>e.tokens()),e.slice(p,m+1).flatMap(e=>e.tokens()))}}function hasTopLevelImportant(e){for(let t=0;t{n=!0,t?.onParseError?.(e)}});return!n&&parseCustomFunction(s)}function parse(e,t){return parseFromTokens(m({css:e},{onParseError:t?.onParseError}),t)}const A=new Set(["initial","inherit","unset","revert","revert-layer"]),W=new Set(["em","rem","ex","rex","cap","rcap","ch","rch","ic","ric","lh","rlh","vw","vh","vi","vb","vmin","vmax","svw","svh","svi","svb","svmin","svmax","lvw","lvh","lvi","lvb","lvmin","lvmax","dvw","dvh","dvi","dvb","dvmin","dvmax","cqw","cqh","cqi","cqb","cqmin","cqmax","fr"]),I=new Set(["var","env","attr","light-dark","image-set","-webkit-image-set"]),E=new Set(["rgb","rgba","hsl","hsla","hwb","lab","lch","oklab","oklch","color","color-mix","color-contrast","device-cmyk"]);function areComponentsIndependent(e,t){for(const r of e){if(s(r)){const e=r.getName().toLowerCase();if(I.has(e))return!1;if(!areComponentsIndependent(r.value,t||E.has(e)))return!1;continue}if(u(r)){if(!areComponentsIndependent(r.value,t))return!1;continue}if(!o(r))continue;const e=r.value;if(F(e)){if(W.has(e[4].unit.toLowerCase()))return!1}else if(k(e)){if(!t)return!1}else if(d(e)){const t=e[4].value.toLowerCase();if("currentcolor"===t||A.has(t))return!1}}return!0}function isSupportedCustomFunction(e){return!e.getReturnType()&&e.parameters.every(e=>{const t=e.getArgumentType(),n=!!t&&"*"!==t,s=!!e.getDefaultValue();return n&&s?(o=e.getDefaultValue(),areComponentsIndependent(r(m({css:o})),!1)):!n&&!s;var o})}function getCustomFunctions(e,t,r){const n=new Map,s=new Map,o=collectCascadeLayerOrder(e),a=[];if(e.walkAtRules(e=>{if(!isProcessableRule(e))return;const r=parse(e.params.trim(),{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!r)return;const u=r.getName(),i=isSupportedCustomFunction(r),c=(f=o,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?f.has(l.parent)?f.get(l.parent)+1:0:1e7);var l,f;c>=(s.get(u)??-1)&&(s.set(u,c),n.set(u,{node:e,function:r,supported:i})),i&&a.push(e)}),!r.preserve)for(const e of a){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const B=new Set(["function","keyframes"]);function isProcessableDeclaration(e){if(e.prop.startsWith("--_csstools-cf"))return!1;if(!e.parent||"rule"!==e.parent.type)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&B.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const H="--_csstools-cf",K=`${H}-invalid`,O=new Set(["initial","inherit","unset","revert","revert-layer"]),q=new Map;class Scope{names=new Map;parent;constructor(e){this.parent=e}get(e){const t=this.names.get(e);return void 0!==t?t:this.parent?.get(e)}}class CustomFunctionTranspiler{customFunctions=new Map;counter=0;frames=[];registrations=[];sourceHash="0";setCustomFunctions(e){this.customFunctions=e}getRegistrations(){return this.registrations}processDeclaration(e){const t=m({css:e.value});if(!t.some(e=>b(e)&&e[4].value.startsWith("--")))return null;const n=e.parent;if(!n||"rule"!==n.type)return null;this.sourceHash=sourceHashFor(e.source?.input.from);const o=n,a=r(t);i([a],e=>{if(!s(e))return;const t=e.getName();if(!t.startsWith("--"))return;const r=this.customFunctions.get(t);return r&&r.supported?this.processCall(e,o,new Scope(null)):void 0});const u=c([a]);return u===e.value?null:u}processCall(e,t,r){const n=e.getName(),s=this.customFunctions.get(n);if(!s||!s.supported)return[this.varReference(K)];const o=this.frames.find(e=>e.name===n);if(o)return o.cyclic=!0,[this.varReference(K)];const a=this.parseArguments(e);if(!a)return[this.varReference(K)];const u=s.function.parameters;if(a.length>u.length)return[this.varReference(K)];for(let e=a.length;ethis.rewriteValue(c([e]),t,r)),p=(this.counter++).toString(36),m=new Scope(r),h=new Map;for(let e=0;e{a.length&&(o.push(t.clone({nodes:a})),a=[])};for(const u of e){if("decl"===u.type){const e=this.processBodyDeclaration(u,t,r,n,s);e&&a.push(e);continue}if("atrule"===u.type){flush();const e=this.emitBody(u.nodes||[],t,r,n,s);e.length&&o.push(u.clone({nodes:e}));continue}}return flush(),o}processBodyDeclaration(e,t,r,n,s){let o;if("result"===e.prop.toLowerCase())o=this.resultName(s);else{if(!e.prop.startsWith("--"))return null;{const t=r.get(e.prop);if(!t)return null;o=t;const s=e.value.trim(),a=n.get(e.prop);if(a&&"initial"===s.toLowerCase())return e.clone({prop:o,value:`var(${a})`});if("inherit"===s.toLowerCase()){const t=r.parent?.get(e.prop)??e.prop;return e.clone({prop:o,value:`var(${t})`})}}}return e.clone({prop:o,value:this.rewriteValue(e.value,t,r)})}rewriteValue(e,t,n){const o=m({css:e});if(!o.some(e=>b(e)&&("var"===e[4].value.toLowerCase()||e[4].value.startsWith("--"))))return e;const a=r(o);return i([a],e=>{if(!s(e))return;const r=e.getName();if("var"===r.toLowerCase())return void this.rewriteVarReference(e,n);if(!r.startsWith("--"))return;const o=this.customFunctions.get(r);return o&&o.supported?this.processCall(e,t,n):void 0}),c([a])}rewriteVarReference(e,t){for(const r of e.value){if(n(r))continue;if(!o(r)||!d(r.value))return;const e=r.value[4].value;if(!e.startsWith("--"))return;const s=t.get(e);if(!s)return;return void L(r.value,s)}}parseArguments(e){if(0===e.value.filter(e=>!n(e)).length)return[];const t=f(e.value.flatMap(e=>e.tokens())),r=[];for(const e of t){const t=unwrapArgument(e);if(!t)return!1;r.push(t)}return r}varReference(e){return new p([N.Function,"var(",-1,-1,{value:"var"}],[N.CloseParen,")",-1,-1,void 0],[new l([N.Ident,e,-1,-1,{value:e}])])}argName(e,t){return`${H}-${this.sourceHash}-${e}-arg-${t}`}localName(e,t){return`${H}-${this.sourceHash}-${e}-local-${t.slice(2)}`}resultName(e){return`${H}-${this.sourceHash}-${e}-result`}}function unwrapArgument(e){const t=e.filter(e=>!n(e));return 0!==t.length&&(1===t.length&&u(t[0])&&C(t[0].startToken)?t[0].value:e)}function collectLocalNames(e){const t=new Set;return e.walkDecls(e=>{e.prop.startsWith("--")&&t.add(e.prop)}),t}function parameterType(e){const t=e.getArgumentType();return t&&"*"!==t?t:null}function sourceHashFor(e){if(!e)return"0";const t=q.get(e);if(t)return t;const r=M.createHash("md5");r.update(R.basename(R.dirname(e))+"/"+R.basename(e),"utf8");const n=parseInt(r.digest("hex"),16).toString(36).slice(0,8);return q.set(e,n),n}function staticResultKeyword(e){let t=!1;if(e.walkAtRules(e=>{e.walkDecls(e=>{"result"===e.prop.toLowerCase()&&(t=!0)})}),t)return null;let r=null;for(const t of e.nodes||[])"decl"===t.type&&"result"===t.prop.toLowerCase()&&(r=t.value.trim());return r&&O.has(r.toLowerCase())?r:null}const z=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=new CustomFunctionTranspiler;return{postcssPlugin:"postcss-custom-functions",Once(n,{result:s}){e=getCustomFunctions(n,s,{preserve:t.preserve}),r.setCustomFunctions(e)},OnceExit(e){for(const t of r.getRegistrations())e.append(t)},Declaration(e){if(!z.test(e.value))return;if(!isProcessableDeclaration(e))return;const n=r.processDeclaration(e);null!==n&&(e.cloneBefore({value:n}),t.preserve||e.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts index 573fc9ba77..1f7d9e59ff 100644 --- a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts +++ b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts @@ -3,6 +3,7 @@ import { cascadeLayerNumberForNode, collectCascadeLayerOrder } from './cascade-l import { isProcessableRule } from './is-processable-rule'; import type { CustomFunction } from '@csstools/custom-function-parser'; import { parse } from '@csstools/custom-function-parser'; +import { isComputationallyIndependent } from './is-computationally-independent'; export type CustomFunctionAndNode = { function: CustomFunction; @@ -27,7 +28,13 @@ function isSupportedCustomFunction(customFunction: CustomFunction): boolean { const typed = !!type && type !== '*'; const hasDefault = !!parameter.getDefaultValue(); - return (typed && hasDefault) || (!typed && !hasDefault); + if (typed && hasDefault) { + // A default value becomes the `initial-value` of the generated + // `@property` registration and must be computationally independent. + return isComputationallyIndependent(parameter.getDefaultValue()); + } + + return !typed && !hasDefault; }); } diff --git a/plugins/postcss-custom-functions/src/is-computationally-independent.ts b/plugins/postcss-custom-functions/src/is-computationally-independent.ts new file mode 100644 index 0000000000..2156f9fe1c --- /dev/null +++ b/plugins/postcss-custom-functions/src/is-computationally-independent.ts @@ -0,0 +1,107 @@ +import type { ComponentValue } from '@csstools/css-parser-algorithms'; +import { isFunctionNode, isSimpleBlockNode, isTokenNode, parseListOfComponentValues } from '@csstools/css-parser-algorithms'; +import { isTokenDimension, isTokenIdent, isTokenPercentage, tokenize } from '@csstools/css-tokenizer'; + +const CSS_WIDE_KEYWORDS = new Set(['initial', 'inherit', 'unset', 'revert', 'revert-layer']); + +// Units that depend on the element, its container or the viewport. +const RELATIVE_UNITS = new Set([ + // font relative + 'em', 'rem', 'ex', 'rex', 'cap', 'rcap', 'ch', 'rch', 'ic', 'ric', 'lh', 'rlh', + // viewport relative + 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', + 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', + 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax', + 'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax', + // container relative + 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', + // flex + 'fr', +]); + +// Functions that depend on context or other values. +const NON_INDEPENDENT_FUNCTIONS = new Set([ + 'var', + 'env', + 'attr', + 'light-dark', + 'image-set', + '-webkit-image-set', +]); + +// Inside these functions a percentage is an absolute component, not a relative length. +const COLOR_FUNCTIONS = new Set([ + 'rgb', 'rgba', 'hsl', 'hsla', 'hwb', + 'lab', 'lch', 'oklab', 'oklch', + 'color', 'color-mix', 'color-contrast', 'device-cmyk', +]); + +/** + * Whether a value is computationally independent. + * + * `@property` initial values must be computationally independent. This is a + * conservative check: when in doubt the value is rejected. + * + * https://drafts.css-houdini.org/css-properties-values-api-1/#computationally-independent + */ +export function isComputationallyIndependent(value: string): boolean { + const componentValues = parseListOfComponentValues(tokenize({ css: value })); + + return areComponentsIndependent(componentValues, false); +} + +function areComponentsIndependent(componentValues: Array, inColor: boolean): boolean { + for (const componentValue of componentValues) { + if (isFunctionNode(componentValue)) { + const name = componentValue.getName().toLowerCase(); + if (NON_INDEPENDENT_FUNCTIONS.has(name)) { + return false; + } + + if (!areComponentsIndependent(componentValue.value, inColor || COLOR_FUNCTIONS.has(name))) { + return false; + } + + continue; + } + + if (isSimpleBlockNode(componentValue)) { + if (!areComponentsIndependent(componentValue.value, inColor)) { + return false; + } + + continue; + } + + if (!isTokenNode(componentValue)) { + continue; + } + + const token = componentValue.value; + + if (isTokenDimension(token)) { + if (RELATIVE_UNITS.has(token[4].unit.toLowerCase())) { + return false; + } + + continue; + } + + if (isTokenPercentage(token)) { + if (!inColor) { + return false; + } + + continue; + } + + if (isTokenIdent(token)) { + const name = token[4].value.toLowerCase(); + if (name === 'currentcolor' || CSS_WIDE_KEYWORDS.has(name)) { + return false; + } + } + } + + return true; +} diff --git a/plugins/postcss-custom-functions/src/transpile.ts b/plugins/postcss-custom-functions/src/transpile.ts index 31661c3131..4e34dc747f 100644 --- a/plugins/postcss-custom-functions/src/transpile.ts +++ b/plugins/postcss-custom-functions/src/transpile.ts @@ -166,10 +166,10 @@ export class CustomFunctionTranspiler { // CSS-wide keywords in `result` are left unresolved by the spec, so they // must be substituted directly instead of going through `var()`. + // + // This is only used when the function is not cyclic. A cycle anywhere in + // the body makes the whole evaluation invalid. const keywordResult = staticResultKeyword(entry.node); - if (keywordResult) { - return [new CSSATokenNode([TokenType.Ident, keywordResult, -1, -1, { value: keywordResult }])]; - } // Arguments are resolved in the scope of the caller, before the function // itself is evaluated. @@ -260,6 +260,14 @@ export class CustomFunctionTranspiler { this.frames.pop(); + if (frame.cyclic) { + return [this.varReference(INVALID_IDENT)]; + } + + if (keywordResult) { + return [new CSSATokenNode([TokenType.Ident, keywordResult, -1, -1, { value: keywordResult }])]; + } + return [this.varReference(this.resultName(id))]; } diff --git a/plugins/postcss-custom-functions/test/cycles.css b/plugins/postcss-custom-functions/test/cycles.css index 9d476c8a7e..4aa21160a8 100644 --- a/plugins/postcss-custom-functions/test/cycles.css +++ b/plugins/postcss-custom-functions/test/cycles.css @@ -29,3 +29,13 @@ --tmp: --ping(); --actual: var(--tmp, PASS); } + +@function --keyword-with-cycle() { + --unused: --keyword-with-cycle(); + result: inherit; +} + +.keyword-with-cycle { + --tmp: --keyword-with-cycle(); + --actual: var(--tmp, PASS); +} diff --git a/plugins/postcss-custom-functions/test/cycles.expect.css b/plugins/postcss-custom-functions/test/cycles.expect.css index 448e023940..2c6abbf404 100644 --- a/plugins/postcss-custom-functions/test/cycles.expect.css +++ b/plugins/postcss-custom-functions/test/cycles.expect.css @@ -7,7 +7,7 @@ } .cycle-through-unused-local { - --tmp: var(--_csstools-cf-63y219qj-0-result); + --tmp: var(--_csstools-cf-invalid); --actual: var(--tmp, PASS); } @@ -19,7 +19,7 @@ } .self-cycle { - --tmp: var(--_csstools-cf-63y219qj-1-result); + --tmp: var(--_csstools-cf-invalid); --actual: var(--tmp, PASS); } @@ -35,6 +35,19 @@ } .ping-pong { - --tmp: var(--_csstools-cf-63y219qj-2-result); + --tmp: var(--_csstools-cf-invalid); + --actual: var(--tmp, PASS); +} + +.keyword-with-cycle { + --_csstools-cf-63y219qj-4-local-unused: var(--_csstools-cf-invalid); + --_csstools-cf-63y219qj-4-result: inherit; +} + +.keyword-with-cycle {--_csstools-cf-63y219qj-4-result: var(--_csstools-cf-invalid); +} + +.keyword-with-cycle { + --tmp: var(--_csstools-cf-invalid); --actual: var(--tmp, PASS); } diff --git a/plugins/postcss-custom-functions/test/ignore.css b/plugins/postcss-custom-functions/test/ignore.css index b971ae2e08..e957222cdc 100644 --- a/plugins/postcss-custom-functions/test/ignore.css +++ b/plugins/postcss-custom-functions/test/ignore.css @@ -21,3 +21,19 @@ .typed-return { width: --typed-return(); } + +@function --em-default(--x : 1em) { + result: var(--x); +} + +.em-default { + width: --em-default(10px); +} + +@function --var-default(--x : var(--y)) { + result: var(--x); +} + +.var-default { + width: --var-default(10px); +} diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html index 1352339702..20f7e2bb51 100644 --- a/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html @@ -338,6 +338,20 @@ } `, }, + { + name: 'Cycle invalidates a CSS-wide keyword result', + style: ` + @function --f() { + --unused: --f(); + result: inherit; + } + #target { + --tmp: --f(); + --actual: var(--tmp, PASS); + --expected: PASS; + } + `, + }, ]; for (const testCase of testCases) { From a416c09c7e0096d734687117c5c746bb2e05ec6e Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 30 Sep 2026 11:53:13 +0200 Subject: [PATCH 09/21] wip --- plugins/postcss-custom-functions/README.md | 41 ++- .../postcss-custom-functions/dist/index.mjs | 2 +- .../postcss-custom-functions/docs/README.md | 6 +- .../src/custom-functions-from-root.ts | 99 ++++-- plugins/postcss-custom-functions/src/index.ts | 4 +- .../src/is-processable-rule.ts | 16 +- .../src/static-result-keyword.ts | 39 +++ .../postcss-custom-functions/src/transpile.ts | 282 +++++++++--------- .../test/_browser.mjs | 59 +++- .../postcss-custom-functions/test/_tape.mjs | 9 + .../test/basic.expect.css | 22 +- .../test/basic.preserve-true.expect.css | 18 +- .../test/conditional-definitions.css | 51 ++++ .../test/conditional-definitions.expect.css | 47 +++ ...ional-definitions.preserve-true.expect.css | 92 ++++++ .../test/conditionals.expect.css | 8 +- .../test/cycles.expect.css | 18 +- .../test/examples/example.expect.css | 23 +- .../examples/example.preserve-true.expect.css | 12 +- .../postcss-custom-functions/test/ignore.css | 11 + .../test/invalid.expect.css | 15 +- .../test/locals.expect.css | 30 +- .../test/nested.expect.css | 24 +- .../test/typed.expect.css | 36 ++- .../test/typed.preserve-true.expect.css | 32 +- .../test/wpt/dashed-function-cycles.html | 6 +- .../test/wpt/dashed-function-eval.html | 6 +- .../dashed-function-standard-property.html | 6 +- .../wpt/function-conditional-definitions.html | 152 ++++++++++ .../test/wpt/function-conditionals.html | 6 +- .../test/wpt/function-in-media.html | 72 +++++ .../test/wpt/function-layer.html | 6 +- .../test/wpt/function-parameter-types.html | 24 +- .../test/wpt/local-var-substitution.html | 6 +- 34 files changed, 949 insertions(+), 331 deletions(-) create mode 100644 plugins/postcss-custom-functions/src/static-result-keyword.ts create mode 100644 plugins/postcss-custom-functions/test/conditional-definitions.css create mode 100644 plugins/postcss-custom-functions/test/conditional-definitions.expect.css create mode 100644 plugins/postcss-custom-functions/test/conditional-definitions.preserve-true.expect.css create mode 100644 plugins/postcss-custom-functions/test/wpt/function-conditional-definitions.html create mode 100644 plugins/postcss-custom-functions/test/wpt/function-in-media.html diff --git a/plugins/postcss-custom-functions/README.md b/plugins/postcss-custom-functions/README.md index 03a24757d2..bc689ec636 100644 --- a/plugins/postcss-custom-functions/README.md +++ b/plugins/postcss-custom-functions/README.md @@ -15,11 +15,15 @@ Unsupported: - typed parameters without default values - untyped parameters with default values - return types +- `@function` definitions inside `@container` or `@scope` + +Definitions inside `@layer`, `@media` and `@supports` are supported. When a name +has several active definitions, the browser cascade selects the strongest one. Typed parameters with default values are checked by the browser through a generated `@property` registration. A default value that is not computationally independent (for example `1em` or `var(--x)`) can not be used -as an `initial-value` and disables type checking for that parameter. +as an `initial-value` and makes the function unsupported. ```css @function --negative(--value) { @@ -38,22 +42,17 @@ html { /* becomes */ -html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); -} -html { - --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); -} -html {--_csstools-cf-1agle9gi-1-arg-0: 4px; -} -html { - --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); -} -html { +html {--_csstools-cf-1agle9gi-1-arg-0: var(--gap); +}html { + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-1-arg-0)); +}html {--_csstools-cf-1agle9gi-3-arg-0: 4px; +}html { + --_csstools-cf-1agle9gi-2-result: calc(2 * var(--_csstools-cf-1agle9gi-3-arg-0)); +}html { --gap: 1em; padding: var(--_csstools-cf-1agle9gi-0-result); - margin: var(--_csstools-cf-1agle9gi-1-result); -} -@property --_csstools-cf-1agle9gi-1-arg-0 { + margin: var(--_csstools-cf-1agle9gi-2-result); +}@property --_csstools-cf-1agle9gi-3-arg-0 { syntax: ""; inherits: false; initial-value: 1px; @@ -126,29 +125,29 @@ html { result: calc(2 * var(--value)); } -html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); +html {--_csstools-cf-1agle9gi-1-arg-0: var(--gap); } html { - --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-1-arg-0)); } -html {--_csstools-cf-1agle9gi-1-arg-0: 4px; +html {--_csstools-cf-1agle9gi-3-arg-0: 4px; } html { - --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); + --_csstools-cf-1agle9gi-2-result: calc(2 * var(--_csstools-cf-1agle9gi-3-arg-0)); } html { --gap: 1em; padding: var(--_csstools-cf-1agle9gi-0-result); padding: --negative(var(--gap)); - margin: var(--_csstools-cf-1agle9gi-1-result); + margin: var(--_csstools-cf-1agle9gi-2-result); margin: --double(4px); } -@property --_csstools-cf-1agle9gi-1-arg-0 { +@property --_csstools-cf-1agle9gi-3-arg-0 { syntax: ""; inherits: false; initial-value: 1px; diff --git a/plugins/postcss-custom-functions/dist/index.mjs b/plugins/postcss-custom-functions/dist/index.mjs index f0cdca5531..bfc4b4185b 100644 --- a/plugins/postcss-custom-functions/dist/index.mjs +++ b/plugins/postcss-custom-functions/dist/index.mjs @@ -1 +1 @@ -import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parseListOfComponentValues as r,isWhiteSpaceOrCommentNode as n,isFunctionNode as s,isTokenNode as o,isWhitespaceNode as a,isSimpleBlockNode as u,replaceComponentValues as i,stringify as c,TokenNode as l,parseCommaSeparatedListOfComponentValues as f,FunctionNode as p}from"@csstools/css-parser-algorithms";import{tokenize as m,stringify as h,isTokenComment as v,isTokenComma as g,isTokenIdent as d,isTokenDelim as w,isTokenColon as y,isTokenOpenCurly as C,isTokenDimension as F,isTokenPercentage as k,isTokenFunction as b,TokenType as N,mutateIdent as L}from"@csstools/css-tokenizer";import{Declaration as T,AtRule as P}from"postcss";import M from"node:crypto";import R from"node:path";const S=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let u=e(a);if(u?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(u=u.map(e=>t.concat(e)),e=e.parent):e=e.parent}}if(t(o,u),r.nodes){const e=u[0].concat(S);n.set(r,e),s.set(r,u[0])}}});for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex(e=>t.equal(e)));return a}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const $=new Set(["layer"]),V=/^function$/i;function isProcessableRule(e){if(!V.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!$.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}var x,D;(D=x||(x={})).CustomFunction="custom-function",D.FunctionParameter="function-parameter";class CustomFunction{type=x.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],o=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const e=r(this.returnType).filter(e=>!n(e));return 1===e.length&&s(e[0])&&"type"===e[0].getName().toLowerCase()?h(...e[0].value.flatMap(e=>e.tokens()).filter(e=>!v(e))).trim():h(...this.returnType.filter(e=>!v(e))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return h(...this.before)+h(...this.function.tokens())+h(...this.returnsKeyword)+h(...this.returnType)+h(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(e=>e.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&e instanceof CustomFunction&&e.type===x.CustomFunction}}class FunctionParameter{type=x.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],o=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return h(...this.argumentType.filter(e=>!v(e))).trim()}getDefaultValue(){return h(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&e instanceof FunctionParameter&&e.type===x.FunctionParameter}}function parseCustomFunction(e){let t=null,r=-1,a=-1,u=-1,i=e.length;for(let i=0;iu;t--){const r=e[t];if(!n(r))break;i=t}const c=parseFunctionParameters(t.value);return!!c&&new CustomFunction(t,c,-1===u?[]:e.slice(r+1,u).flatMap(e=>e.tokens()),-1===u?[]:e.slice(u,i).flatMap(e=>e.tokens()),e.slice(0,r).flatMap(e=>e.tokens()),e.slice(i).flatMap(e=>e.tokens()))}function parseFunctionParameters(e){if(!e.some(e=>!n(e)))return[];const t=[],r=new Set;for(let n=0;n0&&o(s)&&w(s.value)&&">"===s.value[4].value)t--;else if(-1===c&&0===t&&o(s)&&y(s.value))c=m,l=s.value;else if(-1===c){if(-1!==c)return!1}else f=m}}if(!i)return!1;if(-1!==c&&-1===f)return!1;const h=-1!==c?c-1:m-1;for(let t=e.length-1;t>Math.max(h,f);t--){const r=e[t];if(!a(r))break;p=t}let v;{const t=e.slice(r+1,h+1),o=t.filter(e=>!n(e));v=1===o.length&&s(o[0])&&"type"===o[0].getName().toLowerCase()?o[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}let F=[];if(-1!==f){const t=e.slice(c+1,p);if(hasTopLevelImportant(t))return!1;const r=t.filter(e=>!n(e));F=1===r.length&&u(r[0])&&C(r[0].startToken)?r[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}return{advance:m,node:new FunctionParameter(i,v,l,F,e.slice(0,r).flatMap(e=>e.tokens()),e.slice(p,m+1).flatMap(e=>e.tokens()))}}function hasTopLevelImportant(e){for(let t=0;t{n=!0,t?.onParseError?.(e)}});return!n&&parseCustomFunction(s)}function parse(e,t){return parseFromTokens(m({css:e},{onParseError:t?.onParseError}),t)}const A=new Set(["initial","inherit","unset","revert","revert-layer"]),W=new Set(["em","rem","ex","rex","cap","rcap","ch","rch","ic","ric","lh","rlh","vw","vh","vi","vb","vmin","vmax","svw","svh","svi","svb","svmin","svmax","lvw","lvh","lvi","lvb","lvmin","lvmax","dvw","dvh","dvi","dvb","dvmin","dvmax","cqw","cqh","cqi","cqb","cqmin","cqmax","fr"]),I=new Set(["var","env","attr","light-dark","image-set","-webkit-image-set"]),E=new Set(["rgb","rgba","hsl","hsla","hwb","lab","lch","oklab","oklch","color","color-mix","color-contrast","device-cmyk"]);function areComponentsIndependent(e,t){for(const r of e){if(s(r)){const e=r.getName().toLowerCase();if(I.has(e))return!1;if(!areComponentsIndependent(r.value,t||E.has(e)))return!1;continue}if(u(r)){if(!areComponentsIndependent(r.value,t))return!1;continue}if(!o(r))continue;const e=r.value;if(F(e)){if(W.has(e[4].unit.toLowerCase()))return!1}else if(k(e)){if(!t)return!1}else if(d(e)){const t=e[4].value.toLowerCase();if("currentcolor"===t||A.has(t))return!1}}return!0}function isSupportedCustomFunction(e){return!e.getReturnType()&&e.parameters.every(e=>{const t=e.getArgumentType(),n=!!t&&"*"!==t,s=!!e.getDefaultValue();return n&&s?(o=e.getDefaultValue(),areComponentsIndependent(r(m({css:o})),!1)):!n&&!s;var o})}function getCustomFunctions(e,t,r){const n=new Map,s=new Map,o=collectCascadeLayerOrder(e),a=[];if(e.walkAtRules(e=>{if(!isProcessableRule(e))return;const r=parse(e.params.trim(),{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!r)return;const u=r.getName(),i=isSupportedCustomFunction(r),c=(f=o,(l=e).parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?f.has(l.parent)?f.get(l.parent)+1:0:1e7);var l,f;c>=(s.get(u)??-1)&&(s.set(u,c),n.set(u,{node:e,function:r,supported:i})),i&&a.push(e)}),!r.preserve)for(const e of a){const t=e.parent;e.remove(),removeEmptyAncestorBlocks(t)}return n}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const B=new Set(["function","keyframes"]);function isProcessableDeclaration(e){if(e.prop.startsWith("--_csstools-cf"))return!1;if(!e.parent||"rule"!==e.parent.type)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&B.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const H="--_csstools-cf",K=`${H}-invalid`,O=new Set(["initial","inherit","unset","revert","revert-layer"]),q=new Map;class Scope{names=new Map;parent;constructor(e){this.parent=e}get(e){const t=this.names.get(e);return void 0!==t?t:this.parent?.get(e)}}class CustomFunctionTranspiler{customFunctions=new Map;counter=0;frames=[];registrations=[];sourceHash="0";setCustomFunctions(e){this.customFunctions=e}getRegistrations(){return this.registrations}processDeclaration(e){const t=m({css:e.value});if(!t.some(e=>b(e)&&e[4].value.startsWith("--")))return null;const n=e.parent;if(!n||"rule"!==n.type)return null;this.sourceHash=sourceHashFor(e.source?.input.from);const o=n,a=r(t);i([a],e=>{if(!s(e))return;const t=e.getName();if(!t.startsWith("--"))return;const r=this.customFunctions.get(t);return r&&r.supported?this.processCall(e,o,new Scope(null)):void 0});const u=c([a]);return u===e.value?null:u}processCall(e,t,r){const n=e.getName(),s=this.customFunctions.get(n);if(!s||!s.supported)return[this.varReference(K)];const o=this.frames.find(e=>e.name===n);if(o)return o.cyclic=!0,[this.varReference(K)];const a=this.parseArguments(e);if(!a)return[this.varReference(K)];const u=s.function.parameters;if(a.length>u.length)return[this.varReference(K)];for(let e=a.length;ethis.rewriteValue(c([e]),t,r)),p=(this.counter++).toString(36),m=new Scope(r),h=new Map;for(let e=0;e{a.length&&(o.push(t.clone({nodes:a})),a=[])};for(const u of e){if("decl"===u.type){const e=this.processBodyDeclaration(u,t,r,n,s);e&&a.push(e);continue}if("atrule"===u.type){flush();const e=this.emitBody(u.nodes||[],t,r,n,s);e.length&&o.push(u.clone({nodes:e}));continue}}return flush(),o}processBodyDeclaration(e,t,r,n,s){let o;if("result"===e.prop.toLowerCase())o=this.resultName(s);else{if(!e.prop.startsWith("--"))return null;{const t=r.get(e.prop);if(!t)return null;o=t;const s=e.value.trim(),a=n.get(e.prop);if(a&&"initial"===s.toLowerCase())return e.clone({prop:o,value:`var(${a})`});if("inherit"===s.toLowerCase()){const t=r.parent?.get(e.prop)??e.prop;return e.clone({prop:o,value:`var(${t})`})}}}return e.clone({prop:o,value:this.rewriteValue(e.value,t,r)})}rewriteValue(e,t,n){const o=m({css:e});if(!o.some(e=>b(e)&&("var"===e[4].value.toLowerCase()||e[4].value.startsWith("--"))))return e;const a=r(o);return i([a],e=>{if(!s(e))return;const r=e.getName();if("var"===r.toLowerCase())return void this.rewriteVarReference(e,n);if(!r.startsWith("--"))return;const o=this.customFunctions.get(r);return o&&o.supported?this.processCall(e,t,n):void 0}),c([a])}rewriteVarReference(e,t){for(const r of e.value){if(n(r))continue;if(!o(r)||!d(r.value))return;const e=r.value[4].value;if(!e.startsWith("--"))return;const s=t.get(e);if(!s)return;return void L(r.value,s)}}parseArguments(e){if(0===e.value.filter(e=>!n(e)).length)return[];const t=f(e.value.flatMap(e=>e.tokens())),r=[];for(const e of t){const t=unwrapArgument(e);if(!t)return!1;r.push(t)}return r}varReference(e){return new p([N.Function,"var(",-1,-1,{value:"var"}],[N.CloseParen,")",-1,-1,void 0],[new l([N.Ident,e,-1,-1,{value:e}])])}argName(e,t){return`${H}-${this.sourceHash}-${e}-arg-${t}`}localName(e,t){return`${H}-${this.sourceHash}-${e}-local-${t.slice(2)}`}resultName(e){return`${H}-${this.sourceHash}-${e}-result`}}function unwrapArgument(e){const t=e.filter(e=>!n(e));return 0!==t.length&&(1===t.length&&u(t[0])&&C(t[0].startToken)?t[0].value:e)}function collectLocalNames(e){const t=new Set;return e.walkDecls(e=>{e.prop.startsWith("--")&&t.add(e.prop)}),t}function parameterType(e){const t=e.getArgumentType();return t&&"*"!==t?t:null}function sourceHashFor(e){if(!e)return"0";const t=q.get(e);if(t)return t;const r=M.createHash("md5");r.update(R.basename(R.dirname(e))+"/"+R.basename(e),"utf8");const n=parseInt(r.digest("hex"),16).toString(36).slice(0,8);return q.set(e,n),n}function staticResultKeyword(e){let t=!1;if(e.walkAtRules(e=>{e.walkDecls(e=>{"result"===e.prop.toLowerCase()&&(t=!0)})}),t)return null;let r=null;for(const t of e.nodes||[])"decl"===t.type&&"result"===t.prop.toLowerCase()&&(r=t.value.trim());return r&&O.has(r.toLowerCase())?r:null}const z=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=new CustomFunctionTranspiler;return{postcssPlugin:"postcss-custom-functions",Once(n,{result:s}){e=getCustomFunctions(n,s,{preserve:t.preserve}),r.setCustomFunctions(e)},OnceExit(e){for(const t of r.getRegistrations())e.append(t)},Declaration(e){if(!z.test(e.value))return;if(!isProcessableDeclaration(e))return;const n=r.processDeclaration(e);null!==n&&(e.cloneBefore({value:n}),t.preserve||e.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; +import{parse as e,addLayerToModel as t}from"@csstools/cascade-layer-name-parser";import{parseListOfComponentValues as r,isFunctionNode as n,isSimpleBlockNode as s,isTokenNode as o,isWhiteSpaceOrCommentNode as a,isWhitespaceNode as i,replaceComponentValues as u,stringify as l,TokenNode as c,parseCommaSeparatedListOfComponentValues as f,FunctionNode as p}from"@csstools/css-parser-algorithms";import{tokenize as m,isTokenDimension as h,isTokenPercentage as v,isTokenIdent as d,stringify as g,isTokenComment as w,isTokenComma as y,isTokenDelim as C,isTokenColon as F,isTokenOpenCurly as k,isTokenFunction as b,TokenType as N,mutateIdent as T}from"@csstools/css-tokenizer";import{Declaration as L,AtRule as P}from"postcss";import R from"node:crypto";import S from"node:path";const M=e("csstools-implicit-layer")[0];function collectCascadeLayerOrder(r){const n=new Map,s=new Map,o=[];r.walkAtRules(r=>{if("layer"!==r.name.toLowerCase())return;{let e=r.parent;for(;e;){if("atrule"!==e.type||"layer"!==e.name.toLowerCase()){if(e===r.root())break;return}e=e.parent}}let a;if(r.nodes)a=normalizeLayerName(r.params,1);else{if(!r.params.trim())return;a=r.params}let i=e(a);if(i?.length){{let e=r.parent;for(;e&&"atrule"===e.type&&"layer"===e.name.toLowerCase();){const t=s.get(e);t?(i=i.map(e=>t.concat(e)),e=e.parent):e=e.parent}}if(t(o,i),r.nodes){const e=i[0].concat(M);n.set(r,e),s.set(r,i[0])}}});for(const e of n.values())t(o,[e]);const a=new WeakMap;for(const[e,t]of n)a.set(e,o.findIndex(e=>t.equal(e)));return a}function normalizeLayerName(e,t){return e.trim()?e:`csstools-anon-layer--${t}`}const $=new Set(["layer","media","supports"]),D=/^function$/i;function isProcessableRule(e){if(!D.test(e.name))return!1;if(!e.params||!e.params.includes("--"))return!1;if(!e.nodes?.length)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&!$.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const V=new Set(["initial","inherit","unset","revert","revert-layer"]),x=new Set(["em","rem","ex","rex","cap","rcap","ch","rch","ic","ric","lh","rlh","vw","vh","vi","vb","vmin","vmax","svw","svh","svi","svb","svmin","svmax","lvw","lvh","lvi","lvb","lvmin","lvmax","dvw","dvh","dvi","dvb","dvmin","dvmax","cqw","cqh","cqi","cqb","cqmin","cqmax","fr"]),A=new Set(["var","env","attr","light-dark","image-set","-webkit-image-set"]),I=new Set(["rgb","rgba","hsl","hsla","hwb","lab","lch","oklab","oklch","color","color-mix","color-contrast","device-cmyk"]);function areComponentsIndependent(e,t){for(const r of e){if(n(r)){const e=r.getName().toLowerCase();if(A.has(e))return!1;if(!areComponentsIndependent(r.value,t||I.has(e)))return!1;continue}if(s(r)){if(!areComponentsIndependent(r.value,t))return!1;continue}if(!o(r))continue;const e=r.value;if(h(e)){if(x.has(e[4].unit.toLowerCase()))return!1}else if(v(e)){if(!t)return!1}else if(d(e)){const t=e[4].value.toLowerCase();if("currentcolor"===t||V.has(t))return!1}}return!0}const W=new Set(["initial","inherit","unset","revert","revert-layer"]);function staticResultKeyword(e){let t=!1;if(e.walkAtRules(e=>{e.walkDecls(e=>{"result"===e.prop.toLowerCase()&&(t=!0)})}),t)return null;let r=null;for(const t of e.nodes||[])"decl"===t.type&&"result"===t.prop.toLowerCase()&&(r=t.value.trim());return r&&W.has(r.toLowerCase())?r:null}var E,K;(K=E||(E={})).CustomFunction="custom-function",K.FunctionParameter="function-parameter";class CustomFunction{type=E.CustomFunction;function;parameters;returnsKeyword;returnType;before;after;constructor(e,t,r,n,s=[],o=[]){this.function=e,this.parameters=t,this.returnsKeyword=r,this.returnType=n,this.before=s,this.after=o}getName(){return this.function.getName()}getReturnType(){const e=r(this.returnType).filter(e=>!a(e));return 1===e.length&&n(e[0])&&"type"===e[0].getName().toLowerCase()?g(...e[0].value.flatMap(e=>e.tokens()).filter(e=>!w(e))).trim():g(...this.returnType.filter(e=>!w(e))).trim()}tokens(){return[...this.before,...this.function.tokens(),...this.returnsKeyword,...this.returnType,...this.after]}toString(){return g(...this.before)+g(...this.function.tokens())+g(...this.returnsKeyword)+g(...this.returnType)+g(...this.after)}toJSON(){return{type:this.type,name:this.getName(),string:this.toString(),tokens:this.tokens(),parameters:this.parameters.map(e=>e.toJSON()),returnsKeyword:this.returnsKeyword,returnType:this.returnType,before:this.before,after:this.after}}isCustomFunction(){return CustomFunction.isCustomFunction(this)}static isCustomFunction(e){return!!e&&e instanceof CustomFunction&&e.type===E.CustomFunction}}class FunctionParameter{type=E.FunctionParameter;name;argumentType;colon;defaultValue;before;after;constructor(e,t,r,n,s=[],o=[]){this.name=e,this.argumentType=t,this.colon=r,this.defaultValue=n,this.before=s,this.after=o}getName(){return this.name[4].value}getNameToken(){return this.name}getArgumentType(){return g(...this.argumentType.filter(e=>!w(e))).trim()}getDefaultValue(){return g(...this.defaultValue).trim()}toJSON(){return{type:this.type,name:this.getName(),argumentType:this.argumentType,colon:this.colon,defaultValue:this.defaultValue,before:this.before,after:this.after}}isFunctionParameter(){return FunctionParameter.isFunctionParameter(this)}static isFunctionParameter(e){return!!e&&e instanceof FunctionParameter&&e.type===E.FunctionParameter}}function parseCustomFunction(e){let t=null,r=-1,s=-1,i=-1,u=e.length;for(let u=0;ui;t--){const r=e[t];if(!a(r))break;u=t}const l=parseFunctionParameters(t.value);return!!l&&new CustomFunction(t,l,-1===i?[]:e.slice(r+1,i).flatMap(e=>e.tokens()),-1===i?[]:e.slice(i,u).flatMap(e=>e.tokens()),e.slice(0,r).flatMap(e=>e.tokens()),e.slice(u).flatMap(e=>e.tokens()))}function parseFunctionParameters(e){if(!e.some(e=>!a(e)))return[];const t=[],r=new Set;for(let n=0;n0&&o(n)&&C(n.value)&&">"===n.value[4].value)t--;else if(-1===l&&0===t&&o(n)&&F(n.value))l=m,c=n.value;else if(-1===l){if(-1!==l)return!1}else f=m}}if(!u)return!1;if(-1!==l&&-1===f)return!1;const h=-1!==l?l-1:m-1;for(let t=e.length-1;t>Math.max(h,f);t--){const r=e[t];if(!i(r))break;p=t}let v;{const t=e.slice(r+1,h+1),s=t.filter(e=>!a(e));v=1===s.length&&n(s[0])&&"type"===s[0].getName().toLowerCase()?s[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}let g=[];if(-1!==f){const t=e.slice(l+1,p);if(hasTopLevelImportant(t))return!1;const r=t.filter(e=>!a(e));g=1===r.length&&s(r[0])&&k(r[0].startToken)?r[0].value.flatMap(e=>e.tokens()):t.flatMap(e=>e.tokens())}return{advance:m,node:new FunctionParameter(u,v,c,g,e.slice(0,r).flatMap(e=>e.tokens()),e.slice(p,m+1).flatMap(e=>e.tokens()))}}function hasTopLevelImportant(e){for(let t=0;t{n=!0,t?.onParseError?.(e)}});return!n&&parseCustomFunction(s)}function parse(e,t){return parseFromTokens(m({css:e},{onParseError:t?.onParseError}),t)}const B=/^(media|supports|container|starting-style)$/i;function isSupportedCustomFunction(e){return!e.getReturnType()&&e.parameters.every(e=>{const t=e.getArgumentType(),n=!!t&&"*"!==t,s=!!e.getDefaultValue();return n&&s?(o=e.getDefaultValue(),areComponentsIndependent(r(m({css:o})),!1)):!n&&!s;var o})}function getCustomFunctions(e,t,r){const n=new Map,s=collectCascadeLayerOrder(e);let o=0;e.walkAtRules(e=>{if(!isProcessableRule(e))return;const r=parse(e.params.trim(),{onParseError:r=>{e.warn(t,`Failed to parse custom function : "${e.params}" with message: "${r instanceof Error?r.message:r}"`)}});if(!r)return;const a=r.getName(),i=isSupportedCustomFunction(r),u={node:e,function:r,supported:i,conditionals:collectConditionalAncestors(e),layer:(l=e,c=s,l.parent&&"atrule"===l.parent.type&&"layer"===l.parent.name.toLowerCase()?c.has(l.parent)?c.get(l.parent)+1:0:1e7),order:o++};var l,c;const f=n.get(a)??{definitions:[],supported:!0};f.definitions.push(u),f.supported=f.supported&&i,n.set(a,f)});for(const e of n.values())e.definitions.sort((e,t)=>e.layer-t.layer||e.order-t.order),e.definitions.length>1&&e.definitions.some(e=>staticResultKeyword(e.node))&&(e.supported=!1);if(!r.preserve)for(const e of n.values())if(e.supported)for(const t of e.definitions){const e=t.node.parent;t.node.remove(),removeEmptyAncestorBlocks(e)}return n}function collectConditionalAncestors(e){const t=[];let r=e.parent;for(;r;)"atrule"===r.type&&B.test(r.name)&&t.unshift(r),r=r.parent;return t}function removeEmptyAncestorBlocks(e){if(!e)return;let t=e;for(;t;){if(t.nodes&&t.nodes.length>0)return;const e=t.parent;t.remove(),t=e}}const H=new Set(["function","keyframes"]);function isProcessableDeclaration(e){if(e.prop.startsWith("--_csstools-cf"))return!1;if(!e.parent||"rule"!==e.parent.type)return!1;let t=e.parent;for(;t;){if("atrule"===t.type&&H.has(t.name.toLowerCase()))return!1;t=t.parent}return!0}const O="--_csstools-cf",q=`${O}-invalid`,z=new Map;class Scope{names=new Map;parent;constructor(e){this.parent=e}get(e){const t=this.names.get(e);return void 0!==t?t:this.parent?.get(e)}}class CustomFunctionTranspiler{customFunctions=new Map;counter=0;frames=[];registrations=[];sourceHash="0";setCustomFunctions(e){this.customFunctions=e}getRegistrations(){return this.registrations}processDeclaration(e){const t=m({css:e.value});if(!t.some(e=>b(e)&&e[4].value.startsWith("--")))return null;const s=e.parent;if(!s||"rule"!==s.type)return null;this.sourceHash=sourceHashFor(e.source?.input.from);const o=s,a=r(t);u([a],e=>{if(!n(e))return;const t=e.getName();if(!t.startsWith("--"))return;const r=this.customFunctions.get(t);return r&&r.supported?this.processCall(e,o,new Scope(null)):void 0});const i=l([a]);return i===e.value?null:i}processCall(e,t,r){const n=e.getName(),s=this.customFunctions.get(n);if(!s||!s.supported)return[this.varReference(q)];const o=this.frames.find(e=>e.name===n);if(o)return o.cyclic=!0,[this.varReference(q)];const a=this.parseArguments(e);if(!a)return[this.varReference(q)];const i=1===s.definitions.length?staticResultKeyword(s.definitions[0].node):null,u=a.map(e=>this.rewriteValue(l([e]),t,r)),f=(this.counter++).toString(36),p=this.resultName(f),m={name:n,cyclic:!1};this.frames.push(m);for(const e of s.definitions)this.emitDefinition(e,a,u,t,r,p);return m.cyclic&&t.before(t.clone({nodes:[this.invalidResultDeclaration(p,t)]})),this.frames.pop(),m.cyclic?[this.varReference(q)]:i?[new c([N.Ident,i,-1,-1,{value:i}])]:[this.varReference(p)]}emitDefinition(e,t,r,n,s,o){const a=e.function.parameters;let i=t.length<=a.length;if(i)for(let e=t.length;e{u.length&&(i.push(t.clone({nodes:u})),u=[])};for(const l of e){if("decl"===l.type){const e=this.processBodyDeclaration(l,t,r,n,s,o,a);e&&u.push(e);continue}if("atrule"===l.type){flush();const e=this.emitBody(l.nodes||[],t,r,n,s,o,a);e.length&&i.push(l.clone({nodes:e}));continue}}return flush(),i}processBodyDeclaration(e,t,r,n,s,o,a){let i;if("result"===e.prop.toLowerCase())i=a;else{if(!e.prop.startsWith("--"))return null;{const t=r.get(e.prop);if(!t)return null;i=t;const o=e.value.trim(),a=n.get(e.prop);if(a&&"initial"===o.toLowerCase())return e.clone({prop:i,value:s.has(e.prop)?`var(${q})`:`var(${a})`});if("inherit"===o.toLowerCase()){const t=r.parent?.get(e.prop)??e.prop;return e.clone({prop:i,value:`var(${t})`})}}}return e.clone({prop:i,value:this.rewriteValue(e.value,t,r)})}rewriteValue(e,t,s){const o=m({css:e});if(!o.some(e=>b(e)&&("var"===e[4].value.toLowerCase()||e[4].value.startsWith("--"))))return e;const a=r(o);return u([a],e=>{if(!n(e))return;const r=e.getName();if("var"===r.toLowerCase())return void this.rewriteVarReference(e,s);if(!r.startsWith("--"))return;const o=this.customFunctions.get(r);return o&&o.supported?this.processCall(e,t,s):void 0}),l([a])}rewriteVarReference(e,t){for(const r of e.value){if(a(r))continue;if(!o(r)||!d(r.value))return;const e=r.value[4].value;if(!e.startsWith("--"))return;const n=t.get(e);if(!n)return;return void T(r.value,n)}}parseArguments(e){if(0===e.value.filter(e=>!a(e)).length)return[];const t=f(e.value.flatMap(e=>e.tokens())),r=[];for(const e of t){const t=unwrapArgument(e);if(!t)return!1;r.push(t)}return r}invalidResultDeclaration(e,t){return new L({prop:e,value:`var(${q})`,source:t.source})}varReference(e){return new p([N.Function,"var(",-1,-1,{value:"var"}],[N.CloseParen,")",-1,-1,void 0],[new c([N.Ident,e,-1,-1,{value:e}])])}argName(e,t){return`${O}-${this.sourceHash}-${e}-arg-${t}`}localName(e,t){return`${O}-${this.sourceHash}-${e}-local-${t.slice(2)}`}resultName(e){return`${O}-${this.sourceHash}-${e}-result`}}function wrapInConditionals(e,t){let r=e;for(let e=t.length-1;e>=0;e--)r=[t[e].clone({nodes:r})];return r}function unwrapArgument(e){const t=e.filter(e=>!a(e));return 0!==t.length&&(1===t.length&&s(t[0])&&k(t[0].startToken)?t[0].value:e)}function collectLocalNames(e){const t=new Set;return e.walkDecls(e=>{e.prop.startsWith("--")&&t.add(e.prop)}),t}function parameterType(e){const t=e.getArgumentType();return t&&"*"!==t?t:null}function sourceHashFor(e){if(!e)return"0";const t=z.get(e);if(t)return t;const r=R.createHash("md5");r.update(S.basename(S.dirname(e))+"/"+S.basename(e),"utf8");const n=parseInt(r.digest("hex"),16).toString(36).slice(0,8);return z.set(e,n),n}const J=/--.*?\(/i,creator=e=>{const t=Object.assign({preserve:!1},e);return{postcssPlugin:"postcss-custom-functions",prepare(){let e=new Map;const r=new CustomFunctionTranspiler;return{postcssPlugin:"postcss-custom-functions",Once(n,{result:s}){e=getCustomFunctions(n,s,{preserve:t.preserve}),r.setCustomFunctions(e)},OnceExit(e){for(const t of r.getRegistrations())e.append(t)},Declaration(e){if(!J.test(e.value))return;if(!isProcessableDeclaration(e))return;const n=r.processDeclaration(e);null!==n&&(e.cloneBefore({value:n}),t.preserve||e.remove())}}}}};creator.postcss=!0;export{creator as default,creator as"module.exports"}; diff --git a/plugins/postcss-custom-functions/docs/README.md b/plugins/postcss-custom-functions/docs/README.md index 0013c2483f..73820d297f 100644 --- a/plugins/postcss-custom-functions/docs/README.md +++ b/plugins/postcss-custom-functions/docs/README.md @@ -26,11 +26,15 @@ Unsupported: - typed parameters without default values - untyped parameters with default values - return types +- `@function` definitions inside `@container` or `@scope` + +Definitions inside `@layer`, `@media` and `@supports` are supported. When a name +has several active definitions, the browser cascade selects the strongest one. Typed parameters with default values are checked by the browser through a generated `@property` registration. A default value that is not computationally independent (for example `1em` or `var(--x)`) can not be used -as an `initial-value` and disables type checking for that parameter. +as an `initial-value` and makes the function unsupported. ```css diff --git a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts index 1f7d9e59ff..803d245068 100644 --- a/plugins/postcss-custom-functions/src/custom-functions-from-root.ts +++ b/plugins/postcss-custom-functions/src/custom-functions-from-root.ts @@ -1,19 +1,35 @@ import type { ChildNode, Container, Document, Result, Root as PostCSSRoot, AtRule } from 'postcss'; import { cascadeLayerNumberForNode, collectCascadeLayerOrder } from './cascade-layers'; import { isProcessableRule } from './is-processable-rule'; +import { isComputationallyIndependent } from './is-computationally-independent'; +import { staticResultKeyword } from './static-result-keyword'; import type { CustomFunction } from '@csstools/custom-function-parser'; import { parse } from '@csstools/custom-function-parser'; -import { isComputationallyIndependent } from './is-computationally-independent'; -export type CustomFunctionAndNode = { +export type CustomFunctionDefinition = { function: CustomFunction; node: AtRule; supported: boolean; + /** Ancestor conditional group rules, outermost first. Cascade layers are excluded. */ + conditionals: Array; + /** Cascade layer strength. Higher numbers win. */ + layer: number; + /** Source order. */ + order: number; +}; + +export type CustomFunctionGroup = { + definitions: Array; + /** Every definition is supported. Unsupported definitions make the whole name unsupported. */ + supported: boolean; }; +const IS_CONDITIONAL_AT_RULE_REGEX = /^(media|supports|container|starting-style)$/i; + /** * A function is supported when it has no return type and every parameter is - * either untyped without a default, or typed with a default value. + * either untyped without a default, or typed with a computationally + * independent default value. * * Typed parameters without defaults and untyped parameters with defaults are * not supported yet. @@ -39,13 +55,11 @@ function isSupportedCustomFunction(customFunction: CustomFunction): boolean { } // Return custom functions from the css root, conditionally removing them. -export function getCustomFunctions(root: PostCSSRoot, result: Result, opts: { preserve?: boolean }): Map { - const customFunctions = new Map(); - const customFunctionsCascadeLayerMapping: Map = new Map(); - +export function getCustomFunctions(root: PostCSSRoot, result: Result, opts: { preserve?: boolean }): Map { + const groups = new Map(); const cascadeLayersOrder = collectCascadeLayerOrder(root); - const removable: Array = []; + let order = 0; root.walkAtRules((atRule) => { if (!isProcessableRule(atRule)) { @@ -66,33 +80,66 @@ export function getCustomFunctions(root: PostCSSRoot, result: Result, opts: { pr const name = customFunction.getName(); const supported = isSupportedCustomFunction(customFunction); - const thisCascadeLayer = cascadeLayerNumberForNode(atRule, cascadeLayersOrder); - const existingCascadeLayer = customFunctionsCascadeLayerMapping.get(name) ?? -1; + const definition: CustomFunctionDefinition = { + node: atRule, + function: customFunction, + supported, + conditionals: collectConditionalAncestors(atRule), + layer: cascadeLayerNumberForNode(atRule, cascadeLayersOrder), + order: order++, + }; + + const group = groups.get(name) ?? { definitions: [], supported: true }; + group.definitions.push(definition); + group.supported = group.supported && supported; + groups.set(name, group); + }); - if (thisCascadeLayer >= existingCascadeLayer) { - customFunctionsCascadeLayerMapping.set(name, thisCascadeLayer); - customFunctions.set(name, { - node: atRule, - function: customFunction, - supported, - }); - } + for (const group of groups.values()) { + // Definitions are emitted weakest first so the browser cascade picks the + // strongest active definition at runtime. + group.definitions.sort((a, b) => { + return (a.layer - b.layer) || (a.order - b.order); + }); - if (supported) { - removable.push(atRule); + // A CSS-wide keyword result can only be substituted directly when there is + // exactly one definition. + if (group.definitions.length > 1 && group.definitions.some((definition) => staticResultKeyword(definition.node))) { + group.supported = false; } - }); + } if (!opts.preserve) { - for (const atRule of removable) { - const parent = atRule.parent; - atRule.remove(); + for (const group of groups.values()) { + if (!group.supported) { + continue; + } + + for (const definition of group.definitions) { + const parent = definition.node.parent; + definition.node.remove(); - removeEmptyAncestorBlocks(parent); + removeEmptyAncestorBlocks(parent); + } } } - return customFunctions; + return groups; +} + +function collectConditionalAncestors(atRule: AtRule): Array { + const ancestors: Array = []; + + let parent: Container | Document | undefined = atRule.parent; + while (parent) { + if (parent.type === 'atrule' && IS_CONDITIONAL_AT_RULE_REGEX.test((parent as AtRule).name)) { + ancestors.unshift(parent as AtRule); + } + + parent = parent.parent; + } + + return ancestors; } function removeEmptyAncestorBlocks(block: Container | undefined): void { diff --git a/plugins/postcss-custom-functions/src/index.ts b/plugins/postcss-custom-functions/src/index.ts index 1f3e68baec..231aaf63fa 100644 --- a/plugins/postcss-custom-functions/src/index.ts +++ b/plugins/postcss-custom-functions/src/index.ts @@ -1,5 +1,5 @@ import type { Plugin, PluginCreator } from 'postcss'; -import type { CustomFunctionAndNode } from './custom-functions-from-root'; +import type { CustomFunctionGroup } from './custom-functions-from-root'; import { getCustomFunctions } from './custom-functions-from-root'; import { isProcessableDeclaration } from './is-processable-declaration'; import { CustomFunctionTranspiler } from './transpile'; @@ -25,7 +25,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return { postcssPlugin: 'postcss-custom-functions', prepare(): Plugin { - let customFunctions: Map = new Map(); + let customFunctions: Map = new Map(); const transpiler = new CustomFunctionTranspiler(); return { diff --git a/plugins/postcss-custom-functions/src/is-processable-rule.ts b/plugins/postcss-custom-functions/src/is-processable-rule.ts index 133d7ccbef..fa336125e5 100644 --- a/plugins/postcss-custom-functions/src/is-processable-rule.ts +++ b/plugins/postcss-custom-functions/src/is-processable-rule.ts @@ -1,9 +1,17 @@ import type { AtRule, ChildNode, Container, Document } from 'postcss'; -// `@function` definitions inside dynamic conditional rules (`@media`, -// `@supports`, `@container`, `@scope`, ...) are conditionally available and -// can not be resolved statically. Only cascade layers are supported. -const allowedParentAtRules = new Set(['layer']); +// `@function` definitions may appear inside cascade layers and inside +// conditional group rules. Dynamic conditional rules are handled by emitting a +// guarded result per definition. +// +// Excluded on purpose: +// - `@container`: browsers currently ignore the container condition on +// `@function` definitions, so a spec correct transformation would not match +// the browser. +// - `@scope`: tree-scoped, it changes where the name resolves rather than just +// whether the definition is active. +// - `@starting-style`: first style update only. +const allowedParentAtRules = new Set(['layer', 'media', 'supports']); const IS_FUNCTION_REGEX = /^function$/i; diff --git a/plugins/postcss-custom-functions/src/static-result-keyword.ts b/plugins/postcss-custom-functions/src/static-result-keyword.ts new file mode 100644 index 0000000000..8f32f270a4 --- /dev/null +++ b/plugins/postcss-custom-functions/src/static-result-keyword.ts @@ -0,0 +1,39 @@ +import type { AtRule } from 'postcss'; + +const CSS_WIDE_KEYWORDS = new Set(['initial', 'inherit', 'unset', 'revert', 'revert-layer']); + +/** + * When the winning `result` descriptor is a CSS-wide keyword and no conditional + * rule can change it, the call evaluates to that keyword directly. + */ +export function staticResultKeyword(atRule: AtRule): string | null { + let conditionalResult = false; + atRule.walkAtRules((child) => { + child.walkDecls((decl) => { + if (decl.prop.toLowerCase() === 'result') { + conditionalResult = true; + } + }); + }); + + if (conditionalResult) { + return null; + } + + let value: string | null = null; + for (const node of atRule.nodes || []) { + if (node.type === 'decl' && node.prop.toLowerCase() === 'result') { + value = node.value.trim(); + } + } + + if (!value) { + return null; + } + + if (CSS_WIDE_KEYWORDS.has(value.toLowerCase())) { + return value; + } + + return null; +} diff --git a/plugins/postcss-custom-functions/src/transpile.ts b/plugins/postcss-custom-functions/src/transpile.ts index 4e34dc747f..2b97f14153 100644 --- a/plugins/postcss-custom-functions/src/transpile.ts +++ b/plugins/postcss-custom-functions/src/transpile.ts @@ -16,12 +16,12 @@ import { stringify, } from '@csstools/css-parser-algorithms'; import { TokenType, isTokenFunction, isTokenIdent, isTokenOpenCurly, mutateIdent, tokenize } from '@csstools/css-tokenizer'; -import type { CustomFunctionAndNode } from './custom-functions-from-root'; +import type { CustomFunctionDefinition, CustomFunctionGroup } from './custom-functions-from-root'; import type { FunctionParameter } from '@csstools/custom-function-parser'; +import { staticResultKeyword } from './static-result-keyword'; const GENERATED_PREFIX = '--_csstools-cf'; const INVALID_IDENT = `${GENERATED_PREFIX}-invalid`; -const CSS_WIDE_KEYWORDS = new Set(['initial', 'inherit', 'unset', 'revert', 'revert-layer']); const sourceHashes = new Map(); @@ -63,15 +63,20 @@ type CallFrame = { * with a generated name per parameter, local variable and result. The browser * then performs the actual `var()` substitution, type checking, cascade and * conditional handling. + * + * When a name has several definitions (layers and/or conditional rules), one + * result property is shared and each definition writes to it inside its own + * conditional context. Definitions are emitted weakest first so the browser + * cascade selects the strongest *active* definition at runtime. */ export class CustomFunctionTranspiler { - private customFunctions: Map = new Map(); + private customFunctions: Map = new Map(); private counter = 0; private frames: Array = []; private registrations: Array = []; private sourceHash = '0'; - setCustomFunctions(customFunctions: Map): void { + setCustomFunctions(customFunctions: Map): void { this.customFunctions = customFunctions; } @@ -113,8 +118,8 @@ export class CustomFunctionTranspiler { return; } - const entry = this.customFunctions.get(name); - if (!entry || !entry.supported) { + const group = this.customFunctions.get(name); + if (!group || !group.supported) { return; } @@ -131,9 +136,9 @@ export class CustomFunctionTranspiler { private processCall(fn: FunctionNode, element: Rule, parentScope: Scope): Array { const name = fn.getName(); - const entry = this.customFunctions.get(name); + const group = this.customFunctions.get(name); - if (!entry || !entry.supported) { + if (!group || !group.supported) { return [this.varReference(INVALID_IDENT)]; } @@ -150,26 +155,12 @@ export class CustomFunctionTranspiler { return [this.varReference(INVALID_IDENT)]; } - const parameters = entry.function.parameters; - - // More arguments than parameters is invalid. - if (args.length > parameters.length) { - return [this.varReference(INVALID_IDENT)]; - } - - // A parameter without a default value must be provided. - for (let i = args.length; i < parameters.length; i++) { - if (!parameters[i].getDefaultValue()) { - return [this.varReference(INVALID_IDENT)]; - } - } - // CSS-wide keywords in `result` are left unresolved by the spec, so they // must be substituted directly instead of going through `var()`. // // This is only used when the function is not cyclic. A cycle anywhere in // the body makes the whole evaluation invalid. - const keywordResult = staticResultKeyword(entry.node); + const keywordResult = group.definitions.length === 1 ? staticResultKeyword(group.definitions[0].node) : null; // Arguments are resolved in the scope of the caller, before the function // itself is evaluated. @@ -177,101 +168,132 @@ export class CustomFunctionTranspiler { return this.rewriteValue(stringify([arg]), element, parentScope); }); - const id = (this.counter++).toString(36); - const scope = new Scope(parentScope); - const parameterArgs = new Map(); + const callId = (this.counter++).toString(36); + const resultName = this.resultName(callId); - for (let i = 0; i < parameters.length; i++) { - const argName = this.argName(id, i); - scope.names.set(parameters[i].getName(), argName); - parameterArgs.set(parameters[i].getName(), argName); + const frame: CallFrame = { name, cyclic: false }; + this.frames.push(frame); + + for (const definition of group.definitions) { + this.emitDefinition(definition, args, evaluatedArgs, element, parentScope, resultName); } - for (const localName of collectLocalNames(entry.node)) { - scope.names.set(localName, this.localName(id, localName)); + // Once a substitution context is marked as cyclic, the whole evaluation + // returns the guaranteed-invalid value. + if (frame.cyclic) { + element.before(element.clone({ + nodes: [this.invalidResultDeclaration(resultName, element)], + })); } - const frame: CallFrame = { name, cyclic: false }; - this.frames.push(frame); + this.frames.pop(); - const argDecls: Array = []; - for (let i = 0; i < parameters.length; i++) { - if (i < evaluatedArgs.length) { - argDecls.push(new PostCSSDeclaration({ - prop: this.argName(id, i), - value: evaluatedArgs[i], - source: element.source, - })); + if (frame.cyclic) { + return [this.varReference(INVALID_IDENT)]; + } - continue; - } + if (keywordResult) { + return [new CSSATokenNode([TokenType.Ident, keywordResult, -1, -1, { value: keywordResult }])]; + } - // A missing argument resolves to the default value in the callee frame. - const defaultValue = parameters[i].getDefaultValue(); - if (defaultValue) { - argDecls.push(new PostCSSDeclaration({ - prop: this.argName(id, i), - value: this.rewriteValue(defaultValue, element, scope), - source: element.source, - })); + return [this.varReference(resultName)]; + } + + private emitDefinition(definition: CustomFunctionDefinition, args: Array>, evaluatedArgs: Array, element: Rule, parentScope: Scope, resultName: string): void { + const parameters = definition.function.parameters; + + // More arguments than parameters is invalid. + // A parameter without a default value must be provided. + let validArity = args.length <= parameters.length; + if (validArity) { + for (let i = args.length; i < parameters.length; i++) { + if (!parameters[i].getDefaultValue()) { + validArity = false; + break; + } } } - if (argDecls.length) { - element.before(element.clone({ nodes: argDecls })); - } + const definitionId = (this.counter++).toString(36); + const scope = new Scope(parentScope); + const parameterArgs = new Map(); + const typedParameters = new Set(); for (let i = 0; i < parameters.length; i++) { - const type = parameterType(parameters[i]); - if (!type) { - continue; - } + const argName = this.argName(definitionId, i); + scope.names.set(parameters[i].getName(), argName); + parameterArgs.set(parameters[i].getName(), argName); - this.registrations.push(new PostCSSAtRule({ - name: 'property', - params: this.argName(id, i), - nodes: [ - new PostCSSDeclaration({ prop: 'syntax', value: `"${type}"`, source: element.source }), - new PostCSSDeclaration({ prop: 'inherits', value: 'false', source: element.source }), - new PostCSSDeclaration({ prop: 'initial-value', value: parameters[i].getDefaultValue(), source: element.source }), - ], - source: element.source, - })); + if (parameterType(parameters[i])) { + typedParameters.add(parameters[i].getName()); + } } - const bodyNodes = this.emitBody(entry.node.nodes || [], element, scope, parameterArgs, id); - for (const node of bodyNodes) { - element.before(node); + for (const localName of collectLocalNames(definition.node)) { + scope.names.set(localName, this.localName(definitionId, localName)); } - // Once a substitution context is marked as cyclic, the whole evaluation - // returns the guaranteed-invalid value. - if (frame.cyclic) { - element.before(element.clone({ - nodes: [ - new PostCSSDeclaration({ - prop: this.resultName(id), - value: `var(${INVALID_IDENT})`, + const nodes: Array = []; + + if (!validArity) { + nodes.push(element.clone({ nodes: [this.invalidResultDeclaration(resultName, element)] })); + } else { + const argDecls: Array = []; + for (let i = 0; i < parameters.length; i++) { + if (i < evaluatedArgs.length) { + argDecls.push(new PostCSSDeclaration({ + prop: this.argName(definitionId, i), + value: evaluatedArgs[i], source: element.source, - }), - ], - })); - } + })); - this.frames.pop(); + continue; + } - if (frame.cyclic) { - return [this.varReference(INVALID_IDENT)]; - } + // A missing argument resolves to the default value in the callee frame. + const defaultValue = parameters[i].getDefaultValue(); + if (defaultValue) { + argDecls.push(new PostCSSDeclaration({ + prop: this.argName(definitionId, i), + value: this.rewriteValue(defaultValue, element, scope), + source: element.source, + })); + } + } - if (keywordResult) { - return [new CSSATokenNode([TokenType.Ident, keywordResult, -1, -1, { value: keywordResult }])]; + if (argDecls.length) { + nodes.push(element.clone({ nodes: argDecls })); + } + + for (let i = 0; i < parameters.length; i++) { + const type = parameterType(parameters[i]); + if (!type) { + continue; + } + + this.registrations.push(new PostCSSAtRule({ + name: 'property', + params: this.argName(definitionId, i), + nodes: [ + new PostCSSDeclaration({ prop: 'syntax', value: `"${type}"`, source: element.source }), + new PostCSSDeclaration({ prop: 'inherits', value: 'false', source: element.source }), + new PostCSSDeclaration({ prop: 'initial-value', value: parameters[i].getDefaultValue(), source: element.source }), + ], + source: element.source, + })); + } + + nodes.push(...this.emitBody(definition.node.nodes || [], element, scope, parameterArgs, typedParameters, definitionId, resultName)); } - return [this.varReference(this.resultName(id))]; + const wrapped = wrapInConditionals(nodes, definition.conditionals); + + for (const node of wrapped) { + element.before(node); + } } - private emitBody(containerNodes: Array, element: Rule, scope: Scope, parameterArgs: Map, id: string): Array { + private emitBody(containerNodes: Array, element: Rule, scope: Scope, parameterArgs: Map, typedParameters: Set, definitionId: string, resultName: string): Array { const out: Array = []; let pendingDecls: Array = []; @@ -286,7 +308,7 @@ export class CustomFunctionTranspiler { for (const node of containerNodes) { if (node.type === 'decl') { - const processed = this.processBodyDeclaration(node, element, scope, parameterArgs, id); + const processed = this.processBodyDeclaration(node, element, scope, parameterArgs, typedParameters, definitionId, resultName); if (processed) { pendingDecls.push(processed); } @@ -297,7 +319,7 @@ export class CustomFunctionTranspiler { if (node.type === 'atrule') { flush(); - const children = this.emitBody(node.nodes || [], element, scope, parameterArgs, id); + const children = this.emitBody(node.nodes || [], element, scope, parameterArgs, typedParameters, definitionId, resultName); if (children.length) { out.push(node.clone({ nodes: children })); } @@ -311,11 +333,11 @@ export class CustomFunctionTranspiler { return out; } - private processBodyDeclaration(decl: Declaration, element: Rule, scope: Scope, parameterArgs: Map, id: string): Declaration | null { + private processBodyDeclaration(decl: Declaration, element: Rule, scope: Scope, parameterArgs: Map, typedParameters: Set, definitionId: string, resultName: string): Declaration | null { let prop: string; if (decl.prop.toLowerCase() === 'result') { - prop = this.resultName(id); + prop = resultName; } else if (decl.prop.startsWith('--')) { const mapped = scope.get(decl.prop); if (!mapped) { @@ -326,12 +348,14 @@ export class CustomFunctionTranspiler { const trimmedValue = decl.value.trim(); - // `initial` resolves to the parameter's own value (argument or default). + // `initial` resolves to the parameter's own value for untyped + // parameters. For typed parameters the browser resolves it to the + // guaranteed-invalid value. const parameterArg = parameterArgs.get(decl.prop); if (parameterArg && trimmedValue.toLowerCase() === 'initial') { return decl.clone({ prop, - value: `var(${parameterArg})`, + value: typedParameters.has(decl.prop) ? `var(${INVALID_IDENT})` : `var(${parameterArg})`, }); } @@ -379,8 +403,8 @@ export class CustomFunctionTranspiler { return; } - const entry = this.customFunctions.get(name); - if (!entry || !entry.supported) { + const group = this.customFunctions.get(name); + if (!group || !group.supported) { return; } @@ -436,6 +460,14 @@ export class CustomFunctionTranspiler { return args; } + private invalidResultDeclaration(resultName: string, element: Rule): Declaration { + return new PostCSSDeclaration({ + prop: resultName, + value: `var(${INVALID_IDENT})`, + source: element.source, + }); + } + private varReference(name: string): FunctionNode { return new CSSAFunctionNode( [TokenType.Function, 'var(', -1, -1, { value: 'var' }], @@ -459,6 +491,20 @@ export class CustomFunctionTranspiler { } } +/** + * Wrap a list of nodes in the given conditional group rules. + * The list is ordered outermost first. + */ +function wrapInConditionals(nodes: Array, conditionals: Array): Array { + let current = nodes; + + for (let i = conditionals.length - 1; i >= 0; i--) { + current = [conditionals[i].clone({ nodes: current })]; + } + + return current; +} + /** * Strip a `{}` wrapper from a single argument. * @@ -532,39 +578,3 @@ function sourceHashFor(from: string | undefined): string { return value; } - -/** - * When the winning `result` descriptor is a CSS-wide keyword and no conditional - * rule can change it, the call evaluates to that keyword directly. - */ -function staticResultKeyword(atRule: AtRule): string | null { - let conditionalResult = false; - atRule.walkAtRules((child) => { - child.walkDecls((decl) => { - if (decl.prop.toLowerCase() === 'result') { - conditionalResult = true; - } - }); - }); - - if (conditionalResult) { - return null; - } - - let value: string | null = null; - for (const node of atRule.nodes || []) { - if (node.type === 'decl' && node.prop.toLowerCase() === 'result') { - value = node.value.trim(); - } - } - - if (!value) { - return null; - } - - if (CSS_WIDE_KEYWORDS.has(value.toLowerCase())) { - return value; - } - - return null; -} diff --git a/plugins/postcss-custom-functions/test/_browser.mjs b/plugins/postcss-custom-functions/test/_browser.mjs index b19f21f50f..30f0ed04b6 100644 --- a/plugins/postcss-custom-functions/test/_browser.mjs +++ b/plugins/postcss-custom-functions/test/_browser.mjs @@ -8,9 +8,9 @@ import process from 'node:process'; // The transformed cases run the source through this plugin. // -// The untransformed cases are handed to the browser as-is and only pass when -// the browser itself implements custom functions. Point Puppeteer at such a -// browser via `PUPPETEER_EXECUTABLE_PATH`. +// When the browser implements custom functions natively, the same cases are +// also run untransformed and must produce the same outcome. Point Puppeteer at +// such a browser via `PUPPETEER_EXECUTABLE_PATH`. const PAGES = [ 'wpt/dashed-function-eval.html', @@ -20,6 +20,8 @@ const PAGES = [ 'wpt/dashed-function-cycles.html', 'wpt/function-parameter-types.html', 'wpt/function-layer.html', + 'wpt/function-conditional-definitions.html', + 'wpt/function-in-media.html', ]; const requestListener = async function (req, res) { @@ -94,6 +96,29 @@ function startServers() { }; } +async function hasNativeSupport(page) { + return await page.evaluate(async () => { + /* eslint-disable no-undef */ + const css = '@function --support-probe() { result: 12px; } #support-probe { --actual: --support-probe(); }'; + const response = await fetch('/test/styles.css?transform=false', { method: 'POST', body: css }); + const styleElement = document.createElement('style'); + styleElement.textContent = await response.text(); + document.head.append(styleElement); + + const probe = document.createElement('div'); + probe.id = 'support-probe'; + document.body.append(probe); + + const value = window.getComputedStyle(probe).getPropertyValue('--actual'); + + probe.remove(); + styleElement.remove(); + + return value; + /* eslint-enable no-undef */ + }) === '12px'; +} + if (!process.env.DEBUG) { test('browser', { skip: process.env.GITHUB_ACTIONS && !process.env.BROWSER_TESTS }, async () => { const cleanup = startServers(); @@ -112,14 +137,28 @@ if (!process.env.DEBUG) { throw msg; }); + await page.goto('http://localhost:8080/'); + + const nativeSupport = await hasNativeSupport(page); + const transforms = nativeSupport ? [true, false] : [true]; + for (const url of PAGES) { - await page.goto('http://localhost:8080/' + url); - const result = await page.evaluate(async () => { - // eslint-disable-next-line no-undef - return await window.runTest(); - }); - if (!result) { - throw new Error('Test failed, expected "window.runTest()" to return true'); + for (const transform of transforms) { + await page.evaluateOnNewDocument((value) => { + // eslint-disable-next-line no-undef + window.__TRANSFORM__ = value; + }, transform); + + await page.goto('http://localhost:8080/' + url); + + const result = await page.evaluate(async () => { + // eslint-disable-next-line no-undef + return await window.runTest(); + }); + + if (!result) { + throw new Error(`Test failed, expected "window.runTest()" to return true (${url}, transform=${transform})`); + } } } } finally { diff --git a/plugins/postcss-custom-functions/test/_tape.mjs b/plugins/postcss-custom-functions/test/_tape.mjs index 3c2740495e..1bd39466c3 100644 --- a/plugins/postcss-custom-functions/test/_tape.mjs +++ b/plugins/postcss-custom-functions/test/_tape.mjs @@ -32,6 +32,15 @@ postcssTape(plugin)({ 'conditionals': { message: 'supports conditional rules', }, + 'conditional-definitions': { + message: 'supports definitions inside conditional rules', + }, + 'conditional-definitions:preserve-true': { + message: 'supports definitions inside conditional rules with { preserve: true }', + options: { + preserve: true, + }, + }, 'invalid': { message: 'handles invalid invocations', }, diff --git a/plugins/postcss-custom-functions/test/basic.expect.css b/plugins/postcss-custom-functions/test/basic.expect.css index 0b38eed39b..a222134b8c 100644 --- a/plugins/postcss-custom-functions/test/basic.expect.css +++ b/plugins/postcss-custom-functions/test/basic.expect.css @@ -1,8 +1,6 @@ -.negative-a {--_csstools-cf-86hq2t3p-0-arg-0: var(--gap); -} - -.negative-a { - --_csstools-cf-86hq2t3p-0-result: calc(-1 * var(--_csstools-cf-86hq2t3p-0-arg-0)); +.negative-a {--_csstools-cf-86hq2t3p-1-arg-0: var(--gap); +}.negative-a { + --_csstools-cf-86hq2t3p-0-result: calc(-1 * var(--_csstools-cf-86hq2t3p-1-arg-0)); } .negative-a { @@ -10,25 +8,25 @@ padding: var(--_csstools-cf-86hq2t3p-0-result); } -.negative-b {--_csstools-cf-86hq2t3p-1-arg-0: 1em; +.negative-b {--_csstools-cf-86hq2t3p-3-arg-0: 1em; } .negative-b { - --_csstools-cf-86hq2t3p-1-result: calc(-1 * var(--_csstools-cf-86hq2t3p-1-arg-0)); + --_csstools-cf-86hq2t3p-2-result: calc(-1 * var(--_csstools-cf-86hq2t3p-3-arg-0)); } .negative-b { - padding: var(--_csstools-cf-86hq2t3p-1-result); + padding: var(--_csstools-cf-86hq2t3p-2-result); } -.circle {--_csstools-cf-86hq2t3p-2-arg-0: 2px; +.circle {--_csstools-cf-86hq2t3p-5-arg-0: 2px; } .circle { - --_csstools-cf-86hq2t3p-2-local-r2: calc(var(--_csstools-cf-86hq2t3p-2-arg-0) * var(--_csstools-cf-86hq2t3p-2-arg-0)); - --_csstools-cf-86hq2t3p-2-result: calc(3.14159 * var(--_csstools-cf-86hq2t3p-2-local-r2)); + --_csstools-cf-86hq2t3p-5-local-r2: calc(var(--_csstools-cf-86hq2t3p-5-arg-0) * var(--_csstools-cf-86hq2t3p-5-arg-0)); + --_csstools-cf-86hq2t3p-4-result: calc(3.14159 * var(--_csstools-cf-86hq2t3p-5-local-r2)); } .circle { - width: var(--_csstools-cf-86hq2t3p-2-result); + width: var(--_csstools-cf-86hq2t3p-4-result); } diff --git a/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css b/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css index 39723cc57f..d24376520d 100644 --- a/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/basic.preserve-true.expect.css @@ -2,11 +2,11 @@ result: calc(-1 * var(--value)); } -.negative-a {--_csstools-cf-86hq2t3p-0-arg-0: var(--gap); +.negative-a {--_csstools-cf-86hq2t3p-1-arg-0: var(--gap); } .negative-a { - --_csstools-cf-86hq2t3p-0-result: calc(-1 * var(--_csstools-cf-86hq2t3p-0-arg-0)); + --_csstools-cf-86hq2t3p-0-result: calc(-1 * var(--_csstools-cf-86hq2t3p-1-arg-0)); } .negative-a { @@ -15,15 +15,15 @@ padding: --negative(var(--gap)); } -.negative-b {--_csstools-cf-86hq2t3p-1-arg-0: 1em; +.negative-b {--_csstools-cf-86hq2t3p-3-arg-0: 1em; } .negative-b { - --_csstools-cf-86hq2t3p-1-result: calc(-1 * var(--_csstools-cf-86hq2t3p-1-arg-0)); + --_csstools-cf-86hq2t3p-2-result: calc(-1 * var(--_csstools-cf-86hq2t3p-3-arg-0)); } .negative-b { - padding: var(--_csstools-cf-86hq2t3p-1-result); + padding: var(--_csstools-cf-86hq2t3p-2-result); padding: --negative(1em); } @@ -32,15 +32,15 @@ result: calc(3.14159 * var(--r2)); } -.circle {--_csstools-cf-86hq2t3p-2-arg-0: 2px; +.circle {--_csstools-cf-86hq2t3p-5-arg-0: 2px; } .circle { - --_csstools-cf-86hq2t3p-2-local-r2: calc(var(--_csstools-cf-86hq2t3p-2-arg-0) * var(--_csstools-cf-86hq2t3p-2-arg-0)); - --_csstools-cf-86hq2t3p-2-result: calc(3.14159 * var(--_csstools-cf-86hq2t3p-2-local-r2)); + --_csstools-cf-86hq2t3p-5-local-r2: calc(var(--_csstools-cf-86hq2t3p-5-arg-0) * var(--_csstools-cf-86hq2t3p-5-arg-0)); + --_csstools-cf-86hq2t3p-4-result: calc(3.14159 * var(--_csstools-cf-86hq2t3p-5-local-r2)); } .circle { - width: var(--_csstools-cf-86hq2t3p-2-result); + width: var(--_csstools-cf-86hq2t3p-4-result); width: --circle-area(2px); } diff --git a/plugins/postcss-custom-functions/test/conditional-definitions.css b/plugins/postcss-custom-functions/test/conditional-definitions.css new file mode 100644 index 0000000000..af7edf559c --- /dev/null +++ b/plugins/postcss-custom-functions/test/conditional-definitions.css @@ -0,0 +1,51 @@ +@supports (width: 100px) { + @function --f() { + result: PASS; + } +} + +@supports (not (width: 100px)) { + @function --f() { + result: FAIL; + } +} + +.conditional { + width: --f(); +} + +@media (width >= 50px) { + @function --g() { + result: 50px; + } +} + +@media (width >= 100px) { + @function --g() { + result: 100px; + } +} + +.media { + width: --g(); +} + +@layer base { + @media (width >= 50px) { + @function --h() { + result: 1px; + } + } +} + +@layer theme { + @media (width >= 99999px) { + @function --h() { + result: 2px; + } + } +} + +.layered-media { + width: --h(); +} diff --git a/plugins/postcss-custom-functions/test/conditional-definitions.expect.css b/plugins/postcss-custom-functions/test/conditional-definitions.expect.css new file mode 100644 index 0000000000..214a833a27 --- /dev/null +++ b/plugins/postcss-custom-functions/test/conditional-definitions.expect.css @@ -0,0 +1,47 @@ +@supports (width: 100px) {.conditional { + --_csstools-cf-7hov6p4v-0-result: PASS; +} +}@supports (not (width: 100px)) { + +.conditional { + --_csstools-cf-7hov6p4v-0-result: FAIL; +} +} + +.conditional { + width: var(--_csstools-cf-7hov6p4v-0-result); +} + +@media (width >= 50px) { + +.media { + --_csstools-cf-7hov6p4v-3-result: 50px; +} +} + +@media (width >= 100px) { + +.media { + --_csstools-cf-7hov6p4v-3-result: 100px; +} +} + +.media { + width: var(--_csstools-cf-7hov6p4v-3-result); +} + @media (width >= 50px) { + +.layered-media { + --_csstools-cf-7hov6p4v-6-result: 1px; +} + } + @media (width >= 99999px) { + +.layered-media { + --_csstools-cf-7hov6p4v-6-result: 2px; +} + } + +.layered-media { + width: var(--_csstools-cf-7hov6p4v-6-result); +} diff --git a/plugins/postcss-custom-functions/test/conditional-definitions.preserve-true.expect.css b/plugins/postcss-custom-functions/test/conditional-definitions.preserve-true.expect.css new file mode 100644 index 0000000000..75c25c82db --- /dev/null +++ b/plugins/postcss-custom-functions/test/conditional-definitions.preserve-true.expect.css @@ -0,0 +1,92 @@ +@supports (width: 100px) { + @function --f() { + result: PASS; + } +} + +@supports (not (width: 100px)) { + @function --f() { + result: FAIL; + } +}@supports (width: 100px) { + +.conditional { + --_csstools-cf-7hov6p4v-0-result: PASS; +} +} + +@supports (not (width: 100px)) { + +.conditional { + --_csstools-cf-7hov6p4v-0-result: FAIL; +} +} + +.conditional { + width: var(--_csstools-cf-7hov6p4v-0-result); + width: --f(); +} + +@media (width >= 50px) { + @function --g() { + result: 50px; + } +} + +@media (width >= 100px) { + @function --g() { + result: 100px; + } +} + +@media (width >= 50px) { + +.media { + --_csstools-cf-7hov6p4v-3-result: 50px; +} +} + +@media (width >= 100px) { + +.media { + --_csstools-cf-7hov6p4v-3-result: 100px; +} +} + +.media { + width: var(--_csstools-cf-7hov6p4v-3-result); + width: --g(); +} + +@layer base { + @media (width >= 50px) { + @function --h() { + result: 1px; + } + } +} + +@layer theme { + @media (width >= 99999px) { + @function --h() { + result: 2px; + } + } +} + @media (width >= 50px) { + +.layered-media { + --_csstools-cf-7hov6p4v-6-result: 1px; +} + } + @media (width >= 99999px) { + +.layered-media { + --_csstools-cf-7hov6p4v-6-result: 2px; +} + } + +.layered-media { + width: var(--_csstools-cf-7hov6p4v-6-result); + width: --h(); +} diff --git a/plugins/postcss-custom-functions/test/conditionals.expect.css b/plugins/postcss-custom-functions/test/conditionals.expect.css index ac2b8ce978..cb258dfe40 100644 --- a/plugins/postcss-custom-functions/test/conditionals.expect.css +++ b/plugins/postcss-custom-functions/test/conditionals.expect.css @@ -11,19 +11,19 @@ } .conditional-locals { - --_csstools-cf-c7hag91p-1-local-x: FAIL; + --_csstools-cf-c7hag91p-3-local-x: FAIL; } @media (width > 0px) { .conditional-locals { - --_csstools-cf-c7hag91p-1-local-x: PASS; + --_csstools-cf-c7hag91p-3-local-x: PASS; } } .conditional-locals { - --_csstools-cf-c7hag91p-1-result: var(--_csstools-cf-c7hag91p-1-local-x); + --_csstools-cf-c7hag91p-2-result: var(--_csstools-cf-c7hag91p-3-local-x); } .conditional-locals { - width: var(--_csstools-cf-c7hag91p-1-result); + width: var(--_csstools-cf-c7hag91p-2-result); } diff --git a/plugins/postcss-custom-functions/test/cycles.expect.css b/plugins/postcss-custom-functions/test/cycles.expect.css index 2c6abbf404..0742b94ecc 100644 --- a/plugins/postcss-custom-functions/test/cycles.expect.css +++ b/plugins/postcss-custom-functions/test/cycles.expect.css @@ -1,5 +1,5 @@ .cycle-through-unused-local { - --_csstools-cf-63y219qj-0-local-unused: var(--_csstools-cf-invalid); + --_csstools-cf-63y219qj-1-local-unused: var(--_csstools-cf-invalid); --_csstools-cf-63y219qj-0-result: FAIL-result; } @@ -12,10 +12,10 @@ } .self-cycle { - --_csstools-cf-63y219qj-1-result: var(--_csstools-cf-invalid); + --_csstools-cf-63y219qj-2-result: var(--_csstools-cf-invalid); } -.self-cycle {--_csstools-cf-63y219qj-1-result: var(--_csstools-cf-invalid); +.self-cycle {--_csstools-cf-63y219qj-2-result: var(--_csstools-cf-invalid); } .self-cycle { @@ -24,14 +24,14 @@ } .ping-pong { - --_csstools-cf-63y219qj-3-result: var(--_csstools-cf-invalid); + --_csstools-cf-63y219qj-6-result: var(--_csstools-cf-invalid); } .ping-pong { - --_csstools-cf-63y219qj-2-result: var(--_csstools-cf-63y219qj-3-result); + --_csstools-cf-63y219qj-4-result: var(--_csstools-cf-63y219qj-6-result); } -.ping-pong {--_csstools-cf-63y219qj-2-result: var(--_csstools-cf-invalid); +.ping-pong {--_csstools-cf-63y219qj-4-result: var(--_csstools-cf-invalid); } .ping-pong { @@ -40,11 +40,11 @@ } .keyword-with-cycle { - --_csstools-cf-63y219qj-4-local-unused: var(--_csstools-cf-invalid); - --_csstools-cf-63y219qj-4-result: inherit; + --_csstools-cf-63y219qj-9-local-unused: var(--_csstools-cf-invalid); + --_csstools-cf-63y219qj-8-result: inherit; } -.keyword-with-cycle {--_csstools-cf-63y219qj-4-result: var(--_csstools-cf-invalid); +.keyword-with-cycle {--_csstools-cf-63y219qj-8-result: var(--_csstools-cf-invalid); } .keyword-with-cycle { diff --git a/plugins/postcss-custom-functions/test/examples/example.expect.css b/plugins/postcss-custom-functions/test/examples/example.expect.css index 389a422b65..c836cab9eb 100644 --- a/plugins/postcss-custom-functions/test/examples/example.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.expect.css @@ -1,19 +1,14 @@ -html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); -} -html { - --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); -} -html {--_csstools-cf-1agle9gi-1-arg-0: 4px; -} -html { - --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); -} -html { +html {--_csstools-cf-1agle9gi-1-arg-0: var(--gap); +}html { + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-1-arg-0)); +}html {--_csstools-cf-1agle9gi-3-arg-0: 4px; +}html { + --_csstools-cf-1agle9gi-2-result: calc(2 * var(--_csstools-cf-1agle9gi-3-arg-0)); +}html { --gap: 1em; padding: var(--_csstools-cf-1agle9gi-0-result); - margin: var(--_csstools-cf-1agle9gi-1-result); -} -@property --_csstools-cf-1agle9gi-1-arg-0 { + margin: var(--_csstools-cf-1agle9gi-2-result); +}@property --_csstools-cf-1agle9gi-3-arg-0 { syntax: ""; inherits: false; initial-value: 1px; diff --git a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css index 7fd089ca5c..72f295b27a 100644 --- a/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/examples/example.preserve-true.expect.css @@ -6,29 +6,29 @@ result: calc(2 * var(--value)); } -html {--_csstools-cf-1agle9gi-0-arg-0: var(--gap); +html {--_csstools-cf-1agle9gi-1-arg-0: var(--gap); } html { - --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-0-arg-0)); + --_csstools-cf-1agle9gi-0-result: calc(-1 * var(--_csstools-cf-1agle9gi-1-arg-0)); } -html {--_csstools-cf-1agle9gi-1-arg-0: 4px; +html {--_csstools-cf-1agle9gi-3-arg-0: 4px; } html { - --_csstools-cf-1agle9gi-1-result: calc(2 * var(--_csstools-cf-1agle9gi-1-arg-0)); + --_csstools-cf-1agle9gi-2-result: calc(2 * var(--_csstools-cf-1agle9gi-3-arg-0)); } html { --gap: 1em; padding: var(--_csstools-cf-1agle9gi-0-result); padding: --negative(var(--gap)); - margin: var(--_csstools-cf-1agle9gi-1-result); + margin: var(--_csstools-cf-1agle9gi-2-result); margin: --double(4px); } -@property --_csstools-cf-1agle9gi-1-arg-0 { +@property --_csstools-cf-1agle9gi-3-arg-0 { syntax: ""; inherits: false; initial-value: 1px; diff --git a/plugins/postcss-custom-functions/test/ignore.css b/plugins/postcss-custom-functions/test/ignore.css index e957222cdc..9e79cda0a4 100644 --- a/plugins/postcss-custom-functions/test/ignore.css +++ b/plugins/postcss-custom-functions/test/ignore.css @@ -37,3 +37,14 @@ .var-default { width: --var-default(10px); } + +/* Browsers ignore `@container` on `@function` definitions, so this is left alone. */ +@container (width = 100px) { + @function --container-fn() { + result: 1px; + } +} + +.container-fn { + width: --container-fn(); +} diff --git a/plugins/postcss-custom-functions/test/invalid.expect.css b/plugins/postcss-custom-functions/test/invalid.expect.css index b92a2385f7..f3dff05a2d 100644 --- a/plugins/postcss-custom-functions/test/invalid.expect.css +++ b/plugins/postcss-custom-functions/test/invalid.expect.css @@ -1,15 +1,24 @@ +.missing {--_csstools-cf-3ft7zk9k-0-result: var(--_csstools-cf-invalid); +} + .missing { - width: var(--_csstools-cf-invalid); + width: var(--_csstools-cf-3ft7zk9k-0-result); +} + +.extra {--_csstools-cf-3ft7zk9k-2-result: var(--_csstools-cf-invalid); } .extra { - width: var(--_csstools-cf-invalid); + width: var(--_csstools-cf-3ft7zk9k-2-result); } .unknown { width: --unknown(1); } +.empty-args {--_csstools-cf-3ft7zk9k-4-result: var(--_csstools-cf-invalid); +} + .empty-args { - width: var(--_csstools-cf-invalid); + width: var(--_csstools-cf-3ft7zk9k-4-result); } diff --git a/plugins/postcss-custom-functions/test/locals.expect.css b/plugins/postcss-custom-functions/test/locals.expect.css index d9e45d1dbf..5ab9905e40 100644 --- a/plugins/postcss-custom-functions/test/locals.expect.css +++ b/plugins/postcss-custom-functions/test/locals.expect.css @@ -1,10 +1,8 @@ -.initial-local {--_csstools-cf-e2576d6c-0-arg-0: 5px; -} - -.initial-local { - --_csstools-cf-e2576d6c-0-local-x: 20px; - --_csstools-cf-e2576d6c-0-local-x: var(--_csstools-cf-e2576d6c-0-arg-0); - --_csstools-cf-e2576d6c-0-result: var(--_csstools-cf-e2576d6c-0-local-x); +.initial-local {--_csstools-cf-e2576d6c-1-arg-0: 5px; +}.initial-local { + --_csstools-cf-e2576d6c-1-local-x: 20px; + --_csstools-cf-e2576d6c-1-local-x: var(--_csstools-cf-invalid); + --_csstools-cf-e2576d6c-0-result: var(--_csstools-cf-e2576d6c-1-local-x); } .initial-local { @@ -12,31 +10,31 @@ } .inherit-local { - --_csstools-cf-e2576d6c-1-local-x: var(--x); - --_csstools-cf-e2576d6c-1-result: var(--_csstools-cf-e2576d6c-1-local-x); + --_csstools-cf-e2576d6c-3-local-x: var(--x); + --_csstools-cf-e2576d6c-2-result: var(--_csstools-cf-e2576d6c-3-local-x); } .inherit-local { --x: 20px; - width: var(--_csstools-cf-e2576d6c-1-result); + width: var(--_csstools-cf-e2576d6c-2-result); } .inherit-outer { - --_csstools-cf-e2576d6c-3-local-x: var(--_csstools-cf-e2576d6c-2-local-x); - --_csstools-cf-e2576d6c-3-result: var(--_csstools-cf-e2576d6c-3-local-x); + --_csstools-cf-e2576d6c-7-local-x: var(--_csstools-cf-e2576d6c-5-local-x); + --_csstools-cf-e2576d6c-6-result: var(--_csstools-cf-e2576d6c-7-local-x); } .inherit-outer { - --_csstools-cf-e2576d6c-2-local-x: 20px; - --_csstools-cf-e2576d6c-2-result: var(--_csstools-cf-e2576d6c-3-result); + --_csstools-cf-e2576d6c-5-local-x: 20px; + --_csstools-cf-e2576d6c-4-result: var(--_csstools-cf-e2576d6c-6-result); } .inherit-outer { --x: 1px; - width: var(--_csstools-cf-e2576d6c-2-result); + width: var(--_csstools-cf-e2576d6c-4-result); } -@property --_csstools-cf-e2576d6c-0-arg-0 { +@property --_csstools-cf-e2576d6c-1-arg-0 { syntax: ""; inherits: false; initial-value: 10px; diff --git a/plugins/postcss-custom-functions/test/nested.expect.css b/plugins/postcss-custom-functions/test/nested.expect.css index e441f8d6ff..095e337047 100644 --- a/plugins/postcss-custom-functions/test/nested.expect.css +++ b/plugins/postcss-custom-functions/test/nested.expect.css @@ -1,12 +1,10 @@ -.scope {--_csstools-cf-ln0emwu0-0-arg-0: PASS; -} - .scope { - --_csstools-cf-ln0emwu0-1-result: var(--_csstools-cf-ln0emwu0-0-arg-0); + --_csstools-cf-ln0emwu0-2-result: var(--_csstools-cf-ln0emwu0-1-arg-0); +}.scope {--_csstools-cf-ln0emwu0-1-arg-0: PASS; } .scope { - --_csstools-cf-ln0emwu0-0-result: var(--_csstools-cf-ln0emwu0-1-result); + --_csstools-cf-ln0emwu0-0-result: var(--_csstools-cf-ln0emwu0-2-result); } .scope { @@ -15,28 +13,28 @@ } .scopes { - --_csstools-cf-ln0emwu0-3-result: var(--_csstools-cf-ln0emwu0-2-local-x); + --_csstools-cf-ln0emwu0-6-result: var(--_csstools-cf-ln0emwu0-5-local-x); } .scopes { - --_csstools-cf-ln0emwu0-2-local-x: 1; - --_csstools-cf-ln0emwu0-2-result: var(--_csstools-cf-ln0emwu0-3-result); + --_csstools-cf-ln0emwu0-5-local-x: 1; + --_csstools-cf-ln0emwu0-4-result: var(--_csstools-cf-ln0emwu0-6-result); } .scopes { - --_csstools-cf-ln0emwu0-5-result: var(--_csstools-cf-ln0emwu0-4-local-x); + --_csstools-cf-ln0emwu0-a-result: var(--_csstools-cf-ln0emwu0-9-local-x); } .scopes { - --_csstools-cf-ln0emwu0-4-local-x: 2; - --_csstools-cf-ln0emwu0-4-result: var(--_csstools-cf-ln0emwu0-5-result); + --_csstools-cf-ln0emwu0-9-local-x: 2; + --_csstools-cf-ln0emwu0-8-result: var(--_csstools-cf-ln0emwu0-a-result); } .scopes { - --_csstools-cf-ln0emwu0-6-result: var(--x); + --_csstools-cf-ln0emwu0-c-result: var(--x); } .scopes { --x: 0; - width: var(--_csstools-cf-ln0emwu0-2-result) var(--_csstools-cf-ln0emwu0-4-result) var(--_csstools-cf-ln0emwu0-6-result); + width: var(--_csstools-cf-ln0emwu0-4-result) var(--_csstools-cf-ln0emwu0-8-result) var(--_csstools-cf-ln0emwu0-c-result); } diff --git a/plugins/postcss-custom-functions/test/typed.expect.css b/plugins/postcss-custom-functions/test/typed.expect.css index 44b56b1f93..cadb337497 100644 --- a/plugins/postcss-custom-functions/test/typed.expect.css +++ b/plugins/postcss-custom-functions/test/typed.expect.css @@ -1,72 +1,70 @@ -.sized-a {--_csstools-cf-3ammwpqj-0-arg-0: 20px; -} - -.sized-a { - --_csstools-cf-3ammwpqj-0-result: var(--_csstools-cf-3ammwpqj-0-arg-0); +.sized-a {--_csstools-cf-3ammwpqj-1-arg-0: 20px; +}.sized-a { + --_csstools-cf-3ammwpqj-0-result: var(--_csstools-cf-3ammwpqj-1-arg-0); } .sized-a { width: var(--_csstools-cf-3ammwpqj-0-result); } -.sized-b {--_csstools-cf-3ammwpqj-1-arg-0: 10px; +.sized-b {--_csstools-cf-3ammwpqj-3-arg-0: 10px; } .sized-b { - --_csstools-cf-3ammwpqj-1-result: var(--_csstools-cf-3ammwpqj-1-arg-0); + --_csstools-cf-3ammwpqj-2-result: var(--_csstools-cf-3ammwpqj-3-arg-0); } .sized-b { - width: var(--_csstools-cf-3ammwpqj-1-result); + width: var(--_csstools-cf-3ammwpqj-2-result); } -.colorized {--_csstools-cf-3ammwpqj-2-arg-0: blue; +.colorized {--_csstools-cf-3ammwpqj-5-arg-0: blue; } .colorized { - --_csstools-cf-3ammwpqj-2-result: var(--_csstools-cf-3ammwpqj-2-arg-0); + --_csstools-cf-3ammwpqj-4-result: var(--_csstools-cf-3ammwpqj-5-arg-0); } .colorized { - color: var(--_csstools-cf-3ammwpqj-2-result); + color: var(--_csstools-cf-3ammwpqj-4-result); } -.mixed {--_csstools-cf-3ammwpqj-3-arg-0: 1px;--_csstools-cf-3ammwpqj-3-arg-1: 2px;--_csstools-cf-3ammwpqj-3-arg-2: 3px; +.mixed {--_csstools-cf-3ammwpqj-7-arg-0: 1px;--_csstools-cf-3ammwpqj-7-arg-1: 2px;--_csstools-cf-3ammwpqj-7-arg-2: 3px; } .mixed { - --_csstools-cf-3ammwpqj-3-result: var(--_csstools-cf-3ammwpqj-3-arg-0) var(--_csstools-cf-3ammwpqj-3-arg-1) var(--_csstools-cf-3ammwpqj-3-arg-2); + --_csstools-cf-3ammwpqj-6-result: var(--_csstools-cf-3ammwpqj-7-arg-0) var(--_csstools-cf-3ammwpqj-7-arg-1) var(--_csstools-cf-3ammwpqj-7-arg-2); } .mixed { - width: var(--_csstools-cf-3ammwpqj-3-result); + width: var(--_csstools-cf-3ammwpqj-6-result); } -@property --_csstools-cf-3ammwpqj-0-arg-0 { +@property --_csstools-cf-3ammwpqj-1-arg-0 { syntax: ""; inherits: false; initial-value: 10px; } -@property --_csstools-cf-3ammwpqj-1-arg-0 { +@property --_csstools-cf-3ammwpqj-3-arg-0 { syntax: ""; inherits: false; initial-value: 10px; } -@property --_csstools-cf-3ammwpqj-2-arg-0 { +@property --_csstools-cf-3ammwpqj-5-arg-0 { syntax: ""; inherits: false; initial-value: red; } -@property --_csstools-cf-3ammwpqj-3-arg-1 { +@property --_csstools-cf-3ammwpqj-7-arg-1 { syntax: ""; inherits: false; initial-value: 2px; } -@property --_csstools-cf-3ammwpqj-3-arg-2 { +@property --_csstools-cf-3ammwpqj-7-arg-2 { syntax: ""; inherits: false; initial-value: 3px; diff --git a/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css b/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css index 2fbf589f29..438ee9e735 100644 --- a/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css +++ b/plugins/postcss-custom-functions/test/typed.preserve-true.expect.css @@ -2,11 +2,11 @@ result: var(--x); } -.sized-a {--_csstools-cf-3ammwpqj-0-arg-0: 20px; +.sized-a {--_csstools-cf-3ammwpqj-1-arg-0: 20px; } .sized-a { - --_csstools-cf-3ammwpqj-0-result: var(--_csstools-cf-3ammwpqj-0-arg-0); + --_csstools-cf-3ammwpqj-0-result: var(--_csstools-cf-3ammwpqj-1-arg-0); } .sized-a { @@ -14,15 +14,15 @@ width: --sized(20px); } -.sized-b {--_csstools-cf-3ammwpqj-1-arg-0: 10px; +.sized-b {--_csstools-cf-3ammwpqj-3-arg-0: 10px; } .sized-b { - --_csstools-cf-3ammwpqj-1-result: var(--_csstools-cf-3ammwpqj-1-arg-0); + --_csstools-cf-3ammwpqj-2-result: var(--_csstools-cf-3ammwpqj-3-arg-0); } .sized-b { - width: var(--_csstools-cf-3ammwpqj-1-result); + width: var(--_csstools-cf-3ammwpqj-2-result); width: --sized(); } @@ -30,15 +30,15 @@ result: var(--c); } -.colorized {--_csstools-cf-3ammwpqj-2-arg-0: blue; +.colorized {--_csstools-cf-3ammwpqj-5-arg-0: blue; } .colorized { - --_csstools-cf-3ammwpqj-2-result: var(--_csstools-cf-3ammwpqj-2-arg-0); + --_csstools-cf-3ammwpqj-4-result: var(--_csstools-cf-3ammwpqj-5-arg-0); } .colorized { - color: var(--_csstools-cf-3ammwpqj-2-result); + color: var(--_csstools-cf-3ammwpqj-4-result); color: --colorized(blue); } @@ -46,43 +46,43 @@ result: var(--x) var(--y) var(--z); } -.mixed {--_csstools-cf-3ammwpqj-3-arg-0: 1px;--_csstools-cf-3ammwpqj-3-arg-1: 2px;--_csstools-cf-3ammwpqj-3-arg-2: 3px; +.mixed {--_csstools-cf-3ammwpqj-7-arg-0: 1px;--_csstools-cf-3ammwpqj-7-arg-1: 2px;--_csstools-cf-3ammwpqj-7-arg-2: 3px; } .mixed { - --_csstools-cf-3ammwpqj-3-result: var(--_csstools-cf-3ammwpqj-3-arg-0) var(--_csstools-cf-3ammwpqj-3-arg-1) var(--_csstools-cf-3ammwpqj-3-arg-2); + --_csstools-cf-3ammwpqj-6-result: var(--_csstools-cf-3ammwpqj-7-arg-0) var(--_csstools-cf-3ammwpqj-7-arg-1) var(--_csstools-cf-3ammwpqj-7-arg-2); } .mixed { - width: var(--_csstools-cf-3ammwpqj-3-result); + width: var(--_csstools-cf-3ammwpqj-6-result); width: --mixed(1px); } -@property --_csstools-cf-3ammwpqj-0-arg-0 { +@property --_csstools-cf-3ammwpqj-1-arg-0 { syntax: ""; inherits: false; initial-value: 10px; } -@property --_csstools-cf-3ammwpqj-1-arg-0 { +@property --_csstools-cf-3ammwpqj-3-arg-0 { syntax: ""; inherits: false; initial-value: 10px; } -@property --_csstools-cf-3ammwpqj-2-arg-0 { +@property --_csstools-cf-3ammwpqj-5-arg-0 { syntax: ""; inherits: false; initial-value: red; } -@property --_csstools-cf-3ammwpqj-3-arg-1 { +@property --_csstools-cf-3ammwpqj-7-arg-1 { syntax: ""; inherits: false; initial-value: 2px; } -@property --_csstools-cf-3ammwpqj-3-arg-2 { +@property --_csstools-cf-3ammwpqj-7-arg-2 { syntax: ""; inherits: false; initial-value: 3px; diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html index 20f7e2bb51..ae111c2785 100644 --- a/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-cycles.html @@ -13,6 +13,10 @@ // // Cases that require typed parameters, return types or default values are // omitted. + function stylesUrl() { + return window.__TRANSFORM__ === false ? '/test/styles.css?transform=false' : '/test/styles.css'; + } + window.runTest = async function runTest() { function assert_equals(actual, expected, message) { if (actual !== expected) { @@ -357,7 +361,7 @@ for (const testCase of testCases) { const styleElement = document.createElement('style'); - const testCaseStyle = await (fetch('/test/styles.css', { + const testCaseStyle = await (fetch(stylesUrl(), { method: 'POST', body: testCase.style, }).then((response) => response.text())); diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html index 81b57860ab..b36a2a7538 100644 --- a/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-eval.html @@ -14,6 +14,10 @@ // Only the subset without typed parameters or default values is asserted here. // Each case produces matching computed values for --actual and --expected // on #target. + function stylesUrl() { + return window.__TRANSFORM__ === false ? '/test/styles.css?transform=false' : '/test/styles.css'; + } + window.runTest = async function runTest() { function assert_equals(actual, expected, message) { if (actual !== expected) { @@ -695,7 +699,7 @@ for (const testCase of testCases) { const styleElement = document.createElement('style'); - const testCaseStyle = await (fetch('/test/styles.css', { + const testCaseStyle = await (fetch(stylesUrl(), { method: 'POST', body: testCase.style, }).then((response) => response.text())); diff --git a/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html b/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html index a9514ff518..2a3c3d1c54 100644 --- a/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html +++ b/plugins/postcss-custom-functions/test/wpt/dashed-function-standard-property.html @@ -10,6 +10,10 @@ // // The typed return case is omitted because typed return values are not // supported yet. + function stylesUrl() { + return window.__TRANSFORM__ === false ? '/test/styles.css?transform=false' : '/test/styles.css'; + } + window.runTest = async function runTest() { function assert_equals(actual, expected, message) { if (actual !== expected) { @@ -32,7 +36,7 @@ `; const styleElement = document.createElement('style'); - styleElement.textContent = await (fetch('/test/styles.css', { + styleElement.textContent = await (fetch(stylesUrl(), { method: 'POST', body: style, }).then((response) => response.text())); diff --git a/plugins/postcss-custom-functions/test/wpt/function-conditional-definitions.html b/plugins/postcss-custom-functions/test/wpt/function-conditional-definitions.html new file mode 100644 index 0000000000..39ad7bb20f --- /dev/null +++ b/plugins/postcss-custom-functions/test/wpt/function-conditional-definitions.html @@ -0,0 +1,152 @@ + +Custom Functions: @function inside conditional rules + + + + +
+
+
+
+ + diff --git a/plugins/postcss-custom-functions/test/wpt/function-conditionals.html b/plugins/postcss-custom-functions/test/wpt/function-conditionals.html index 6ecdf1f247..2feec8f506 100644 --- a/plugins/postcss-custom-functions/test/wpt/function-conditionals.html +++ b/plugins/postcss-custom-functions/test/wpt/function-conditionals.html @@ -20,6 +20,10 @@ diff --git a/plugins/postcss-custom-functions/test/wpt/function-layer.html b/plugins/postcss-custom-functions/test/wpt/function-layer.html index 81da78d1e8..1755589574 100644 --- a/plugins/postcss-custom-functions/test/wpt/function-layer.html +++ b/plugins/postcss-custom-functions/test/wpt/function-layer.html @@ -8,6 +8,10 @@