diff --git a/.github/bin/generate-docs/readme.mjs b/.github/bin/generate-docs/readme.mjs
index 957e7a6b0..5c8cf232a 100644
--- a/.github/bin/generate-docs/readme.mjs
+++ b/.github/bin/generate-docs/readme.mjs
@@ -28,10 +28,14 @@ installDoc = installDoc.replace(`
// Insert "Header" section
installDoc = installDoc.replace('', `# [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]`);
+` + `[
][npm-url] ` +
+`${
+ packageJSONInfo.csstools?.cssdbId ?
+ `[
][css-url] ` :
+ ''
+}` +
+`[
][cli-url] ` +
+`[
][discord]`);
// Insert "Usage" section
installDoc = installDoc.replace('', `## Usage
@@ -62,7 +66,11 @@ instructions for:
// Insert "Link List" section
installDoc = installDoc.replace('', `[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
-[css-url]: https://cssdb.org/#
+${
+ packageJSONInfo.csstools?.cssdbId ?
+ `[css-url]: https://cssdb.org/#` :
+ ''
+}
[discord]: https://discord.gg/bUadyRwkJS
[npm-url]: https://www.npmjs.com/package/
diff --git a/package-lock.json b/package-lock.json
index 6de3ab45a..72f75f66f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1775,6 +1775,10 @@
"resolved": "plugins/postcss-color-function",
"link": true
},
+ "node_modules/@csstools/postcss-design-tokens": {
+ "resolved": "plugins/postcss-design-tokens",
+ "link": true
+ },
"node_modules/@csstools/postcss-font-format-keywords": {
"resolved": "plugins/postcss-font-format-keywords",
"link": true
@@ -6181,6 +6185,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-dictionary-design-tokens-example": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/style-dictionary-design-tokens-example/-/style-dictionary-design-tokens-example-1.0.0.tgz",
+ "integrity": "sha512-dUjgpSbx4uOAj3qZ1Tja3Vur3w+/UbVlT7JW2aAH3U7w8Hqm7nKwE0dCVU8O94sSFCs0jsdfPlvcYsEIIzLtvA==",
+ "dev": true
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -6979,6 +6989,28 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-design-tokens": {
+ "name": "@csstools/postcss-design-tokens",
+ "version": "1.0.0",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "devDependencies": {
+ "postcss-import": "^14.1.0",
+ "style-dictionary-design-tokens-example": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3"
+ }
+ },
"plugins/postcss-dir-pseudo-class": {
"version": "6.0.4",
"license": "CC0-1.0",
@@ -8610,6 +8642,14 @@
"postcss-value-parser": "^4.2.0"
}
},
+ "@csstools/postcss-design-tokens": {
+ "version": "file:plugins/postcss-design-tokens",
+ "requires": {
+ "postcss-import": "^14.1.0",
+ "postcss-value-parser": "^4.2.0",
+ "style-dictionary-design-tokens-example": "^1.0.0"
+ }
+ },
"@csstools/postcss-font-format-keywords": {
"version": "file:plugins/postcss-font-format-keywords",
"requires": {
@@ -11920,6 +11960,12 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
+ "style-dictionary-design-tokens-example": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/style-dictionary-design-tokens-example/-/style-dictionary-design-tokens-example-1.0.0.tgz",
+ "integrity": "sha512-dUjgpSbx4uOAj3qZ1Tja3Vur3w+/UbVlT7JW2aAH3U7w8Hqm7nKwE0dCVU8O94sSFCs0jsdfPlvcYsEIIzLtvA==",
+ "dev": true
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
diff --git a/plugins/postcss-base-plugin/README.md b/plugins/postcss-base-plugin/README.md
index 0d18299e7..9fa7056e9 100644
--- a/plugins/postcss-base-plugin/README.md
+++ b/plugins/postcss-base-plugin/README.md
@@ -1,11 +1,8 @@
# PostCSS Base Plugin [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
-[PostCSS Base Plugin] lets easily create new plugins following some [CSS Specification].
+[PostCSS Base Plugin] lets you easily create new plugins following some [CSS Specification].
```pcss
.foo {
diff --git a/plugins/postcss-base-plugin/docs/README.md b/plugins/postcss-base-plugin/docs/README.md
index 3bc455892..8680a65c8 100644
--- a/plugins/postcss-base-plugin/docs/README.md
+++ b/plugins/postcss-base-plugin/docs/README.md
@@ -14,7 +14,7 @@
-[] lets easily create new plugins following some [CSS Specification].
+[] lets you easily create new plugins following some [CSS Specification].
```pcss
diff --git a/plugins/postcss-cascade-layers/README.md b/plugins/postcss-cascade-layers/README.md
index cfa0007c3..6f5b7954b 100644
--- a/plugins/postcss-cascade-layers/README.md
+++ b/plugins/postcss-cascade-layers/README.md
@@ -1,9 +1,6 @@
# PostCSS Cascade Layers [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
[PostCSS Cascade Layers] lets you use `@layer` following the [Cascade Layers Specification]. For more information on layers, checkout [A Complete Guide to CSS Cascade Layers] by Miriam Suzanne.
diff --git a/plugins/postcss-color-function/README.md b/plugins/postcss-color-function/README.md
index c19e804e1..62d6b294d 100644
--- a/plugins/postcss-color-function/README.md
+++ b/plugins/postcss-color-function/README.md
@@ -1,9 +1,6 @@
# PostCSS Color Function [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
[PostCSS Color Function] lets you use the `color` function in
CSS, following the [CSS Color] specification.
diff --git a/plugins/postcss-design-tokens/.gitignore b/plugins/postcss-design-tokens/.gitignore
new file mode 100644
index 000000000..7172b04f1
--- /dev/null
+++ b/plugins/postcss-design-tokens/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+dist/*
diff --git a/plugins/postcss-design-tokens/.nvmrc b/plugins/postcss-design-tokens/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/plugins/postcss-design-tokens/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs
new file mode 100644
index 000000000..075221bb4
--- /dev/null
+++ b/plugins/postcss-design-tokens/.tape.mjs
@@ -0,0 +1,104 @@
+import postcssTape from '../../packages/postcss-tape/dist/index.mjs';
+import plugin from '@csstools/postcss-design-tokens';
+import postcssImport from 'postcss-import';
+
+postcssTape(plugin)({
+ basic: {
+ message: "supports basic usage",
+ plugins: [
+ postcssImport(),
+ plugin()
+ ]
+ },
+ 'basic:rootFontSize-20': {
+ message: "supports basic usage with { unitsAndValues { rootFontSize: 20 } }",
+ plugins: [
+ postcssImport(),
+ plugin({
+ unitsAndValues: {
+ rootFontSize: 20
+ }
+ })
+ ]
+ },
+ 'basic:rootFontSize-NaN': {
+ message: "supports basic usage with { unitsAndValues { rootFontSize: NaN } }",
+ plugins: [
+ postcssImport(),
+ plugin({
+ unitsAndValues: {
+ rootFontSize: NaN
+ }
+ })
+ ]
+ },
+ 'basic:rootFontSize-invalid': {
+ message: "supports basic usage with { unitsAndValues { rootFontSize: invalid } }",
+ plugins: [
+ postcssImport(),
+ plugin({
+ unitsAndValues: {
+ rootFontSize: 'invalid'
+ }
+ })
+ ]
+ },
+ 'errors': {
+ message: "handles issues correctly",
+ options: {},
+ warnings: 4
+ },
+ 'is': {
+ message: "supports basic usage with { is ['dark', 'tablet', 'branded-green'] }",
+ options: {
+ is: ['dark', 'tablet', 'branded-green']
+ }
+ },
+ 'value-parsing-a': {
+ message: "supports value parsing (A)",
+ },
+ 'value-parsing-b': {
+ message: "supports value parsing (B)",
+ },
+ 'value-parsing-c': {
+ message: "supports value parsing (C)",
+ warnings: 2
+ },
+ 'value-parsing-d': {
+ message: "supports value parsing (D)",
+ warnings: 2
+ },
+ 'value-parsing-e': {
+ message: "supports value parsing (E)",
+ warnings: 2
+ },
+ 'value-parsing-f': {
+ message: "supports value parsing (F)",
+ },
+ 'value-parsing-g': {
+ message: "supports value parsing (G)",
+ warnings: 2
+ },
+ 'examples/example': {
+ message: 'minimal example',
+ options: {},
+ },
+ 'examples/example-conditional': {
+ message: 'minimal example with conditional imports : default',
+ options: {},
+ },
+ 'examples/example-conditional:dark': {
+ message: 'minimal example with conditional imports : dark',
+ options: {
+ is: ['dark']
+ },
+ },
+ 'examples/example:rootFontSize-20': {
+ message: "minimal example with { unitsAndValues { rootFontSize: 20 } }",
+ options: {
+ unitsAndValues: {
+ rootFontSize: 20
+ }
+ }
+ },
+});
diff --git a/plugins/postcss-design-tokens/CHANGELOG.md b/plugins/postcss-design-tokens/CHANGELOG.md
new file mode 100644
index 000000000..ea750358a
--- /dev/null
+++ b/plugins/postcss-design-tokens/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changes to PostCSS Design Tokens
+
+### 1.0.0 (Unreleased)
+
+- Initial version
diff --git a/plugins/postcss-design-tokens/INSTALL.md b/plugins/postcss-design-tokens/INSTALL.md
new file mode 100644
index 000000000..a6635e80b
--- /dev/null
+++ b/plugins/postcss-design-tokens/INSTALL.md
@@ -0,0 +1,176 @@
+# Installing PostCSS Design Tokens
+
+[PostCSS Design Tokens] runs in all Node environments, with special instructions for:
+
+| [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) |
+| --- | --- | --- | --- | --- | --- |
+
+## Node
+
+Add [PostCSS Design Tokens] to your project:
+
+```bash
+npm install postcss @csstools/postcss-design-tokens --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssDesignTokens = require('@csstools/postcss-design-tokens');
+
+postcss([
+ postcssDesignTokens(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## PostCSS CLI
+
+Add [PostCSS CLI] to your project:
+
+```bash
+npm install postcss-cli @csstools/postcss-design-tokens --save-dev
+```
+
+Use [PostCSS Design Tokens] in your `postcss.config.js` configuration file:
+
+```js
+const postcssDesignTokens = require('@csstools/postcss-design-tokens');
+
+module.exports = {
+ plugins: [
+ postcssDesignTokens(/* pluginOptions */)
+ ]
+}
+```
+
+## Webpack
+
+_Webpack version 5_
+
+Add [PostCSS Loader] to your project:
+
+```bash
+npm install postcss-loader @csstools/postcss-design-tokens --save-dev
+```
+
+Use [PostCSS Design Tokens] 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: [
+ [
+ "@csstools/postcss-design-tokens",
+ {
+ // Options
+ },
+ ],
+ ],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+```
+
+## Create React App
+
+Add [React App Rewired] and [React App Rewire PostCSS] to your project:
+
+```bash
+npm install react-app-rewired react-app-rewire-postcss @csstools/postcss-design-tokens --save-dev
+```
+
+Use [React App Rewire PostCSS] and [PostCSS Design Tokens] in your
+`config-overrides.js` file:
+
+```js
+const reactAppRewirePostcss = require('react-app-rewire-postcss');
+const postcssDesignTokens = require('@csstools/postcss-design-tokens');
+
+module.exports = config => reactAppRewirePostcss(config, {
+ plugins: () => [
+ postcssDesignTokens(/* pluginOptions */)
+ ]
+});
+```
+
+## Gulp
+
+Add [Gulp PostCSS] to your project:
+
+```bash
+npm install gulp-postcss @csstools/postcss-design-tokens --save-dev
+```
+
+Use [PostCSS Design Tokens] in your Gulpfile:
+
+```js
+const postcss = require('gulp-postcss');
+const postcssDesignTokens = require('@csstools/postcss-design-tokens');
+
+gulp.task('css', function () {
+ var plugins = [
+ postcssDesignTokens(/* 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-design-tokens --save-dev
+```
+
+Use [PostCSS Design Tokens] in your Gruntfile:
+
+```js
+const postcssDesignTokens = require('@csstools/postcss-design-tokens');
+
+grunt.loadNpmTasks('grunt-postcss');
+
+grunt.initConfig({
+ postcss: {
+ options: {
+ processors: [
+ postcssDesignTokens(/* 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 Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens
+[React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss
+[React App Rewired]: https://github.com/timarney/react-app-rewired
diff --git a/plugins/postcss-design-tokens/LICENSE.md b/plugins/postcss-design-tokens/LICENSE.md
new file mode 100644
index 000000000..0bc1fa706
--- /dev/null
+++ b/plugins/postcss-design-tokens/LICENSE.md
@@ -0,0 +1,108 @@
+# CC0 1.0 Universal
+
+## Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an “owner”) of an original work of
+authorship and/or a database (each, a “Work”).
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific works
+(“Commons”) that the public can reliably and without fear of later claims of
+infringement build upon, modify, incorporate in other works, reuse and
+redistribute as freely as possible in any form whatsoever and for any purposes,
+including without limitation commercial purposes. These owners may contribute
+to the Commons to promote the ideal of a free culture and the further
+production of creative, cultural and scientific works, or to gain reputation or
+greater distribution for their Work in part through the use and efforts of
+others.
+
+For these and/or other purposes and motivations, and without any expectation of
+additional consideration or compensation, the person associating CC0 with a
+Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
+publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+ protected by copyright and related or neighboring rights (“Copyright and
+ Related Rights”). Copyright and Related Rights include, but are not limited
+ to, the following:
+ 1. the right to reproduce, adapt, distribute, perform, display, communicate,
+ and translate a Work;
+ 2. moral rights retained by the original author(s) and/or performer(s);
+ 3. publicity and privacy rights pertaining to a person’s image or likeness
+ depicted in a Work;
+ 4. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(i), below;
+ 5. rights protecting the extraction, dissemination, use and reuse of data in
+ a Work;
+ 6. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation thereof,
+ including any amended or successor version of such directive); and
+ 7. other similar, equivalent or corresponding rights throughout the world
+ based on applicable law or treaty, and any national implementations
+ thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+ applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+ unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
+ and Related Rights and associated claims and causes of action, whether now
+ known or unknown (including existing as well as future claims and causes of
+ action), in the Work (i) in all territories worldwide, (ii) for the maximum
+ duration provided by applicable law or treaty (including future time
+ extensions), (iii) in any current or future medium and for any number of
+ copies, and (iv) for any purpose whatsoever, including without limitation
+ commercial, advertising or promotional purposes (the “Waiver”). Affirmer
+ makes the Waiver for the benefit of each member of the public at large and
+ to the detriment of Affirmer’s heirs and successors, fully intending that
+ such Waiver shall not be subject to revocation, rescission, cancellation,
+ termination, or any other legal or equitable action to disrupt the quiet
+ enjoyment of the Work by the public as contemplated by Affirmer’s express
+ Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+ judged legally invalid or ineffective under applicable law, then the Waiver
+ shall be preserved to the maximum extent permitted taking into account
+ Affirmer’s express Statement of Purpose. In addition, to the extent the
+ Waiver is so judged Affirmer hereby grants to each affected person a
+ royalty-free, non transferable, non sublicensable, non exclusive,
+ irrevocable and unconditional license to exercise Affirmer’s Copyright and
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
+ maximum duration provided by applicable law or treaty (including future time
+ extensions), (iii) in any current or future medium and for any number of
+ copies, and (iv) for any purpose whatsoever, including without limitation
+ commercial, advertising or promotional purposes (the “License”). The License
+ shall be deemed effective as of the date CC0 was applied by Affirmer to the
+ Work. Should any part of the License for any reason be judged legally
+ invalid or ineffective under applicable law, such partial invalidity or
+ ineffectiveness shall not invalidate the remainder of the License, and in
+ such case Affirmer hereby affirms that he or she will not (i) exercise any
+ of his or her remaining Copyright and Related Rights in the Work or (ii)
+ assert any associated claims and causes of action with respect to the Work,
+ in either case contrary to Affirmer’s express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+ 1. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ 2. Affirmer offers the Work as-is and makes no representations or warranties
+ of any kind concerning the Work, express, implied, statutory or
+ otherwise, including without limitation warranties of title,
+ merchantability, fitness for a particular purpose, non infringement, or
+ the absence of latent or other defects, accuracy, or the present or
+ absence of errors, whether or not discoverable, all to the greatest
+ extent permissible under applicable law.
+ 3. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person’s Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the Work.
+ 4. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to this
+ CC0 or use of the Work.
+
+For more information, please see
+http://creativecommons.org/publicdomain/zero/1.0/.
diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md
new file mode 100644
index 000000000..87614e8e1
--- /dev/null
+++ b/plugins/postcss-design-tokens/README.md
@@ -0,0 +1,287 @@
+# PostCSS Design Tokens [
][postcss]
+
+[
][npm-url] [
][cli-url] [
][discord]
+
+[PostCSS Design Tokens] lets you use design tokens in your CSS source files.
+
+```json
+{
+ "color": {
+ "background": {
+ "primary": { "value": "#fff" }
+ }
+ },
+ "size": {
+ "spacing": {
+ "small": { "value": "16px" }
+ }
+ }
+}
+```
+
+```pcss
+@design-tokens url('./tokens.json') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+ padding-top: design-token('size.spacing.small');
+ padding-left: design-token('size.spacing.small' to px);
+ padding-bottom: design-token('size.spacing.small' to rem);
+}
+
+/* becomes */
+
+.foo {
+ color: #fff;
+ padding-top: 16px;
+ padding-left: 16px;
+ padding-bottom: 1rem;
+}
+```
+
+## Usage
+
+Add [PostCSS Design Tokens] to your project:
+
+```bash
+npm install postcss @csstools/postcss-design-tokens --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssDesignTokens = require('@csstools/postcss-design-tokens');
+
+postcss([
+ postcssDesignTokens(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+[PostCSS Design Tokens] runs in all Node environments, with special
+instructions for:
+
+| [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) |
+| --- | --- | --- | --- | --- | --- |
+
+## Formats
+
+At this time there is no standardized format for design tokens.
+Although there is an ongoing effort to create this, we feel it is still too early to adopt this.
+
+For the moment we only support [Style Dictionary](https://amzn.github.io/style-dictionary/#/).
+Use `style-dictionary3` in `@design-tokens` rules to pick this format.
+
+## Options
+
+### is
+
+The `is` option determines which design tokens are used.
+This allows you to generate multiple themed stylesheets.
+
+By default only `@design-tokens` without any `when('foo')` conditions are used.
+
+#### Example usage
+
+**For these two token files :**
+
+```json
+{
+ "color": {
+ "background": {
+ "primary": { "value": "#fff" }
+ }
+ }
+}
+```
+
+```json
+{
+ "color": {
+ "background": {
+ "primary": { "value": "#000" }
+ }
+ }
+}
+```
+
+**And this CSS :**
+
+```pcss
+@design-tokens url('./tokens-light.json') format('style-dictionary3');
+@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+}
+```
+
+**You can configure :**
+
+##### No `is` option.
+
+```js
+postcssDesignTokens()
+```
+
+```pcss
+@design-tokens url('./tokens-light.json') format('style-dictionary3');
+@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+}
+
+/* becomes */
+
+.foo {
+ color: #fff;
+}
+```
+
+##### `is` option set to 'dark'.
+
+```js
+postcssDesignTokens({ is: ['dark'] })
+```
+
+```pcss
+@design-tokens url('./tokens-light.json') format('style-dictionary3');
+@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+}
+
+/* becomes */
+
+.foo {
+ color: #000;
+}
+```
+
+### unitsAndValues
+
+The `unitsAndValues` option allows you to control some aspects of how design values are converted to CSS.
+`rem` <-> `px` for example can only be calculated when we know the root font size.
+
+#### rootFontSize
+
+defaults to `16`
+
+```js
+postcssDesignTokens({
+ unitsAndValues: {
+ rootFontSize: 20,
+ },
+})
+```
+
+```pcss
+@design-tokens url('./tokens.json') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+ padding-top: design-token('size.spacing.small');
+ padding-left: design-token('size.spacing.small' to px);
+ padding-bottom: design-token('size.spacing.small' to rem);
+}
+
+/* becomes */
+
+.foo {
+ color: #fff;
+ padding-top: 16px;
+ padding-left: 16px;
+ padding-bottom: 0.8rem;
+}
+```
+
+## Syntax
+
+[PostCSS Design Tokens] is non-standard and is not part of any official CSS Specification.
+
+### Editor support
+
+This is all very new and we hope that one day design tokens will become first class citizens in editors and other tools.
+Until then we will do our best to provide extensions.
+These will have rough edges but should illustrate were we want to go.
+
+| editor | plugin |
+| --- | --- |
+| VSCode | [CSSTools Design Tokens](https://marketplace.visualstudio.com/items?itemName=RomainMenke.csstools-design-tokens) |
+
+### `@design-tokens` rule
+
+The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS.
+
+```pcss
+@design-tokens url('./tokens.json') format('style-dictionary3');
+```
+
+```pcss
+@design-tokens url('./tokens.json') format('style-dictionary3');
+@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark');
+```
+
+```
+@design-tokens [ | ]
+ [ when(*) ]?
+ format();
+
+ =
+
+ = [ 'style-dictionary3' ]
+```
+
+All `@design-tokens` rules in a document are evaluated in order of appearance.
+If a token with the same path and name already exists it will be overridden.
+
+All `@design-tokens` rules are evaluated before any `design-token()` functions.
+
+`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`.
+Multiple conditions always have an `AND` relationship.
+
+> ```css
+> /* only evaluated when tooling receives 'blue' and 'muted' as arguments */
+> @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted');
+> ```
+
+`@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules.
+
+> ```css
+> @media (min-width: 500px) {
+> @design-tokens url('./tokens.json') format('style-dictionary3'); /* always evaluated */
+> }
+> ```
+
+Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level.
+
+
+### `design-token()` function
+
+The `design-token()` function takes a token path and returns the token value.
+
+```pcss
+.foo {
+ color: design-token('color.background.primary');
+}
+```
+
+```
+design-token() = design-token( [ to ]? )
+
+ =
+ = [ px | rem ]
+```
+
+[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/postcss-design-tokens
+
+[Gulp PostCSS]: https://github.com/postcss/gulp-postcss
+[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS Loader]: https://github.com/postcss/postcss-loader
+[PostCSS Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens
diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md
new file mode 100644
index 000000000..e105b158c
--- /dev/null
+++ b/plugins/postcss-design-tokens/docs/README.md
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[] lets you use design tokens in your CSS source files.
+
+```json
+
+```
+
+```pcss
+
+
+/* becomes */
+
+
+```
+
+
+
+
+
+## Formats
+
+At this time there is no standardized format for design tokens.
+Although there is an ongoing effort to create this, we feel it is still too early to adopt this.
+
+For the moment we only support [Style Dictionary](https://amzn.github.io/style-dictionary/#/).
+Use `style-dictionary3` in `@design-tokens` rules to pick this format.
+
+## Options
+
+### is
+
+The `is` option determines which design tokens are used.
+This allows you to generate multiple themed stylesheets.
+
+By default only `@design-tokens` without any `when('foo')` conditions are used.
+
+#### Example usage
+
+**For these two token files :**
+
+```json
+
+```
+
+```json
+
+```
+
+**And this CSS :**
+
+```pcss
+
+```
+
+**You can configure :**
+
+##### No `is` option.
+
+```js
+()
+```
+
+```pcss
+
+
+/* becomes */
+
+
+```
+
+##### `is` option set to 'dark'.
+
+```js
+({ is: ['dark'] })
+```
+
+```pcss
+
+
+/* becomes */
+
+
+```
+
+### unitsAndValues
+
+The `unitsAndValues` option allows you to control some aspects of how design values are converted to CSS.
+`rem` <-> `px` for example can only be calculated when we know the root font size.
+
+#### rootFontSize
+
+defaults to `16`
+
+```js
+({
+ unitsAndValues: {
+ rootFontSize: 20,
+ },
+})
+```
+
+```pcss
+
+
+/* becomes */
+
+
+```
+
+## Syntax
+
+[] is non-standard and is not part of any official CSS Specification.
+
+### Editor support
+
+This is all very new and we hope that one day design tokens will become first class citizens in editors and other tools.
+Until then we will do our best to provide extensions.
+These will have rough edges but should illustrate were we want to go.
+
+| editor | plugin |
+| --- | --- |
+| VSCode | [CSSTools Design Tokens](https://marketplace.visualstudio.com/items?itemName=RomainMenke.csstools-design-tokens) |
+
+### `@design-tokens` rule
+
+The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS.
+
+```pcss
+@design-tokens url('./tokens.json') format('style-dictionary3');
+```
+
+```pcss
+@design-tokens url('./tokens.json') format('style-dictionary3');
+@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark');
+```
+
+```
+@design-tokens [ | ]
+ [ when(*) ]?
+ format();
+
+ =
+
+ = [ 'style-dictionary3' ]
+```
+
+All `@design-tokens` rules in a document are evaluated in order of appearance.
+If a token with the same path and name already exists it will be overridden.
+
+All `@design-tokens` rules are evaluated before any `design-token()` functions.
+
+`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`.
+Multiple conditions always have an `AND` relationship.
+
+> ```css
+> /* only evaluated when tooling receives 'blue' and 'muted' as arguments */
+> @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted');
+> ```
+
+`@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules.
+
+> ```css
+> @media (min-width: 500px) {
+> @design-tokens url('./tokens.json') format('style-dictionary3'); /* always evaluated */
+> }
+> ```
+
+Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level.
+
+
+### `design-token()` function
+
+The `design-token()` function takes a token path and returns the token value.
+
+```pcss
+.foo {
+ color: design-token('color.background.primary');
+}
+```
+
+```
+design-token() = design-token( [ to ]? )
+
+ =
+ = [ px | rem ]
+```
+
+
diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json
new file mode 100644
index 000000000..e0cc6333a
--- /dev/null
+++ b/plugins/postcss-design-tokens/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@csstools/postcss-design-tokens",
+ "description": "Use design tokens in your CSS",
+ "version": "1.0.0",
+ "contributors": [
+ {
+ "name": "Antonio Laguna",
+ "email": "antonio@laguna.es",
+ "url": "https://antonio.laguna.es"
+ },
+ {
+ "name": "Romain Menke ",
+ "email": "romainmenke@gmail.com"
+ }
+ ],
+ "license": "CC0-1.0",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md",
+ "dist"
+ ],
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3"
+ },
+ "devDependencies": {
+ "postcss-import": "^14.1.0",
+ "style-dictionary-design-tokens-example": "^1.0.0"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.js",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"",
+ "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs",
+ "lint": "npm run lint:eslint && npm run lint:package-json",
+ "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern",
+ "lint:package-json": "node ../../.github/bin/format-package-json.mjs",
+ "prepublishOnly": "npm run clean && npm run build && npm run test",
+ "test": "node .tape.mjs && npm run test:exports",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "plugins/postcss-design-tokens"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "design-tokens",
+ "postcss-plugin"
+ ],
+ "csstools": {
+ "exportName": "postcssDesignTokens",
+ "humanReadableName": "PostCSS Design Tokens"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/postcss-design-tokens/src/constants.ts b/plugins/postcss-design-tokens/src/constants.ts
new file mode 100644
index 000000000..c77d745b5
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/constants.ts
@@ -0,0 +1,2 @@
+// a random, but shared default condition
+export const DEFAULT_CONDITION = '6b4e71e7-4787-42f7-a092-8684961895db';
diff --git a/plugins/postcss-design-tokens/src/data-formats/base/token.ts b/plugins/postcss-design-tokens/src/data-formats/base/token.ts
new file mode 100644
index 000000000..5838f8695
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/base/token.ts
@@ -0,0 +1,10 @@
+export interface TokenTransformOptions {
+ pluginOptions: {
+ rootFontSize: number;
+ };
+ toUnit?: string;
+}
+
+export interface Token {
+ cssValue(opts?: TokenTransformOptions): string
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts
new file mode 100644
index 000000000..f3089c72c
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts
@@ -0,0 +1,68 @@
+import valueParser from 'postcss-value-parser';
+import { Token } from './base/token';
+import { extractStyleDictionaryTokens } from './style-dictionary/style-dictionary';
+import path from 'path';
+import { promises as fsp } from 'fs';
+import { DEFAULT_CONDITION } from '../constants';
+
+function parseImport(statement: string): { filePath: string, format: string, conditions: Array } {
+ const importAST = valueParser(statement);
+
+ const result = {
+ filePath: '',
+ format: 'standard',
+ conditions: [DEFAULT_CONDITION],
+ };
+
+ importAST.walk((node) => {
+ if (node.type === 'function' && node.value === 'url') {
+ result.filePath = node.nodes[0].value;
+ }
+
+ if (node.type === 'function' && node.value === 'format') {
+ result.format = node.nodes[0].value;
+ }
+
+ if (node.type === 'function' && node.value === 'when') {
+ result.conditions = node.nodes.filter((child) => {
+ return child.type === 'string';
+ }).map((child) => child.value);
+ }
+ });
+
+ if (!result.conditions.length) {
+ result.conditions = [DEFAULT_CONDITION];
+ }
+
+ return result;
+}
+
+export async function tokensFromImport(buildIs: Array, sourceFilePath: string, statement: string, alreadyImported: Set): Promise<{ filePath: string, tokens: Map }|false> {
+ const { filePath, format, conditions } = parseImport(statement);
+ if (!conditions.every((condition) => buildIs.includes(condition))) {
+ return false;
+ }
+
+ const resolvedPath = path.resolve(path.dirname(sourceFilePath), filePath);
+ if (alreadyImported.has(resolvedPath)) {
+ return false;
+ }
+
+ alreadyImported.add(resolvedPath);
+
+ const fileContents = await fsp.readFile(resolvedPath, 'utf8');
+ const tokenContents = JSON.parse(fileContents);
+
+ switch (format) {
+ case 'style-dictionary3':
+ return {
+ filePath: path.resolve(filePath),
+ tokens: extractStyleDictionaryTokens('3', tokenContents, resolvedPath),
+ };
+
+ default:
+ break;
+ }
+
+ throw new Error('Unsupported format: ' + format);
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts
new file mode 100644
index 000000000..a2b588294
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts
@@ -0,0 +1,13 @@
+
+import { Token } from '../base/token';
+import { extractStyleDictionaryV3Tokens, StyleDictionaryV3TokenGroup } from './v3/group';
+
+export const latestStyleDictionaryVersion = '3';
+
+export function extractStyleDictionaryTokens(version: string, node: unknown, filePath: string): Map {
+ if (version === '3') {
+ return extractStyleDictionaryV3Tokens(node as StyleDictionaryV3TokenGroup, filePath);
+ }
+
+ throw new Error('Unsupported version: ' + version);
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts
new file mode 100644
index 000000000..5ac37c07b
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts
@@ -0,0 +1,248 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { TokenTransformOptions } from '../../base/token';
+import { toposort } from '../../toposort/toposort';
+import { applyTransformsToValue, StyleDictionaryV3TokenValue } from './value';
+
+export function dereferenceTokenValues(tokens: Map): Map {
+ const tainted = new Set();
+ const referenceASTs = new Map>();
+
+ // Gather all references.
+ {
+ for (const [id, token] of tokens.entries()) {
+ const referenceAST = parseReferences(token.value);
+ if (!referenceAST.length) {
+ continue;
+ }
+
+ tainted.add(id);
+ referenceASTs.set(id, referenceAST);
+ }
+ }
+
+ // Fast dereference.
+ // Only handles references for which a value can found immediately.
+ {
+ for (const [id, referenceAST] of referenceASTs.entries()) {
+ for (let i = 0; i < referenceAST.length; i++) {
+ const reference = referenceAST[i];
+ if (reference.type !== 'value-reference') {
+ continue;
+ }
+
+ if (tainted.has(reference.raw)) {
+ continue;
+ }
+
+ if (!tokens.has(reference.raw)) {
+ throw new Error('Alias "' + reference.raw + '" not found');
+ }
+
+ const sourceToken = tokens.get(reference.raw)!;
+ referenceAST[i] = {
+ type: 'value-resolved',
+ raw: reference.raw,
+ value: sourceToken.cssValue(),
+ };
+ }
+
+ const hasFurtherReferences = referenceAST.some(part => part.type === 'value-reference');
+ if (hasFurtherReferences) {
+ continue;
+ }
+
+ const value = (referenceAST as ValuePartsResolved).map(part => part.value).join('');
+ const currentToken = tokens.get(id)!;
+
+ currentToken.value = value;
+ currentToken.cssValue = (transformOptions: TokenTransformOptions) => {
+ return applyTransformsToValue(value, transformOptions);
+ };
+
+ tokens.set(id, currentToken);
+ tainted.delete(id);
+ referenceASTs.delete(id);
+ }
+
+ if (tainted.size === 0) {
+ return tokens;
+ }
+ }
+
+ // Topological dereference.
+ {
+ const nodes : Array = Array.from(tokens.keys());
+ const edges: Array> = [];
+
+ for (const [id, referenceAST] of referenceASTs.entries()) {
+ for (let i = 0; i < referenceAST.length; i++) {
+ const reference = referenceAST[i];
+ if (reference.type !== 'value-reference') {
+ continue;
+ }
+
+ edges.push([reference.raw, id]);
+ }
+ }
+
+ const sorted = toposort(nodes, edges);
+ if (!sorted) {
+ throw new Error('Circular reference detected');
+ }
+
+ for (const id of sorted) {
+ if (!referenceASTs.has(id)) {
+ continue;
+ }
+
+ const referenceAST = referenceASTs.get(id)!;
+ for (let i = 0; i < referenceAST.length; i++) {
+ const reference = referenceAST[i];
+ if (reference.type !== 'value-reference') {
+ continue;
+ }
+
+ if (tainted.has(reference.raw)) {
+ throw new Error('Alias "' + reference.raw + '" can not be resolved');
+ }
+
+ if (!tokens.has(reference.raw)) {
+ throw new Error('Alias "' + reference.raw + '" not found');
+ }
+
+ const sourceToken = tokens.get(reference.raw)!;
+ referenceAST[i] = {
+ type: 'value-resolved',
+ raw: reference.raw,
+ value: sourceToken.cssValue(),
+ };
+ }
+
+ const hasFurtherReferences = referenceAST.some(part => part.type === 'value-reference');
+ if (hasFurtherReferences) {
+ throw new Error('Token "' + id + '" can not be fully resolved');
+ }
+
+ const value = (referenceAST as ValuePartsResolved).map(part => part.value).join('');
+ const currentToken = tokens.get(id)!;
+
+ currentToken.value = value;
+ currentToken.cssValue = (transformOptions: TokenTransformOptions) => {
+ return applyTransformsToValue(value, transformOptions);
+ };
+
+ tokens.set(id, currentToken);
+ tainted.delete(id);
+ referenceASTs.delete(id);
+ }
+
+ if (tainted.size === 0) {
+ return tokens;
+ }
+ }
+
+ return tokens;
+}
+
+type ValuePartsResolved = Array;
+type ValuePart = ValueReference | ValueNonReference | ValueResolved;
+
+type ValueReference = {
+ type: 'value-reference',
+ raw: string,
+}
+
+type ValueResolved = {
+ type: 'value-resolved',
+ raw: string,
+ value: string,
+}
+
+type ValueNonReference = {
+ type: 'value-non-reference',
+ value: string,
+}
+
+function parseReferences(valueWithReferences: unknown): Array {
+ if (typeof valueWithReferences !== 'string') {
+ return [];
+ }
+
+ if (valueWithReferences.indexOf('{') === -1) {
+ return [];
+ }
+
+ const result: Array = [];
+ let hasReferences = false;
+
+ let inReference = false;
+ let buf = '';
+
+ for (let index = 0; index < valueWithReferences.length; index++) {
+ const char = valueWithReferences[index];
+
+ switch (char) {
+ case '{':
+ if (inReference) {
+ throw new Error('Unexpected "{" in "' + valueWithReferences + '" at ' + index);
+ }
+
+ if (buf.length > 0) {
+ result.push({
+ type: 'value-non-reference',
+ value: buf,
+ });
+ buf = '';
+ }
+
+ inReference = true;
+ break;
+ case '}':
+ if (!inReference) {
+ throw new Error('Unexpected "}" in "' + valueWithReferences + '" at ' + index);
+ }
+
+ if (buf.length === 0) {
+ throw new Error('Empty alias "{}" in "' + valueWithReferences + '" at ' + index);
+ }
+
+ {
+ let reference = buf.trim();
+ if (reference.slice(-6) === '.value') {
+ reference = reference.slice(0, -6);
+ }
+
+ result.push({
+ type: 'value-reference',
+ raw: reference,
+ });
+ buf = '';
+ }
+
+ hasReferences = true;
+ inReference = false;
+ break;
+
+ default:
+ buf += char;
+ break;
+ }
+ }
+
+ if (inReference) {
+ throw new Error('Unexpected end of alias in "' + valueWithReferences + '"');
+ }
+
+ if (buf.length > 0) {
+ result.push({
+ type: 'value-non-reference',
+ value: buf,
+ });
+ }
+
+ if (!hasReferences) {
+ return [];
+ }
+
+ return result;
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts
new file mode 100644
index 000000000..b21c3955b
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts
@@ -0,0 +1,38 @@
+import { Token } from '../../base/token';
+import { dereferenceTokenValues } from './dereference';
+import { extractStyleDictionaryV3Token, StyleDictionaryV3TokenValue } from './value';
+
+export type StyleDictionaryV3TokenGroup = {
+ [key: string]: StyleDictionaryV3TokenGroup | StyleDictionaryV3TokenValue;
+}
+
+function extractTokens(node: StyleDictionaryV3TokenGroup, path: Array, filePath: string): Map {
+ const result: Map = new Map();
+ for (const key in node) {
+ if (Object.hasOwnProperty.call(node, key)) {
+ const child = Object(node[key]);
+ if (!child) {
+ throw new Error('Parsing error');
+ }
+
+ if (typeof child['value'] !== 'undefined') {
+ const token = extractStyleDictionaryV3Token(child, key, path, filePath);
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ result.set(token.metadata!.path.join('.'), token );
+ continue;
+ }
+
+ for (const [tokenPath, token] of extractTokens(child, [...path, key], filePath).entries()) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ result.set(tokenPath, token);
+ continue;
+ }
+ }
+ }
+
+ return result;
+}
+
+export function extractStyleDictionaryV3Tokens(node: StyleDictionaryV3TokenGroup, filePath: string): Map {
+ return dereferenceTokenValues(extractTokens(node, [], filePath));
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts
new file mode 100644
index 000000000..6a854e099
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts
@@ -0,0 +1,115 @@
+import { TokenTransformOptions } from '../../base/token';
+import valueParser from 'postcss-value-parser';
+
+export type StyleDictionaryV3TokenValue = {
+ cssValue(transformOptions?: TokenTransformOptions): string
+ // The value of the design token. This can be any type of data, a hex string, an integer, a file path to a file, even an object or array.
+ value: unknown
+ // Usually the name for a design token is generated with a name transform, but you can write your own if you choose. By default Style Dictionary will add a default name which is the key of the design token object.
+ name?: string
+ // The comment attribute will show up in a code comment in output files if the format supports it.
+ comment?: string
+ // This is used in formats that support override-able or themeable values like the !default flag in Sass.
+ themeable?: boolean
+ // Extra information about the design token you want to include. Attribute transforms will modify this object so be careful
+ attributes?: unknown
+
+ // Set by the token parser
+ metadata?: {
+ // A default name of the design token that is set to the key of the design token. This is only added if you do not provide one.
+ name?: string
+ // The object path of the design token.
+ path: Array
+ // The file path of the file the token is defined in. This file path is derived from the source or include file path arrays defined in the configuration.
+ filePath: string
+ // If the token is from a file defined in the source array as opposed to include in the configuration.
+ isSource: boolean
+ }
+}
+
+export function extractStyleDictionaryV3Token(node: Record, key: string, path: Array, filePath: string): StyleDictionaryV3TokenValue {
+ if (typeof node['value'] === 'undefined') {
+ throw new Error('Token value is undefined for "' + [...path, key].join('.') + '"');
+ }
+
+ switch (typeof node['value']) {
+ case 'string':
+ case 'number':
+ break;
+
+ default:
+ throw new Error('Token value is not a string or a number for "' + [...path, key].join('.') + '"');
+ }
+
+ const value = String(node['value']);
+
+ return {
+ value: value,
+ cssValue: (transformOptions?: TokenTransformOptions) => {
+ return applyTransformsToValue(value, transformOptions);
+ },
+ name: String(node['name'] ?? '') || key,
+ comment: String(node['comment'] ?? '') || undefined,
+ metadata: {
+ name: String(node['name'] ?? '') ? key : undefined,
+ path: [...path, key],
+ filePath: filePath,
+ isSource: true,
+ },
+ };
+}
+
+export function applyTransformsToValue(value: string|undefined|null, transformOptions?: TokenTransformOptions): string {
+ if (!value) {
+ return '';
+ }
+
+ if (!transformOptions) {
+ return value;
+ }
+
+ if (transformOptions.toUnit) {
+ const dimension = valueParser.unit(value ?? '');
+ if (!dimension || dimension.unit === transformOptions.toUnit) {
+ return `${value}`;
+ }
+
+ if (dimension.unit === 'rem' && transformOptions.toUnit === 'px') {
+ return remToPx(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16);
+ }
+
+ if (dimension.unit === 'px' && transformOptions.toUnit === 'rem') {
+ return pxToRem(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16);
+ }
+ }
+
+ return value;
+}
+
+function remToPx(value: number, rootFontSize: number): string {
+ return `${formatFloat(value * rootFontSize)}px`;
+}
+
+function pxToRem(value: number, rootFontSize: number): string {
+ return `${formatFloat(value / rootFontSize)}rem`;
+}
+
+function formatFloat(value: number): string {
+ if (Number.isInteger(value)) {
+ return value.toString();
+ }
+
+ let fixedPrecision = value.toFixed(5);
+ for (let i = fixedPrecision.length; i > 0; i--) {
+ if (fixedPrecision[i] === '.') {
+ break;
+ }
+
+ if (fixedPrecision[i] !== '0') {
+ fixedPrecision = fixedPrecision.slice(0, i + 1);
+ continue;
+ }
+ }
+
+ return fixedPrecision;
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/token.ts b/plugins/postcss-design-tokens/src/data-formats/token.ts
new file mode 100644
index 000000000..704cb796c
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/token.ts
@@ -0,0 +1,9 @@
+import { Token } from './base/token';
+
+export function mergeTokens(a: Map, b: Map): Map {
+ const result = new Map(a);
+ for (const [key, value] of b) {
+ result.set(key, value);
+ }
+ return result;
+}
diff --git a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts
new file mode 100644
index 000000000..73b773d40
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts
@@ -0,0 +1,104 @@
+// Toposort - Topological sorting for node.js
+// Copyright (c) 2012 by Marcel Klehr
+// MIT LICENSE
+// 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.
+
+export function toposort(nodes: Array, edges: Array>): Array {
+ let cursor = nodes.length;
+ const sorted: Array = new Array(cursor);
+ const visited: Record = {};
+ let i = cursor;
+ // Better data structures make algorithm much faster.
+ const outgoingEdges = makeOutgoingEdges(edges);
+ const nodesHash = makeNodesHash(nodes);
+
+ // check for unknown nodes
+ edges.forEach(function (edge) {
+ if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) {
+ throw new Error('Unknown token. Make sure to provide all tokens used in aliases.');
+ }
+ });
+
+ while (i--) {
+ if (!visited[i]) {
+ visit(nodes[i], i, new Set());
+ }
+ }
+
+ return sorted;
+
+ function visit(node: string, j: number, predecessors: Set) {
+ if (predecessors.has(node)) {
+ let nodeRep;
+ try {
+ nodeRep = ', token was: ' + JSON.stringify(node);
+ } catch (e) {
+ nodeRep = '';
+ }
+ throw new Error('Cyclic dependency' + nodeRep);
+ }
+
+ if (!nodesHash.has(node)) {
+ throw new Error('Found unknown token. Make sure to provided all involved tokens. Unknown token: ' + JSON.stringify(node));
+ }
+
+ if (visited[j]) {
+ return;
+ }
+ visited[j] = true;
+
+ let outgoing = outgoingEdges.get(node) || new Set();
+ outgoing = Array.from(outgoing);
+
+ // eslint-disable-next-line no-cond-assign
+ if (j = outgoing.length) {
+ predecessors.add(node);
+ do {
+ const child = outgoing[--j];
+ visit(child, nodesHash.get(child), predecessors);
+ } while (j);
+ predecessors.delete(node);
+ }
+
+ sorted[--cursor] = node;
+ }
+}
+
+function makeOutgoingEdges(arr: Array>) {
+ const edges = new Map();
+ for (let i = 0, len = arr.length; i < len; i++) {
+ const edge = arr[i];
+ if (!edges.has(edge[0])) {
+ edges.set(edge[0], new Set());
+ }
+ if (!edges.has(edge[1])) {
+ edges.set(edge[1], new Set());
+ }
+ edges.get(edge[0]).add(edge[1]);
+ }
+ return edges;
+}
+
+function makeNodesHash(arr: Array) {
+ const res = new Map();
+ for (let i = 0, len = arr.length; i < len; i++) {
+ res.set(arr[i], i);
+ }
+ return res;
+}
diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts
new file mode 100644
index 000000000..0cd1ecada
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/index.ts
@@ -0,0 +1,83 @@
+import type { Node, PluginCreator } from 'postcss';
+import { Token } from './data-formats/base/token';
+import { tokensFromImport } from './data-formats/parse-import';
+import { mergeTokens } from './data-formats/token';
+import { parsePluginOptions, pluginOptions } from './options';
+import { onCSSValue } from './values';
+
+
+const creator: PluginCreator = (opts?: pluginOptions) => {
+ const options = parsePluginOptions(opts);
+
+ return {
+ postcssPlugin: 'postcss-design-tokens',
+ prepare() {
+ let tokens = new Map();
+ let importedFiles = new Set();
+
+ return {
+ OnceExit() {
+ tokens = new Map();
+ importedFiles = new Set();
+ },
+ Once: async (root, { result }) => {
+ const designTokenAtRules: Array<{filePath: string, params: string, node: Node}> = [];
+ root.walkAtRules('design-tokens', (atRule) => {
+ if (!atRule?.source?.input?.file) {
+ return;
+ }
+
+ const filePath = atRule.source.input.file;
+ const params = atRule.params;
+ atRule.remove();
+
+ designTokenAtRules.push({
+ filePath: filePath,
+ params: params,
+ node: atRule,
+ });
+ });
+
+ for (const atRule of designTokenAtRules.values()) {
+ let importResult: { filePath: string, tokens: Map }|false;
+ try {
+ importResult = await tokensFromImport(options.is, atRule.filePath, atRule.params, importedFiles);
+ if (!importResult) {
+ continue;
+ }
+ } catch (e) {
+ atRule.node.warn(result, `Failed to import design tokens from "${atRule.params}" with error:\n\t` + (e as Error).message);
+ continue;
+ }
+
+ result.messages.push({
+ type: 'dependency',
+ plugin: 'postcss-design-tokens',
+ file: importResult.filePath,
+ parent: atRule.filePath,
+ });
+
+ tokens = mergeTokens(tokens, importResult.tokens);
+ }
+ },
+ Declaration(decl, { result }) {
+ if (decl.value.indexOf('design-token') === -1) {
+ return;
+ }
+
+ const modifiedValue = onCSSValue(tokens, result, decl, options);
+ if (modifiedValue === decl.value) {
+ return;
+ }
+
+ decl.value = modifiedValue;
+ },
+ };
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
+
diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts
new file mode 100644
index 000000000..adebc06b5
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/options.ts
@@ -0,0 +1,50 @@
+import { DEFAULT_CONDITION } from './constants';
+
+export type pluginOptions = {
+ is?: Array
+ unitsAndValues?: {
+ rootFontSize?: number
+ }
+}
+
+export type parsedPluginOptions = {
+ is: Array
+ unitsAndValues: {
+ rootFontSize: number
+ }
+}
+
+export function parsePluginOptions(opts?: pluginOptions): parsedPluginOptions {
+ const options: parsedPluginOptions = {
+ is: [DEFAULT_CONDITION],
+ unitsAndValues: {
+ rootFontSize: 16,
+ },
+ };
+
+ if (!opts) {
+ return options;
+ }
+
+ if (typeof opts !== 'object') {
+ return options;
+ }
+
+ if (Array.isArray(opts.is)) {
+ options.is = opts.is.filter((x) => typeof x === 'string');
+ }
+
+ if (options.is.length === 0) {
+ options.is = [DEFAULT_CONDITION];
+ }
+
+ if (typeof opts.unitsAndValues === 'object' && typeof opts.unitsAndValues.rootFontSize === 'number' && isPositiveAndNotZero(opts.unitsAndValues.rootFontSize)) {
+ options.unitsAndValues.rootFontSize = opts.unitsAndValues.rootFontSize;
+ }
+
+ return options;
+}
+
+function isPositiveAndNotZero(x: number) {
+ return x > 0 && x !== Infinity;
+}
diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts
new file mode 100644
index 000000000..f3447bcf6
--- /dev/null
+++ b/plugins/postcss-design-tokens/src/values.ts
@@ -0,0 +1,61 @@
+import type { Declaration, Result } from 'postcss';
+import valueParser from 'postcss-value-parser';
+import { Token, TokenTransformOptions } from './data-formats/base/token';
+import { parsedPluginOptions } from './options';
+
+export function onCSSValue(tokens: Map, result: Result, decl: Declaration, opts: parsedPluginOptions) {
+ const valueAST = valueParser(decl.value);
+
+ valueAST.walk(node => {
+ if (node.type !== 'function' || node.value !== 'design-token') {
+ return;
+ }
+
+ if (!node.nodes || node.nodes.length === 0) {
+ decl.warn(result, 'Expected at least a single string literal for the design-token function.');
+ return;
+ }
+
+ if (node.nodes[0].type !== 'string') {
+ decl.warn(result, 'Expected at least a single string literal for the design-token function.');
+ return;
+ }
+
+ const tokenName = node.nodes[0].value;
+ const replacement = tokens.get(tokenName);
+ if (!replacement) {
+ decl.warn(result, `design-token: "${tokenName}" is not configured.`);
+ return;
+ }
+
+ const remainingNodes = node.nodes.slice(1).filter(x => x.type === 'word');
+ if (!remainingNodes.length) {
+ node.value = replacement.cssValue();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (node as any).nodes = undefined;
+ return;
+ }
+
+ const transformOptions: TokenTransformOptions = {
+ pluginOptions: opts.unitsAndValues,
+ };
+ for (let i = 0; i < remainingNodes.length; i++) {
+ if (
+ remainingNodes[i].type === 'word' &&
+ remainingNodes[i].value === 'to' &&
+ remainingNodes[i + 1] &&
+ remainingNodes[i + 1].type === 'word' &&
+ ['px', 'rem'].includes(remainingNodes[i + 1].value)
+ ) {
+ transformOptions.toUnit = remainingNodes[i + 1].value;
+ i++;
+ }
+ }
+
+ node.value = replacement.cssValue(transformOptions);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (node as any).nodes = undefined;
+ });
+
+ return String(valueAST);
+}
diff --git a/plugins/postcss-design-tokens/test/_import.mjs b/plugins/postcss-design-tokens/test/_import.mjs
new file mode 100644
index 000000000..97e23a010
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/_import.mjs
@@ -0,0 +1,6 @@
+import assert from 'assert';
+import plugin from '@csstools/postcss-design-tokens';
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-design-tokens/test/_require.cjs b/plugins/postcss-design-tokens/test/_require.cjs
new file mode 100644
index 000000000..e48299ce9
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/_require.cjs
@@ -0,0 +1,6 @@
+const assert = require('assert');
+const plugin = require('@csstools/postcss-design-tokens');
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css
new file mode 100644
index 000000000..9fc8643f7
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/basic.css
@@ -0,0 +1,54 @@
+@import url('./imported.css');
+@design-tokens url('./tokens/basic.json') format('style-dictionary3');
+
+.foo {
+ font-family: design-token('font.family.base');
+ font-size: design-token('size.font.small');
+ color: design-token('color.font.base');
+}
+
+.card {
+ background-color: design-token('card.background');
+ color: design-token('card.foreground');
+ color: design-token( 'card.foreground');
+ color: design-token('card.foreground' );
+ color: design-token(
+ /* a foreground color */
+ 'card.foreground'
+ );
+ color: design-token(
+ 'card.foreground'
+ /* a foreground color */
+ );
+}
+
+.px-to-px {
+ padding-bottom: design-token('space.small' to px);
+ padding-bottom: design-token('space.default' to px);
+ padding-bottom: design-token('space.large' to px);
+}
+
+.px-to-rem {
+ padding-bottom: design-token('space.small' to rem);
+ padding-bottom: design-token('space.default' to rem);
+ padding-bottom: design-token('space.large' to rem);
+}
+
+.rem-to-rem {
+ padding-bottom: design-token('space.small-b' to rem);
+ padding-bottom: design-token('space.default-b' to rem);
+ padding-bottom: design-token('space.large-b' to rem);
+}
+
+.rem-to-px {
+ padding-bottom: design-token('space.small-b' to px);
+ padding-bottom: design-token('space.default-b' to px);
+ padding-bottom: design-token('space.large-b' to px);
+}
+
+.invalid-conversion {
+ color: design-token('card.foreground' to rem);
+ color: design-token('card.foreground' to px);
+ color: design-token('space.lh' to rem);
+ color: design-token('space.lh' to px);
+}
diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css
new file mode 100644
index 000000000..d4b93d305
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/basic.expect.css
@@ -0,0 +1,39 @@
+.foo {
+ font-family: Helvetica sans;
+ font-size: 10;
+ color: #111111;
+}
+.card {
+ background-color: blue;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+}
+.px-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.px-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.1rem;
+ padding-bottom: 2rem;
+}
+.rem-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.125rem;
+ padding-bottom: 2rem;
+}
+.rem-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.invalid-conversion {
+ color: red;
+ color: red;
+ color: 1lh;
+ color: 1lh;
+}
diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css
new file mode 100644
index 000000000..d1915ac36
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css
@@ -0,0 +1,39 @@
+.foo {
+ font-family: Helvetica sans;
+ font-size: 10;
+ color: #111111;
+}
+.card {
+ background-color: blue;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+}
+.px-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.px-to-rem {
+ padding-bottom: 0.4rem;
+ padding-bottom: 0.9rem;
+ padding-bottom: 1.6rem;
+}
+.rem-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.125rem;
+ padding-bottom: 2rem;
+}
+.rem-to-px {
+ padding-bottom: 10px;
+ padding-bottom: 22.5px;
+ padding-bottom: 40px;
+}
+.invalid-conversion {
+ color: red;
+ color: red;
+ color: 1lh;
+ color: 1lh;
+}
diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css
new file mode 100644
index 000000000..d4b93d305
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css
@@ -0,0 +1,39 @@
+.foo {
+ font-family: Helvetica sans;
+ font-size: 10;
+ color: #111111;
+}
+.card {
+ background-color: blue;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+}
+.px-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.px-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.1rem;
+ padding-bottom: 2rem;
+}
+.rem-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.125rem;
+ padding-bottom: 2rem;
+}
+.rem-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.invalid-conversion {
+ color: red;
+ color: red;
+ color: 1lh;
+ color: 1lh;
+}
diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css
new file mode 100644
index 000000000..d4b93d305
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css
@@ -0,0 +1,39 @@
+.foo {
+ font-family: Helvetica sans;
+ font-size: 10;
+ color: #111111;
+}
+.card {
+ background-color: blue;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+ color: red;
+}
+.px-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.px-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.1rem;
+ padding-bottom: 2rem;
+}
+.rem-to-rem {
+ padding-bottom: 0.5rem;
+ padding-bottom: 1.125rem;
+ padding-bottom: 2rem;
+}
+.rem-to-px {
+ padding-bottom: 8px;
+ padding-bottom: 18px;
+ padding-bottom: 32px;
+}
+.invalid-conversion {
+ color: red;
+ color: red;
+ color: 1lh;
+ color: 1lh;
+}
diff --git a/plugins/postcss-design-tokens/test/errors.css b/plugins/postcss-design-tokens/test/errors.css
new file mode 100644
index 000000000..92fb33bab
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/errors.css
@@ -0,0 +1,4 @@
+@design-tokens url('./tokens/does-not-exist.json') format('style-dictionary3');
+@design-tokens url('./tokens/basic.json') format('does not exist');
+@design-tokens url('./tokens/circular.json') format('style-dictionary3');
+@design-tokens url('./tokens/missing.json') format('style-dictionary3');
diff --git a/plugins/postcss-design-tokens/test/errors.expect.css b/plugins/postcss-design-tokens/test/errors.expect.css
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/errors.expect.css
@@ -0,0 +1 @@
+
diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.css b/plugins/postcss-design-tokens/test/examples/example-conditional.css
new file mode 100644
index 000000000..8592a21da
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/example-conditional.css
@@ -0,0 +1,6 @@
+@design-tokens url('./tokens-light.json') format('style-dictionary3');
+@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+}
diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css b/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css
new file mode 100644
index 000000000..cab059366
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css
@@ -0,0 +1,3 @@
+.foo {
+ color: #000;
+}
diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css b/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css
new file mode 100644
index 000000000..33a418a59
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css
@@ -0,0 +1,3 @@
+.foo {
+ color: #fff;
+}
diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css
new file mode 100644
index 000000000..e4e3e2c99
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/example.css
@@ -0,0 +1,8 @@
+@design-tokens url('./tokens.json') format('style-dictionary3');
+
+.foo {
+ color: design-token('color.background.primary');
+ padding-top: design-token('size.spacing.small');
+ padding-left: design-token('size.spacing.small' to px);
+ padding-bottom: design-token('size.spacing.small' to rem);
+}
diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css
new file mode 100644
index 000000000..5c5964adb
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/example.expect.css
@@ -0,0 +1,6 @@
+.foo {
+ color: #fff;
+ padding-top: 16px;
+ padding-left: 16px;
+ padding-bottom: 1rem;
+}
diff --git a/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css
new file mode 100644
index 000000000..6b28e7c0e
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css
@@ -0,0 +1,6 @@
+.foo {
+ color: #fff;
+ padding-top: 16px;
+ padding-left: 16px;
+ padding-bottom: 0.8rem;
+}
diff --git a/plugins/postcss-design-tokens/test/examples/tokens-dark.json b/plugins/postcss-design-tokens/test/examples/tokens-dark.json
new file mode 100644
index 000000000..09de442b7
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/tokens-dark.json
@@ -0,0 +1,7 @@
+{
+ "color": {
+ "background": {
+ "primary": { "value": "#000" }
+ }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/examples/tokens-light.json b/plugins/postcss-design-tokens/test/examples/tokens-light.json
new file mode 100644
index 000000000..9776bbaed
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/tokens-light.json
@@ -0,0 +1,7 @@
+{
+ "color": {
+ "background": {
+ "primary": { "value": "#fff" }
+ }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/examples/tokens.json b/plugins/postcss-design-tokens/test/examples/tokens.json
new file mode 100644
index 000000000..1ff537046
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/examples/tokens.json
@@ -0,0 +1,12 @@
+{
+ "color": {
+ "background": {
+ "primary": { "value": "#fff" }
+ }
+ },
+ "size": {
+ "spacing": {
+ "small": { "value": "16px" }
+ }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/imported.css b/plugins/postcss-design-tokens/test/imported.css
new file mode 100644
index 000000000..6c68c5991
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/imported.css
@@ -0,0 +1 @@
+@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') format('style-dictionary3');
diff --git a/plugins/postcss-design-tokens/test/is.css b/plugins/postcss-design-tokens/test/is.css
new file mode 100644
index 000000000..78b783e71
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/is.css
@@ -0,0 +1,12 @@
+@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') when('dark' 'branded-blue') format('style-dictionary3');
+@design-tokens url('./tokens/color_light_branded-blue.tokens.json') when('light' 'branded-blue') format('style-dictionary3');
+@design-tokens url('./tokens/color_dark_branded-green.tokens.json') when('dark' 'branded-green') format('style-dictionary3');
+@design-tokens url('./tokens/color_light_branded-green.tokens.json') when('light' 'branded-green') format('style-dictionary3');
+@design-tokens url('./tokens/size_mobile.tokens.json') when('mobile') format('style-dictionary3');
+@design-tokens url('./tokens/size_tablet.tokens.json') when('tablet') format('style-dictionary3');
+@design-tokens url('./tokens/size_desktop.tokens.json') when('desktop') format('style-dictionary3');
+
+.foo {
+ color: design-token('color');
+ font-size: design-token('size');
+}
diff --git a/plugins/postcss-design-tokens/test/is.expect.css b/plugins/postcss-design-tokens/test/is.expect.css
new file mode 100644
index 000000000..6b837bc2b
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/is.expect.css
@@ -0,0 +1,4 @@
+.foo {
+ color: rgb(0, 120, 0);
+ font-size: 18px;
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/basic.json b/plugins/postcss-design-tokens/test/tokens/basic.json
new file mode 100644
index 000000000..1ec411322
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/basic.json
@@ -0,0 +1,45 @@
+{
+ "font": {
+ "family": {
+ "sans": { "value": "sans" },
+ "helvetica": { "value": "Helvetica" },
+ "serif": { "value": "serif" },
+ "base": { "value": "{ font.family.helvetica.value} {font.family.sans.value}" }
+ }
+ },
+ "card": {
+ "foreground": { "value": "{logical-color.foreground}" },
+ "background": { "value": "{logical-color.background}" }
+ },
+ "base-color": {
+ "red": { "value": "red" },
+ "blue": { "value": "blue" }
+ },
+ "logical-color": {
+ "foreground": { "value": "{base-color.red}" },
+ "background": { "value": "{base-color.blue}" }
+ },
+ "space": {
+ "small": {
+ "value": "8px"
+ },
+ "default": {
+ "value": "18px"
+ },
+ "large": {
+ "value": "32px"
+ },
+ "small-b": {
+ "value": "0.5rem"
+ },
+ "default-b": {
+ "value": "1.125rem"
+ },
+ "large-b": {
+ "value": "2rem"
+ },
+ "lh": {
+ "value": "1lh"
+ }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/circular.json b/plugins/postcss-design-tokens/test/tokens/circular.json
new file mode 100644
index 000000000..805827672
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/circular.json
@@ -0,0 +1,6 @@
+{
+ "a": {
+ "1": { "value": "{a.2}" },
+ "2": { "value": "{a.1}" }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json
new file mode 100644
index 000000000..8554a449d
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json
@@ -0,0 +1,5 @@
+{
+ "color": {
+ "value": "rgb(0, 0, 120)"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json
new file mode 100644
index 000000000..aac06ff82
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json
@@ -0,0 +1,5 @@
+{
+ "color": {
+ "value": "rgb(0, 120, 0)"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json
new file mode 100644
index 000000000..8569a6d3a
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json
@@ -0,0 +1,5 @@
+{
+ "color": {
+ "value": "rgb(50, 50, 250)"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json
new file mode 100644
index 000000000..3e9ac3d69
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json
@@ -0,0 +1,5 @@
+{
+ "color": {
+ "value": "rgb(50, 250, 50)"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/missing.json b/plugins/postcss-design-tokens/test/tokens/missing.json
new file mode 100644
index 000000000..e08a0b0bc
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/missing.json
@@ -0,0 +1,5 @@
+{
+ "a": {
+ "1": { "value": "{a.2}" }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json
new file mode 100644
index 000000000..0e36ff29e
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json
@@ -0,0 +1,5 @@
+{
+ "size": {
+ "value": "20px"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json
new file mode 100644
index 000000000..06069df89
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json
@@ -0,0 +1,5 @@
+{
+ "size": {
+ "value": "16px"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json
new file mode 100644
index 000000000..8102cac0c
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json
@@ -0,0 +1,5 @@
+{
+ "size": {
+ "value": "18px"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json
new file mode 100644
index 000000000..f3970cd27
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json
@@ -0,0 +1,5 @@
+{
+ "token": {
+ "value": 20
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json
new file mode 100644
index 000000000..22b517934
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json
@@ -0,0 +1,5 @@
+{
+ "token": {
+ "value": "foo"
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json
new file mode 100644
index 000000000..474f5a633
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json
@@ -0,0 +1,7 @@
+{
+ "token": {
+ "value": {
+ "an-object": true
+ }
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json
new file mode 100644
index 000000000..728e997aa
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json
@@ -0,0 +1,5 @@
+{
+ "token": {
+ "value": false
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json
new file mode 100644
index 000000000..adac6202e
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json
@@ -0,0 +1,5 @@
+{
+ "token": {
+ "value": null
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json
new file mode 100644
index 000000000..b16797c95
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json
@@ -0,0 +1,5 @@
+{
+ "token": {
+ "value": "\"foo\""
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json
new file mode 100644
index 000000000..f94530ca5
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json
@@ -0,0 +1,5 @@
+{
+ "token": {
+ "value": [20]
+ }
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-a.css b/plugins/postcss-design-tokens/test/value-parsing-a.css
new file mode 100644
index 000000000..7ee8b4fb8
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-a.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-a.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-a.expect.css b/plugins/postcss-design-tokens/test/value-parsing-a.expect.css
new file mode 100644
index 000000000..ed650bb67
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-a.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: 20;
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-b.css b/plugins/postcss-design-tokens/test/value-parsing-b.css
new file mode 100644
index 000000000..d29163c4a
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-b.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-b.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-b.expect.css b/plugins/postcss-design-tokens/test/value-parsing-b.expect.css
new file mode 100644
index 000000000..e98253051
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-b.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: foo;
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-c.css b/plugins/postcss-design-tokens/test/value-parsing-c.css
new file mode 100644
index 000000000..57ad788c7
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-c.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-c.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-c.expect.css b/plugins/postcss-design-tokens/test/value-parsing-c.expect.css
new file mode 100644
index 000000000..4ffb47baf
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-c.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-d.css b/plugins/postcss-design-tokens/test/value-parsing-d.css
new file mode 100644
index 000000000..5bcc9dcba
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-d.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-d.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-d.expect.css b/plugins/postcss-design-tokens/test/value-parsing-d.expect.css
new file mode 100644
index 000000000..4ffb47baf
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-d.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-e.css b/plugins/postcss-design-tokens/test/value-parsing-e.css
new file mode 100644
index 000000000..cabafcf23
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-e.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-e.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-e.expect.css b/plugins/postcss-design-tokens/test/value-parsing-e.expect.css
new file mode 100644
index 000000000..4ffb47baf
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-e.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-f.css b/plugins/postcss-design-tokens/test/value-parsing-f.css
new file mode 100644
index 000000000..34a46664b
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-f.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-f.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-f.expect.css b/plugins/postcss-design-tokens/test/value-parsing-f.expect.css
new file mode 100644
index 000000000..4c99d32ef
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-f.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: "foo";
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-g.css b/plugins/postcss-design-tokens/test/value-parsing-g.css
new file mode 100644
index 000000000..801cfb750
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-g.css
@@ -0,0 +1,5 @@
+@design-tokens url('./tokens/value-parsing-g.json') format('style-dictionary3');
+
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/test/value-parsing-g.expect.css b/plugins/postcss-design-tokens/test/value-parsing-g.expect.css
new file mode 100644
index 000000000..4ffb47baf
--- /dev/null
+++ b/plugins/postcss-design-tokens/test/value-parsing-g.expect.css
@@ -0,0 +1,3 @@
+.value-parsing {
+ order: design-token('token');
+}
diff --git a/plugins/postcss-design-tokens/tsconfig.json b/plugins/postcss-design-tokens/tsconfig.json
new file mode 100644
index 000000000..64a39afa7
--- /dev/null
+++ b/plugins/postcss-design-tokens/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": ".",
+ "module": "es2020",
+ "strict": true,
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/plugins/postcss-gradients-interpolation-method/README.md b/plugins/postcss-gradients-interpolation-method/README.md
index fbb284fd8..690e33012 100644
--- a/plugins/postcss-gradients-interpolation-method/README.md
+++ b/plugins/postcss-gradients-interpolation-method/README.md
@@ -1,9 +1,6 @@
# PostCSS Gradients Interpolation Method [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
[PostCSS Gradients Interpolation Method] lets you use different interpolation methods in CSS gradient functions following [CSS Specification].
diff --git a/plugins/postcss-stepped-value-functions/README.md b/plugins/postcss-stepped-value-functions/README.md
index aaa9a4dbd..3cd235169 100644
--- a/plugins/postcss-stepped-value-functions/README.md
+++ b/plugins/postcss-stepped-value-functions/README.md
@@ -1,9 +1,6 @@
# PostCSS Stepped Value Functions [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
[PostCSS Stepped Value Functions] lets you use `round`, `rem` and `mod` stepped value functions, following the [CSS Values 4].
diff --git a/plugins/postcss-trigonometric-functions/README.md b/plugins/postcss-trigonometric-functions/README.md
index 5b5973d2b..d52d95fc7 100644
--- a/plugins/postcss-trigonometric-functions/README.md
+++ b/plugins/postcss-trigonometric-functions/README.md
@@ -1,9 +1,6 @@
# PostCSS Trigonometric Functions [
][postcss]
-[
][npm-url]
-[
][css-url]
-[
][cli-url]
-[
][discord]
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
[PostCSS Trigonometric Functions] lets you use `sin`, `cos`, `tan`, `asin`, `acos`, `atan` and `atan2` to be able to compute trigonometric relationships following the [CSS Values 4] specification.