diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d98bf10d0..71d9340e2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -47,49 +47,24 @@ jobs:
with:
node-version: ${{ matrix.node }}
- - name: env
- run: |
- echo 'DIST_RESTORE_KEYS<> $GITHUB_ENV
- echo "$(git --no-pager log -9 --skip 1 --no-merges --pretty=format:'%H--test-dist-${{ matrix.node }}-${{ matrix.os }}')" >> $GITHUB_ENV
- echo 'EOF' >> $GITHUB_ENV
- echo "COMMIT_SHA=$(git --no-pager log -1 --no-merges --pretty=format:'%H')" >> $GITHUB_ENV
- echo "BUILD_AND_TEST_ALL_PACKAGES=$(echo ${{ github.event.inputs.run_index }})" >> $GITHUB_ENV
-
- - name: previous build artifacts cache
- uses: actions/cache@v3.0.11
- with:
- path: |
- .cached-commit
- cli/*/dist/**
- experimental/*/dist/**
- packages/*/dist/**
- plugin-packs/*/dist/**
- plugins/*/dist/**
- key: ${{ env.COMMIT_SHA }}--test-dist-${{ matrix.node }}-${{ matrix.os }}
- restore-keys: ${{ env.DIST_RESTORE_KEYS }}
-
- name: npm ci
run: |
npm ci --ignore-scripts
- - name: determine modified workspaces
- run: |
- echo "MODIFIED_WORKSPACES=$(node './.github/bin/modified-workspaces/log-modified-workspaces.mjs')" >> $GITHUB_ENV
-
# Build, lint and PostCSS Tape tests must all work and pass :
# - with exact dependencies from package-lock.json
# - without requiring postinstall scripts from dependencies to run
- name: build
run: |
- npm run build --if-present $MODIFIED_WORKSPACES
+ npm run build --if-present
- name: lint
- run: npm run lint --if-present $MODIFIED_WORKSPACES
+ run: npm run lint --if-present
if: matrix.is_base_node_version && matrix.is_base_os_version
# Basic tests
- name: test
- run: npm run test --if-present $MODIFIED_WORKSPACES
+ run: npm run test --if-present
env:
ENABLE_ANNOTATIONS_FOR_NODE: ${{ matrix.is_base_node_version }}
ENABLE_ANNOTATIONS_FOR_OS: ${{ matrix.is_base_os_version }}
@@ -99,14 +74,14 @@ jobs:
- name: test:cli
run: |
npm install --ignore-scripts
- npm run test:cli --if-present $MODIFIED_WORKSPACES
+ npm run test:cli --if-present
# Browser Tests
# running "npm ci" again, but allowing scripts so that Chrome is installed
- name: test:browser
run: |
npm ci
- npm run test:browser --if-present $MODIFIED_WORKSPACES
+ npm run test:browser --if-present
if: matrix.is_base_node_version && matrix.is_base_os_version
# E2E Tests
@@ -125,11 +100,5 @@ jobs:
if: matrix.is_base_node_version && matrix.is_base_os_version
- name: test:deno
- run: npm run test:deno --if-present $MODIFIED_WORKSPACES
+ run: npm run test:deno --if-present
if: matrix.is_base_node_version && matrix.is_base_os_version
-
- # record the current commit for the cache at the end of the job
- # must be the last step
- - name: record current commit
- run: |
- echo "$(git --no-pager log -1 --no-merges --pretty=format:'%H')" > .cached-commit
diff --git a/package-lock.json b/package-lock.json
index e14d9cce1..8ba40711a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,9 @@
"version": "1.0.0",
"license": "CC0-1.0",
"workspaces": [
+ "packages/css-tokenizer",
+ "packages/css-parser-algorithms",
+ "packages/media-query-list-parser",
"packages/*",
"plugins/postcss-progressive-custom-properties",
"plugins/*",
@@ -115,14 +118,14 @@
"postcss-selector-parser": "^6.0.10"
},
"engines": {
- "node": "^12 || ^14 || >=16"
+ "node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
- "postcss": "^8.2"
+ "postcss": "^8.4"
}
},
"node_modules/@ampproject/remapping": {
@@ -1812,6 +1815,14 @@
"resolved": "experimental/css-has-pseudo",
"link": true
},
+ "node_modules/@csstools/css-parser-algorithms": {
+ "resolved": "packages/css-parser-algorithms",
+ "link": true
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "resolved": "packages/css-tokenizer",
+ "link": true
+ },
"node_modules/@csstools/csstools-cli": {
"resolved": "cli/csstools-cli",
"link": true
@@ -1820,6 +1831,10 @@
"resolved": "packages/generate-test-cases",
"link": true
},
+ "node_modules/@csstools/media-query-list-parser": {
+ "resolved": "packages/media-query-list-parser",
+ "link": true
+ },
"node_modules/@csstools/postcss-base-plugin": {
"resolved": "plugins/postcss-base-plugin",
"link": true
@@ -6911,6 +6926,49 @@
"url": "https://opencollective.com/csstools"
}
},
+ "packages/css-parser": {
+ "name": "@csstools/css-parser",
+ "version": "1.0.0",
+ "extraneous": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ },
+ "packages/css-parser-algorithms": {
+ "name": "@csstools/css-parser-algorithms",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ },
+ "packages/css-tokenizer": {
+ "name": "@csstools/css-tokenizer",
+ "version": "1.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ },
"packages/generate-test-cases": {
"name": "@csstools/generate-test-cases",
"version": "1.0.0",
@@ -6926,6 +6984,35 @@
"url": "https://opencollective.com/csstools"
}
},
+ "packages/media-query-list-parser": {
+ "name": "@csstools/media-query-list-parser",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^1.0.0",
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ },
+ "packages/postcss-media-query-list-parser": {
+ "name": "@csstools/postcss-media-query-list-parser",
+ "version": "1.0.0",
+ "extraneous": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ },
"packages/postcss-tape": {
"name": "@csstools/postcss-tape",
"version": "1.0.0",
@@ -6962,6 +7049,19 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "packages/virtual-media": {
+ "name": "@csstools/virtual-media",
+ "version": "1.0.0",
+ "extraneous": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ },
"plugin-packs/postcss-preset-env": {
"version": "8.0.0-alpha.0",
"license": "CC0-1.0",
@@ -7243,7 +7343,8 @@
"version": "8.0.2",
"license": "MIT",
"dependencies": {
- "postcss-value-parser": "^4.2.0"
+ "@csstools/css-tokenizer": "^1.0.0",
+ "@csstools/media-query-list-parser": "^1.0.0"
},
"engines": {
"node": "^14 || ^16 || >=18"
@@ -7731,14 +7832,14 @@
"postcss-selector-parser": "^6.0.10"
},
"engines": {
- "node": "^12 || ^14 || >=16"
+ "node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
- "postcss": "^8.2"
+ "postcss": "^8.4"
}
},
"plugins/postcss-selector-not": {
@@ -9003,6 +9104,15 @@
"version": "file:experimental/css-has-pseudo",
"requires": {}
},
+ "@csstools/css-parser-algorithms": {
+ "version": "file:packages/css-parser-algorithms",
+ "requires": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ }
+ },
+ "@csstools/css-tokenizer": {
+ "version": "file:packages/css-tokenizer"
+ },
"@csstools/csstools-cli": {
"version": "file:cli/csstools-cli",
"requires": {
@@ -9051,6 +9161,13 @@
"mdn-data": "^2.0.28"
}
},
+ "@csstools/media-query-list-parser": {
+ "version": "file:packages/media-query-list-parser",
+ "requires": {
+ "@csstools/css-parser-algorithms": "^1.0.0",
+ "@csstools/css-tokenizer": "^1.0.0"
+ }
+ },
"@csstools/postcss-base-plugin": {
"version": "file:plugins/postcss-base-plugin",
"requires": {}
@@ -11703,7 +11820,8 @@
"postcss-custom-media": {
"version": "file:plugins/postcss-custom-media",
"requires": {
- "postcss-value-parser": "^4.2.0"
+ "@csstools/css-tokenizer": "^1.0.0",
+ "@csstools/media-query-list-parser": "^1.0.0"
}
},
"postcss-custom-properties": {
diff --git a/package.json b/package.json
index 63de9f4f5..92b4a687e 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
"node": "^14 || ^16 || >=18"
},
"workspaces": [
+ "packages/css-tokenizer",
+ "packages/css-parser-algorithms",
+ "packages/media-query-list-parser",
"packages/*",
"plugins/postcss-progressive-custom-properties",
"plugins/*",
diff --git a/packages/css-parser-algorithms/.gitignore b/packages/css-parser-algorithms/.gitignore
new file mode 100644
index 000000000..f548255b0
--- /dev/null
+++ b/packages/css-parser-algorithms/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+*.result.json
+dist/*
diff --git a/packages/css-parser-algorithms/.nvmrc b/packages/css-parser-algorithms/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/packages/css-parser-algorithms/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/packages/css-parser-algorithms/CHANGELOG.md b/packages/css-parser-algorithms/CHANGELOG.md
new file mode 100644
index 000000000..b0ff6b082
--- /dev/null
+++ b/packages/css-parser-algorithms/CHANGELOG.md
@@ -0,0 +1,3 @@
+### 1.0.0
+
+- Initial version
diff --git a/packages/css-parser-algorithms/LICENSE.md b/packages/css-parser-algorithms/LICENSE.md
new file mode 100644
index 000000000..af5411fa2
--- /dev/null
+++ b/packages/css-parser-algorithms/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/css-parser-algorithms/README.md b/packages/css-parser-algorithms/README.md
new file mode 100644
index 000000000..aeda87344
--- /dev/null
+++ b/packages/css-parser-algorithms/README.md
@@ -0,0 +1,120 @@
+# CSS Parser Algorithms
+
+[
][npm-url]
+[
][cli-url]
+[
][discord]
+
+Implemented from : https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/
+
+## Usage
+
+Add [CSS Parser Algorithms] to your project:
+
+```bash
+npm install postcss @csstools/css-parser-algorithms --save-dev
+```
+
+[CSS Parser Algorithms] only accepts tokenized CSS.
+It must be used together with `@csstools/css-tokenizer`.
+
+
+```js
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+import { parseComponentValue } from '@csstools/css-parser-algorithms';
+
+const myCSS = `@media only screen and (min-width: 768rem) {
+ .foo {
+ content: 'Some content!' !important;
+ }
+}
+`;
+
+const t = tokenizer({
+ css: myCSS,
+});
+
+const tokens = [];
+
+{
+ while (!t.endOfFile()) {
+ tokens.push(t.nextToken());
+ }
+
+ tokens.push(t.nextToken()); // EOF-token
+}
+
+const options = {
+ onParseError: ((err) => {
+ throw new Error(JSON.stringify(err));
+ }),
+};
+
+const result = parseComponentValue(tokens, options);
+
+console.log(result);
+```
+
+### Available functions
+
+- [`parseComponentValue`](https://www.w3.org/TR/css-syntax-3/#parse-component-value)
+- [`parseListOfComponentValues`](https://www.w3.org/TR/css-syntax-3/#parse-list-of-component-values)
+- [`parseCommaSeparatedListOfComponentValues`](https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values)
+
+### Utilities
+
+#### `gatherNodeAncestry`
+
+The AST does not expose the entire ancestry of each node.
+The walker methods do provide access to the current parent, but also not the entire ancestry.
+
+To gather the entire ancestry for a a given sub tree of the AST you can use `gatherNodeAncestry`.
+The result is a `Map` with the child nodes as keys and the parents as values.
+This allows you to lookup any ancestor of any node.
+
+```css
+import { parseComponentValue } from '@csstools/css-parser-algorithms';
+
+const result = parseComponentValue(tokens, options);
+const ancestry = gatherNodeAncestry(result);
+```
+
+### Options
+
+```ts
+{
+ onParseError?: (error: ParserError) => void
+}
+```
+
+#### `onParseError`
+
+The parser algorithms are forgiving and won't stop when a parse error is encountered.
+Parse errors also aren't tokens.
+
+To receive parsing error information you can set a callback.
+
+Parser errors will try to inform you about the point in the parsing logic the error happened.
+This tells you the kind of error.
+
+`start` and `end` are the location in your CSS source code.
+
+`UnclosedSimpleBlockNode` and `UnclosedFunctionNode` entries will be added to the output.
+This allows you to recover from errors and/or show warnings.
+
+## Goals and non-goals
+
+Things this package aims to be:
+- specification compliant CSS parser
+- a reliable low level package to be used in CSS sub-grammars
+
+What it is not:
+- opinionated
+- fast
+- small
+- a replacement for PostCSS (PostCSS is fast and also an ecosystem)
+
+[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/css-parser-algorithms
+
+[CSS Parser Algorithms]: https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms
diff --git a/packages/css-parser-algorithms/package.json b/packages/css-parser-algorithms/package.json
new file mode 100644
index 000000000..35048273d
--- /dev/null
+++ b/packages/css-parser-algorithms/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@csstools/css-parser-algorithms",
+ "description": "Algorithms to help you parse CSS from an array of tokens.",
+ "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": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "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": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.js",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"",
+ "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",
+ "stryker": "stryker run --logLevel error",
+ "test": "node ./test/test.mjs",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node ./test/test.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/css-parser-algorithms"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "parser"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
new file mode 100644
index 000000000..726b51843
--- /dev/null
+++ b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
@@ -0,0 +1,621 @@
+import { CSSToken, mirrorVariantType, stringify, TokenType, isToken, TokenFunction } from '@csstools/css-tokenizer';
+import { Context } from '../interfaces/context';
+import { ComponentValueType } from '../util/component-value-type';
+
+export type ContainerNode = FunctionNode | SimpleBlockNode;
+
+export type ComponentValue = FunctionNode | SimpleBlockNode | WhitespaceNode | CommentNode | TokenNode | UnclosedSimpleBlockNode | UnclosedFunctionNode;
+
+// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
+export function consumeComponentValue(ctx: Context, tokens: Array): { advance: number, node: ComponentValue } {
+ const token = tokens[0];
+ if (
+ token[0] === TokenType.OpenParen ||
+ token[0] === TokenType.OpenCurly ||
+ token[0] === TokenType.OpenSquare
+ ) {
+ const r = consumeSimpleBlock(ctx, tokens);
+ return {
+ advance: r.advance,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Function) {
+ const r = consumeFunction(ctx, tokens);
+ return {
+ advance: r.advance,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Whitespace) {
+ const r = consumeWhitespace(ctx, tokens);
+ return {
+ advance: r.advance,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Comment) {
+ const r = consumeComment(ctx, tokens);
+ return {
+ advance: r.advance,
+ node: r.node,
+ };
+ }
+
+ return {
+ advance: 1,
+ node: new TokenNode(token),
+ };
+}
+
+export class FunctionNode {
+ type: ComponentValueType = ComponentValueType.Function;
+
+ name: TokenFunction;
+ endToken: CSSToken;
+ value: Array;
+
+ constructor(name: TokenFunction, endToken: CSSToken, value: Array) {
+ this.name = name;
+ this.endToken = endToken;
+ this.value = value;
+ }
+
+ nameTokenValue(): string {
+ return this.name[4].value;
+ }
+
+ tokens(): Array {
+ return [
+ this.name,
+ ...this.value.flatMap((x) => {
+ if (isToken(x)) {
+ return x;
+ }
+
+ return x.tokens();
+ }),
+ this.endToken,
+ ];
+ }
+
+ toString(): string {
+ const valueString = this.value.map((x) => {
+ if (isToken(x)) {
+ return stringify(x);
+ }
+
+ return x.toString();
+ }).join('');
+
+ return stringify(this.name) + valueString + stringify(this.endToken);
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ return this.value.indexOf(item);
+ }
+
+ at(index: number | string) {
+ if (typeof index === 'number') {
+ if (index < 0) {
+ index = this.value.length + index;
+ }
+ return this.value[index];
+ }
+ }
+
+ walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number | string) => boolean | void) {
+ let aborted = false;
+
+ this.value.forEach((child, index) => {
+ if (aborted) {
+ return;
+ }
+
+ if (cb({ node: child, parent: this }, index) === false) {
+ aborted = true;
+ return;
+ }
+
+ if ('walk' in child) {
+ if (child.walk(cb) === false) {
+ aborted = true;
+ return;
+ }
+ }
+ });
+
+ if (aborted) {
+ return false;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.nameTokenValue(),
+ tokens: this.tokens(),
+ value: this.value.map((x) => x.toJSON()),
+ };
+ }
+
+ isFunctionNode(): this is FunctionNode {
+ return FunctionNode.isFunctionNode(this);
+ }
+
+ static isFunctionNode(x: unknown): x is FunctionNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof FunctionNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.Function;
+ }
+}
+
+// https://www.w3.org/TR/css-syntax-3/#consume-function
+export function consumeFunction(ctx: Context, tokens: Array): { advance: number, node: FunctionNode | UnclosedFunctionNode } {
+ const value: Array = [];
+
+ let i = 1;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = tokens[i];
+ if (!token || token[0] === TokenType.EOF) {
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming a function.',
+ start: tokens[0][2],
+ end: tokens[tokens.length - 1][3],
+ state: [
+ '5.4.9. Consume a function',
+ 'Unexpected EOF',
+ ],
+ });
+
+ return {
+ advance: tokens.length,
+ node: new UnclosedFunctionNode(tokens),
+ };
+ }
+
+ if (token[0] === TokenType.CloseParen) {
+ return {
+ advance: i + 1,
+ node: new FunctionNode(tokens[0] as TokenFunction, token, value),
+ };
+ }
+
+ if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) {
+ const result = consumeAllCommentsAndWhitespace(ctx, tokens.slice(i));
+ i += result.advance;
+ value.push(...result.nodes);
+ continue;
+ }
+
+ const result = consumeComponentValue(ctx, tokens.slice(i));
+ i += result.advance;
+ value.push(result.node);
+ }
+}
+
+export class SimpleBlockNode {
+ type: ComponentValueType = ComponentValueType.SimpleBlock;
+
+ startToken: CSSToken;
+ endToken: CSSToken;
+ value: Array;
+
+ constructor(startToken: CSSToken, endToken: CSSToken, value: Array) {
+ this.startToken = startToken;
+ this.endToken = endToken;
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return [
+ this.startToken,
+ ...this.value.flatMap((x) => {
+ if (isToken(x)) {
+ return x;
+ }
+
+ return x.tokens();
+ }),
+ this.endToken,
+ ];
+ }
+
+ toString(): string {
+ const valueString = this.value.map((x) => {
+ if (isToken(x)) {
+ return stringify(x);
+ }
+
+ return x.toString();
+ }).join('');
+
+ return stringify(this.startToken) + valueString + stringify(this.endToken);
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ return this.value.indexOf(item);
+ }
+
+ at(index: number | string) {
+ if (typeof index === 'number') {
+ if (index < 0) {
+ index = this.value.length + index;
+ }
+ return this.value[index];
+ }
+ }
+
+ walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number | string) => boolean | void) {
+ let aborted = false;
+
+ this.value.forEach((child, index) => {
+ if (aborted) {
+ return;
+ }
+
+ if (cb({ node: child, parent: this }, index) === false) {
+ aborted = true;
+ return;
+ }
+
+ if ('walk' in child) {
+ if (child.walk(cb) === false) {
+ aborted = true;
+ return;
+ }
+ }
+ });
+
+ if (aborted) {
+ return false;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ startToken: this.startToken,
+ tokens: this.tokens(),
+ value: this.value.map((x) => x.toJSON()),
+ };
+ }
+
+ isSimpleBlockNode(): this is SimpleBlockNode {
+ return SimpleBlockNode.isSimpleBlockNode(this);
+ }
+
+ static isSimpleBlockNode(x: unknown): x is SimpleBlockNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof SimpleBlockNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.SimpleBlock;
+ }
+}
+
+/** https://www.w3.org/TR/css-syntax-3/#consume-simple-block */
+export function consumeSimpleBlock(ctx: Context, tokens: Array): { advance: number, node: SimpleBlockNode | UnclosedSimpleBlockNode } {
+ const endingTokenType = mirrorVariantType(tokens[0][0]);
+ if (!endingTokenType) {
+ throw new Error('Failed to parse, a mirror variant must exist for all block open tokens.');
+ }
+
+ const value: Array = [];
+
+ let i = 1;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = tokens[i];
+ if (!token || token[0] === TokenType.EOF) {
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming a simple block.',
+ start: tokens[0][2],
+ end: tokens[tokens.length - 1][3],
+ state: [
+ '5.4.8. Consume a simple block',
+ 'Unexpected EOF',
+ ],
+ });
+
+ return {
+ advance: tokens.length,
+ node: new UnclosedSimpleBlockNode(tokens),
+ };
+ }
+
+ if (token[0] === endingTokenType) {
+ return {
+ advance: i + 1,
+ node: new SimpleBlockNode(tokens[0], token, value),
+ };
+ }
+
+ if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) {
+ const result = consumeAllCommentsAndWhitespace(ctx, tokens.slice(i));
+ i += result.advance;
+ value.push(...result.nodes);
+ continue;
+ }
+
+ const result = consumeComponentValue(ctx, tokens.slice(i));
+ i += result.advance;
+ value.push(result.node);
+ }
+}
+
+export class WhitespaceNode {
+ type: ComponentValueType = ComponentValueType.Whitespace;
+
+ value: Array;
+
+ constructor(value: Array) {
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return this.value;
+ }
+
+ toString(): string {
+ return stringify(...this.value);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ tokens: this.tokens(),
+ };
+ }
+
+ isWhitespaceNode(): this is WhitespaceNode {
+ return WhitespaceNode.isWhitespaceNode(this);
+ }
+
+ static isWhitespaceNode(x: unknown): x is WhitespaceNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof WhitespaceNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.Whitespace;
+ }
+}
+
+export function consumeWhitespace(ctx: Context, tokens: Array): { advance: number, node: WhitespaceNode } {
+ let i = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = tokens[i];
+ if (token[0] !== TokenType.Whitespace) {
+ return {
+ advance: i,
+ node: new WhitespaceNode(tokens.slice(0, i)),
+ };
+ }
+
+ i++;
+ }
+}
+
+export class CommentNode {
+ type: ComponentValueType = ComponentValueType.Comment;
+
+ value: CSSToken;
+
+ constructor(value: CSSToken) {
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return [
+ this.value,
+ ];
+ }
+
+ toString(): string {
+ return stringify(this.value);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ tokens: this.tokens(),
+ };
+ }
+
+ isCommentNode(): this is CommentNode {
+ return CommentNode.isCommentNode(this);
+ }
+
+ static isCommentNode(x: unknown): x is CommentNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof CommentNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.Comment;
+ }
+}
+
+export function consumeComment(ctx: Context, tokens: Array): { advance: number, node: CommentNode } {
+ return {
+ advance: 1,
+ node: new CommentNode(tokens[0]),
+ };
+}
+
+export function consumeAllCommentsAndWhitespace(ctx: Context, tokens: Array): { advance: number, nodes: Array } {
+ const nodes: Array = [];
+
+ let i = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (tokens[i][0] === TokenType.Whitespace) {
+ const result = consumeWhitespace(ctx, tokens.slice(i));
+ i += result.advance;
+ nodes.push(result.node);
+ continue;
+ }
+
+ if (tokens[i][0] === TokenType.Comment) {
+ nodes.push(new CommentNode(tokens[i]));
+ i++;
+ continue;
+ }
+
+ return {
+ advance: i,
+ nodes: nodes,
+ };
+ }
+}
+
+export class TokenNode {
+ type: ComponentValueType = ComponentValueType.Token;
+
+ value: CSSToken;
+
+ constructor(value: CSSToken) {
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return [
+ this.value,
+ ];
+ }
+
+ toString(): string {
+ return stringify(this.value);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ tokens: this.tokens(),
+ };
+ }
+
+ isTokenNode(): this is TokenNode {
+ return TokenNode.isTokenNode(this);
+ }
+
+ static isTokenNode(x: unknown): x is TokenNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof TokenNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.Token;
+ }
+}
+
+export class UnclosedFunctionNode {
+ type: ComponentValueType = ComponentValueType.UnclosedFunction;
+
+ value: Array;
+
+ constructor(value: Array) {
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return this.value;
+ }
+
+ toString(): string {
+ return stringify(...this.value);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ tokens: this.tokens(),
+ };
+ }
+
+ isUnclosedFunctionNode(): this is UnclosedFunctionNode {
+ return UnclosedFunctionNode.isUnclosedFunctionNode(this);
+ }
+
+ static isUnclosedFunctionNode(x: unknown): x is UnclosedFunctionNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof UnclosedFunctionNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.UnclosedFunction;
+ }
+}
+
+export class UnclosedSimpleBlockNode {
+ type: ComponentValueType = ComponentValueType.UnclosedSimpleBlock;
+
+ value: Array;
+
+ constructor(value: Array) {
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return this.value;
+ }
+
+ toString(): string {
+ return stringify(...this.value);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ tokens: this.tokens(),
+ };
+ }
+
+ isUnclosedSimpleBlockNode(): this is UnclosedSimpleBlockNode {
+ return UnclosedSimpleBlockNode.isUnclosedSimpleBlockNode(this);
+ }
+
+ static isUnclosedSimpleBlockNode(x: unknown): x is UnclosedSimpleBlockNode {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof UnclosedSimpleBlockNode)) {
+ return false;
+ }
+
+ return x.type === ComponentValueType.UnclosedSimpleBlock;
+ }
+}
diff --git a/packages/css-parser-algorithms/src/index.ts b/packages/css-parser-algorithms/src/index.ts
new file mode 100644
index 000000000..5bb92c097
--- /dev/null
+++ b/packages/css-parser-algorithms/src/index.ts
@@ -0,0 +1,16 @@
+export * from './consume/consume-component-block-function';
+export { parseComponentValue } from './parse/parse-component-value';
+export { parseListOfComponentValues } from './parse/parse-list-of-component-values';
+export { parseCommaSeparatedListOfComponentValues } from './parse/parse-comma-separated-list-of-component-values';
+export { gatherNodeAncestry } from './util/node-ancestry';
+export { ParserError } from './interfaces/error';
+export { ComponentValueType } from './util/component-value-type';
+export {
+ isCommentNode,
+ isFunctionNode,
+ isSimpleBlockNode,
+ isTokenNode,
+ isUnclosedFunctionNode,
+ isUnclosedSimpleBlockNode,
+ isWhitespaceNode,
+} from './util/type-predicates';
diff --git a/packages/css-parser-algorithms/src/interfaces/context.ts b/packages/css-parser-algorithms/src/interfaces/context.ts
new file mode 100644
index 000000000..27eb77de6
--- /dev/null
+++ b/packages/css-parser-algorithms/src/interfaces/context.ts
@@ -0,0 +1,5 @@
+import { ParserError } from './error';
+
+export type Context = {
+ onParseError: (error: ParserError) => void
+}
diff --git a/packages/css-parser-algorithms/src/interfaces/error.ts b/packages/css-parser-algorithms/src/interfaces/error.ts
new file mode 100644
index 000000000..e8c677745
--- /dev/null
+++ b/packages/css-parser-algorithms/src/interfaces/error.ts
@@ -0,0 +1,6 @@
+export type ParserError = {
+ message: string,
+ start: number,
+ end: number,
+ state: Array
+}
diff --git a/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts b/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts
new file mode 100644
index 000000000..a4136fcb8
--- /dev/null
+++ b/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts
@@ -0,0 +1,59 @@
+import { ParserError } from '../interfaces/error';
+import { CSSToken, TokenType } from '@csstools/css-tokenizer';
+import { ComponentValue, consumeComponentValue } from '../consume/consume-component-block-function';
+
+export function parseCommaSeparatedListOfComponentValues(tokens: Array, options?: { onParseError?: (error: ParserError) => void }) {
+ const ctx = {
+ onParseError: options?.onParseError ?? (() => { /* noop */ }),
+ };
+
+ const tokensCopy = [
+ ...tokens,
+ ];
+
+ if (tokens.length === 0) {
+ return [];
+ }
+
+ // We expect the last token to be an EOF token.
+ // Passing slices of tokens to this function can easily cause the EOF token to be missing.
+ if (tokensCopy[tokensCopy.length - 1][0] !== TokenType.EOF) {
+ tokensCopy.push([
+ TokenType.EOF,
+ '',
+ tokensCopy[tokensCopy.length - 1][2],
+ tokensCopy[tokensCopy.length - 1][3],
+ undefined,
+ ]);
+ }
+
+ const listOfCvls: Array> = [];
+ let list: Array = [];
+
+ let i = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (!tokensCopy[i] || tokensCopy[i][0] === TokenType.EOF) {
+ if (list.length) {
+ listOfCvls.push(list);
+ }
+
+ return listOfCvls;
+ }
+
+ if (tokensCopy[i][0] === TokenType.Comma) {
+ if (list.length) {
+ listOfCvls.push(list);
+ }
+
+ list = [];
+ i++;
+ continue;
+ }
+
+ const result = consumeComponentValue(ctx, tokens.slice(i));
+ list.push(result.node);
+ i += result.advance;
+ }
+}
diff --git a/packages/css-parser-algorithms/src/parse/parse-component-value.ts b/packages/css-parser-algorithms/src/parse/parse-component-value.ts
new file mode 100644
index 000000000..1fbb8ca01
--- /dev/null
+++ b/packages/css-parser-algorithms/src/parse/parse-component-value.ts
@@ -0,0 +1,40 @@
+import { ParserError } from '../interfaces/error';
+import { CSSToken, TokenType } from '@csstools/css-tokenizer';
+import { consumeComponentValue } from '../consume/consume-component-block-function';
+
+export function parseComponentValue(tokens: Array, options?: { onParseError?: (error: ParserError) => void }) {
+ const ctx = {
+ onParseError: options?.onParseError ?? (() => { /* noop */ }),
+ };
+
+ const tokensCopy = [
+ ...tokens,
+ ];
+
+ // We expect the last token to be an EOF token.
+ // Passing slices of tokens to this function can easily cause the EOF token to be missing.
+ if (tokensCopy[tokensCopy.length - 1][0] !== TokenType.EOF) {
+ tokensCopy.push([
+ TokenType.EOF,
+ '',
+ tokensCopy[tokensCopy.length - 1][2],
+ tokensCopy[tokensCopy.length - 1][3],
+ undefined,
+ ]);
+ }
+
+ const result = consumeComponentValue(ctx, tokensCopy);
+ if (tokensCopy[Math.min(result.advance, tokensCopy.length - 1)][0] === TokenType.EOF) {
+ return result.node;
+ }
+
+ ctx.onParseError({
+ message: 'Expected EOF after parsing a component value.',
+ start: tokens[0][2],
+ end: tokens[tokens.length - 1][3],
+ state: [
+ '5.3.9. Parse a component value',
+ 'Expected EOF',
+ ],
+ });
+}
diff --git a/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts b/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts
new file mode 100644
index 000000000..833a5d17a
--- /dev/null
+++ b/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts
@@ -0,0 +1,40 @@
+import { ParserError } from '../interfaces/error';
+import { CSSToken, TokenType } from '@csstools/css-tokenizer';
+import { ComponentValue, consumeComponentValue } from '../consume/consume-component-block-function';
+
+export function parseListOfComponentValues(tokens: Array, options?: { onParseError?: (error: ParserError) => void }) {
+ const ctx = {
+ onParseError: options?.onParseError ?? (() => { /* noop */ }),
+ };
+
+ const tokensCopy = [
+ ...tokens,
+ ];
+
+ // We expect the last token to be an EOF token.
+ // Passing slices of tokens to this function can easily cause the EOF token to be missing.
+ if (tokensCopy[tokensCopy.length - 1][0] !== TokenType.EOF) {
+ tokensCopy.push([
+ TokenType.EOF,
+ '',
+ tokensCopy[tokensCopy.length - 1][2],
+ tokensCopy[tokensCopy.length - 1][3],
+ undefined,
+ ]);
+ }
+
+ const list: Array = [];
+
+ let i = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (!tokensCopy[i] || tokensCopy[i][0] === TokenType.EOF) {
+ return list;
+ }
+
+ const result = consumeComponentValue(ctx, tokensCopy.slice(i));
+ list.push(result.node);
+ i += result.advance;
+ }
+}
diff --git a/packages/css-parser-algorithms/src/util/component-value-type.ts b/packages/css-parser-algorithms/src/util/component-value-type.ts
new file mode 100644
index 000000000..228ae5fc7
--- /dev/null
+++ b/packages/css-parser-algorithms/src/util/component-value-type.ts
@@ -0,0 +1,9 @@
+export enum ComponentValueType {
+ Function = 'function',
+ SimpleBlock = 'simple-block',
+ Whitespace = 'whitespace',
+ Comment = 'comment',
+ Token = 'token',
+ UnclosedFunction = 'unclosed-function',
+ UnclosedSimpleBlock = 'unclosed-simple-block'
+}
diff --git a/packages/css-parser-algorithms/src/util/node-ancestry.ts b/packages/css-parser-algorithms/src/util/node-ancestry.ts
new file mode 100644
index 000000000..6a908ee6e
--- /dev/null
+++ b/packages/css-parser-algorithms/src/util/node-ancestry.ts
@@ -0,0 +1,19 @@
+export interface walkable {
+ walk(cb: (entry: { node: Array | unknown, parent: unknown }, index: number | string) => boolean | void)
+}
+
+export function gatherNodeAncestry(node: walkable) {
+ const ancestry: Map = new Map();
+
+ node.walk((entry) => {
+ if (Array.isArray(entry.node)) {
+ entry.node.forEach((x) => {
+ ancestry.set(x, entry.parent);
+ });
+ } else {
+ ancestry.set(entry.node, entry.parent);
+ }
+ });
+
+ return ancestry;
+}
diff --git a/packages/css-parser-algorithms/src/util/type-predicates.ts b/packages/css-parser-algorithms/src/util/type-predicates.ts
new file mode 100644
index 000000000..a5cb96eda
--- /dev/null
+++ b/packages/css-parser-algorithms/src/util/type-predicates.ts
@@ -0,0 +1,29 @@
+import { CommentNode, FunctionNode, SimpleBlockNode, TokenNode, UnclosedFunctionNode, UnclosedSimpleBlockNode, WhitespaceNode } from '../consume/consume-component-block-function';
+
+export function isSimpleBlockNode(x: unknown): x is SimpleBlockNode {
+ return SimpleBlockNode.isSimpleBlockNode(x);
+}
+
+export function isFunctionNode(x: unknown): x is FunctionNode {
+ return FunctionNode.isFunctionNode(x);
+}
+
+export function isUnclosedSimpleBlockNode(x: unknown): x is UnclosedSimpleBlockNode {
+ return UnclosedSimpleBlockNode.isUnclosedSimpleBlockNode(x);
+}
+
+export function isUnclosedFunctionNode(x: unknown): x is UnclosedFunctionNode {
+ return UnclosedFunctionNode.isUnclosedFunctionNode(x);
+}
+
+export function isWhitespaceNode(x: unknown): x is WhitespaceNode {
+ return WhitespaceNode.isWhitespaceNode(x);
+}
+
+export function isCommentNode(x: unknown): x is CommentNode {
+ return CommentNode.isCommentNode(x);
+}
+
+export function isTokenNode(x: unknown): x is TokenNode {
+ return TokenNode.isTokenNode(x);
+}
diff --git a/packages/css-parser-algorithms/stryker.conf.json b/packages/css-parser-algorithms/stryker.conf.json
new file mode 100644
index 000000000..015ebbb73
--- /dev/null
+++ b/packages/css-parser-algorithms/stryker.conf.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
+ "mutate": [
+ "src/**/*.ts"
+ ],
+ "buildCommand": "npm run build",
+ "testRunner": "command",
+ "coverageAnalysis": "perTest",
+ "tempDirName": "../../.stryker-tmp",
+ "commandRunner": {
+ "command": "npm run test"
+ },
+ "thresholds": {
+ "high": 100,
+ "low": 100,
+ "break": 100
+ },
+ "inPlace": true
+}
diff --git a/packages/css-parser-algorithms/test/_import.mjs b/packages/css-parser-algorithms/test/_import.mjs
new file mode 100644
index 000000000..44050c1ac
--- /dev/null
+++ b/packages/css-parser-algorithms/test/_import.mjs
@@ -0,0 +1,11 @@
+import { parseComponentValue } from '@csstools/css-parser-algorithms';
+import { TokenType } from '@csstools/css-tokenizer';
+
+parseComponentValue([
+ [
+ TokenType.Ident, 'any', 0, 0, undefined,
+ ],
+ [
+ TokenType.EOF, '', 0, 0, undefined,
+ ],
+]);
diff --git a/packages/css-parser-algorithms/test/_require.cjs b/packages/css-parser-algorithms/test/_require.cjs
new file mode 100644
index 000000000..1e519d1d4
--- /dev/null
+++ b/packages/css-parser-algorithms/test/_require.cjs
@@ -0,0 +1,11 @@
+const { parseComponentValue } = require('@csstools/css-parser-algorithms');
+const { TokenType } = require('@csstools/css-tokenizer');
+
+parseComponentValue([
+ [
+ TokenType.Ident, 'any', 0, 0, undefined,
+ ],
+ [
+ TokenType.EOF, '', 0, 0, undefined,
+ ],
+]);
diff --git a/packages/css-parser-algorithms/test/cases/media-not/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/media-not/0001.list-comma.expect.json
new file mode 100644
index 000000000..33eb1719a
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/media-not/0001.list-comma.expect.json
@@ -0,0 +1,81 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 5,
+ 9,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 5,
+ 9,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/media-not/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/media-not/0001.list-space.expect.json
new file mode 100644
index 000000000..88ce51c60
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/media-not/0001.list-space.expect.json
@@ -0,0 +1,79 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 5,
+ 9,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 5,
+ 9,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/media-not/0001.mjs b/packages/css-parser-algorithms/test/cases/media-not/0001.mjs
new file mode 100644
index 000000000..5eb55f343
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/media-not/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not (color)',
+ 'media-not/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-comma.expect.json
new file mode 100644
index 000000000..7afd42c4d
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-comma.expect.json
@@ -0,0 +1,55 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-space.expect.json
new file mode 100644
index 000000000..5c51b53b7
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-space.expect.json
@@ -0,0 +1,53 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0001.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.mjs
new file mode 100644
index 000000000..2686abd60
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color)',
+ 'mf-boolean/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-comma.expect.json
new file mode 100644
index 000000000..031cdcd31
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-comma.expect.json
@@ -0,0 +1,117 @@
+[
+ [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 31,
+ 35,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 36,
+ 50,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 51,
+ 51,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 31,
+ 35,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 3 */",
+ 36,
+ 50,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 4 */",
+ 52,
+ 66,
+ null
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-space.expect.json
new file mode 100644
index 000000000..c3cc37c5c
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-space.expect.json
@@ -0,0 +1,115 @@
+[
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 31,
+ 35,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 36,
+ 50,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 51,
+ 51,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 31,
+ 35,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 3 */",
+ 36,
+ 50,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 4 */",
+ 52,
+ 66,
+ null
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0002.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.mjs
new file mode 100644
index 000000000..f8266d441
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '/* comment 1 */(/* comment 2 */color/* comment 3 */)/* comment 4 */',
+ 'mf-boolean/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-comma.expect.json
new file mode 100644
index 000000000..ea4485beb
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-comma.expect.json
@@ -0,0 +1,55 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "true",
+ 1,
+ 4,
+ {
+ "value": "true"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "true",
+ 1,
+ 4,
+ {
+ "value": "true"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-space.expect.json
new file mode 100644
index 000000000..72c7af5f3
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-space.expect.json
@@ -0,0 +1,53 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "true",
+ 1,
+ 4,
+ {
+ "value": "true"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "true",
+ 1,
+ 4,
+ {
+ "value": "true"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0003.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.mjs
new file mode 100644
index 000000000..c0c9f5645
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(true)',
+ 'mf-boolean/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-comma.expect.json
new file mode 100644
index 000000000..2c9b6e948
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-comma.expect.json
@@ -0,0 +1,55 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "false",
+ 1,
+ 5,
+ {
+ "value": "false"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "false",
+ 1,
+ 5,
+ {
+ "value": "false"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-space.expect.json
new file mode 100644
index 000000000..8d6e7f3be
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-space.expect.json
@@ -0,0 +1,53 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "false",
+ 1,
+ 5,
+ {
+ "value": "false"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "false",
+ 1,
+ 5,
+ {
+ "value": "false"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0004.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.mjs
new file mode 100644
index 000000000..0a34a2dc8
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(false)',
+ 'mf-boolean/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-comma.expect.json
new file mode 100644
index 000000000..b6014d3df
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-comma.expect.json
@@ -0,0 +1,71 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "color(",
+ 1,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 8,
+ 8,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "color",
+ "tokens": [
+ [
+ "function-token",
+ "color(",
+ 1,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ]
+ ],
+ "value": []
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-space.expect.json
new file mode 100644
index 000000000..536dbad65
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-space.expect.json
@@ -0,0 +1,69 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "color(",
+ 1,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 8,
+ 8,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "color",
+ "tokens": [
+ [
+ "function-token",
+ "color(",
+ 1,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ]
+ ],
+ "value": []
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0005.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.mjs
new file mode 100644
index 000000000..c44c8c1f7
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color())',
+ 'mf-boolean/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-comma.expect.json
new file mode 100644
index 000000000..297d582fc
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-comma.expect.json
@@ -0,0 +1,120 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 1,
+ 9,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 1,
+ 9,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-space.expect.json
new file mode 100644
index 000000000..511342310
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-space.expect.json
@@ -0,0 +1,118 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 1,
+ 9,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 1,
+ 9,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0001.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0001.mjs
new file mode 100644
index 000000000..b674721b7
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(min-width: 300px)',
+ 'mf-plain/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-comma.expect.json
new file mode 100644
index 000000000..8669e19b6
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-comma.expect.json
@@ -0,0 +1,201 @@
+[
+ [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 31,
+ 39,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 40,
+ 54,
+ null
+ ],
+ [
+ "colon-token",
+ ":",
+ 55,
+ 55,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 4 */",
+ 56,
+ 70,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 5 */",
+ 76,
+ 90,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 91,
+ 91,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 31,
+ 39,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 3 */",
+ 40,
+ 54,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 55,
+ 55,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 4 */",
+ 56,
+ 70,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 5 */",
+ 76,
+ 90,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 6 */",
+ 92,
+ 106,
+ null
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-space.expect.json
new file mode 100644
index 000000000..e61e96cf2
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-space.expect.json
@@ -0,0 +1,199 @@
+[
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 31,
+ 39,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 40,
+ 54,
+ null
+ ],
+ [
+ "colon-token",
+ ":",
+ 55,
+ 55,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 4 */",
+ 56,
+ 70,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 5 */",
+ 76,
+ 90,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 91,
+ 91,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 31,
+ 39,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 3 */",
+ 40,
+ 54,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 55,
+ 55,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 4 */",
+ 56,
+ 70,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 5 */",
+ 76,
+ 90,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 6 */",
+ 92,
+ 106,
+ null
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0002.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0002.mjs
new file mode 100644
index 000000000..90eb3e1f9
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '/* comment 1 */(/* comment 2 */min-width/* comment 3 */:/* comment 4 */300px/* comment 5 */)/* comment 6 */',
+ 'mf-plain/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-comma.expect.json
new file mode 100644
index 000000000..f27dd647b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-comma.expect.json
@@ -0,0 +1,116 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-space.expect.json
new file mode 100644
index 000000000..704164d67
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-space.expect.json
@@ -0,0 +1,114 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0003.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0003.mjs
new file mode 100644
index 000000000..ad7c3adf2
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(resolution: infinite)',
+ 'mf-plain/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-comma.expect.json
new file mode 100644
index 000000000..cd0b720eb
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-comma.expect.json
@@ -0,0 +1,165 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "resolution(",
+ 1,
+ 11,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "colon-token",
+ ":",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 18,
+ 25,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "resolution",
+ "tokens": [
+ [
+ "function-token",
+ "resolution(",
+ 1,
+ 11,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 18,
+ 25,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-space.expect.json
new file mode 100644
index 000000000..1410e6a8b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-space.expect.json
@@ -0,0 +1,163 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "resolution(",
+ 1,
+ 11,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "colon-token",
+ ":",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 18,
+ 25,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "resolution",
+ "tokens": [
+ [
+ "function-token",
+ "resolution(",
+ 1,
+ 11,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 18,
+ 25,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0004.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0004.mjs
new file mode 100644
index 000000000..6d9a61a7b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(resolution(foo): infinite)',
+ 'mf-plain/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-comma.expect.json
new file mode 100644
index 000000000..3afb8dc07
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-comma.expect.json
@@ -0,0 +1,116 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-space.expect.json
new file mode 100644
index 000000000..a7c69ba1c
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-space.expect.json
@@ -0,0 +1,114 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0005.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0005.mjs
new file mode 100644
index 000000000..946d31898
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/mf-plain/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '[resolution: infinite]',
+ 'mf-plain/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-comma.expect.json
new file mode 100644
index 000000000..45a0eace6
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-comma.expect.json
@@ -0,0 +1,18 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-space.expect.json
new file mode 100644
index 000000000..f8d27aa46
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-space.expect.json
@@ -0,0 +1,16 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0001.mjs b/packages/css-parser-algorithms/test/cases/query-with-type/0001.mjs
new file mode 100644
index 000000000..b8d1c3f6b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'screen',
+ 'query-with-type/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-comma.expect.json
new file mode 100644
index 000000000..d708f4353
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-comma.expect.json
@@ -0,0 +1,44 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-space.expect.json
new file mode 100644
index 000000000..e27303e35
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-space.expect.json
@@ -0,0 +1,42 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0002.mjs b/packages/css-parser-algorithms/test/cases/query-with-type/0002.mjs
new file mode 100644
index 000000000..d9da6523a
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'only screen',
+ 'query-with-type/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-comma.expect.json
new file mode 100644
index 000000000..4d70580d2
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-comma.expect.json
@@ -0,0 +1,44 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 4,
+ 9,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-space.expect.json
new file mode 100644
index 000000000..445e7635b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-space.expect.json
@@ -0,0 +1,42 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 4,
+ 9,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0003.mjs b/packages/css-parser-algorithms/test/cases/query-with-type/0003.mjs
new file mode 100644
index 000000000..1684cf679
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/query-with-type/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not screen',
+ 'query-with-type/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0001.list-comma.expect.json
new file mode 100644
index 000000000..e2cb41189
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0001.list-comma.expect.json
@@ -0,0 +1,145 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 1,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "foo",
+ 16,
+ 18,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 20,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 1,
+ 15,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "foo",
+ 16,
+ 18,
+ {
+ "value": "foo"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 20,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "something",
+ 23,
+ 31,
+ {
+ "value": "something"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 32,
+ 32,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "else",
+ 33,
+ 36,
+ {
+ "value": "else"
+ }
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0001.list-space.expect.json
new file mode 100644
index 000000000..1fb3acae1
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0001.list-space.expect.json
@@ -0,0 +1,143 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 1,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "foo",
+ 16,
+ 18,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 20,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 1,
+ 15,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "foo",
+ 16,
+ 18,
+ {
+ "value": "foo"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 20,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "something",
+ 23,
+ 31,
+ {
+ "value": "something"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 32,
+ 32,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "else",
+ 33,
+ 36,
+ {
+ "value": "else"
+ }
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0001.mjs b/packages/css-parser-algorithms/test/cases/various/0001.mjs
new file mode 100644
index 000000000..c5764560e
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(/* a comment */foo ) something else',
+ 'various/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0002.list-comma.expect.json
new file mode 100644
index 000000000..b88eee50e
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0002.list-comma.expect.json
@@ -0,0 +1,790 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 4,
+ 9,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 11,
+ 13,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 34,
+ 36,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 38,
+ 38,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 38,
+ 38,
+ null
+ ],
+ [
+ "ident-token",
+ "prefers-color-scheme",
+ 39,
+ 58,
+ {
+ "value": "prefers-color-scheme"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 59,
+ 59,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 60,
+ 74,
+ null
+ ],
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 79,
+ 79,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "prefers-color-scheme",
+ 39,
+ 58,
+ {
+ "value": "prefers-color-scheme"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 59,
+ 59,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 60,
+ 74,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 80,
+ 80,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 81,
+ 83,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 84,
+ 84,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 85,
+ 85,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 85,
+ 85,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 86,
+ 90,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 91,
+ 91,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 92,
+ 92,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 93,
+ 93,
+ null
+ ],
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 98,
+ 98,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 86,
+ 90,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 91,
+ 91,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 92,
+ 92,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 93,
+ 93,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 99,
+ 99,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 100,
+ 102,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 103,
+ 103,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 104,
+ 104,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 104,
+ 104,
+ null
+ ],
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 109,
+ 109,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 110,
+ 110,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 111,
+ 111,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 112,
+ 116,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 117,
+ 117,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 118,
+ 118,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 119,
+ 119,
+ null
+ ],
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 125,
+ 125,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 109,
+ 109,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 110,
+ 110,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 111,
+ 111,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 112,
+ 116,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 117,
+ 117,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 118,
+ 118,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 119,
+ 119,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0002.list-space.expect.json
new file mode 100644
index 000000000..1df900bfc
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0002.list-space.expect.json
@@ -0,0 +1,788 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 4,
+ 9,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 11,
+ 13,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 34,
+ 36,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 38,
+ 38,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 38,
+ 38,
+ null
+ ],
+ [
+ "ident-token",
+ "prefers-color-scheme",
+ 39,
+ 58,
+ {
+ "value": "prefers-color-scheme"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 59,
+ 59,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 60,
+ 74,
+ null
+ ],
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 79,
+ 79,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "prefers-color-scheme",
+ 39,
+ 58,
+ {
+ "value": "prefers-color-scheme"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 59,
+ 59,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 60,
+ 74,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 80,
+ 80,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 81,
+ 83,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 84,
+ 84,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 85,
+ 85,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 85,
+ 85,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 86,
+ 90,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 91,
+ 91,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 92,
+ 92,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 93,
+ 93,
+ null
+ ],
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 98,
+ 98,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 86,
+ 90,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 91,
+ 91,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 92,
+ 92,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 93,
+ 93,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 99,
+ 99,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 100,
+ 102,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 103,
+ 103,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 104,
+ 104,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 104,
+ 104,
+ null
+ ],
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 109,
+ 109,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 110,
+ 110,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 111,
+ 111,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 112,
+ 116,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 117,
+ 117,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 118,
+ 118,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 119,
+ 119,
+ null
+ ],
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 125,
+ 125,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 109,
+ 109,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 110,
+ 110,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 111,
+ 111,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 112,
+ 116,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 117,
+ 117,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 118,
+ 118,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 119,
+ 119,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0002.mjs b/packages/css-parser-algorithms/test/cases/various/0002.mjs
new file mode 100644
index 000000000..964b4d9b9
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)',
+ 'various/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0003.list-comma.expect.json
new file mode 100644
index 000000000..ad2407cda
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0003.list-comma.expect.json
@@ -0,0 +1,347 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 12,
+ 12,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 22,
+ 22,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 12,
+ 12,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 23,
+ 23,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 24,
+ 26,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 38,
+ 38,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 39,
+ 39,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 40,
+ 40,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 41,
+ 50,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 51,
+ 51,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 38,
+ 38,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "=",
+ 39,
+ 39,
+ {
+ "value": "="
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 40,
+ 40,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 41,
+ 50,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 52,
+ 52,
+ null
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0003.list-space.expect.json
new file mode 100644
index 000000000..3bfcbac73
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0003.list-space.expect.json
@@ -0,0 +1,345 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 12,
+ 12,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 22,
+ 22,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 12,
+ 12,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 23,
+ 23,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 24,
+ 26,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 38,
+ 38,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 39,
+ 39,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 40,
+ 40,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 41,
+ 50,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 51,
+ 51,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 38,
+ 38,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "=",
+ 39,
+ 39,
+ {
+ "value": "="
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 40,
+ 40,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 41,
+ 50,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 52,
+ 52,
+ null
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0003.mjs b/packages/css-parser-algorithms/test/cases/various/0003.mjs
new file mode 100644
index 000000000..2af154b33
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(resolution < infinite) and (infinite <= resolution) ',
+ 'various/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0004.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0004.list-comma.expect.json
new file mode 100644
index 000000000..72fb943f6
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0004.list-comma.expect.json
@@ -0,0 +1,316 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0004.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0004.list-space.expect.json
new file mode 100644
index 000000000..5653f5136
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0004.list-space.expect.json
@@ -0,0 +1,314 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0004.mjs b/packages/css-parser-algorithms/test/cases/various/0004.mjs
new file mode 100644
index 000000000..62f73bf0a
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(width < calc(50vw - 3rem))',
+ 'various/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0005.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0005.list-comma.expect.json
new file mode 100644
index 000000000..8e0833bda
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0005.list-comma.expect.json
@@ -0,0 +1,198 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 7,
+ 9,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 11,
+ 13,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0005.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0005.list-space.expect.json
new file mode 100644
index 000000000..b41645b59
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0005.list-space.expect.json
@@ -0,0 +1,196 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 7,
+ 9,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 11,
+ 13,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0005.mjs b/packages/css-parser-algorithms/test/cases/various/0005.mjs
new file mode 100644
index 000000000..7c55c608d
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'screen and not (min-width: 300px)',
+ 'various/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0006.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0006.list-comma.expect.json
new file mode 100644
index 000000000..0e5184edc
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0006.list-comma.expect.json
@@ -0,0 +1,56 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0006.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0006.list-space.expect.json
new file mode 100644
index 000000000..75bf497e4
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0006.list-space.expect.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0006.mjs b/packages/css-parser-algorithms/test/cases/various/0006.mjs
new file mode 100644
index 000000000..2d9faf758
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0006.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'only screen ',
+ 'various/0006',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0007.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0007.list-comma.expect.json
new file mode 100644
index 000000000..e500341e8
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0007.list-comma.expect.json
@@ -0,0 +1,59 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0007.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0007.list-space.expect.json
new file mode 100644
index 000000000..c9ead936b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0007.list-space.expect.json
@@ -0,0 +1,57 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0007.mjs b/packages/css-parser-algorithms/test/cases/various/0007.mjs
new file mode 100644
index 000000000..f3e178c40
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0007.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '[10px]',
+ 'various/0007',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0008.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0008.list-comma.expect.json
new file mode 100644
index 000000000..9fa9596f9
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0008.list-comma.expect.json
@@ -0,0 +1,110 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0008.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0008.list-space.expect.json
new file mode 100644
index 000000000..75447e844
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0008.list-space.expect.json
@@ -0,0 +1,108 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0008.mjs b/packages/css-parser-algorithms/test/cases/various/0008.mjs
new file mode 100644
index 000000000..6ea1fad9d
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0008.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(calc(10px))',
+ 'various/0008',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0009.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0009.list-comma.expect.json
new file mode 100644
index 000000000..70fea63dd
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0009.list-comma.expect.json
@@ -0,0 +1,129 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 12,
+ 12,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0009.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0009.list-space.expect.json
new file mode 100644
index 000000000..9efe5dbe6
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0009.list-space.expect.json
@@ -0,0 +1,127 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 12,
+ 12,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0009.mjs b/packages/css-parser-algorithms/test/cases/various/0009.mjs
new file mode 100644
index 000000000..bdf46f868
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0009.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(calc(10px) )',
+ 'various/0009',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0010.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0010.list-comma.expect.json
new file mode 100644
index 000000000..38a3b8c7a
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0010.list-comma.expect.json
@@ -0,0 +1,129 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0010.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0010.list-space.expect.json
new file mode 100644
index 000000000..bc616fb21
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0010.list-space.expect.json
@@ -0,0 +1,127 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 1,
+ 5,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 6,
+ 9,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0010.mjs b/packages/css-parser-algorithms/test/cases/various/0010.mjs
new file mode 100644
index 000000000..9b314124f
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0010.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(calc(10px)/* a comment */)',
+ 'various/0010',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0011.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0011.list-comma.expect.json
new file mode 100644
index 000000000..b7d0d623d
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0011.list-comma.expect.json
@@ -0,0 +1,298 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 2,
+ 6,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 12,
+ 26,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 29,
+ 29,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 30,
+ 34,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 35,
+ 35,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 36,
+ 36,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 2,
+ 6,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 12,
+ 26,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 27,
+ 27,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 2,
+ 6,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 12,
+ 26,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 29,
+ 29,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 29,
+ 29,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 30,
+ 34,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 35,
+ 35,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "other",
+ 30,
+ 34,
+ {
+ "value": "other"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0011.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0011.list-space.expect.json
new file mode 100644
index 000000000..ce17e46da
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0011.list-space.expect.json
@@ -0,0 +1,296 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 2,
+ 6,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 12,
+ 26,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 29,
+ 29,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 30,
+ 34,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 35,
+ 35,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 36,
+ 36,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 2,
+ 6,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 12,
+ 26,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 27,
+ 27,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 2,
+ 6,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 7,
+ 10,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 12,
+ 26,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 29,
+ 29,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 29,
+ 29,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 30,
+ 34,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 35,
+ 35,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "other",
+ 30,
+ 34,
+ {
+ "value": "other"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0011.mjs b/packages/css-parser-algorithms/test/cases/various/0011.mjs
new file mode 100644
index 000000000..dc4fa6514
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0011.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '((calc(10px)/* a comment */) (other))',
+ 'various/0011',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0012.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0012.list-comma.expect.json
new file mode 100644
index 000000000..b99cb9831
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0012.list-comma.expect.json
@@ -0,0 +1,55 @@
+[
+ [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 9,
+ 9,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0012.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0012.list-space.expect.json
new file mode 100644
index 000000000..ed15ba06a
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0012.list-space.expect.json
@@ -0,0 +1,53 @@
+[
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 9,
+ 9,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0012.mjs b/packages/css-parser-algorithms/test/cases/various/0012.mjs
new file mode 100644
index 000000000..6f2b31339
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0012.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'calc(10px)',
+ 'various/0012',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0013.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0013.list-comma.expect.json
new file mode 100644
index 000000000..8f8bd84d1
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0013.list-comma.expect.json
@@ -0,0 +1,74 @@
+[
+ [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0013.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0013.list-space.expect.json
new file mode 100644
index 000000000..18a984378
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0013.list-space.expect.json
@@ -0,0 +1,72 @@
+[
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0013.mjs b/packages/css-parser-algorithms/test/cases/various/0013.mjs
new file mode 100644
index 000000000..94cc52dee
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0013.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'calc(10px )',
+ 'various/0013',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0014.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0014.list-comma.expect.json
new file mode 100644
index 000000000..c7e67316b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0014.list-comma.expect.json
@@ -0,0 +1,74 @@
+[
+ [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 9,
+ 23,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 24,
+ 24,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 9,
+ 23,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0014.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0014.list-space.expect.json
new file mode 100644
index 000000000..c59243b26
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0014.list-space.expect.json
@@ -0,0 +1,72 @@
+[
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 9,
+ 23,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 24,
+ 24,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 9,
+ 23,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0014.mjs b/packages/css-parser-algorithms/test/cases/various/0014.mjs
new file mode 100644
index 000000000..26a323ac3
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0014.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'calc(10px/* a comment */)',
+ 'various/0014',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0015.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0015.list-comma.expect.json
new file mode 100644
index 000000000..d5108b8fd
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0015.list-comma.expect.json
@@ -0,0 +1,347 @@
+[
+ [
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comma-token",
+ ",",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ],
+ [
+ "comma-token",
+ ",",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 29,
+ 33,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 35,
+ 39,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 44,
+ 44,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 45,
+ 45,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 46,
+ 46,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 9,
+ 9,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 29,
+ 33,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 35,
+ 39,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 44,
+ 44,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 45,
+ 45,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "other",
+ 29,
+ 33,
+ {
+ "value": "other"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 35,
+ 39,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 44,
+ 44,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0015.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0015.list-space.expect.json
new file mode 100644
index 000000000..9bf015a26
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0015.list-space.expect.json
@@ -0,0 +1,345 @@
+[
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 0,
+ 4,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comma-token",
+ ",",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ],
+ [
+ "comma-token",
+ ",",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 29,
+ 33,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 35,
+ 39,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 44,
+ 44,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 45,
+ 45,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 46,
+ 46,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 5,
+ 8,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 9,
+ 9,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 11,
+ 25,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "ident-token",
+ "other",
+ 29,
+ 33,
+ {
+ "value": "other"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 35,
+ 39,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 44,
+ 44,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 45,
+ 45,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "other",
+ 29,
+ 33,
+ {
+ "value": "other"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 35,
+ 39,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 44,
+ 44,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "more",
+ 40,
+ 43,
+ {
+ "value": "more"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0015.mjs b/packages/css-parser-algorithms/test/cases/various/0015.mjs
new file mode 100644
index 000000000..656a7b017
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0015.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'calc(10px, /* a comment */, (other calc(more)))',
+ 'various/0015',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0016.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0016.list-comma.expect.json
new file mode 100644
index 000000000..28f35b5cd
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0016.list-comma.expect.json
@@ -0,0 +1,162 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ],
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 13,
+ 16,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 13,
+ 16,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0016.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0016.list-space.expect.json
new file mode 100644
index 000000000..bc31e9fd7
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0016.list-space.expect.json
@@ -0,0 +1,170 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 13,
+ 16,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 13,
+ 16,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0016.mjs b/packages/css-parser-algorithms/test/cases/various/0016.mjs
new file mode 100644
index 000000000..238192b31
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0016.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(10px,12px),(10px)',
+ 'various/0016',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0017.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0017.list-comma.expect.json
new file mode 100644
index 000000000..51b1cc6a1
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0017.list-comma.expect.json
@@ -0,0 +1,151 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ],
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 12,
+ 15,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 17,
+ 20,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0017.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0017.list-space.expect.json
new file mode 100644
index 000000000..c5cf8279c
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0017.list-space.expect.json
@@ -0,0 +1,159 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 1,
+ 4,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "12px",
+ 6,
+ 9,
+ {
+ "value": 12,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "comma-token",
+ ",",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 12,
+ 15,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 17,
+ 20,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0017.mjs b/packages/css-parser-algorithms/test/cases/various/0017.mjs
new file mode 100644
index 000000000..3ffaa1e3b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0017.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(10px,12px),10px 10px',
+ 'various/0017',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0018.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0018.list-comma.expect.json
new file mode 100644
index 000000000..d025163ca
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0018.list-comma.expect.json
@@ -0,0 +1,48 @@
+[
+ [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 0,
+ 3,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "15px",
+ 5,
+ 8,
+ {
+ "value": 15,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0018.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0018.list-space.expect.json
new file mode 100644
index 000000000..fd958c8ba
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0018.list-space.expect.json
@@ -0,0 +1,46 @@
+[
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "10px",
+ 0,
+ 3,
+ {
+ "value": 10,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "15px",
+ 5,
+ 8,
+ {
+ "value": 15,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0018.mjs b/packages/css-parser-algorithms/test/cases/various/0018.mjs
new file mode 100644
index 000000000..342074128
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0018.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '10px 15px',
+ 'various/0018',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/cases/various/0019.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0019.list-comma.expect.json
new file mode 100644
index 000000000..1bd898203
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0019.list-comma.expect.json
@@ -0,0 +1,1704 @@
+[
+ [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 2,
+ 2,
+ null
+ ],
+ [
+ "ident-token",
+ "aa",
+ 3,
+ 4,
+ {
+ "value": "aa"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "ab",
+ 8,
+ 9,
+ {
+ "value": "ab"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "ac",
+ 13,
+ 14,
+ {
+ "value": "ac"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 17,
+ 18,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "ad",
+ 19,
+ 20,
+ {
+ "value": "ad"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 23,
+ 23,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 1,
+ 1,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 2,
+ 2,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 2,
+ 2,
+ null
+ ],
+ [
+ "ident-token",
+ "aa",
+ 3,
+ 4,
+ {
+ "value": "aa"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "aa",
+ 3,
+ 4,
+ {
+ "value": "aa"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 7,
+ 7,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "ab",
+ 8,
+ 9,
+ {
+ "value": "ab"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ab",
+ 8,
+ 9,
+ {
+ "value": "ab"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 12,
+ 12,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "ac",
+ 13,
+ 14,
+ {
+ "value": "ac"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ac",
+ 13,
+ 14,
+ {
+ "value": "ac"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 17,
+ 18,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "ad",
+ 19,
+ 20,
+ {
+ "value": "ad"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ad",
+ 19,
+ 20,
+ {
+ "value": "ad"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 25,
+ 25,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "ident-token",
+ "ba",
+ 28,
+ 29,
+ {
+ "value": "ba"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 31,
+ 31,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 32,
+ 32,
+ null
+ ],
+ [
+ "ident-token",
+ "bb",
+ 33,
+ 34,
+ {
+ "value": "bb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 35,
+ 35,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 36,
+ 36,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "ident-token",
+ "bc",
+ 38,
+ 39,
+ {
+ "value": "bc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 40,
+ 40,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 41,
+ 41,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 42,
+ 43,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "bd",
+ 44,
+ 45,
+ {
+ "value": "bd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 46,
+ 46,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 47,
+ 47,
+ null
+ ],
+ [
+ "]-token",
+ "]",
+ 48,
+ 48,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "ident-token",
+ "ba",
+ 28,
+ 29,
+ {
+ "value": "ba"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ba",
+ 28,
+ 29,
+ {
+ "value": "ba"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 31,
+ 31,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 32,
+ 32,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 32,
+ 32,
+ null
+ ],
+ [
+ "ident-token",
+ "bb",
+ 33,
+ 34,
+ {
+ "value": "bb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 35,
+ 35,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "bb",
+ 33,
+ 34,
+ {
+ "value": "bb"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 36,
+ 36,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 37,
+ 37,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "ident-token",
+ "bc",
+ 38,
+ 39,
+ {
+ "value": "bc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 40,
+ 40,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "bc",
+ 38,
+ 39,
+ {
+ "value": "bc"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 41,
+ 41,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 42,
+ 43,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "bd",
+ 44,
+ 45,
+ {
+ "value": "bd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 46,
+ 46,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "bd",
+ 44,
+ 45,
+ {
+ "value": "bd"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 47,
+ 47,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 49,
+ 49,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 50,
+ 50,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 50,
+ 50,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 51,
+ 51,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 52,
+ 52,
+ null
+ ],
+ [
+ "ident-token",
+ "ca",
+ 53,
+ 54,
+ {
+ "value": "ca"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 55,
+ 55,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 56,
+ 56,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 57,
+ 57,
+ null
+ ],
+ [
+ "ident-token",
+ "cb",
+ 58,
+ 59,
+ {
+ "value": "cb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 60,
+ 60,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 61,
+ 61,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 62,
+ 62,
+ null
+ ],
+ [
+ "ident-token",
+ "cc",
+ 63,
+ 64,
+ {
+ "value": "cc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 65,
+ 65,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 66,
+ 66,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 67,
+ 68,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "cd",
+ 69,
+ 70,
+ {
+ "value": "cd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 71,
+ 71,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 72,
+ 72,
+ null
+ ],
+ [
+ "}-token",
+ "}",
+ 73,
+ 73,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 51,
+ 51,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 52,
+ 52,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 52,
+ 52,
+ null
+ ],
+ [
+ "ident-token",
+ "ca",
+ 53,
+ 54,
+ {
+ "value": "ca"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 55,
+ 55,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ca",
+ 53,
+ 54,
+ {
+ "value": "ca"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 56,
+ 56,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 57,
+ 57,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 57,
+ 57,
+ null
+ ],
+ [
+ "ident-token",
+ "cb",
+ 58,
+ 59,
+ {
+ "value": "cb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 60,
+ 60,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "cb",
+ 58,
+ 59,
+ {
+ "value": "cb"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 61,
+ 61,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 62,
+ 62,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 62,
+ 62,
+ null
+ ],
+ [
+ "ident-token",
+ "cc",
+ 63,
+ 64,
+ {
+ "value": "cc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 65,
+ 65,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "cc",
+ 63,
+ 64,
+ {
+ "value": "cc"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 66,
+ 66,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 67,
+ 68,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "cd",
+ 69,
+ 70,
+ {
+ "value": "cd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 71,
+ 71,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "cd",
+ 69,
+ 70,
+ {
+ "value": "cd"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 72,
+ 72,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 74,
+ 74,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 75,
+ 76,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 77,
+ 77,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 78,
+ 78,
+ null
+ ],
+ [
+ "ident-token",
+ "da",
+ 79,
+ 80,
+ {
+ "value": "da"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 81,
+ 81,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 82,
+ 82,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 83,
+ 83,
+ null
+ ],
+ [
+ "ident-token",
+ "db",
+ 84,
+ 85,
+ {
+ "value": "db"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 86,
+ 86,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 87,
+ 87,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 88,
+ 88,
+ null
+ ],
+ [
+ "ident-token",
+ "dc",
+ 89,
+ 90,
+ {
+ "value": "dc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 91,
+ 91,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 92,
+ 92,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 93,
+ 94,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "dd",
+ 95,
+ 96,
+ {
+ "value": "dd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 97,
+ 97,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 98,
+ 98,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 99,
+ 99,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 77,
+ 77,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 78,
+ 78,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 78,
+ 78,
+ null
+ ],
+ [
+ "ident-token",
+ "da",
+ 79,
+ 80,
+ {
+ "value": "da"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 81,
+ 81,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "da",
+ 79,
+ 80,
+ {
+ "value": "da"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 82,
+ 82,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 83,
+ 83,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 83,
+ 83,
+ null
+ ],
+ [
+ "ident-token",
+ "db",
+ 84,
+ 85,
+ {
+ "value": "db"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 86,
+ 86,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "db",
+ 84,
+ 85,
+ {
+ "value": "db"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 87,
+ 87,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 88,
+ 88,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 88,
+ 88,
+ null
+ ],
+ [
+ "ident-token",
+ "dc",
+ 89,
+ 90,
+ {
+ "value": "dc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 91,
+ 91,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dc",
+ 89,
+ 90,
+ {
+ "value": "dc"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 92,
+ 92,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 93,
+ 94,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "dd",
+ 95,
+ 96,
+ {
+ "value": "dd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 97,
+ 97,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dd",
+ 95,
+ 96,
+ {
+ "value": "dd"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 98,
+ 98,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0019.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0019.list-space.expect.json
new file mode 100644
index 000000000..72ba46a67
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0019.list-space.expect.json
@@ -0,0 +1,1702 @@
+[
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 2,
+ 2,
+ null
+ ],
+ [
+ "ident-token",
+ "aa",
+ 3,
+ 4,
+ {
+ "value": "aa"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "ab",
+ 8,
+ 9,
+ {
+ "value": "ab"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "ac",
+ 13,
+ 14,
+ {
+ "value": "ac"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 17,
+ 18,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "ad",
+ 19,
+ 20,
+ {
+ "value": "ad"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 23,
+ 23,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 1,
+ 1,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 2,
+ 2,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 2,
+ 2,
+ null
+ ],
+ [
+ "ident-token",
+ "aa",
+ 3,
+ 4,
+ {
+ "value": "aa"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "aa",
+ 3,
+ 4,
+ {
+ "value": "aa"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 7,
+ 7,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "ab",
+ 8,
+ 9,
+ {
+ "value": "ab"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 10,
+ 10,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ab",
+ 8,
+ 9,
+ {
+ "value": "ab"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 12,
+ 12,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "ac",
+ 13,
+ 14,
+ {
+ "value": "ac"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ac",
+ 13,
+ 14,
+ {
+ "value": "ac"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 17,
+ 18,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "ad",
+ 19,
+ 20,
+ {
+ "value": "ad"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ad",
+ 19,
+ 20,
+ {
+ "value": "ad"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 25,
+ 25,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "ident-token",
+ "ba",
+ 28,
+ 29,
+ {
+ "value": "ba"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 31,
+ 31,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 32,
+ 32,
+ null
+ ],
+ [
+ "ident-token",
+ "bb",
+ 33,
+ 34,
+ {
+ "value": "bb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 35,
+ 35,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 36,
+ 36,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "ident-token",
+ "bc",
+ 38,
+ 39,
+ {
+ "value": "bc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 40,
+ 40,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 41,
+ 41,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 42,
+ 43,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "bd",
+ 44,
+ 45,
+ {
+ "value": "bd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 46,
+ 46,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 47,
+ 47,
+ null
+ ],
+ [
+ "]-token",
+ "]",
+ 48,
+ 48,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "ident-token",
+ "ba",
+ 28,
+ 29,
+ {
+ "value": "ba"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ba",
+ 28,
+ 29,
+ {
+ "value": "ba"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 31,
+ 31,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 32,
+ 32,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 32,
+ 32,
+ null
+ ],
+ [
+ "ident-token",
+ "bb",
+ 33,
+ 34,
+ {
+ "value": "bb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 35,
+ 35,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "bb",
+ 33,
+ 34,
+ {
+ "value": "bb"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 36,
+ 36,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 37,
+ 37,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "ident-token",
+ "bc",
+ 38,
+ 39,
+ {
+ "value": "bc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 40,
+ 40,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "bc",
+ 38,
+ 39,
+ {
+ "value": "bc"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 41,
+ 41,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 42,
+ 43,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "bd",
+ 44,
+ 45,
+ {
+ "value": "bd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 46,
+ 46,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "bd",
+ 44,
+ 45,
+ {
+ "value": "bd"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 47,
+ 47,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 49,
+ 49,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 50,
+ 50,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 50,
+ 50,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 51,
+ 51,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 52,
+ 52,
+ null
+ ],
+ [
+ "ident-token",
+ "ca",
+ 53,
+ 54,
+ {
+ "value": "ca"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 55,
+ 55,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 56,
+ 56,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 57,
+ 57,
+ null
+ ],
+ [
+ "ident-token",
+ "cb",
+ 58,
+ 59,
+ {
+ "value": "cb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 60,
+ 60,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 61,
+ 61,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 62,
+ 62,
+ null
+ ],
+ [
+ "ident-token",
+ "cc",
+ 63,
+ 64,
+ {
+ "value": "cc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 65,
+ 65,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 66,
+ 66,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 67,
+ 68,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "cd",
+ 69,
+ 70,
+ {
+ "value": "cd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 71,
+ 71,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 72,
+ 72,
+ null
+ ],
+ [
+ "}-token",
+ "}",
+ 73,
+ 73,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 51,
+ 51,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 52,
+ 52,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 52,
+ 52,
+ null
+ ],
+ [
+ "ident-token",
+ "ca",
+ 53,
+ 54,
+ {
+ "value": "ca"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 55,
+ 55,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "ca",
+ 53,
+ 54,
+ {
+ "value": "ca"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 56,
+ 56,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 57,
+ 57,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 57,
+ 57,
+ null
+ ],
+ [
+ "ident-token",
+ "cb",
+ 58,
+ 59,
+ {
+ "value": "cb"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 60,
+ 60,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "cb",
+ 58,
+ 59,
+ {
+ "value": "cb"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 61,
+ 61,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 62,
+ 62,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 62,
+ 62,
+ null
+ ],
+ [
+ "ident-token",
+ "cc",
+ 63,
+ 64,
+ {
+ "value": "cc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 65,
+ 65,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "cc",
+ 63,
+ 64,
+ {
+ "value": "cc"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 66,
+ 66,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 67,
+ 68,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "cd",
+ 69,
+ 70,
+ {
+ "value": "cd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 71,
+ 71,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "cd",
+ 69,
+ 70,
+ {
+ "value": "cd"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 72,
+ 72,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 74,
+ 74,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 75,
+ 76,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 77,
+ 77,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 78,
+ 78,
+ null
+ ],
+ [
+ "ident-token",
+ "da",
+ 79,
+ 80,
+ {
+ "value": "da"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 81,
+ 81,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 82,
+ 82,
+ null
+ ],
+ [
+ "[-token",
+ "[",
+ 83,
+ 83,
+ null
+ ],
+ [
+ "ident-token",
+ "db",
+ 84,
+ 85,
+ {
+ "value": "db"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 86,
+ 86,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 87,
+ 87,
+ null
+ ],
+ [
+ "{-token",
+ "{",
+ 88,
+ 88,
+ null
+ ],
+ [
+ "ident-token",
+ "dc",
+ 89,
+ 90,
+ {
+ "value": "dc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 91,
+ 91,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 92,
+ 92,
+ null
+ ],
+ [
+ "function-token",
+ "z(",
+ 93,
+ 94,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "dd",
+ 95,
+ 96,
+ {
+ "value": "dd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 97,
+ 97,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 98,
+ 98,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 99,
+ 99,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 77,
+ 77,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 78,
+ 78,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 78,
+ 78,
+ null
+ ],
+ [
+ "ident-token",
+ "da",
+ 79,
+ 80,
+ {
+ "value": "da"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 81,
+ 81,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "da",
+ 79,
+ 80,
+ {
+ "value": "da"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 82,
+ 82,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 83,
+ 83,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 83,
+ 83,
+ null
+ ],
+ [
+ "ident-token",
+ "db",
+ 84,
+ 85,
+ {
+ "value": "db"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 86,
+ 86,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "db",
+ 84,
+ 85,
+ {
+ "value": "db"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 87,
+ 87,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "{-token",
+ "{",
+ 88,
+ 88,
+ null
+ ],
+ "tokens": [
+ [
+ "{-token",
+ "{",
+ 88,
+ 88,
+ null
+ ],
+ [
+ "ident-token",
+ "dc",
+ 89,
+ 90,
+ {
+ "value": "dc"
+ }
+ ],
+ [
+ "}-token",
+ "}",
+ 91,
+ 91,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dc",
+ 89,
+ 90,
+ {
+ "value": "dc"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 92,
+ 92,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "function",
+ "name": "z",
+ "tokens": [
+ [
+ "function-token",
+ "z(",
+ 93,
+ 94,
+ {
+ "value": "z"
+ }
+ ],
+ [
+ "ident-token",
+ "dd",
+ 95,
+ 96,
+ {
+ "value": "dd"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 97,
+ 97,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dd",
+ 95,
+ 96,
+ {
+ "value": "dd"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 98,
+ 98,
+ null
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/css-parser-algorithms/test/cases/various/0019.mjs b/packages/css-parser-algorithms/test/cases/various/0019.mjs
new file mode 100644
index 000000000..32c9385e6
--- /dev/null
+++ b/packages/css-parser-algorithms/test/cases/various/0019.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '( (aa) [ab] {ac} z(ad) ) [ (ba) [bb] {bc} z(bd) ] { (ca) [cb] {cc} z(cd) } z( (da) [db] {dc} z(dd) )',
+ 'various/0019',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/css-parser-algorithms/test/test.mjs b/packages/css-parser-algorithms/test/test.mjs
new file mode 100644
index 000000000..fa184c135
--- /dev/null
+++ b/packages/css-parser-algorithms/test/test.mjs
@@ -0,0 +1,37 @@
+import './cases/media-not/0001.mjs';
+
+import './cases/mf-boolean/0001.mjs';
+import './cases/mf-boolean/0002.mjs';
+import './cases/mf-boolean/0003.mjs';
+import './cases/mf-boolean/0004.mjs';
+import './cases/mf-boolean/0005.mjs';
+
+import './cases/mf-plain/0001.mjs';
+import './cases/mf-plain/0002.mjs';
+import './cases/mf-plain/0003.mjs';
+import './cases/mf-plain/0004.mjs';
+import './cases/mf-plain/0005.mjs';
+
+import './cases/query-with-type/0001.mjs';
+import './cases/query-with-type/0002.mjs';
+import './cases/query-with-type/0003.mjs';
+
+import './cases/various/0001.mjs';
+import './cases/various/0002.mjs';
+import './cases/various/0003.mjs';
+import './cases/various/0004.mjs';
+import './cases/various/0005.mjs';
+import './cases/various/0006.mjs';
+import './cases/various/0007.mjs';
+import './cases/various/0008.mjs';
+import './cases/various/0009.mjs';
+import './cases/various/0010.mjs';
+import './cases/various/0011.mjs';
+import './cases/various/0012.mjs';
+import './cases/various/0013.mjs';
+import './cases/various/0014.mjs';
+import './cases/various/0015.mjs';
+import './cases/various/0016.mjs';
+import './cases/various/0017.mjs';
+import './cases/various/0018.mjs';
+import './cases/various/0019.mjs';
diff --git a/packages/css-parser-algorithms/test/util/run-test.mjs b/packages/css-parser-algorithms/test/util/run-test.mjs
new file mode 100644
index 000000000..5d58211c9
--- /dev/null
+++ b/packages/css-parser-algorithms/test/util/run-test.mjs
@@ -0,0 +1,65 @@
+import fs from 'fs';
+import path from 'path';
+import { parseCommaSeparatedListOfComponentValues, parseListOfComponentValues } from '@csstools/css-parser-algorithms';
+import { tokenizer } from '@csstools/css-tokenizer';
+
+export function runTest(source, testPath, assertEqual) {
+ const onParseError = (err) => {
+ console.warn(err);
+ throw new Error(`Unable to parse "${source}"`);
+ };
+ const t = tokenizer({ css: source }, {
+ commentsAreTokens: true,
+ onParseError: onParseError,
+ });
+
+ const tokens = [];
+
+ {
+ while (!t.endOfFile()) {
+ tokens.push(t.nextToken());
+ }
+
+ tokens.push(t.nextToken()); // EOF-token
+ }
+
+ {
+ // Space separated list of component values
+ const resultAST = parseListOfComponentValues(tokens, {
+ onParseError: onParseError,
+ });
+ const resultAST_JSON = JSON.stringify(resultAST, null, '\t');
+
+ if (process.env['REWRITE_EXPECTS'] === 'true') {
+ fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-space.expect.json`), resultAST_JSON);
+ fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-space.result.json`), resultAST_JSON);
+ } else {
+ const expectData = JSON.parse(fs.readFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-space.expect.json`)).toString());
+
+ assertEqual(
+ JSON.parse(resultAST_JSON),
+ expectData,
+ );
+ }
+ }
+
+ {
+ // Comma separated list of component values
+ const resultAST = parseCommaSeparatedListOfComponentValues(tokens, {
+ onParseError: onParseError,
+ });
+ const resultAST_JSON = JSON.stringify(resultAST, null, '\t');
+
+ if (process.env['REWRITE_EXPECTS'] === 'true') {
+ fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-comma.expect.json`), resultAST_JSON);
+ fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-comma.result.json`), resultAST_JSON);
+ } else {
+ const expectData = JSON.parse(fs.readFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-comma.expect.json`)).toString());
+
+ assertEqual(
+ JSON.parse(resultAST_JSON),
+ expectData,
+ );
+ }
+ }
+}
diff --git a/packages/css-parser-algorithms/tsconfig.json b/packages/css-parser-algorithms/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/packages/css-parser-algorithms/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/packages/css-tokenizer/.gitignore b/packages/css-tokenizer/.gitignore
new file mode 100644
index 000000000..7172b04f1
--- /dev/null
+++ b/packages/css-tokenizer/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+dist/*
diff --git a/packages/css-tokenizer/.nvmrc b/packages/css-tokenizer/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/packages/css-tokenizer/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/packages/css-tokenizer/CHANGELOG.md b/packages/css-tokenizer/CHANGELOG.md
new file mode 100644
index 000000000..b0ff6b082
--- /dev/null
+++ b/packages/css-tokenizer/CHANGELOG.md
@@ -0,0 +1,3 @@
+### 1.0.0
+
+- Initial version
diff --git a/packages/css-tokenizer/LICENSE.md b/packages/css-tokenizer/LICENSE.md
new file mode 100644
index 000000000..af5411fa2
--- /dev/null
+++ b/packages/css-tokenizer/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/css-tokenizer/README.md b/packages/css-tokenizer/README.md
new file mode 100644
index 000000000..3fc1cef61
--- /dev/null
+++ b/packages/css-tokenizer/README.md
@@ -0,0 +1,128 @@
+# CSS Tokenizer
+
+[
][npm-url]
+[
][cli-url]
+[
][discord]
+
+Implemented from : https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/
+
+## Usage
+
+Add [CSS Tokenizer] to your project:
+
+```bash
+npm install postcss @csstools/css-tokenizer --save-dev
+```
+
+```js
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+
+const myCSS = `@media only screen and (min-width: 768rem) {
+ .foo {
+ content: 'Some content!' !important;
+ }
+}
+`;
+
+const t = tokenizer({
+ css: myCSS,
+});
+
+while (true) {
+ const token = t.nextToken();
+ if (token[0] === TokenType.EOF) {
+ break;
+ }
+
+ console.log(token);
+}
+```
+
+### Options
+
+```ts
+{
+ commentsAreTokens?: false,
+ onParseError?: (error: ParserError) => void
+}
+```
+
+#### `commentsAreTokens`
+
+Following the CSS specification comments are never returned by the tokenizer.
+For many tools however it is desirable to be able to convert tokens back to a string.
+
+```js
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+
+const t = tokenizer({
+ css: `/* a comment */`,
+}, { commentsAreTokens: true });
+
+while (true) {
+ const token = t.nextToken();
+ if (token[0] === TokenType.EOF) {
+ break;
+ }
+
+ console.log(token);
+}
+```
+
+logs : `['comment', '/* a comment */', , , undefined]`
+
+
+#### `onParseError`
+
+The tokenizer is forgiving and won't stop when a parse error is encountered.
+Parse errors also aren't tokens.
+
+To receive parsing error information you can set a callback.
+
+```js
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+
+const t = tokenizer({
+ css: '\\',
+}, { onParseError: (err) => console.warn(err) });
+
+while (true) {
+ const token = t.nextToken();
+ if (token[0] === TokenType.EOF) {
+ break;
+ }
+}
+```
+
+logs :
+
+```js
+{
+ message: 'Unexpected EOF while consuming an escaped code point.',
+ start: 0,
+ end: 0,
+ state: ['4.3.7. Consume an escaped code point', 'Unexpected EOF'],
+}
+```
+
+Parser errors will try to inform you about the point in the tokenizer logic the error happened.
+This tells you the kind of error.
+
+`start` and `end` are the location in your CSS source code.
+
+## Goals and non-goals
+
+Things this package aims to be:
+- specification compliant CSS tokenizer
+- a reliable low level package to be used in CSS parsers
+
+What it is not:
+- opinionated
+- fast
+- small
+
+[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/css-tokenizer
+
+[CSS Tokenizer]: https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer
diff --git a/packages/css-tokenizer/package.json b/packages/css-tokenizer/package.json
new file mode 100644
index 000000000..ec22c6324
--- /dev/null
+++ b/packages/css-tokenizer/package.json
@@ -0,0 +1,65 @@
+{
+ "name": "@csstools/css-tokenizer",
+ "description": "Tokenize 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": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "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"
+ ],
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.js",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"",
+ "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",
+ "stryker": "stryker run --logLevel error",
+ "test": "npm run test:exports && node ./test/test.mjs",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/css-tokenizer"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "tokenizer"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/css-tokenizer/src/checks/four-code-points-would-start-cdo.ts b/packages/css-tokenizer/src/checks/four-code-points-would-start-cdo.ts
new file mode 100644
index 000000000..722a0e6ec
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/four-code-points-would-start-cdo.ts
@@ -0,0 +1,11 @@
+import { EXCLAMATION_MARK, HYPHEN_MINUS, LESS_THAN_SIGN } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-token
+export function checkIfFourCodePointsWouldStartCDO(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekFourCodePoints();
+ const [first, second, third, fourth] = peeked;
+
+ return first === LESS_THAN_SIGN && second === EXCLAMATION_MARK && third === HYPHEN_MINUS && fourth === HYPHEN_MINUS;
+}
diff --git a/packages/css-tokenizer/src/checks/matches-url-ident.ts b/packages/css-tokenizer/src/checks/matches-url-ident.ts
new file mode 100644
index 000000000..b8f7bd829
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/matches-url-ident.ts
@@ -0,0 +1,28 @@
+import { Context } from '../interfaces/context';
+
+const u = 'u'.charCodeAt(0);
+const U = 'U'.charCodeAt(0);
+const r = 'r'.charCodeAt(0);
+const R = 'R'.charCodeAt(0);
+const l = 'l'.charCodeAt(0);
+const L = 'L'.charCodeAt(0);
+
+export function checkIfCodePointsMatchURLIdent(ctx: Context, codePoints: Array): boolean {
+ if (codePoints.length !== 3) {
+ return false;
+ }
+
+ if (codePoints[0] !== u && codePoints[0] !== U) {
+ return false;
+ }
+
+ if (codePoints[1] !== r && codePoints[1] !== R) {
+ return false;
+ }
+
+ if (codePoints[2] !== l && codePoints[2] !== L) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/packages/css-tokenizer/src/checks/next-is-eof.ts b/packages/css-tokenizer/src/checks/next-is-eof.ts
new file mode 100644
index 000000000..18ee497a7
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/next-is-eof.ts
@@ -0,0 +1,11 @@
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+export function checkIfNextIsEOF(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/packages/css-tokenizer/src/checks/three-code-points-would-start-cdc.ts b/packages/css-tokenizer/src/checks/three-code-points-would-start-cdc.ts
new file mode 100644
index 000000000..38cdf70dc
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/three-code-points-would-start-cdc.ts
@@ -0,0 +1,11 @@
+import { GREATER_THAN_SIGN, HYPHEN_MINUS } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-token
+export function checkIfThreeCodePointsWouldStartCDC(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekThreeCodePoints();
+ const [first, second, third] = peeked;
+
+ return first === HYPHEN_MINUS && second === HYPHEN_MINUS && third === GREATER_THAN_SIGN;
+}
diff --git a/packages/css-tokenizer/src/checks/three-code-points-would-start-ident-sequence.ts b/packages/css-tokenizer/src/checks/three-code-points-would-start-ident-sequence.ts
new file mode 100644
index 000000000..28ffc8fc6
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/three-code-points-would-start-ident-sequence.ts
@@ -0,0 +1,48 @@
+import { HYPHEN_MINUS, LINE_FEED, REVERSE_SOLIDUS } from '../code-points/code-points';
+import { isIdentStartCodePoint } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { checkIfTwoCodePointsAreAValidEscape } from './two-code-points-are-valid-escape';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#would-start-an-identifier
+export function checkIfThreeCodePointsWouldStartAnIdentSequence(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekThreeCodePoints();
+ const [first, second, third] = peeked;
+
+ // // U+002D HYPHEN-MINUS
+ if (first === HYPHEN_MINUS) {
+ // If the second code point is a U+002D HYPHEN-MINUS return true
+ if (second === HYPHEN_MINUS) {
+ return true;
+ }
+
+ // If the second code point is an ident-start code point return true
+ if (isIdentStartCodePoint(second)) {
+ return true;
+ }
+
+ // If the second and third code points are a valid escape return true
+ if (second === REVERSE_SOLIDUS && third !== LINE_FEED) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // ident-start code point
+ // Return true.
+ if (isIdentStartCodePoint(first)) {
+ return true;
+ }
+
+ // U+005C REVERSE SOLIDUS (\)
+ if (first === REVERSE_SOLIDUS) {
+ // If the first and second code points are a valid escape, return true.
+ // Otherwise, return false.
+ return checkIfTwoCodePointsAreAValidEscape(ctx, reader);
+ }
+
+ // anything else
+ // Return false.
+ return false;
+}
diff --git a/packages/css-tokenizer/src/checks/three-code-points-would-start-number.ts b/packages/css-tokenizer/src/checks/three-code-points-would-start-number.ts
new file mode 100644
index 000000000..e4b964e9c
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/three-code-points-would-start-number.ts
@@ -0,0 +1,39 @@
+import { FULL_STOP, HYPHEN_MINUS, PLUS_SIGN } from '../code-points/code-points';
+import { isDigitCodePoint } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#starts-with-a-number
+export function checkIfThreeCodePointsWouldStartANumber(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekThreeCodePoints();
+ const [first, second, third] = peeked;
+
+ if (first === PLUS_SIGN || first === HYPHEN_MINUS) { // U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-)
+ // If the second code point is a digit, return true.
+ if (isDigitCodePoint(second)) {
+ return true;
+ }
+
+ // Otherwise, if the second code point is a U+002E FULL STOP (.)
+ if (second === FULL_STOP) {
+ // and the third code point is a digit, return true.
+ return isDigitCodePoint(third);
+ }
+
+ // Otherwise, return false.
+ return false;
+
+ } else if (first === FULL_STOP) { // U+002E FULL STOP (.)
+ // If the second code point is a digit, return true.
+ // Otherwise, return false.
+ return isDigitCodePoint(second);
+
+ } else if (isDigitCodePoint(first)) { // digit
+ // Return true.
+ return true;
+ }
+
+ // anything else
+ // Return false.
+ return false;
+}
diff --git a/packages/css-tokenizer/src/checks/two-code-points-are-valid-escape.ts b/packages/css-tokenizer/src/checks/two-code-points-are-valid-escape.ts
new file mode 100644
index 000000000..4e30730fb
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/two-code-points-are-valid-escape.ts
@@ -0,0 +1,20 @@
+import { LINE_FEED, REVERSE_SOLIDUS } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#starts-with-a-valid-escape
+export function checkIfTwoCodePointsAreAValidEscape(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekTwoCodePoints();
+ // If the first code point is not U+005C REVERSE SOLIDUS (\), return false.
+ if (peeked[0] !== REVERSE_SOLIDUS) { // "\"
+ return false;
+ }
+
+ // Otherwise, if the second code point is a newline, return false.
+ if (peeked[1] === LINE_FEED) {
+ return false;
+ }
+
+ // Otherwise, return true.
+ return true;
+}
diff --git a/packages/css-tokenizer/src/checks/two-code-points-start-comment.ts b/packages/css-tokenizer/src/checks/two-code-points-start-comment.ts
new file mode 100644
index 000000000..f24fabf56
--- /dev/null
+++ b/packages/css-tokenizer/src/checks/two-code-points-start-comment.ts
@@ -0,0 +1,18 @@
+import { ASTERISK, SOLIDUS } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-comments
+export function checkIfTwoCodePointsStartAComment(ctx: Context, reader: CodePointReader): boolean {
+ const peeked = reader.peekTwoCodePoints();
+ if (peeked[0] !== SOLIDUS) {
+ return false;
+ }
+
+ if (peeked[1] !== ASTERISK) {
+ return false;
+ }
+
+ // Otherwise, return true.
+ return true;
+}
diff --git a/packages/css-tokenizer/src/code-points/code-points.ts b/packages/css-tokenizer/src/code-points/code-points.ts
new file mode 100644
index 000000000..fca7ea361
--- /dev/null
+++ b/packages/css-tokenizer/src/code-points/code-points.ts
@@ -0,0 +1,78 @@
+/** ' */
+export const APOSTROPHE = '\u{27}'.charCodeAt(0);
+/** * */
+export const ASTERISK = '\u{2a}'.charCodeAt(0);
+/** \b */
+export const BACKSPACE = '\u{8}'.charCodeAt(0);
+/** \r */
+export const CARRIAGE_RETURN = '\u{d}'.charCodeAt(0);
+/** \t */
+export const CHARACTER_TABULATION = '\u{9}'.charCodeAt(0);
+/** : */
+export const COLON = '\u{3a}'.charCodeAt(0);
+/** , */
+export const COMMA = '\u{2c}'.charCodeAt(0);
+/** @ */
+export const COMMERCIAL_AT = '\u{40}'.charCodeAt(0);
+/** \x7F */
+export const DELETE = '\u{7f}'.charCodeAt(0);
+/** ! */
+export const EXCLAMATION_MARK = '\u{21}'.charCodeAt(0);
+/** \f */
+export const FORM_FEED = '\u{c}'.charCodeAt(0);
+/** . */
+export const FULL_STOP = '\u{2e}'.charCodeAt(0);
+/** > */
+export const GREATER_THAN_SIGN = '\u{3e}'.charCodeAt(0);
+/** - */
+export const HYPHEN_MINUS = '\u{2d}'.charCodeAt(0);
+/** \x1F */
+export const INFORMATION_SEPARATOR_ONE = '\u{1f}'.charCodeAt(0);
+/** E */
+export const LATIN_CAPITAL_LETTER_E = '\u{45}'.charCodeAt(0);
+/** e */
+export const LATIN_SMALL_LETTER_E = '\u{65}'.charCodeAt(0);
+/** { */
+export const LEFT_CURLY_BRACKET = '\u{7b}'.charCodeAt(0);
+/** ( */
+export const LEFT_PARENTHESIS = '\u{28}'.charCodeAt(0);
+/** [ */
+export const LEFT_SQUARE_BRACKET = '\u{5b}'.charCodeAt(0);
+/** < */
+export const LESS_THAN_SIGN = '\u{3c}'.charCodeAt(0);
+/** \n */
+export const LINE_FEED = '\u{a}'.charCodeAt(0);
+/** \v */
+export const LINE_TABULATION = '\u{b}'.charCodeAt(0);
+/** _ */
+export const LOW_LINE = '\u{5f}'.charCodeAt(0);
+/** \x10FFFF */
+export const MAXIMUM_ALLOWED_CODEPOINT = '\u{10FFFF}'.charCodeAt(0);
+/** \x00 */
+export const NULL = '\u{0}'.charCodeAt(0);
+/** # */
+export const NUMBER_SIGN = '\u{23}'.charCodeAt(0);
+/** % */
+export const PERCENTAGE_SIGN = '\u{25}'.charCodeAt(0);
+/** + */
+export const PLUS_SIGN = '\u{2b}'.charCodeAt(0);
+/** " */
+export const QUOTATION_MARK = '\u{22}'.charCodeAt(0);
+/** ๏ฟฝ */
+export const REPLACEMENT_CHARACTER = '\u{0FFFD}'.charCodeAt(0);
+/** \ */
+export const REVERSE_SOLIDUS = '\u{5c}'.charCodeAt(0);
+/** } */
+export const RIGHT_CURLY_BRACKET = '\u{7d}'.charCodeAt(0);
+/** ) */
+export const RIGHT_PARENTHESIS = '\u{29}'.charCodeAt(0);
+/** ] */
+export const RIGHT_SQUARE_BRACKET = '\u{5d}'.charCodeAt(0);
+/** ; */
+export const SEMICOLON = '\u{3b}'.charCodeAt(0);
+/** \u0E */
+export const SHIFT_OUT = '\u{e}'.charCodeAt(0);
+/** / */
+export const SOLIDUS = '\u{2f}'.charCodeAt(0);
+/** \u20 */
+export const SPACE = '\u{20}'.charCodeAt(0);
diff --git a/packages/css-tokenizer/src/code-points/ranges.ts b/packages/css-tokenizer/src/code-points/ranges.ts
new file mode 100644
index 000000000..9aa4628bd
--- /dev/null
+++ b/packages/css-tokenizer/src/code-points/ranges.ts
@@ -0,0 +1,170 @@
+import { BACKSPACE, DELETE, INFORMATION_SEPARATOR_ONE, LINE_TABULATION, LOW_LINE, HYPHEN_MINUS, NULL, SHIFT_OUT, LINE_FEED, CARRIAGE_RETURN, FORM_FEED, CHARACTER_TABULATION, SPACE } from './code-points';
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokenizer-definitions
+
+const digitsLow = '\u{30}'.charCodeAt(0);
+const digitsHigh = '\u{39}'.charCodeAt(0);
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#digit
+export function isDigitCodePoint(search: number): boolean {
+ if (digitsLow <= search && search <= digitsHigh) {
+ return true;
+ }
+
+ return false;
+}
+
+const letterUppercaseLow = '\u{41}'.charCodeAt(0);
+const letterUppercaseHigh = '\u{5a}'.charCodeAt(0);
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#uppercase-letter
+export function isUppercaseLetterCodePoint(search: number): boolean {
+ if (letterUppercaseLow <= search && search <= letterUppercaseHigh) {
+ return true;
+ }
+
+ return false;
+}
+
+const letterLowercaseLow = '\u{61}'.charCodeAt(0);
+const letterLowercaseHigh = '\u{7a}'.charCodeAt(0);
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#lowercase-letter
+export function isLowercaseLetterCodePoint(search: number): boolean {
+ if (letterLowercaseLow <= search && search <= letterLowercaseHigh) {
+ return true;
+ }
+
+ return false;
+}
+
+const afUppercaseHigh = '\u{46}'.charCodeAt(0);
+const afLowercaseHigh = '\u{66}'.charCodeAt(0);
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#hex-digit
+export function isHexDigitCodePoint(search: number): boolean {
+ if (isDigitCodePoint(search)) {
+ return true;
+ }
+
+ if (letterUppercaseLow <= search && search <= afUppercaseHigh) {
+ return true;
+ }
+
+ if (letterLowercaseLow <= search && search <= afLowercaseHigh) {
+ return true;
+ }
+
+ return false;
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#letter
+export function isLetterCodePoint(search: number): boolean {
+ if (isUppercaseLetterCodePoint(search) || isLowercaseLetterCodePoint(search)) {
+ return true;
+ }
+
+ return false;
+}
+
+const nonASCIILow = '\u{80}'.charCodeAt(0);
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#non-ascii-code-point
+export function isNonASCIICodePoint(search: number): boolean {
+ return search >= nonASCIILow;
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#ident-start-code-point
+export function isIdentStartCodePoint(search: number): boolean {
+ if (isLetterCodePoint(search)) {
+ return true;
+ }
+
+ if (isNonASCIICodePoint(search)) {
+ return true;
+ }
+
+ return search === LOW_LINE;
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#ident-code-point
+export function isIdentCodePoint(search: number): boolean {
+ if (isIdentStartCodePoint(search)) {
+ return true;
+ }
+
+ if (isDigitCodePoint(search)) {
+ return true;
+ }
+
+ return search === HYPHEN_MINUS;
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#non-printable-code-point
+export function isNonPrintableCodePoint(search: number): boolean {
+ if (search === LINE_TABULATION) {
+ return true;
+ }
+
+ if (search === DELETE) {
+ return true;
+ }
+
+ if (NULL <= search && search <= BACKSPACE) {
+ return true;
+ }
+
+ if (SHIFT_OUT <= search && search <= INFORMATION_SEPARATOR_ONE) {
+ return true;
+ }
+
+ return false;
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#whitespace
+export function isNewLine(search: number): boolean {
+ switch (search) {
+ case LINE_FEED:
+ case CARRIAGE_RETURN:
+ case FORM_FEED:
+ // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#input-preprocessing
+ // We can not follow the preprocessing rules because our output is text and must be minimally different from the input.
+ // Applying the preprocessing rules would make it impossible to match the input.
+ // A side effect of this is that our definition of whitespace is broader.
+ return true;
+ default:
+ return false;
+ }
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#whitespace
+export function isWhitespace(search: number): boolean {
+ switch (search) {
+ case LINE_FEED:
+ case CARRIAGE_RETURN:
+ case FORM_FEED:
+ // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#input-preprocessing
+ // We can not follow the preprocessing rules because our output is text and must be minimally different from the input.
+ // Applying the preprocessing rules would make it impossible to match the input.
+ // A side effect of this is that our definition of whitespace is broader.
+ return true;
+ case CHARACTER_TABULATION:
+ return true;
+ case SPACE:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+const surrogateLow = '\u{d800}'.charCodeAt(0);
+const surrogateHigh = '\u{dfff}'.charCodeAt(0);
+
+// https://infra.spec.whatwg.org/#surrogate
+export function isSurrogate(search: number): boolean {
+ if (surrogateLow <= search && search <= surrogateHigh) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/packages/css-tokenizer/src/consume/bad-url.ts b/packages/css-tokenizer/src/consume/bad-url.ts
new file mode 100644
index 000000000..40a44f9ab
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/bad-url.ts
@@ -0,0 +1,30 @@
+import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape';
+import { RIGHT_PARENTHESIS } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { consumeEscapedCodePoint } from './escaped-code-point';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-remnants-of-bad-url
+export function consumeBadURL(ctx: Context, reader: CodePointReader) {
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ return;
+ }
+
+ if (peeked === RIGHT_PARENTHESIS) {
+ reader.readCodePoint();
+ return;
+ }
+
+ if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) {
+ reader.readCodePoint();
+ consumeEscapedCodePoint(ctx, reader);
+ continue;
+ }
+
+ reader.readCodePoint();
+ continue;
+ }
+}
diff --git a/packages/css-tokenizer/src/consume/comment.ts b/packages/css-tokenizer/src/consume/comment.ts
new file mode 100644
index 000000000..6fc6d75c0
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/comment.ts
@@ -0,0 +1,50 @@
+import { ASTERISK, SOLIDUS } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { TokenComment, TokenType } from '../interfaces/token';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-comment
+export function consumeComment(ctx: Context, reader: CodePointReader): TokenComment {
+ reader.readCodePoint();
+ reader.readCodePoint();
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const codePoint = reader.readCodePoint();
+ if (codePoint === false) {
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming a comment.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.2. Consume comments',
+ 'Unexpected EOF',
+ ],
+ });
+
+ break;
+ }
+
+ if (codePoint !== ASTERISK) {
+ continue;
+ }
+
+ const close = reader.peekOneCodePoint();
+ if (close === false) {
+ continue;
+ }
+
+ if (close === SOLIDUS) {
+ reader.readCodePoint();
+ break;
+ }
+ }
+
+ return [
+ TokenType.Comment,
+ reader.representationString(),
+ ...reader.representation(),
+ undefined,
+ ];
+}
diff --git a/packages/css-tokenizer/src/consume/escaped-code-point.ts b/packages/css-tokenizer/src/consume/escaped-code-point.ts
new file mode 100644
index 000000000..19caeaaab
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/escaped-code-point.ts
@@ -0,0 +1,53 @@
+import { MAXIMUM_ALLOWED_CODEPOINT, REPLACEMENT_CHARACTER } from '../code-points/code-points';
+import { isHexDigitCodePoint, isSurrogate, isWhitespace } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-escaped-code-point
+export function consumeEscapedCodePoint(ctx: Context, reader: CodePointReader): number {
+ const codePoint = reader.readCodePoint();
+ if (codePoint === false) {
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming an escaped code point.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.7. Consume an escaped code point',
+ 'Unexpected EOF',
+ ],
+ });
+
+ return REPLACEMENT_CHARACTER;
+ }
+
+ if (isHexDigitCodePoint(codePoint)) {
+ const hexSequence: Array = [codePoint];
+
+ let peeked = reader.peekOneCodePoint();
+ while (peeked !== false && isHexDigitCodePoint(peeked) && hexSequence.length < 6) {
+ reader.readCodePoint();
+ hexSequence.push(peeked);
+ peeked = reader.peekOneCodePoint();
+ }
+
+ if (peeked !== false && isWhitespace(peeked)) {
+ reader.readCodePoint();
+ }
+
+ const codePointLiteral = parseInt(hexSequence.map((x) => String.fromCharCode(x)).join(''), 16);
+ if (codePointLiteral === 0) {
+ return REPLACEMENT_CHARACTER;
+ }
+ if (isSurrogate(codePointLiteral)) {
+ return REPLACEMENT_CHARACTER;
+ }
+ if (codePointLiteral > MAXIMUM_ALLOWED_CODEPOINT) {
+ return REPLACEMENT_CHARACTER;
+ }
+
+ return codePointLiteral;
+ }
+
+ return codePoint;
+}
diff --git a/packages/css-tokenizer/src/consume/hash-token.ts b/packages/css-tokenizer/src/consume/hash-token.ts
new file mode 100644
index 000000000..e9000903a
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/hash-token.ts
@@ -0,0 +1,45 @@
+import { checkIfThreeCodePointsWouldStartAnIdentSequence } from '../checks/three-code-points-would-start-ident-sequence';
+import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape';
+import { isIdentCodePoint } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { HashType, TokenDelim, TokenHash, TokenType } from '../interfaces/token';
+import { consumeIdentSequence } from './ident-sequence';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-token
+export function consumeHashToken(ctx: Context, reader: CodePointReader): TokenDelim|TokenHash {
+ reader.readCodePoint();
+
+ const peeked = reader.peekOneCodePoint();
+ if (
+ peeked !== false &&
+ isIdentCodePoint(peeked) ||
+ checkIfTwoCodePointsAreAValidEscape(ctx, reader)
+ ) {
+ let hashType = HashType.Unrestricted;
+
+ if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) {
+ hashType = HashType.ID;
+ }
+
+ const identSequence = consumeIdentSequence(ctx, reader);
+ return [
+ TokenType.Hash,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: identSequence.map((x) => String.fromCharCode(x)).join(''),
+ type: hashType,
+ },
+ ];
+ }
+
+ return [
+ TokenType.Delim,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: '#',
+ },
+ ];
+}
diff --git a/packages/css-tokenizer/src/consume/ident-like-token.ts b/packages/css-tokenizer/src/consume/ident-like-token.ts
new file mode 100644
index 000000000..f828602c2
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/ident-like-token.ts
@@ -0,0 +1,77 @@
+import { checkIfCodePointsMatchURLIdent } from '../checks/matches-url-ident';
+import { APOSTROPHE, LEFT_PARENTHESIS, QUOTATION_MARK } from '../code-points/code-points';
+import { isWhitespace } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { TokenBadURL, TokenFunction, TokenIdent, TokenType, TokenURL } from '../interfaces/token';
+import { consumeIdentSequence } from './ident-sequence';
+import { consumeUrlToken } from './url-token';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-ident-like-token
+export function consumeIdentLikeToken(ctx: Context, reader: CodePointReader): TokenIdent|TokenFunction|TokenURL|TokenBadURL {
+ const codePoints = consumeIdentSequence(ctx, reader);
+ if (checkIfCodePointsMatchURLIdent(ctx, codePoints)) {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === LEFT_PARENTHESIS) {
+ reader.readCodePoint();
+
+ let read = 0;
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const peeked2 = reader.peekTwoCodePoints();
+ if (isWhitespace(peeked2[0]) && isWhitespace(peeked2[1])) {
+ read += 2;
+ reader.readCodePoint();
+ reader.readCodePoint();
+ continue;
+ }
+
+ const firstNonWhitespace = isWhitespace(peeked2[0]) ? peeked2[1] : peeked2[0];
+ if (firstNonWhitespace === QUOTATION_MARK || firstNonWhitespace === APOSTROPHE) {
+ for (let i = 0; i < read; i++) {
+ reader.unreadCodePoint();
+ }
+
+ return [
+ TokenType.Function,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: codePoints.map((x) => String.fromCharCode(x)).join(''),
+ },
+ ];
+ }
+
+ break;
+ }
+
+ for (let i = 0; i < read; i++) {
+ reader.unreadCodePoint();
+ }
+
+ return consumeUrlToken(ctx, reader);
+ }
+ }
+
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === LEFT_PARENTHESIS) {
+ reader.readCodePoint();
+ return [
+ TokenType.Function,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: codePoints.map((x) => String.fromCharCode(x)).join(''),
+ },
+ ];
+ }
+
+ return [
+ TokenType.Ident,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: codePoints.map((x) => String.fromCharCode(x)).join(''),
+ },
+ ];
+}
diff --git a/packages/css-tokenizer/src/consume/ident-sequence.ts b/packages/css-tokenizer/src/consume/ident-sequence.ts
new file mode 100644
index 000000000..a62243771
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/ident-sequence.ts
@@ -0,0 +1,32 @@
+import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape';
+import { isIdentCodePoint } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { consumeEscapedCodePoint } from './escaped-code-point';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-name
+export function consumeIdentSequence(ctx: Context, reader: CodePointReader): Array {
+ const result = [];
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ return result;
+ }
+
+ if (isIdentCodePoint(peeked)) {
+ reader.readCodePoint();
+ result.push(peeked);
+ continue;
+ }
+
+ if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) {
+ reader.readCodePoint();
+ result.push(consumeEscapedCodePoint(ctx, reader));
+ continue;
+ }
+
+ return result;
+ }
+}
diff --git a/packages/css-tokenizer/src/consume/number.ts b/packages/css-tokenizer/src/consume/number.ts
new file mode 100644
index 000000000..5b399cb31
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/number.ts
@@ -0,0 +1,217 @@
+import { FULL_STOP, HYPHEN_MINUS, LATIN_CAPITAL_LETTER_E, LATIN_SMALL_LETTER_E, PLUS_SIGN } from '../code-points/code-points';
+import { isDigitCodePoint } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { NumberType } from '../interfaces/token';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-number
+export function consumeNumber(ctx: Context, reader: CodePointReader): [number, NumberType] {
+ // 1. Initially set type to "integer".
+ // Let repr be the empty string.
+ let type = NumberType.Integer;
+ const repr: Array = [];
+
+ {
+ // 2. If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), consume it and append it to repr.
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === PLUS_SIGN || peeked === HYPHEN_MINUS) {
+ reader.readCodePoint();
+ repr.push(peeked);
+ }
+
+ // 3. While the next input code point is a digit, consume it and append it to repr.
+ repr.push(...consumeDigits(reader));
+ }
+
+ {
+ // 4. If the next 2 input code points are U+002E FULL STOP (.) followed by a digit, then:
+ const peeked = reader.peekTwoCodePoints();
+ if (peeked[0] === FULL_STOP && isDigitCodePoint(peeked[1])) {
+ // 4.1. Consume them.
+ reader.readCodePoint();
+ reader.readCodePoint();
+
+ // 4.2. Append them to repr.
+ repr.push(...peeked);
+
+ // 4.3. Set type to "number".
+ type = NumberType.Number;
+
+ // 4.4. While the next input code point is a digit, consume it and append it to repr.
+ repr.push(...consumeDigits(reader));
+ }
+ }
+
+ {
+ // 5. If the next 2 or 3 input code points are U+0045 LATIN CAPITAL LETTER E (E) or U+0065 LATIN SMALL LETTER E (e),
+ // optionally followed by U+002D HYPHEN-MINUS (-) or U+002B PLUS SIGN (+),
+ // followed by a digit, then:
+ const peeked = reader.peekThreeCodePoints();
+ if (
+ (peeked[0] === LATIN_SMALL_LETTER_E || peeked[0] === LATIN_CAPITAL_LETTER_E) &&
+ isDigitCodePoint(peeked[1])
+ ) {
+ // 5.1. Consume them.
+ reader.readCodePoint();
+ reader.readCodePoint();
+
+ // 5.2. Append them to repr.
+ repr.push(...peeked);
+
+ // 5.3. Set type to "number".
+ type = NumberType.Number;
+
+ // 5.4. While the next input code point is a digit, consume it and append it to repr.
+ repr.push(...consumeDigits(reader));
+ }
+
+ if (
+ (peeked[0] === LATIN_SMALL_LETTER_E || peeked[0] === LATIN_CAPITAL_LETTER_E) &&
+ (
+ (peeked[1] === HYPHEN_MINUS || peeked[1] === PLUS_SIGN) &&
+ isDigitCodePoint(peeked[2])
+ )
+ ) {
+ // 5.1. Consume them.
+ reader.readCodePoint();
+ reader.readCodePoint();
+ reader.readCodePoint();
+
+ // 5.2. Append them to repr.
+ repr.push(...peeked);
+
+ // 5.3. Set type to "number".
+ type = NumberType.Number;
+
+ // 5.4. While the next input code point is a digit, consume it and append it to repr.
+ repr.push(...consumeDigits(reader));
+ }
+ }
+
+ // 6. Convert repr to a number, and set the value to the returned value.
+ const value = convertCodePointsToNumber(repr);
+
+ // 7. Return value and type.
+ return [value, type];
+}
+
+function consumeDigits(reader: CodePointReader): Array {
+ const value: Array = [];
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ return value;
+ }
+
+ if (isDigitCodePoint(peeked)) {
+ value.push(peeked);
+ reader.readCodePoint();
+ } else {
+ return value;
+ }
+ }
+}
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#convert-string-to-number
+function convertCodePointsToNumber(codePoints: Array): number {
+ let s = 1;
+ const iCodePoints: Array = [];
+ let i = 0;
+
+ let d = 0;
+ const fCodePoints: Array = [];
+ let f = 0;
+
+ let t = 1;
+
+ const eCodePoints: Array = [];
+ let e = 0;
+
+ let cursor = 0;
+
+ // 1. A sign: a single U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), or the empty string.
+ // Let s be the number -1 if the sign is U+002D HYPHEN-MINUS (-);
+ // otherwise, let s be the number 1.
+ if (codePoints[cursor] === HYPHEN_MINUS) {
+ cursor++;
+ s = -1;
+ } else if (codePoints[cursor] === PLUS_SIGN) {
+ cursor++;
+ }
+
+ // 2. An integer part: zero or more digits.
+ // If there is at least one digit,
+ // let i be the number formed by interpreting the digits as a base-10 integer;
+ // otherwise, let i be the number 0.
+ while (cursor < codePoints.length && isDigitCodePoint(codePoints[cursor])) {
+ iCodePoints.push(codePoints[cursor]);
+ cursor++;
+ }
+
+ i = digitCodePointsToInteger(iCodePoints);
+
+ // 3. A decimal point: a single U+002E FULL STOP (.), or the empty string.
+ if (codePoints[cursor] === FULL_STOP) {
+ cursor++;
+ }
+
+ // 4. A fractional part: zero or more digits.
+ // If there is at least one digit,
+ // let f be the number formed by interpreting the digits as a base-10 integer and d be the number of digits;
+ // otherwise, let f and d be the number 0.
+ while (cursor < codePoints.length && isDigitCodePoint(codePoints[cursor])) {
+ fCodePoints.push(codePoints[cursor]);
+ cursor++;
+ }
+
+ d = fCodePoints.length;
+ f = (digitCodePointsToInteger(fCodePoints) / Math.pow(10, d));
+
+ // 5. An exponent indicator: a single U+0045 LATIN CAPITAL LETTER E (E) or U+0065 LATIN SMALL LETTER E (e), or the empty string.
+ if (codePoints[cursor] === LATIN_SMALL_LETTER_E || codePoints[cursor] === LATIN_CAPITAL_LETTER_E) {
+ cursor++;
+ }
+
+ // 6. An exponent sign: a single U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), or the empty string.
+ // Let t be the number -1 if the sign is U+002D HYPHEN-MINUS (-);
+ // otherwise, let t be the number 1.
+ if (codePoints[cursor] === HYPHEN_MINUS) {
+ cursor++;
+ t = -1;
+ } else if (codePoints[cursor] === PLUS_SIGN) {
+ cursor++;
+ }
+
+ // 7. An exponent: zero or more digits.
+ // If there is at least one digit,
+ // let e be the number formed by interpreting the digits as a base-10 integer;
+ // otherwise, let e be the number 0.
+ while (cursor < codePoints.length && isDigitCodePoint(codePoints[cursor])) {
+ eCodePoints.push(codePoints[cursor]);
+ cursor++;
+ }
+
+ e = digitCodePointsToInteger(eCodePoints);
+
+ // Return the number sยท(i + fยท10-d)ยท10te.
+ return s * (i + f) * Math.pow(10, t * e);
+}
+
+function digitCodePointsToInteger(codePoints: Array): number {
+ if (codePoints.length === 0) {
+ return 0;
+ }
+
+ const stringValue = codePoints.map((x) => {
+ return String.fromCharCode(x);
+ }).join('');
+
+ const integerValue = Number.parseInt(stringValue, 10);
+ if (Number.isNaN(integerValue)) {
+ throw new Error(`Unexpected "NaN" result when parsing a number from digit code points: "${stringValue}"`);
+ }
+
+ return integerValue;
+}
diff --git a/packages/css-tokenizer/src/consume/numeric-token.ts b/packages/css-tokenizer/src/consume/numeric-token.ts
new file mode 100644
index 000000000..3059005c5
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/numeric-token.ts
@@ -0,0 +1,53 @@
+import { checkIfThreeCodePointsWouldStartAnIdentSequence } from '../checks/three-code-points-would-start-ident-sequence';
+import { PERCENTAGE_SIGN } from '../code-points/code-points';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { TokenDimension, TokenNumber, TokenPercentage, TokenType } from '../interfaces/token';
+import { consumeIdentSequence } from './ident-sequence';
+import { consumeNumber } from './number';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-numeric-token
+export function consumeNumericToken(ctx: Context, reader: CodePointReader): TokenPercentage|TokenNumber|TokenDimension {
+ const numberValue = consumeNumber(ctx, reader);
+
+ if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) {
+ const unit = consumeIdentSequence(ctx, reader);
+
+ return [
+ TokenType.Dimension,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: numberValue[0],
+ type: numberValue[1],
+ unit: unit.map((x) => String.fromCharCode(x)).join(''),
+ },
+ ];
+ }
+
+ {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === PERCENTAGE_SIGN) {
+ reader.readCodePoint();
+
+ return [
+ TokenType.Percentage,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: numberValue[0],
+ },
+ ];
+ }
+ }
+
+ return [
+ TokenType.Number,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: numberValue[0],
+ type: numberValue[1],
+ },
+ ];
+}
diff --git a/packages/css-tokenizer/src/consume/string-token.ts b/packages/css-tokenizer/src/consume/string-token.ts
new file mode 100644
index 000000000..5bc5ff921
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/string-token.ts
@@ -0,0 +1,74 @@
+import { REVERSE_SOLIDUS } from '../code-points/code-points';
+import { isNewLine } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { TokenBadString, TokenString, TokenType } from '../interfaces/token';
+import { consumeEscapedCodePoint } from './escaped-code-point';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-string-token
+export function consumeStringToken(ctx: Context, reader: CodePointReader): TokenBadString|TokenString {
+ let result = '';
+
+ const first = reader.readCodePoint();
+ if (first === false) {
+ throw new Error('Unexpected EOF');
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const next = reader.readCodePoint();
+ if (next === false) {
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming a string token.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.5. Consume a string token',
+ 'Unexpected EOF',
+ ],
+ });
+
+ return [TokenType.String, reader.representationString(), ...representation, { value: result }];
+ }
+
+ if (isNewLine(next)) {
+ {
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected newline while consuming a string token.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.5. Consume a string token',
+ 'Unexpected newline',
+ ],
+ });
+ }
+
+ reader.unreadCodePoint();
+ return [TokenType.BadString, reader.representationString(), ...reader.representation(), undefined];
+ }
+
+ if (next === first) {
+ return [TokenType.String, reader.representationString(), ...reader.representation(), { value: result }];
+ }
+
+ if (next === REVERSE_SOLIDUS) {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ continue;
+ }
+
+ if (isNewLine(peeked)) {
+ reader.readCodePoint();
+ continue;
+ }
+
+ result += String.fromCharCode(consumeEscapedCodePoint(ctx, reader));
+ continue;
+ }
+
+ result += String.fromCharCode(next);
+ }
+}
diff --git a/packages/css-tokenizer/src/consume/url-token.ts b/packages/css-tokenizer/src/consume/url-token.ts
new file mode 100644
index 000000000..bac0bd77a
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/url-token.ts
@@ -0,0 +1,153 @@
+import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape';
+import { APOSTROPHE, LEFT_PARENTHESIS, QUOTATION_MARK, REVERSE_SOLIDUS, RIGHT_PARENTHESIS } from '../code-points/code-points';
+import { isNonPrintableCodePoint, isWhitespace } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { TokenBadURL, TokenType, TokenURL } from '../interfaces/token';
+import { consumeBadURL } from './bad-url';
+import { consumeEscapedCodePoint } from './escaped-code-point';
+import { consumeWhiteSpace } from './whitespace-token';
+
+// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-url-token
+export function consumeUrlToken(ctx: Context, reader: CodePointReader): TokenURL|TokenBadURL {
+ consumeWhiteSpace(ctx, reader);
+ let string = '';
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming a url token.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.6. Consume a url token',
+ 'Unexpected EOF',
+ ],
+ });
+
+ return [
+ TokenType.URL,
+ reader.representationString(),
+ ...representation,
+ {
+ value: string,
+ },
+ ];
+ }
+
+ if (peeked === RIGHT_PARENTHESIS) {
+ reader.readCodePoint();
+ return [
+ TokenType.URL,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: string,
+ },
+ ];
+ }
+
+ if (isWhitespace(peeked)) {
+ consumeWhiteSpace(ctx, reader);
+ const peeked2 = reader.peekOneCodePoint();
+ if (peeked2 === false) {
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected EOF while consuming a url token.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.6. Consume a url token',
+ 'Consume as much whitespace as possible',
+ 'Unexpected EOF',
+ ],
+ });
+
+ return [
+ TokenType.URL,
+ reader.representationString(),
+ ...representation,
+ {
+ value: string,
+ },
+ ];
+ }
+
+ if (peeked2 === RIGHT_PARENTHESIS) {
+ reader.readCodePoint();
+ return [
+ TokenType.URL,
+ reader.representationString(),
+ ...reader.representation(),
+ {
+ value: string,
+ },
+ ];
+ }
+
+ consumeBadURL(ctx, reader);
+ return [
+ TokenType.BadURL,
+ reader.representationString(),
+ ...reader.representation(),
+ undefined,
+ ];
+ }
+
+ if (peeked === QUOTATION_MARK || peeked === APOSTROPHE || peeked === LEFT_PARENTHESIS || isNonPrintableCodePoint(peeked)) {
+ consumeBadURL(ctx, reader);
+
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Unexpected character while consuming a url token.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.6. Consume a url token',
+ 'Unexpected U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE (\'), U+0028 LEFT PARENTHESIS (() or non-printable code point',
+ ],
+ });
+
+ return [
+ TokenType.BadURL,
+ reader.representationString(),
+ ...representation,
+ undefined,
+ ];
+ }
+
+ if (peeked === REVERSE_SOLIDUS) {
+ if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) {
+ string += String.fromCharCode(consumeEscapedCodePoint(ctx, reader));
+ continue;
+ }
+
+ consumeBadURL(ctx, reader);
+
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Invalid escape sequence while consuming a url token.',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.6. Consume a url token',
+ 'U+005C REVERSE SOLIDUS (\\)',
+ 'The input stream does not start with a valid escape sequence',
+ ],
+ });
+
+ return [
+ TokenType.BadURL,
+ reader.representationString(),
+ ...representation,
+ undefined,
+ ];
+ }
+
+ reader.readCodePoint();
+ string += String.fromCharCode(peeked);
+ }
+}
diff --git a/packages/css-tokenizer/src/consume/whitespace-token.ts b/packages/css-tokenizer/src/consume/whitespace-token.ts
new file mode 100644
index 000000000..b23c3f285
--- /dev/null
+++ b/packages/css-tokenizer/src/consume/whitespace-token.ts
@@ -0,0 +1,44 @@
+import { isWhitespace } from '../code-points/ranges';
+import { CodePointReader } from '../interfaces/code-point-reader';
+import { Context } from '../interfaces/context';
+import { TokenType, TokenWhitespace } from '../interfaces/token';
+
+export function consumeWhiteSpace(ctx: Context, reader: CodePointReader, max = -1): TokenWhitespace {
+ let current = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (max !== -1 && current === max) {
+ return [
+ TokenType.Whitespace,
+ reader.representationString(),
+ ...reader.representation(),
+ undefined,
+ ];
+ }
+
+ current++;
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ return [
+ TokenType.Whitespace,
+ reader.representationString(),
+ ...reader.representation(),
+ undefined,
+ ];
+ }
+
+ if (!isWhitespace(peeked)) {
+ break;
+ }
+
+ reader.readCodePoint();
+ }
+
+ return [
+ TokenType.Whitespace,
+ reader.representationString(),
+ ...reader.representation(),
+ undefined,
+ ];
+}
diff --git a/packages/css-tokenizer/src/index.ts b/packages/css-tokenizer/src/index.ts
new file mode 100644
index 000000000..015d99029
--- /dev/null
+++ b/packages/css-tokenizer/src/index.ts
@@ -0,0 +1,35 @@
+export type { CSSToken } from './interfaces/token';
+export { Reader } from './reader';
+export { TokenType, NumberType, mirrorVariantType, isToken } from './interfaces/token';
+export { stringify } from './stringify';
+export { tokenizer } from './tokenizer';
+export { cloneTokens } from './util/clone-tokens';
+
+export type {
+ TokenAtKeyword,
+ TokenBadString,
+ TokenBadURL,
+ TokenCDC,
+ TokenCDO,
+ TokenColon,
+ TokenComma,
+ TokenComment,
+ TokenDelim,
+ TokenDimension,
+ TokenEOF,
+ TokenFunction,
+ TokenHash,
+ TokenIdent,
+ TokenNumber,
+ TokenPercentage,
+ TokenSemicolon,
+ TokenString,
+ TokenURL,
+ TokenWhitespace,
+ TokenOpenParen,
+ TokenCloseParen,
+ TokenOpenSquare,
+ TokenCloseSquare,
+ TokenOpenCurly,
+ TokenCloseCurly,
+} from './interfaces/token';
diff --git a/packages/css-tokenizer/src/interfaces/code-point-reader.ts b/packages/css-tokenizer/src/interfaces/code-point-reader.ts
new file mode 100644
index 000000000..6f1c8b216
--- /dev/null
+++ b/packages/css-tokenizer/src/interfaces/code-point-reader.ts
@@ -0,0 +1,23 @@
+// Note 1
+// The CSS specification refers to units of text as code points.
+// We follow this naming.
+// This must not be confused with `codePointAt`|`charCodeAt` which are JS API's.
+// A char code in JS is equivalent to a code point from the CSS Specification.
+
+export type CodePointReader = {
+ cursorPositionOfLastReadCodePoint(): number;
+
+ peekOneCodePoint(): number | false
+ peekTwoCodePoints(): [number, number] | [number] | []
+ peekThreeCodePoints(): [number, number, number] | [number, number] | [number] | []
+ peekFourCodePoints(): [number, number, number, number] | [number, number, number] | [number, number] | [number] | []
+
+ readCodePoint(): number | false
+ unreadCodePoint(): boolean
+
+ representation(): [number, number]
+ representationString(): string
+ resetRepresentation()
+
+ slice(start: number, end: number): string
+}
diff --git a/packages/css-tokenizer/src/interfaces/context.ts b/packages/css-tokenizer/src/interfaces/context.ts
new file mode 100644
index 000000000..27eb77de6
--- /dev/null
+++ b/packages/css-tokenizer/src/interfaces/context.ts
@@ -0,0 +1,5 @@
+import { ParserError } from './error';
+
+export type Context = {
+ onParseError: (error: ParserError) => void
+}
diff --git a/packages/css-tokenizer/src/interfaces/error.ts b/packages/css-tokenizer/src/interfaces/error.ts
new file mode 100644
index 000000000..e8c677745
--- /dev/null
+++ b/packages/css-tokenizer/src/interfaces/error.ts
@@ -0,0 +1,6 @@
+export type ParserError = {
+ message: string,
+ start: number,
+ end: number,
+ state: Array
+}
diff --git a/packages/css-tokenizer/src/interfaces/token.ts b/packages/css-tokenizer/src/interfaces/token.ts
new file mode 100644
index 000000000..81a916036
--- /dev/null
+++ b/packages/css-tokenizer/src/interfaces/token.ts
@@ -0,0 +1,185 @@
+export enum TokenType {
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#comment-diagram */
+ Comment = 'comment',
+
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-at-keyword-token */
+ AtKeyword = 'at-keyword-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-bad-string-token */
+ BadString = 'bad-string-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-bad-url-token */
+ BadURL = 'bad-url-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-cdc-token */
+ CDC = 'CDC-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-cdo-token */
+ CDO = 'CDO-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-colon-token */
+ Colon = 'colon-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-comma-token */
+ Comma = 'comma-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-delim-token */
+ Delim = 'delim-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-dimension-token */
+ Dimension = 'dimension-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-eof-token */
+ EOF = 'EOF-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-function-token */
+ Function = 'function-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-hash-token */
+ Hash = 'hash-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token */
+ Ident = 'ident-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-percentage-token */
+ Number = 'number-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-percentage-token */
+ Percentage = 'percentage-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-semicolon-token */
+ Semicolon = 'semicolon-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-string-token */
+ String = 'string-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-url-token */
+ URL = 'url-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-whitespace-token */
+ Whitespace = 'whitespace-token',
+
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-open-paren */
+ OpenParen = '(-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-close-paren */
+ CloseParen = ')-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-open-square */
+ OpenSquare = '[-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-close-square */
+ CloseSquare = ']-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-open-curly */
+ OpenCurly = '{-token',
+ /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-close-curly */
+ CloseCurly = '}-token',
+}
+
+export enum NumberType {
+ Integer = 'integer',
+ Number = 'number',
+}
+
+export enum HashType {
+ Unrestricted = 'unrestricted',
+ ID = 'id',
+}
+
+export type TokenAtKeyword = Token;
+export type TokenBadString = Token;
+export type TokenBadURL = Token;
+export type TokenCDC = Token;
+export type TokenCDO = Token;
+export type TokenColon = Token;
+export type TokenComma = Token;
+export type TokenComment = Token;
+export type TokenDelim = Token;
+export type TokenDimension = Token;
+export type TokenEOF = Token;
+export type TokenFunction = Token;
+export type TokenHash = Token;
+export type TokenIdent = Token;
+export type TokenNumber = Token;
+export type TokenPercentage = Token;
+export type TokenSemicolon = Token;
+export type TokenString = Token;
+export type TokenURL = Token;
+export type TokenWhitespace = Token;
+
+export type TokenOpenParen = Token;
+export type TokenCloseParen = Token;
+export type TokenOpenSquare = Token;
+export type TokenCloseSquare = Token;
+export type TokenOpenCurly = Token;
+export type TokenCloseCurly = Token;
+
+export type CSSToken = TokenAtKeyword |
+ TokenBadString |
+ TokenBadURL |
+ TokenCDC |
+ TokenCDO |
+ TokenColon |
+ TokenComma |
+ TokenComment |
+ TokenDelim |
+ TokenDimension |
+ TokenEOF |
+ TokenFunction |
+ TokenHash |
+ TokenIdent |
+ TokenNumber |
+ TokenPercentage |
+ TokenSemicolon |
+ TokenString |
+ TokenURL |
+ TokenWhitespace |
+ TokenOpenParen |
+ TokenCloseParen |
+ TokenOpenSquare |
+ TokenCloseSquare |
+ TokenOpenCurly |
+ TokenCloseCurly;
+
+export type Token = [
+ /** The type of token */
+ T,
+ /** The token representation */
+ string,
+ /** Start position of representation */
+ number,
+ /** End position of representation */
+ number,
+ /** Extra data */
+ U,
+]
+
+export function mirrorVariantType(type: TokenType): TokenType|null {
+ switch (type) {
+ case TokenType.OpenParen:
+ return TokenType.CloseParen;
+ case TokenType.CloseParen:
+ return TokenType.OpenParen;
+
+ case TokenType.OpenCurly:
+ return TokenType.CloseCurly;
+ case TokenType.CloseCurly:
+ return TokenType.OpenCurly;
+
+ case TokenType.OpenSquare:
+ return TokenType.CloseSquare;
+ case TokenType.CloseSquare:
+ return TokenType.OpenSquare;
+
+ default:
+ return null;
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function isToken(x: any): x is CSSToken {
+ if (!Array.isArray(x)) {
+ return false;
+ }
+
+ if (x.length < 4) {
+ return false;
+ }
+
+ if (!(x[0] in TokenType)) {
+ return false;
+ }
+
+ if (typeof x[1] !== 'string') {
+ return false;
+ }
+
+ if (typeof x[2] !== 'number') {
+ return false;
+ }
+
+ if (typeof x[3] !== 'number') {
+ return false;
+ }
+
+ return true;
+}
diff --git a/packages/css-tokenizer/src/reader.ts b/packages/css-tokenizer/src/reader.ts
new file mode 100644
index 000000000..287b5627d
--- /dev/null
+++ b/packages/css-tokenizer/src/reader.ts
@@ -0,0 +1,139 @@
+import { CodePointReader } from './interfaces/code-point-reader';
+
+export class Reader implements CodePointReader {
+ #cursor: number;
+ #stringSource = '';
+ #codePointSource: Array = [];
+ #length = 0;
+
+ #representationStart = 0;
+ #representationEnd = -1;
+
+ constructor(source: string) {
+ this.#cursor = 0;
+ this.#stringSource = source;
+ this.#length = source.length;
+
+ for (let i = 0; i < this.#length; i++) {
+ this.#codePointSource.push(this.#stringSource.charCodeAt(i));
+ }
+ }
+
+ cursorPositionOfLastReadCodePoint(): number {
+ return this.#cursor - 1;
+ }
+
+ peekOneCodePoint(): number | false {
+ const first = this.#codePointSource[this.#cursor];
+ if (typeof first === 'undefined') {
+ return false;
+ }
+
+ return first;
+ }
+
+ peekTwoCodePoints(): [number, number] | [number] | [] {
+ const first = this.#codePointSource[this.#cursor];
+ if (typeof first === 'undefined') {
+ return [];
+ }
+
+ const second = this.#codePointSource[this.#cursor + 1];
+ if (typeof second === 'undefined') {
+ return [first];
+ }
+
+ return [first, second];
+ }
+
+ peekThreeCodePoints(): [number, number, number] | [number, number] | [number] | [] {
+ const first = this.#codePointSource[this.#cursor];
+ if (typeof first === 'undefined') {
+ return [];
+ }
+
+ const second = this.#codePointSource[this.#cursor + 1];
+ if (typeof second === 'undefined') {
+ return [first];
+ }
+
+ const third = this.#codePointSource[this.#cursor + 2];
+ if (typeof third === 'undefined') {
+ return [first, second];
+ }
+
+ return [first, second, third];
+ }
+
+ peekFourCodePoints(): [number, number, number, number] | [number, number, number] | [number, number] | [number] | [] {
+ const first = this.#codePointSource[this.#cursor];
+ if (typeof first === 'undefined') {
+ return [];
+ }
+
+ const second = this.#codePointSource[this.#cursor + 1];
+ if (typeof second === 'undefined') {
+ return [first];
+ }
+
+ const third = this.#codePointSource[this.#cursor + 2];
+ if (typeof third === 'undefined') {
+ return [first, second];
+ }
+
+ const fourth = this.#codePointSource[this.#cursor + 2];
+ if (typeof fourth === 'undefined') {
+ return [first, second, third];
+ }
+
+ return [first, second, third, fourth];
+ }
+
+ readCodePoint(): number | false {
+ const codePoint = this.#codePointSource[this.#cursor];
+ if (typeof codePoint === 'undefined') {
+ return false;
+ }
+
+ this.#representationEnd = this.#cursor;
+ this.#cursor += 1;
+
+ return codePoint;
+ }
+
+ unreadCodePoint(): boolean {
+ if (this.#cursor === 0) {
+ return false;
+ }
+
+ this.#representationEnd = this.#cursor - 1;
+ this.#cursor -= 1;
+
+ return true;
+ }
+
+ representation(): [number, number] {
+ return [
+ this.#representationStart,
+ this.#representationEnd,
+ ];
+ }
+
+ representationString(): string {
+ const representation = this.representation();
+ if (representation[1] === -1) {
+ return '';
+ }
+
+ return this.slice(representation[0], representation[1] + 1);
+ }
+
+ resetRepresentation() {
+ this.#representationStart = this.#cursor;
+ this.#representationEnd = -1;
+ }
+
+ slice(start: number, end: number): string {
+ return this.#stringSource.slice(start, end);
+ }
+}
diff --git a/packages/css-tokenizer/src/stringify.ts b/packages/css-tokenizer/src/stringify.ts
new file mode 100644
index 000000000..90d190871
--- /dev/null
+++ b/packages/css-tokenizer/src/stringify.ts
@@ -0,0 +1,10 @@
+import type { CSSToken } from './interfaces/token';
+
+export function stringify(...tokens: Array): string {
+ let buffer = '';
+ for (let i = 0; i < tokens.length; i++) {
+ buffer = buffer + tokens[i][1];
+ }
+
+ return buffer;
+}
diff --git a/packages/css-tokenizer/src/tokenizer.ts b/packages/css-tokenizer/src/tokenizer.ts
new file mode 100644
index 000000000..b6db134d2
--- /dev/null
+++ b/packages/css-tokenizer/src/tokenizer.ts
@@ -0,0 +1,206 @@
+import { checkIfFourCodePointsWouldStartCDO } from './checks/four-code-points-would-start-cdo';
+import { checkIfThreeCodePointsWouldStartAnIdentSequence } from './checks/three-code-points-would-start-ident-sequence';
+import { checkIfThreeCodePointsWouldStartANumber } from './checks/three-code-points-would-start-number';
+import { checkIfTwoCodePointsStartAComment } from './checks/two-code-points-start-comment';
+import { checkIfThreeCodePointsWouldStartCDC } from './checks/three-code-points-would-start-cdc';
+import { APOSTROPHE, COLON, COMMA, COMMERCIAL_AT, FULL_STOP, HYPHEN_MINUS, LEFT_CURLY_BRACKET, LEFT_PARENTHESIS, LEFT_SQUARE_BRACKET, LESS_THAN_SIGN, NUMBER_SIGN, PLUS_SIGN, QUOTATION_MARK, REVERSE_SOLIDUS, RIGHT_CURLY_BRACKET, RIGHT_PARENTHESIS, RIGHT_SQUARE_BRACKET, SEMICOLON } from './code-points/code-points';
+import { isDigitCodePoint, isIdentStartCodePoint, isWhitespace } from './code-points/ranges';
+import { consumeComment } from './consume/comment';
+import { consumeHashToken } from './consume/hash-token';
+import { consumeIdentSequence } from './consume/ident-sequence';
+import { consumeNumericToken } from './consume/numeric-token';
+import { consumeWhiteSpace } from './consume/whitespace-token';
+import { CSSToken, TokenType } from './interfaces/token';
+import { Reader } from './reader';
+import { consumeStringToken } from './consume/string-token';
+import { consumeIdentLikeToken } from './consume/ident-like-token';
+import { checkIfTwoCodePointsAreAValidEscape } from './checks/two-code-points-are-valid-escape';
+import { ParserError } from './interfaces/error';
+
+interface Stringer {
+ valueOf(): string
+}
+
+export function tokenizer(input: { css: Stringer }, options?: { commentsAreTokens?: boolean, onParseError?: (error: ParserError) => void }) {
+ const css = input.css.valueOf();
+
+ const reader = new Reader(css);
+
+ const ctx = {
+ onParseError: options?.onParseError ?? (() => { /* noop */ }),
+ };
+
+ function endOfFile() {
+ return reader.peekOneCodePoint() === false;
+ }
+
+ function nextToken(): CSSToken | undefined {
+ reader.resetRepresentation();
+
+ if (checkIfTwoCodePointsStartAComment(ctx, reader)) {
+ if (options?.commentsAreTokens) {
+ return consumeComment(ctx, reader);
+ } else {
+ consumeComment(ctx, reader);
+ }
+ }
+
+ reader.resetRepresentation();
+
+ const peeked = reader.peekOneCodePoint();
+ if (peeked === false) {
+ return [TokenType.EOF, '', -1, -1, undefined];
+ }
+
+ // Simple, one character tokens:
+ switch (peeked) {
+ case COMMA:
+ reader.readCodePoint();
+ return [TokenType.Comma, reader.representationString(), ...reader.representation(), undefined];
+ case COLON:
+ reader.readCodePoint();
+ return [TokenType.Colon, reader.representationString(), ...reader.representation(), undefined];
+ case SEMICOLON:
+ reader.readCodePoint();
+ return [TokenType.Semicolon, reader.representationString(), ...reader.representation(), undefined];
+ case LEFT_PARENTHESIS:
+ reader.readCodePoint();
+ return [TokenType.OpenParen, reader.representationString(), ...reader.representation(), undefined];
+ case RIGHT_PARENTHESIS:
+ reader.readCodePoint();
+ return [TokenType.CloseParen, reader.representationString(), ...reader.representation(), undefined];
+ case LEFT_SQUARE_BRACKET:
+ reader.readCodePoint();
+ return [TokenType.OpenSquare, reader.representationString(), ...reader.representation(), undefined];
+ case RIGHT_SQUARE_BRACKET:
+ reader.readCodePoint();
+ return [TokenType.CloseSquare, reader.representationString(), ...reader.representation(), undefined];
+ case LEFT_CURLY_BRACKET:
+ reader.readCodePoint();
+ return [TokenType.OpenCurly, reader.representationString(), ...reader.representation(), undefined];
+ case RIGHT_CURLY_BRACKET:
+ reader.readCodePoint();
+ return [TokenType.CloseCurly, reader.representationString(), ...reader.representation(), undefined];
+ }
+
+ switch (peeked) {
+ case APOSTROPHE:
+ case QUOTATION_MARK:
+ return consumeStringToken(ctx, reader);
+ case NUMBER_SIGN:
+ return consumeHashToken(ctx, reader);
+
+ case PLUS_SIGN:
+ case FULL_STOP: {
+ if (checkIfThreeCodePointsWouldStartANumber(ctx, reader)) {
+ return consumeNumericToken(ctx, reader);
+ }
+
+ reader.readCodePoint();
+ return [TokenType.Delim, reader.representationString(), ...reader.representation(), {
+ value: String.fromCharCode(peeked),
+ }];
+ }
+
+ case HYPHEN_MINUS: {
+ if (checkIfThreeCodePointsWouldStartANumber(ctx, reader)) {
+ return consumeNumericToken(ctx, reader);
+ }
+
+ if (checkIfThreeCodePointsWouldStartCDC(ctx, reader)) {
+ reader.readCodePoint();
+ reader.readCodePoint();
+ reader.readCodePoint();
+
+ return [TokenType.CDC, reader.representationString(), ...reader.representation(), undefined];
+ }
+
+ if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) {
+ return consumeIdentLikeToken(ctx, reader);
+ }
+
+ reader.readCodePoint();
+ return [TokenType.Delim, reader.representationString(), ...reader.representation(), {
+ value: '-',
+ }];
+ }
+
+ case LESS_THAN_SIGN: {
+ if (checkIfFourCodePointsWouldStartCDO(ctx, reader)) {
+ reader.readCodePoint();
+ reader.readCodePoint();
+ reader.readCodePoint();
+ reader.readCodePoint();
+
+ return [TokenType.CDO, reader.representationString(), ...reader.representation(), undefined];
+ }
+
+ reader.readCodePoint();
+ return [TokenType.Delim, reader.representationString(), ...reader.representation(), {
+ value: '<',
+ }];
+ }
+
+ case COMMERCIAL_AT: {
+ reader.readCodePoint();
+ if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) {
+ const identSequence = consumeIdentSequence(ctx, reader);
+
+ return [TokenType.AtKeyword, reader.representationString(), ...reader.representation(), {
+ value: identSequence.map((x) => String.fromCharCode(x)).join(''),
+ }];
+ }
+
+ return [TokenType.Delim, reader.representationString(), ...reader.representation(), {
+ value: '@',
+ }];
+ }
+
+ case REVERSE_SOLIDUS: {
+ if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) {
+ return consumeIdentLikeToken(ctx, reader);
+ }
+
+ reader.readCodePoint();
+
+ const representation = reader.representation();
+ ctx.onParseError({
+ message: 'Invalid escape sequence after "\\"',
+ start: representation[0],
+ end: representation[1],
+ state: [
+ '4.3.1. Consume a token',
+ 'U+005C REVERSE SOLIDUS (\\)',
+ 'The input stream does not start with a valid escape sequence',
+ ],
+ });
+
+ return [TokenType.Delim, reader.representationString(), ...representation, {
+ value: '\\',
+ }];
+ }
+ }
+
+ if (isWhitespace(peeked)) {
+ return consumeWhiteSpace(ctx, reader);
+ }
+
+ if (isDigitCodePoint(peeked)) {
+ return consumeNumericToken(ctx, reader);
+ }
+
+ if (isIdentStartCodePoint(peeked)) {
+ return consumeIdentLikeToken(ctx, reader);
+ }
+
+ reader.readCodePoint();
+ return [TokenType.Delim, reader.representationString(), ...reader.representation(), {
+ value: String.fromCharCode(peeked),
+ }];
+ }
+
+ return {
+ nextToken: nextToken,
+ endOfFile: endOfFile,
+ };
+}
diff --git a/packages/css-tokenizer/src/util/clone-tokens.ts b/packages/css-tokenizer/src/util/clone-tokens.ts
new file mode 100644
index 000000000..e70353307
--- /dev/null
+++ b/packages/css-tokenizer/src/util/clone-tokens.ts
@@ -0,0 +1,9 @@
+import { CSSToken } from '../interfaces/token';
+
+export function cloneTokens(tokens: Array): Array {
+ if ((typeof globalThis !== 'undefined') && 'structuredClone' in globalThis) {
+ return structuredClone(tokens);
+ }
+
+ return JSON.parse(JSON.stringify(tokens));
+}
diff --git a/packages/css-tokenizer/stryker.conf.json b/packages/css-tokenizer/stryker.conf.json
new file mode 100644
index 000000000..015ebbb73
--- /dev/null
+++ b/packages/css-tokenizer/stryker.conf.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
+ "mutate": [
+ "src/**/*.ts"
+ ],
+ "buildCommand": "npm run build",
+ "testRunner": "command",
+ "coverageAnalysis": "perTest",
+ "tempDirName": "../../.stryker-tmp",
+ "commandRunner": {
+ "command": "npm run test"
+ },
+ "thresholds": {
+ "high": 100,
+ "low": 100,
+ "break": 100
+ },
+ "inPlace": true
+}
diff --git a/packages/css-tokenizer/test/_import.mjs b/packages/css-tokenizer/test/_import.mjs
new file mode 100644
index 000000000..c1c61bf2b
--- /dev/null
+++ b/packages/css-tokenizer/test/_import.mjs
@@ -0,0 +1,5 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+
+tokenizer({
+ css: '.some { css: ""; }',
+});
diff --git a/packages/css-tokenizer/test/_require.cjs b/packages/css-tokenizer/test/_require.cjs
new file mode 100644
index 000000000..abc74a96e
--- /dev/null
+++ b/packages/css-tokenizer/test/_require.cjs
@@ -0,0 +1,5 @@
+const { tokenizer } = require('@csstools/css-tokenizer');
+
+tokenizer({
+ css: '.some { css: ""; }',
+});
diff --git a/packages/css-tokenizer/test/code-points/code-points.mjs b/packages/css-tokenizer/test/code-points/code-points.mjs
new file mode 100644
index 000000000..49a0ce318
--- /dev/null
+++ b/packages/css-tokenizer/test/code-points/code-points.mjs
@@ -0,0 +1,401 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+
+// Single characters
+{
+ const testCases = [
+ [
+ 'a',
+ [['ident-token', 'a', 0, 0, { value: 'a' }]],
+ ],
+ [
+ '\\',
+ [['ident-token', '\\', 0, 0, { value: '๏ฟฝ' }]],
+ ],
+ [
+ '\\\\',
+ [['ident-token', '\\\\', 0, 1, { value: '\\' }]],
+ ],
+ [
+ 'ยง',
+ [['ident-token', 'ยง', 0, 0, { value: 'ยง' }]],
+ ],
+ [
+ 'ยฑ',
+ [['ident-token', 'ยฑ', 0, 0, { value: 'ยฑ' }]],
+ ],
+ [
+ '!',
+ [['delim-token', '!', 0, 0, { value: '!' }]],
+ ],
+ [
+ '@',
+ [['delim-token', '@', 0, 0, { value: '@' }]],
+ ],
+ [
+ '#',
+ [['delim-token', '#', 0, 0, { value: '#' }]],
+ ],
+ [
+ '$',
+ [['delim-token', '$', 0, 0, { value: '$' }]],
+ ],
+ [
+ '%',
+ [['delim-token', '%', 0, 0, { value: '%' }]],
+ ],
+ [
+ '^',
+ [['delim-token', '^', 0, 0, { value: '^' }]],
+ ],
+ [
+ '&',
+ [['delim-token', '&', 0, 0, { value: '&' }]],
+ ],
+ [
+ '*',
+ [['delim-token', '*', 0, 0, { value: '*' }]],
+ ],
+ [
+ '(',
+ [['(-token', '(', 0, 0, undefined]],
+ ],
+ [
+ ')',
+ [[')-token', ')', 0, 0, undefined]],
+ ],
+ [
+ '-',
+ [['delim-token', '-', 0, 0, { value: '-' }]],
+ ],
+ [
+ '_',
+ [['ident-token', '_', 0, 0, { value: '_' }]],
+ ],
+ [
+ '+',
+ [['delim-token', '+', 0, 0, { value: '+' }]],
+ ],
+ [
+ '=',
+ [['delim-token', '=', 0, 0, { value: '=' }]],
+ ],
+ [
+ '[',
+ [['[-token', '[', 0, 0, undefined]],
+ ],
+ [
+ ']',
+ [[']-token', ']', 0, 0, undefined]],
+ ],
+ [
+ '{',
+ [['{-token', '{', 0, 0, undefined]],
+ ],
+ [
+ '}',
+ [['}-token', '}', 0, 0, undefined]],
+ ],
+ [
+ ';',
+ [['semicolon-token', ';', 0, 0, undefined]],
+ ],
+ [
+ ':',
+ [['colon-token', ':', 0, 0, undefined]],
+ ],
+ [
+ '`',
+ [['delim-token', '`', 0, 0, { value: '`' }]],
+ ],
+ [
+ '~',
+ [['delim-token', '~', 0, 0, { value: '~' }]],
+ ],
+ [
+ '\'',
+ [['string-token', '\'', 0, 0, { value: '' }]],
+ ],
+ [
+ '"',
+ [['string-token', '"', 0, 0, { value: '' }]],
+ ],
+ [
+ '|',
+ [['delim-token', '|', 0, 0, { value: '|' }]],
+ ],
+ [
+ ',',
+ [['comma-token', ',', 0, 0, undefined]],
+ ],
+ [
+ '<',
+ [['delim-token', '<', 0, 0, { value: '<' }]],
+ ],
+ [
+ '>',
+ [['delim-token', '>', 0, 0, { value: '>' }]],
+ ],
+ [
+ '.',
+ [['delim-token', '.', 0, 0, { value: '.' }]],
+ ],
+ [
+ '?',
+ [['delim-token', '?', 0, 0, { value: '?' }]],
+ ],
+ [
+ '/',
+ [['delim-token', '/', 0, 0, { value: '/' }]],
+ ],
+ ];
+
+ testCases.forEach((testCase) => {
+ const t = tokenizer({
+ css: testCase[0],
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0,-1),
+ testCase[1],
+ );
+ });
+}
+
+
+// Followed by a letter
+{
+ const testCases = [
+ [
+ 'aa',
+ [['ident-token', 'aa', 0, 1, { value: 'aa' }]],
+ ],
+ [
+ '\\a',
+ [['ident-token', '\\a', 0, 1, { value: '\n' }]],
+ ],
+ [
+ '\\\\a',
+ [['ident-token', '\\\\a', 0, 2, { value: '\\a' }]],
+ ],
+ [
+ 'ยงa',
+ [['ident-token', 'ยงa', 0, 1, { value: 'ยงa' }]],
+ ],
+ [
+ 'ยฑa',
+ [['ident-token', 'ยฑa', 0, 1, { value: 'ยฑa' }]],
+ ],
+ [
+ '!a',
+ [
+ ['delim-token', '!', 0, 0, { value: '!' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '@a',
+ [['at-keyword-token', '@a', 0, 1, { value: 'a' }]],
+ ],
+ [
+ '#a',
+ [['hash-token', '#a', 0, 1, { value: 'a', type: 'id' }]],
+ ],
+ [
+ '$a',
+ [
+ ['delim-token', '$', 0, 0, { value: '$' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '%a',
+ [
+ ['delim-token', '%', 0, 0, { value: '%' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '^a',
+ [
+ ['delim-token', '^', 0, 0, { value: '^' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '&a',
+ [
+ ['delim-token', '&', 0, 0, { value: '&' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '*a',
+ [
+ ['delim-token', '*', 0, 0, { value: '*' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '(a',
+ [
+ ['(-token', '(', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ ')a',
+ [
+ [')-token', ')', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '-a',
+ [['ident-token', '-a', 0, 1, { value: '-a' }]],
+ ],
+ [
+ '_a',
+ [['ident-token', '_a', 0, 1, { value: '_a' }]],
+ ],
+ [
+ '+a',
+ [
+ ['delim-token', '+', 0, 0, { value: '+' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '=a',
+ [
+ ['delim-token', '=', 0, 0, { value: '=' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '[a',
+ [
+ ['[-token', '[', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ ']a',
+ [
+ [']-token', ']', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '{a',
+ [
+ ['{-token', '{', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '}a',
+ [
+ ['}-token', '}', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ ';a',
+ [
+ ['semicolon-token', ';', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ ':a',
+ [
+ ['colon-token', ':', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '`a',
+ [
+ ['delim-token', '`', 0, 0, { value: '`' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '~a',
+ [
+ ['delim-token', '~', 0, 0, { value: '~' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '\'a',
+ [['string-token', '\'a', 0, 1, { value: 'a' }]],
+ ],
+ [
+ '"a',
+ [['string-token', '"a', 0, 1, { value: 'a' }]],
+ ],
+ [
+ '|a',
+ [
+ ['delim-token', '|', 0, 0, { value: '|' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ ',a',
+ [
+ ['comma-token', ',', 0, 0, undefined],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '<', 0, 0, { value: '<' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '>a',
+ [
+ ['delim-token', '>', 0, 0, { value: '>' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '.a',
+ [
+ ['delim-token', '.', 0, 0, { value: '.' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '?a',
+ [
+ ['delim-token', '?', 0, 0, { value: '?' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ [
+ '/a',
+ [
+ ['delim-token', '/', 0, 0, { value: '/' }],
+ ['ident-token', 'a', 1, 1, { value: 'a' }],
+ ],
+ ],
+ ];
+
+ testCases.forEach((testCase) => {
+ const t = tokenizer({
+ css: testCase[0],
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ testCase[1],
+ );
+ });
+}
diff --git a/packages/css-tokenizer/test/code-points/ranges.mjs b/packages/css-tokenizer/test/code-points/ranges.mjs
new file mode 100644
index 000000000..f7ff5b573
--- /dev/null
+++ b/packages/css-tokenizer/test/code-points/ranges.mjs
@@ -0,0 +1,60 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+
+// Single characters
+{
+ const testCases = [
+ [
+ '\\00ae',
+ [['ident-token', '\\00ae', 0, 4, { value: 'ยฎ' }]],
+ ],
+ [
+ '\\00aE',
+ [['ident-token', '\\00aE', 0, 4, { value: 'ยฎ' }]],
+ ],
+ [
+ '\\00af',
+ [['ident-token', '\\00af', 0, 4, { value: 'ยฏ' }]],
+ ],
+ [
+ '\\00aF',
+ [['ident-token', '\\00aF', 0, 4, { value: 'ยฏ' }]],
+ ],
+ [
+ '\\00ag',
+ [['ident-token', '\\00ag', 0, 4, { value: '\ng' }]],
+ ],
+ [
+ '\\00aG',
+ [['ident-token', '\\00aG', 0, 4, { value: '\nG' }]],
+ ],
+ [
+ '\\7f',
+ [['ident-token', '\\7f', 0, 2, { value: '\x7F' }]],
+ ],
+ [
+ '\\80',
+ [['ident-token', '\\80', 0, 2, { value: '\x80' }]],
+ ],
+ [
+ '\\81',
+ [['ident-token', '\\81', 0, 2, { value: '\x81' }]],
+ ],
+ [
+ '๐จโ๐จโ๐งโ๐ฆ',
+ [['ident-token', '๐จโ๐จโ๐งโ๐ฆ', 0, 10, { value: '๐จโ๐จโ๐งโ๐ฆ' }]],
+ ],
+ ];
+
+ testCases.forEach((testCase) => {
+ const t = tokenizer({
+ css: testCase[0],
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ testCase[1],
+ );
+ });
+}
diff --git a/packages/css-tokenizer/test/complex/at-media-params.mjs b/packages/css-tokenizer/test/complex/at-media-params.mjs
new file mode 100644
index 000000000..c40a438c0
--- /dev/null
+++ b/packages/css-tokenizer/test/complex/at-media-params.mjs
@@ -0,0 +1,253 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+
+{
+ const testCases = [
+ [
+ 'screen',
+ [['ident-token', 'screen', 0, 5, { value: 'screen' }]],
+ ],
+ [
+ 'true',
+ [['ident-token', 'true', 0, 3, { value: 'true' }]],
+ ],
+ [
+ 'false',
+ [['ident-token', 'false', 0, 4, { value: 'false' }]],
+ ],
+ [
+ 'only screen',
+ [
+ ['ident-token', 'only', 0, 3, { value: 'only' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ['ident-token', 'screen', 5, 10, { value: 'screen' }],
+ ],
+ ],
+ [
+ 'only screen and (min-width: 10px)',
+ [
+ ['ident-token', 'only', 0, 3, { value: 'only' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ['ident-token', 'screen', 5, 10, { value: 'screen' }],
+ ['whitespace-token', ' ', 11, 11, undefined],
+ ['ident-token', 'and', 12, 14, { value: 'and' }],
+ ['whitespace-token', ' ', 15, 15, undefined],
+ ['(-token', '(', 16, 16, undefined],
+ ['ident-token', 'min-width', 17, 25, { value: 'min-width' }],
+ ['colon-token', ':', 26, 26, undefined],
+ ['whitespace-token', ' ', 27, 27, undefined],
+ [
+ 'dimension-token',
+ '10px',
+ 28,
+ 31,
+ { value: 10, type: 'integer', unit: 'px' },
+ ],
+ [')-token', ')', 32, 32, undefined],
+ ],
+ ],
+ [
+ '(10rem <= width <= 40rem)',
+ [
+ ['(-token', '(', 0, 0, undefined],
+ [
+ 'dimension-token',
+ '10rem',
+ 1,
+ 5,
+ { value: 10, type: 'integer', unit: 'rem' },
+ ],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ['delim-token', '<', 7, 7, { value: '<' }],
+ ['delim-token', '=', 8, 8, { value: '=' }],
+ ['whitespace-token', ' ', 9, 9, undefined],
+ ['ident-token', 'width', 10, 14, { value: 'width' }],
+ ['whitespace-token', ' ', 15, 15, undefined],
+ ['delim-token', '<', 16, 16, { value: '<' }],
+ ['delim-token', '=', 17, 17, { value: '=' }],
+ ['whitespace-token', ' ', 18, 18, undefined],
+ [
+ 'dimension-token',
+ '40rem',
+ 19,
+ 23,
+ { value: 40, type: 'integer', unit: 'rem' },
+ ],
+ [')-token', ')', 24, 24, undefined],
+ ],
+ ],
+ [
+ '(40rem > width > 10rem)',
+ [
+ ['(-token', '(', 0, 0, undefined],
+ [
+ 'dimension-token',
+ '40rem',
+ 1,
+ 5,
+ { value: 40, type: 'integer', unit: 'rem' },
+ ],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ['delim-token', '>', 7, 7, { value: '>' }],
+ ['whitespace-token', ' ', 8, 8, undefined],
+ ['ident-token', 'width', 9, 13, { value: 'width' }],
+ ['whitespace-token', ' ', 14, 14, undefined],
+ ['delim-token', '>', 15, 15, { value: '>' }],
+ ['whitespace-token', ' ', 16, 16, undefined],
+ [
+ 'dimension-token',
+ '10rem',
+ 17,
+ 21,
+ { value: 10, type: 'integer', unit: 'rem' },
+ ],
+ [')-token', ')', 22, 22, undefined],
+ ],
+ ],
+ [
+ '(--custom-mq)',
+ [
+ ['(-token', '(', 0, 0, undefined],
+ ['ident-token', '--custom-mq', 1, 11, { value: '--custom-mq' }],
+ [')-token', ')', 12, 12, undefined],
+ ],
+ ],
+ [
+ 'screen/* a comment */(--custom-mq)',
+ [
+ ['ident-token', 'screen', 0, 5, { value: 'screen' }],
+ ['(-token', '(', 21, 21, undefined],
+ ['ident-token', '--custom-mq', 22, 32, { value: '--custom-mq' }],
+ [')-token', ')', 33, 33, undefined],
+ ],
+ ],
+ [
+ '((min-width: 300px) and (prefers-color-scheme: dark))',
+ [
+ ['(-token', '(', 0, 0, undefined],
+ ['(-token', '(', 1, 1, undefined],
+ ['ident-token', 'min-width', 2, 10, { value: 'min-width' }],
+ ['colon-token', ':', 11, 11, undefined],
+ ['whitespace-token', ' ', 12, 12, undefined],
+ [
+ 'dimension-token',
+ '300px',
+ 13,
+ 17,
+ { value: 300, type: 'integer', unit: 'px' },
+ ],
+ [')-token', ')', 18, 18, undefined],
+ ['whitespace-token', ' ', 19, 19, undefined],
+ ['ident-token', 'and', 20, 22, { value: 'and' }],
+ ['whitespace-token', ' ', 23, 23, undefined],
+ ['(-token', '(', 24, 24, undefined],
+ [
+ 'ident-token',
+ 'prefers-color-scheme',
+ 25,
+ 44,
+ { value: 'prefers-color-scheme' },
+ ],
+ ['colon-token', ':', 45, 45, undefined],
+ ['whitespace-token', ' ', 46, 46, undefined],
+ ['ident-token', 'dark', 47, 50, { value: 'dark' }],
+ [')-token', ')', 51, 51, undefined],
+ [')-token', ')', 52, 52, undefined],
+ ],
+ ],
+ [
+ '((min-width:300px)and (prefers-color-scheme:dark))',
+ [
+ ['(-token', '(', 0, 0, undefined],
+ ['(-token', '(', 1, 1, undefined],
+ ['ident-token', 'min-width', 2, 10, { value: 'min-width' }],
+ ['colon-token', ':', 11, 11, undefined],
+ [
+ 'dimension-token',
+ '300px',
+ 12,
+ 16,
+ { value: 300, type: 'integer', unit: 'px' },
+ ],
+ [')-token', ')', 17, 17, undefined],
+ ['ident-token', 'and', 18, 20, { value: 'and' }],
+ ['whitespace-token', ' ', 21, 21, undefined],
+ ['(-token', '(', 22, 22, undefined],
+ [
+ 'ident-token',
+ 'prefers-color-scheme',
+ 23,
+ 42,
+ { value: 'prefers-color-scheme' },
+ ],
+ ['colon-token', ':', 43, 43, undefined],
+ ['ident-token', 'dark', 44, 47, { value: 'dark' }],
+ [')-token', ')', 48, 48, undefined],
+ [')-token', ')', 49, 49, undefined],
+ ],
+ ],
+ [
+ ' ( ( min-width : 300px ) and ( prefers-color-scheme : dark ) ) ',
+ [
+ ['whitespace-token', ' ', 0, 0, undefined],
+ ['(-token', '(', 1, 1, undefined],
+ ['whitespace-token', ' ', 2, 2, undefined],
+ ['(-token', '(', 3, 3, undefined],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ['ident-token', 'min-width', 5, 13, { value: 'min-width' }],
+ ['whitespace-token', ' ', 14, 14, undefined],
+ ['colon-token', ':', 15, 15, undefined],
+ ['whitespace-token', ' ', 16, 16, undefined],
+ [
+ 'dimension-token',
+ '300px',
+ 17,
+ 21,
+ { value: 300, type: 'integer', unit: 'px' },
+ ],
+ ['whitespace-token', ' ', 22, 22, undefined],
+ [')-token', ')', 23, 23, undefined],
+ ['whitespace-token', ' ', 24, 24, undefined],
+ ['ident-token', 'and', 25, 27, { value: 'and' }],
+ ['whitespace-token', ' ', 28, 28, undefined],
+ ['(-token', '(', 29, 29, undefined],
+ ['whitespace-token', ' ', 30, 30, undefined],
+ [
+ 'ident-token',
+ 'prefers-color-scheme',
+ 31,
+ 50,
+ { value: 'prefers-color-scheme' },
+ ],
+ ['whitespace-token', ' ', 51, 51, undefined],
+ ['colon-token', ':', 52, 52, undefined],
+ ['whitespace-token', ' ', 53, 53, undefined],
+ ['ident-token', 'dark', 54, 57, { value: 'dark' }],
+ ['whitespace-token', ' ', 58, 58, undefined],
+ [')-token', ')', 59, 59, undefined],
+ ['whitespace-token', ' ', 60, 60, undefined],
+ [')-token', ')', 61, 61, undefined],
+ ['whitespace-token', ' ', 62, 63, undefined],
+ ],
+ ],
+ ];
+
+ testCases.forEach((testCase) => {
+ const t = tokenizer({
+ css: testCase[0],
+ });
+
+ const tokens = collectTokens(t);
+
+ assert.deepEqual(
+ tokens.slice(0, -1),
+ testCase[1],
+ );
+
+ assert.deepEqual(
+ tokens[tokens.length -1][0],
+ 'EOF-token',
+ );
+ });
+}
diff --git a/packages/css-tokenizer/test/complex/parse-error.mjs b/packages/css-tokenizer/test/complex/parse-error.mjs
new file mode 100644
index 000000000..99517f52f
--- /dev/null
+++ b/packages/css-tokenizer/test/complex/parse-error.mjs
@@ -0,0 +1,36 @@
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+import assert from 'assert';
+
+{
+ const parseErrors = [];
+ const t = tokenizer(
+ {
+ css: '\\',
+ },
+ {
+ onParseError: (err) => {
+ parseErrors.push(err);
+ },
+ },
+ );
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = t.nextToken();
+ if (token[0] === TokenType.EOF) {
+ break;
+ }
+ }
+
+ assert.deepEqual(
+ parseErrors,
+ [
+ {
+ message: 'Unexpected EOF while consuming an escaped code point.',
+ start: 0,
+ end: 0,
+ state: ['4.3.7. Consume an escaped code point', 'Unexpected EOF'],
+ },
+ ],
+ );
+}
diff --git a/packages/css-tokenizer/test/css/multi-line.css b/packages/css-tokenizer/test/css/multi-line.css
new file mode 100644
index 000000000..379fa1193
--- /dev/null
+++ b/packages/css-tokenizer/test/css/multi-line.css
@@ -0,0 +1,8 @@
+.foo {
+ src: url("\
+https: //\
+example.com\
+/some-path/\
+?query=param\
+&more-query=params");
+}
diff --git a/packages/css-tokenizer/test/test-reader.mjs b/packages/css-tokenizer/test/test-reader.mjs
new file mode 100644
index 000000000..69c59e706
--- /dev/null
+++ b/packages/css-tokenizer/test/test-reader.mjs
@@ -0,0 +1,186 @@
+import assert from 'assert';
+import { Reader } from '@csstools/css-tokenizer';
+
+{
+ const r = new Reader('abc๐จโ๐จโ๐งโ๐ฆd');
+
+ {
+ const peeked = r.peekOneCodePoint();
+ assert.deepEqual(
+ peeked,
+ 97,
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(peeked),
+ 'a',
+ );
+
+ assert.deepEqual(
+ r.representation(),
+ [
+ 0,
+ -1,
+ ],
+ );
+ }
+
+ {
+ const peeked = r.peekTwoCodePoints();
+ assert.deepEqual(
+ peeked,
+ [97, 98],
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(peeked[0]),
+ 'a',
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(peeked[1]),
+ 'b',
+ );
+
+ assert.deepEqual(
+ r.representation(),
+ [
+ 0,
+ -1,
+ ],
+ );
+ }
+
+ {
+ const peeked = r.peekThreeCodePoints();
+ assert.deepEqual(
+ peeked,
+ [97, 98, 99],
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(peeked[0]),
+ 'a',
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(peeked[1]),
+ 'b',
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(peeked[2]),
+ 'c',
+ );
+
+ assert.deepEqual(
+ r.representation(),
+ [
+ 0,
+ -1,
+ ],
+ );
+ }
+
+ {
+ const read = r.readCodePoint();
+ assert.deepEqual(
+ read,
+ 97,
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(read),
+ 'a',
+ );
+
+ assert.deepEqual(
+ r.representation(),
+ [
+ 0,
+ 0,
+ ],
+ );
+
+ assert.deepEqual(
+ r.representationString(),
+ 'a',
+ );
+ }
+
+ r.resetRepresentation();
+
+ {
+ const read1 = r.readCodePoint();
+ assert.deepEqual(
+ read1,
+ 98,
+ );
+
+ const read2 = r.readCodePoint();
+ assert.deepEqual(
+ read2,
+ 99,
+ );
+
+ const read3 = r.readCodePoint();
+ assert.deepEqual(
+ read3,
+ 55357,
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(read3),
+ '\uD83D',
+ );
+
+ const read4 = r.readCodePoint();
+ assert.deepEqual(
+ read4,
+ 56424,
+ );
+
+ assert.deepEqual(
+ String.fromCharCode(read4),
+ '\uDC68',
+ );
+
+ assert.deepEqual(
+ r.representation(),
+ [
+ 1,
+ 4,
+ ],
+ );
+
+ // Read to the end
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+ r.readCodePoint();
+
+ assert.deepEqual(
+ r.representation(),
+ [
+ 1,
+ 14,
+ ],
+ );
+
+ assert.deepEqual(
+ r.slice(r.representation()[0], r.representation()[1] + 1),
+ 'bc๐จโ๐จโ๐งโ๐ฆd',
+ );
+
+ assert.deepEqual(
+ r.representationString(),
+ 'bc๐จโ๐จโ๐งโ๐ฆd',
+ );
+ }
+}
diff --git a/packages/css-tokenizer/test/test.mjs b/packages/css-tokenizer/test/test.mjs
new file mode 100644
index 000000000..71d21591a
--- /dev/null
+++ b/packages/css-tokenizer/test/test.mjs
@@ -0,0 +1,13 @@
+// Reader
+import './test-reader.mjs';
+// Code points
+import './code-points/code-points.mjs';
+import './code-points/ranges.mjs';
+// Tokens
+import './token/basic.mjs';
+import './token/comment.mjs';
+import './token/numeric.mjs';
+import './token/url.mjs';
+// Complex
+import './complex/at-media-params.mjs';
+import './complex/parse-error.mjs';
diff --git a/packages/css-tokenizer/test/token/basic.mjs b/packages/css-tokenizer/test/token/basic.mjs
new file mode 100644
index 000000000..46033d7d5
--- /dev/null
+++ b/packages/css-tokenizer/test/token/basic.mjs
@@ -0,0 +1,253 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+
+{
+ const t = tokenizer({
+ css: '@charset "utf-8";',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['at-keyword-token', '@charset', 0, 7, { value: 'charset' }],
+ ['whitespace-token', ' ', 8, 8, undefined],
+ ['string-token', '"utf-8"', 9, 15, { value: 'utf-8' }],
+ ['semicolon-token', ';', 16, 16, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'foo { width: calc(-infinity) }',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['ident-token', 'foo', 0, 2, { value: 'foo' }],
+ ['whitespace-token', ' ', 3, 3, undefined],
+ ['{-token', '{', 4, 4, undefined],
+ ['whitespace-token', ' ', 5, 5, undefined],
+ ['ident-token', 'width', 6, 10, { value: 'width' }],
+ ['colon-token', ':', 11, 11, undefined],
+ ['whitespace-token', ' ', 12, 12, undefined],
+ ['function-token', 'calc(', 13, 17, { value: 'calc' }],
+ ['ident-token', '-infinity', 18, 26, { value: '-infinity' }],
+ [')-token', ')', 27, 27, undefined],
+ ['whitespace-token', ' ', 28, 28, undefined],
+ ['}-token', '}', 29, 29, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '@import url(https://example.com/stylesheet.css) layer( base.tokens ) supports( display: grid ) not screen and ((400px <= width < 1024px) and (prefers-color-scheme: dark));',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['at-keyword-token', '@import', 0, 6, { value: 'import' }],
+ ['whitespace-token', ' ', 7, 7, undefined],
+ [
+ 'url-token',
+ 'url(https://example.com/stylesheet.css)',
+ 8,
+ 46,
+ { value: 'https://example.com/stylesheet.css' },
+ ],
+ ['whitespace-token', ' ', 47, 47, undefined],
+ ['function-token', 'layer(', 48, 53, { value: 'layer' }],
+ ['whitespace-token', ' ', 54, 54, undefined],
+ ['ident-token', 'base', 55, 58, { value: 'base' }],
+ ['delim-token', '.', 59, 59, { value: '.' }],
+ ['ident-token', 'tokens', 60, 65, { value: 'tokens' }],
+ ['whitespace-token', ' ', 66, 66, undefined],
+ [')-token', ')', 67, 67, undefined],
+ ['whitespace-token', ' ', 68, 68, undefined],
+ ['function-token', 'supports(', 69, 77, { value: 'supports' }],
+ ['whitespace-token', ' ', 78, 78, undefined],
+ ['ident-token', 'display', 79, 85, { value: 'display' }],
+ ['colon-token', ':', 86, 86, undefined],
+ ['whitespace-token', ' ', 87, 87, undefined],
+ ['ident-token', 'grid', 88, 91, { value: 'grid' }],
+ ['whitespace-token', ' ', 92, 92, undefined],
+ [')-token', ')', 93, 93, undefined],
+ ['whitespace-token', ' ', 94, 94, undefined],
+ ['ident-token', 'not', 95, 97, { value: 'not' }],
+ ['whitespace-token', ' ', 98, 98, undefined],
+ ['ident-token', 'screen', 99, 104, { value: 'screen' }],
+ ['whitespace-token', ' ', 105, 105, undefined],
+ ['ident-token', 'and', 106, 108, { value: 'and' }],
+ ['whitespace-token', ' ', 109, 109, undefined],
+ ['(-token', '(', 110, 110, undefined],
+ ['(-token', '(', 111, 111, undefined],
+ [
+ 'dimension-token',
+ '400px',
+ 112,
+ 116,
+ { value: 400, type: 'integer', unit: 'px' },
+ ],
+ ['whitespace-token', ' ', 117, 117, undefined],
+ ['delim-token', '<', 118, 118, { value: '<' }],
+ ['delim-token', '=', 119, 119, { value: '=' }],
+ ['whitespace-token', ' ', 120, 120, undefined],
+ ['ident-token', 'width', 121, 125, { value: 'width' }],
+ ['whitespace-token', ' ', 126, 126, undefined],
+ ['delim-token', '<', 127, 127, { value: '<' }],
+ ['whitespace-token', ' ', 128, 128, undefined],
+ [
+ 'dimension-token',
+ '1024px',
+ 129,
+ 134,
+ { value: 1024, type: 'integer', unit: 'px' },
+ ],
+ [')-token', ')', 135, 135, undefined],
+ ['whitespace-token', ' ', 136, 136, undefined],
+ ['ident-token', 'and', 137, 139, { value: 'and' }],
+ ['whitespace-token', ' ', 140, 140, undefined],
+ ['(-token', '(', 141, 141, undefined],
+ [
+ 'ident-token',
+ 'prefers-color-scheme',
+ 142,
+ 161,
+ { value: 'prefers-color-scheme' },
+ ],
+ ['colon-token', ':', 162, 162, undefined],
+ ['whitespace-token', ' ', 163, 163, undefined],
+ ['ident-token', 'dark', 164, 167, { value: 'dark' }],
+ [')-token', ')', 168, 168, undefined],
+ [')-token', ')', 169, 169, undefined],
+ ['semicolon-token', ';', 170, 170, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: `@media only screen and (min-width: 768rem) {
+ .foo {
+ content: 'Some content!' !important;
+ }
+}
+`,
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['at-keyword-token', '@media', 0, 5, { value: 'media' }],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ['ident-token', 'only', 7, 10, { value: 'only' }],
+ ['whitespace-token', ' ', 11, 11, undefined],
+ ['ident-token', 'screen', 12, 17, { value: 'screen' }],
+ ['whitespace-token', ' ', 18, 18, undefined],
+ ['ident-token', 'and', 19, 21, { value: 'and' }],
+ ['whitespace-token', ' ', 22, 22, undefined],
+ ['(-token', '(', 23, 23, undefined],
+ ['ident-token', 'min-width', 24, 32, { value: 'min-width' }],
+ ['colon-token', ':', 33, 33, undefined],
+ ['whitespace-token', ' ', 34, 34, undefined],
+ [
+ 'dimension-token',
+ '768rem',
+ 35,
+ 40,
+ { value: 768, type: 'integer', unit: 'rem' },
+ ],
+ [')-token', ')', 41, 41, undefined],
+ ['whitespace-token', ' ', 42, 42, undefined],
+ ['{-token', '{', 43, 43, undefined],
+ ['whitespace-token', '\n\t', 44, 45, undefined],
+ ['delim-token', '.', 46, 46, { value: '.' }],
+ ['ident-token', 'foo', 47, 49, { value: 'foo' }],
+ ['whitespace-token', ' ', 50, 50, undefined],
+ ['{-token', '{', 51, 51, undefined],
+ ['whitespace-token', '\n\t\t', 52, 54, undefined],
+ ['ident-token', 'content', 55, 61, { value: 'content' }],
+ ['colon-token', ':', 62, 62, undefined],
+ ['whitespace-token', ' ', 63, 63, undefined],
+ [
+ 'string-token',
+ // eslint-disable-next-line quotes
+ "'Some content!'",
+ 64,
+ 78,
+ { value: 'Some content!' },
+ ],
+ ['whitespace-token', ' ', 79, 79, undefined],
+ ['delim-token', '!', 80, 80, { value: '!' }],
+ ['ident-token', 'important', 81, 89, { value: 'important' }],
+ ['semicolon-token', ';', 90, 90, undefined],
+ ['whitespace-token', '\n\t', 91, 92, undefined],
+ ['}-token', '}', 93, 93, undefined],
+ ['whitespace-token', '\n', 94, 94, undefined],
+ ['}-token', '}', 95, 95, undefined],
+ ['whitespace-token', '\n', 96, 96, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: `@media screen and ((min-width: 200px) and (foo: "\\A9\\
+bar") and (fancy(baz))) {}`,
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['at-keyword-token', '@media', 0, 5, { value: 'media' }],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ['ident-token', 'screen', 7, 12, { value: 'screen' }],
+ ['whitespace-token', ' ', 13, 13, undefined],
+ ['ident-token', 'and', 14, 16, { value: 'and' }],
+ ['whitespace-token', ' ', 17, 17, undefined],
+ ['(-token', '(', 18, 18, undefined],
+ ['(-token', '(', 19, 19, undefined],
+ ['ident-token', 'min-width', 20, 28, { value: 'min-width' }],
+ ['colon-token', ':', 29, 29, undefined],
+ ['whitespace-token', ' ', 30, 30, undefined],
+ [
+ 'dimension-token',
+ '200px',
+ 31,
+ 35,
+ { value: 200, type: 'integer', unit: 'px' },
+ ],
+ [')-token', ')', 36, 36, undefined],
+ ['whitespace-token', ' ', 37, 37, undefined],
+ ['ident-token', 'and', 38, 40, { value: 'and' }],
+ ['whitespace-token', ' ', 41, 41, undefined],
+ ['(-token', '(', 42, 42, undefined],
+ ['ident-token', 'foo', 43, 45, { value: 'foo' }],
+ ['colon-token', ':', 46, 46, undefined],
+ ['whitespace-token', ' ', 47, 47, undefined],
+ ['string-token', '"\\A9\\\nbar"', 48, 57, { value: 'ยฉbar' }],
+ [')-token', ')', 58, 58, undefined],
+ ['whitespace-token', ' ', 59, 59, undefined],
+ ['ident-token', 'and', 60, 62, { value: 'and' }],
+ ['whitespace-token', ' ', 63, 63, undefined],
+ ['(-token', '(', 64, 64, undefined],
+ ['function-token', 'fancy(', 65, 70, { value: 'fancy' }],
+ ['ident-token', 'baz', 71, 73, { value: 'baz' }],
+ [')-token', ')', 74, 74, undefined],
+ [')-token', ')', 75, 75, undefined],
+ [')-token', ')', 76, 76, undefined],
+ ['whitespace-token', ' ', 77, 77, undefined],
+ ['{-token', '{', 78, 78, undefined],
+ ['}-token', '}', 79, 79, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
diff --git a/packages/css-tokenizer/test/token/comment.mjs b/packages/css-tokenizer/test/token/comment.mjs
new file mode 100644
index 000000000..230f3965b
--- /dev/null
+++ b/packages/css-tokenizer/test/token/comment.mjs
@@ -0,0 +1,88 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+
+{
+ const t = tokenizer({
+ css: 'a /* a comment */ b',
+ }, { commentsAreTokens : true });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['ident-token', 'a', 0, 0, { value: 'a' }],
+ ['whitespace-token', ' ', 1, 1, undefined],
+ ['comment', '/* a comment */', 2, 16, undefined],
+ ['whitespace-token', ' ', 17, 17, undefined],
+ ['ident-token', 'b', 18, 18, { value: 'b' }],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'a/* a comment */b',
+ }, { commentsAreTokens: true });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['ident-token', 'a', 0, 0, { value: 'a' }],
+ ['comment', '/* a comment */', 1, 15, undefined],
+ ['ident-token', 'b', 16, 16, { value: 'b' }],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'a /* a comment',
+ }, { commentsAreTokens: true });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['ident-token', 'a', 0, 0, { value: 'a' }],
+ ['whitespace-token', ' ', 1, 1, undefined],
+ ['comment', '/* a comment', 2, 13, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: `a /* a comment
+*/`,
+ }, { commentsAreTokens: true });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['ident-token', 'a', 0, 0, { value: 'a' }],
+ ['whitespace-token', ' ', 1, 1, undefined],
+ ['comment', '/* a comment\n*/', 2, 16, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'a /* a comment \\*/ b',
+ }, { commentsAreTokens: true });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['ident-token', 'a', 0, 0, { value: 'a' }],
+ ['whitespace-token', ' ', 1, 1, undefined],
+ ['comment', '/* a comment \\*/', 2, 17, undefined],
+ ['whitespace-token', ' ', 18, 18, undefined],
+ ['ident-token', 'b', 19, 19, { value: 'b' }],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
diff --git a/packages/css-tokenizer/test/token/numeric.mjs b/packages/css-tokenizer/test/token/numeric.mjs
new file mode 100644
index 000000000..0ee6c1f0f
--- /dev/null
+++ b/packages/css-tokenizer/test/token/numeric.mjs
@@ -0,0 +1,427 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+
+{
+ const t = tokenizer({
+ css: '10px ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'dimension-token',
+ '10px',
+ 0,
+ 3,
+ { value: 10, type: 'integer', unit: 'px' },
+ ],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '4.01 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '4.01', 0, 3, { value: 4.01, type: 'number' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-456.8 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'number-token',
+ '-456.8',
+ 0,
+ 5,
+ { value: -456.8, type: 'number' },
+ ],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '0.0 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '0.0', 0, 2, { value: 0, type: 'number' }],
+ ['whitespace-token', ' ', 3, 3, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '+0.0 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '+0.0', 0, 3, { value: 0, type: 'number' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-0.0 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '-0.0', 0, 3, { value: -0, type: 'number' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '.60 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '.60', 0, 2, { value: 0.6, type: 'number' }],
+ ['whitespace-token', ' ', 3, 3, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '10e3 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '10e3', 0, 3, { value: 10000, type: 'number' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-3.4e-2 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'number-token',
+ '-3.4e-2',
+ 0,
+ 6,
+ { value: -0.034, type: 'number' },
+ ],
+ ['whitespace-token', ' ', 7, 7, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-3.4e2 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '-3.4e2', 0, 5, { value: -340, type: 'number' }],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-3.4e+2 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'number-token',
+ '-3.4e+2',
+ 0,
+ 6,
+ { value: -340, type: 'number' },
+ ],
+ ['whitespace-token', ' ', 7, 7, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-3.4e ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'dimension-token',
+ '-3.4e',
+ 0,
+ 4,
+ { value: -3.4, type: 'number', unit: 'e' },
+ ],
+ ['whitespace-token', ' ', 5, 5, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '-3.4ef ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'dimension-token',
+ '-3.4ef',
+ 0,
+ 5,
+ { value: -3.4, type: 'number', unit: 'ef' },
+ ],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '1e2 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'number-token',
+ '1e2',
+ 0,
+ 2,
+ { value: 100, type: 'number' },
+ ],
+ ['whitespace-token', ' ', 3, 3, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '12rem ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'dimension-token',
+ '12rem',
+ 0,
+ 4,
+ { value: 12, type: 'integer', unit: 'rem' },
+ ],
+ ['whitespace-token', ' ', 5, 5, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '12.2rem ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'dimension-token',
+ '12.2rem',
+ 0,
+ 6,
+ { value: 12.2, type: 'number', unit: 'rem' },
+ ],
+ ['whitespace-token', ' ', 7, 7, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '13% ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['percentage-token', '13%', 0, 2, { value: 13 }],
+ ['whitespace-token', ' ', 3, 3, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '0.13% ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['percentage-token', '0.13%', 0, 4, { value: 0.13 }],
+ ['whitespace-token', ' ', 5, 5, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '14--foo ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [
+ 'dimension-token',
+ '14--foo',
+ 0,
+ 6,
+ { value: 14, type: 'integer', unit: '--foo' },
+ ],
+ ['whitespace-token', ' ', 7, 7, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '12. ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '12', 0, 1, { value: 12, type: 'integer' }],
+ ['delim-token', '.', 2, 2, { value: '.' }],
+ ['whitespace-token', ' ', 3, 3, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '+-12.2 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['delim-token', '+', 0, 0, { value: '+' }],
+ ['number-token', '-12.2', 1, 5, { value: -12.2, type: 'number' }],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: '12.1.1 ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['number-token', '12.1', 0, 3, { value: 12.1, type: 'number' }],
+ ['number-token', '.1', 4, 5, { value: 0.1, type: 'number' }],
+ ['whitespace-token', ' ', 6, 6, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: ':nth-last-child(n+3) ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ [ 'colon-token', ':', 0, 0, undefined ],
+ [
+ 'function-token',
+ 'nth-last-child(',
+ 1,
+ 15,
+ { value: 'nth-last-child' },
+ ],
+ [ 'ident-token', 'n', 16, 16, { value: 'n' } ],
+ [ 'number-token', '+3', 17, 18, { value: 3, type: 'integer' } ],
+ [')-token', ')', 19, 19, undefined],
+ ['whitespace-token', ' ', 20, 20, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: ':nth-last-child( n + 3 ) ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t).slice(0, -1),
+ [
+ ['colon-token', ':', 0, 0, undefined],
+ [
+ 'function-token',
+ 'nth-last-child(',
+ 1,
+ 15,
+ { value: 'nth-last-child' },
+ ],
+ ['whitespace-token', ' ', 16, 16, undefined],
+ ['ident-token', 'n', 17, 17, { value: 'n' }],
+ ['whitespace-token', ' ', 18, 18, undefined],
+ ['delim-token', '+', 19, 19, { value: '+' }],
+ ['whitespace-token', ' ', 20, 20, undefined],
+ ['number-token', '3', 21, 21, { value: 3, type: 'integer' }],
+ ['whitespace-token', ' ', 22, 22, undefined],
+ [')-token', ')', 23, 23, undefined],
+ ['whitespace-token', ' ', 24, 24, undefined],
+ ],
+ );
+}
diff --git a/packages/css-tokenizer/test/token/url.mjs b/packages/css-tokenizer/test/token/url.mjs
new file mode 100644
index 000000000..eeada65c4
--- /dev/null
+++ b/packages/css-tokenizer/test/token/url.mjs
@@ -0,0 +1,261 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from '../util/collect-tokens.mjs';
+import fs from 'fs';
+
+{
+ const t = tokenizer({
+ css: `
+url( a )\
+Url( b )\
+uRl( c )\
+urL( d )\
+URl( e )\
+uRL( f )\
+UrL( g )\
+URL( h )\
+`,
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['whitespace-token', '\n', 0, 0, undefined],
+ ['url-token', 'url( a )', 1, 8, { value: 'a' }],
+ ['url-token', 'Url( b )', 9, 16, { value: 'b' }],
+ ['url-token', 'uRl( c )', 17, 24, { value: 'c' }],
+ ['url-token', 'urL( d )', 25, 32, { value: 'd' }],
+ ['url-token', 'URl( e )', 33, 40, { value: 'e' }],
+ ['url-token', 'uRL( f )', 41, 48, { value: 'f' }],
+ ['url-token', 'UrL( g )', 49, 56, { value: 'g' }],
+ ['url-token', 'URL( h )', 57, 64, { value: 'h' }],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: `
+url("\\
+https://\\
+example.com\\
+/some-path/\\
+?query=param\\
+&more-query=params")`,
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['whitespace-token', '\n', 0, 0, undefined],
+ ['function-token', 'url(', 1, 4, { value: 'url' }],
+ [
+ 'string-token',
+ '"\\\n' +
+ 'https://\\\n' +
+ 'example.com\\\n' +
+ '/some-path/\\\n' +
+ '?query=param\\\n' +
+ '&more-query=params"',
+ 5,
+ 76,
+ {
+ value: 'https://example.com/some-path/?query=param&more-query=params',
+ },
+ ],
+ [')-token', ')', 77, 77, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: fs.readFileSync('./test/css/multi-line.css').toString(),
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['delim-token', '.', 0, 0, { value: '.' }],
+ ['ident-token', 'foo', 1, 3, { value: 'foo' }],
+ ['whitespace-token', ' ', 4, 4, undefined],
+ ['{-token', '{', 5, 5, undefined],
+ ['whitespace-token', '\n\t', 6, 7, undefined],
+ ['ident-token', 'src', 8, 10, { value: 'src' }],
+ ['colon-token', ':', 11, 11, undefined],
+ ['whitespace-token', ' ', 12, 12, undefined],
+ ['function-token', 'url(', 13, 16, { value: 'url' }],
+ [
+ 'string-token',
+ '"\\\n' +
+ 'https: //\\\n' +
+ 'example.com\\\n' +
+ '/some-path/\\\n' +
+ '?query=param\\\n' +
+ '&more-query=params"',
+ 17,
+ 89,
+ {
+ value: 'https: //example.com/some-path/?query=param&more-query=params',
+ },
+ ],
+ [')-token', ')', 90, 90, undefined],
+ ['semicolon-token', ';', 91, 91, undefined],
+ ['whitespace-token', '\n', 92, 92, undefined],
+ ['}-token', '}', 93, 93, undefined],
+ ['whitespace-token', '\n', 94, 94, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'url(https://example.com a',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['bad-url-token', 'url(https://example.com a', 0, 24, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'url("https://example.com',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['function-token', 'url(', 0, 3, { value: 'url' }],
+ [
+ 'string-token',
+ '"https://example.com',
+ 4,
+ 23,
+ { value: 'https://example.com' },
+ ],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'url("https://example.com ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['function-token', 'url(', 0, 3, { value: 'url' }],
+ [
+ 'string-token',
+ '"https://example.com ',
+ 4,
+ 24,
+ { value: 'https://example.com ' },
+ ],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'url(https://example.com',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ [
+ 'url-token',
+ 'url(https://example.com',
+ 0,
+ 22,
+ { value: 'https://example.com' },
+ ],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: 'url(https://example.com ',
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ [
+ 'url-token',
+ 'url(https://example.com ',
+ 0,
+ 23,
+ { value: 'https://example.com' },
+ ],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
+
+{
+ const t = tokenizer({
+ css: `
+url( 'single-quoted' )\
+url( "double-quoted" )\
+url( 'mix-quoted" ' )\
+`,
+ });
+
+ assert.deepEqual(
+ collectTokens(t),
+ [
+ ['whitespace-token', '\n', 0, 0, undefined],
+ ['function-token', 'url(', 1, 4, { value: 'url' }],
+ ['whitespace-token', ' ', 5, 5, undefined],
+ [
+ 'string-token',
+ // eslint-disable-next-line quotes
+ "'single-quoted'",
+ 6,
+ 20,
+ { value: 'single-quoted' },
+ ],
+ ['whitespace-token', ' ', 21, 21, undefined],
+ [')-token', ')', 22, 22, undefined],
+ ['function-token', 'url(', 23, 26, { value: 'url' }],
+ ['whitespace-token', ' ', 27, 27, undefined],
+ [
+ 'string-token',
+ '"double-quoted"',
+ 28,
+ 42,
+ { value: 'double-quoted' },
+ ],
+ ['whitespace-token', ' ', 43, 43, undefined],
+ [')-token', ')', 44, 44, undefined],
+ ['function-token', 'url(', 45, 48, { value: 'url' }],
+ ['whitespace-token', ' ', 49, 49, undefined],
+ [
+ 'string-token',
+ // eslint-disable-next-line quotes
+ `'mix-quoted" '`,
+ 50,
+ 63,
+ { value: 'mix-quoted" ' },
+ ],
+ ['whitespace-token', ' ', 64, 64, undefined],
+ [')-token', ')', 65, 65, undefined],
+ ['EOF-token', '', -1, -1, undefined],
+ ],
+ );
+}
diff --git a/packages/css-tokenizer/test/util/collect-tokens.mjs b/packages/css-tokenizer/test/util/collect-tokens.mjs
new file mode 100644
index 000000000..5ab1c998b
--- /dev/null
+++ b/packages/css-tokenizer/test/util/collect-tokens.mjs
@@ -0,0 +1,11 @@
+export function collectTokens(t) {
+ const bag = [];
+
+ while (!t.endOfFile()) {
+ bag.push(t.nextToken());
+ }
+
+ bag.push(t.nextToken()); // EOF-token
+
+ return bag;
+}
diff --git a/packages/css-tokenizer/tsconfig.json b/packages/css-tokenizer/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/packages/css-tokenizer/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/packages/media-query-list-parser/.gitignore b/packages/media-query-list-parser/.gitignore
new file mode 100644
index 000000000..f548255b0
--- /dev/null
+++ b/packages/media-query-list-parser/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+*.result.json
+dist/*
diff --git a/packages/media-query-list-parser/.nvmrc b/packages/media-query-list-parser/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/packages/media-query-list-parser/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/packages/media-query-list-parser/CHANGELOG.md b/packages/media-query-list-parser/CHANGELOG.md
new file mode 100644
index 000000000..b0ff6b082
--- /dev/null
+++ b/packages/media-query-list-parser/CHANGELOG.md
@@ -0,0 +1,3 @@
+### 1.0.0
+
+- Initial version
diff --git a/packages/media-query-list-parser/LICENSE.md b/packages/media-query-list-parser/LICENSE.md
new file mode 100644
index 000000000..af5411fa2
--- /dev/null
+++ b/packages/media-query-list-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/media-query-list-parser/README.md b/packages/media-query-list-parser/README.md
new file mode 100644
index 000000000..4f17a86b8
--- /dev/null
+++ b/packages/media-query-list-parser/README.md
@@ -0,0 +1,60 @@
+# Media Query List Parser
+
+[
][npm-url]
+[
][cli-url]
+[
][discord]
+
+Implemented from : https://www.w3.org/TR/mediaqueries-5/
+
+## Usage
+
+Add [Media Query List Parser] to your project:
+
+```bash
+npm install postcss @csstools/media-query-list-parser --save-dev
+```
+
+```ts
+import { parse } from '@csstools/media-query-list-parser';
+
+export function parseCustomMedia() {
+ const mediaQueryList = parse('screen and (min-width: 300px), (50px < height < 30vw)');
+
+ mediaQueryList.forEach((mediaQuery) => {
+ mediaQuery.walk((entry, index) => {
+ // Index of the current Node in `parent`.
+ console.log(index);
+ // Type of `parent`.
+ console.log(entry.parent.type);
+
+ // Type of `node`
+ {
+ // Sometimes nodes can be arrays.
+ if (Array.isArray(entry.node)) {
+ entry.node.forEach((item) => {
+ console.log(item.type);
+ });
+ }
+
+ if ('type' in entry.node) {
+ console.log(entry.node.type);
+ }
+ }
+
+ // stringified version of the current node.
+ console.log(entry.node.toString());
+
+ // Return `false` to stop the walker.
+ return false;
+ });
+ });
+}
+```
+
+[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/media-query-list-parser
+
+[Media Query List Parser]: https://github.com/csstools/postcss-plugins/tree/main/packages/media-query-list-parser
+
+
diff --git a/packages/media-query-list-parser/package.json b/packages/media-query-list-parser/package.json
new file mode 100644
index 000000000..fa77ed6e8
--- /dev/null
+++ b/packages/media-query-list-parser/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@csstools/media-query-list-parser",
+ "description": "Parse CSS media query lists.",
+ "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": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "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": {
+ "@csstools/css-parser-algorithms": "^1.0.0",
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.js",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"",
+ "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",
+ "stryker": "stryker run --logLevel error",
+ "test": "node ./test/test.mjs",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node ./test/test.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/media-query-list-parser#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/media-query-list-parser"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "media query",
+ "parser"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/media-query-list-parser/src/index.ts b/packages/media-query-list-parser/src/index.ts
new file mode 100644
index 000000000..57680d295
--- /dev/null
+++ b/packages/media-query-list-parser/src/index.ts
@@ -0,0 +1,46 @@
+export { parse, parseFromTokens } from './parser/parse';
+export { NodeType } from './util/node-type';
+export {
+ isGeneralEnclosed,
+ isMediaAnd,
+ isMediaCondition,
+ isMediaConditionList,
+ isMediaConditionListWithAnd,
+ isMediaConditionListWithOr,
+ isMediaFeature,
+ isMediaFeatureBoolean,
+ isMediaFeatureName,
+ isMediaFeaturePlain,
+ isMediaFeatureRange,
+ isMediaFeatureRangeNameValue,
+ isMediaFeatureRangeValueName,
+ isMediaFeatureRangeValueNameValue,
+ isMediaFeatureValue,
+ isMediaInParens,
+ isMediaNot,
+ isMediaOr,
+ isMediaQuery,
+ isMediaQueryInvalid,
+ isMediaQueryWithType,
+ isMediaQueryWithoutType,
+} from './util/type-predicates';
+
+export { GeneralEnclosed } from './nodes/general-enclosed';
+export { MediaAnd } from './nodes/media-and';
+export { MediaCondition } from './nodes/media-condition';
+export { MediaConditionList, MediaConditionListWithAnd, MediaConditionListWithOr } from './nodes/media-condition-list';
+export { MediaFeature, newMediaFeatureBoolean, newMediaFeaturePlain } from './nodes/media-feature';
+export { MediaFeatureBoolean } from './nodes/media-feature-boolean';
+export { MediaFeatureName } from './nodes/media-feature-name';
+export { MediaFeaturePlain } from './nodes/media-feature-plain';
+export { MediaFeatureRange, MediaFeatureRangeNameValue, MediaFeatureRangeValueName, MediaFeatureRangeValueNameValue } from './nodes/media-feature-range';
+export { MediaFeatureValue, matchesRatio, matchesRatioExactly } from './nodes/media-feature-value';
+export { MediaInParens } from './nodes/media-in-parens';
+export { MediaNot } from './nodes/media-not';
+export { MediaOr } from './nodes/media-or';
+export { MediaQuery, MediaQueryWithType, MediaQueryWithoutType, MediaQueryInvalid } from './nodes/media-query';
+
+export { MediaFeatureComparison, MediaFeatureEQ, MediaFeatureGT, MediaFeatureLT, invertComparison, matchesComparison, comparisonFromTokens } from './nodes/media-feature-comparison';
+export { MediaType, typeFromToken } from './nodes/media-type';
+export { MediaQueryModifier, modifierFromToken } from './nodes/media-query-modifier';
+export { cloneMediaQuery } from './util/clone-media-query';
diff --git a/packages/media-query-list-parser/src/nodes/general-enclosed.ts b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
new file mode 100644
index 000000000..73d443cba
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
@@ -0,0 +1,71 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { CSSToken } from '@csstools/css-tokenizer';
+import { NodeType } from '../util/node-type';
+
+export class GeneralEnclosed {
+ type = NodeType.GeneralEnclosed;
+
+ value: ComponentValue;
+
+ constructor(value: ComponentValue) {
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return this.value.tokens();
+ }
+
+ toString(): string {
+ return this.value.toString();
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ if (item === this.value) {
+ return 'value';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'value') {
+ return this.value;
+ }
+ }
+
+ walk(cb: (entry: { node: GeneralEnclosedWalkerEntry, parent: GeneralEnclosedWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ tokens: this.tokens(),
+ };
+ }
+
+ isGeneralEnclosed(): this is GeneralEnclosed {
+ return GeneralEnclosed.isGeneralEnclosed(this);
+ }
+
+ static isGeneralEnclosed(x: unknown): x is GeneralEnclosed {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof GeneralEnclosed)) {
+ return false;
+ }
+
+ return x.type === NodeType.GeneralEnclosed;
+ }
+}
+
+export type GeneralEnclosedWalkerEntry = ComponentValue;
+export type GeneralEnclosedWalkerParent = ContainerNode | GeneralEnclosed;
diff --git a/packages/media-query-list-parser/src/nodes/media-and.ts b/packages/media-query-list-parser/src/nodes/media-and.ts
new file mode 100644
index 000000000..376d79f8b
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-and.ts
@@ -0,0 +1,75 @@
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
+import { NodeType } from '../util/node-type';
+
+export class MediaAnd {
+ type = NodeType.MediaAnd;
+
+ modifier: Array;
+ media: MediaInParens;
+
+ constructor(modifier: Array, media: MediaInParens) {
+ this.modifier = modifier;
+ this.media = media;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.modifier,
+ ...this.media.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.modifier) + this.media.toString();
+ }
+
+ indexOf(item: MediaInParens): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaAndWalkerEntry, parent: MediaAndWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ modifier: this.modifier,
+ media: this.media.toJSON(),
+ };
+ }
+
+ isMediaAnd(): this is MediaAnd {
+ return MediaAnd.isMediaAnd(this);
+ }
+
+ static isMediaAnd(x: unknown): x is MediaAnd {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaAnd)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaAnd;
+ }
+}
+
+export type MediaAndWalkerEntry = MediaInParensWalkerEntry | MediaInParens;
+export type MediaAndWalkerParent = MediaInParensWalkerParent | MediaAnd;
diff --git a/packages/media-query-list-parser/src/nodes/media-condition-list.ts b/packages/media-query-list-parser/src/nodes/media-condition-list.ts
new file mode 100644
index 000000000..9ba0e4499
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-condition-list.ts
@@ -0,0 +1,245 @@
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaAnd, MediaAndWalkerEntry, MediaAndWalkerParent } from './media-and';
+import { MediaInParens } from './media-in-parens';
+import { MediaOr, MediaOrWalkerEntry, MediaOrWalkerParent } from './media-or';
+import { NodeType } from '../util/node-type';
+
+export type MediaConditionList = MediaConditionListWithAnd | MediaConditionListWithOr;
+
+export class MediaConditionListWithAnd {
+ type = NodeType.MediaConditionListWithAnd;
+
+ leading: MediaInParens;
+ list: Array;
+ before: Array;
+ after: Array;
+
+ constructor(leading: MediaInParens, list: Array, before: Array = [], after: Array = []) {
+ this.leading = leading;
+ this.list = list;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.before,
+ ...this.leading.tokens(),
+ ...this.list.flatMap((item) => item.tokens()),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.before) + this.leading.toString() + this.list.map((item) => item.toString()).join('') + stringify(...this.after);
+ }
+
+ indexOf(item: MediaInParens | MediaAnd): number | string {
+ if (item === this.leading) {
+ return 'leading';
+ }
+
+ if (item.type === 'media-and') {
+ return this.list.indexOf(item as MediaAnd);
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'leading') {
+ return this.leading;
+ }
+
+ if (typeof index === 'number') {
+ if (index < 0) {
+ index = this.list.length + index;
+ }
+ return this.list[index];
+ }
+ }
+
+ walk(cb: (entry: { node: MediaConditionListWithAndWalkerEntry, parent: MediaConditionListWithAndWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.leading, parent: this }, 'leading') === false) {
+ return false;
+ }
+
+ if ('walk' in this.leading) {
+ if (this.leading.walk(cb) === false) {
+ return false;
+ }
+ }
+
+ let aborted = false;
+
+ this.list.forEach((child, index) => {
+ if (aborted) {
+ return;
+ }
+
+ if (cb({ node: child, parent: this }, index) === false) {
+ aborted = true;
+ return;
+ }
+
+ if ('walk' in child) {
+ if (child.walk(cb) === false) {
+ aborted = true;
+ return;
+ }
+ }
+ });
+
+ if (aborted) {
+ return false;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ leading: this.leading.toJSON(),
+ list: this.list.map((x) => x.toJSON()),
+ before: this.before,
+ after: this.after,
+ };
+ }
+
+ isMediaConditionListWithAnd(): this is MediaConditionListWithAnd {
+ return MediaConditionListWithAnd.isMediaConditionListWithAnd(this);
+ }
+
+ static isMediaConditionListWithAnd(x: unknown): x is MediaConditionListWithAnd {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaConditionListWithAnd)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaConditionListWithAnd;
+ }
+}
+
+export type MediaConditionListWithAndWalkerEntry = MediaAndWalkerEntry | MediaAnd;
+export type MediaConditionListWithAndWalkerParent = MediaAndWalkerParent | MediaConditionListWithAnd;
+
+export class MediaConditionListWithOr {
+ type = NodeType.MediaConditionListWithOr;
+
+ leading: MediaInParens;
+ list: Array;
+ before: Array;
+ after: Array;
+
+ constructor(leading: MediaInParens, list: Array, before: Array = [], after: Array = []) {
+ this.leading = leading;
+ this.list = list;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.before,
+ ...this.leading.tokens(),
+ ...this.list.flatMap((item) => item.tokens()),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.before) + this.leading.toString() + this.list.map((item) => item.toString()).join('') + stringify(...this.after);
+ }
+
+ indexOf(item: MediaInParens | MediaOr): number | string {
+ if (item === this.leading) {
+ return 'leading';
+ }
+
+ if (item.type === 'media-or') {
+ return this.list.indexOf(item as MediaOr);
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'leading') {
+ return this.leading;
+ }
+
+ if (typeof index === 'number') {
+ if (index < 0) {
+ index = this.list.length + index;
+ }
+ return this.list[index];
+ }
+ }
+
+ walk(cb: (entry: { node: MediaConditionListWithOrWalkerEntry, parent: MediaConditionListWithOrWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.leading, parent: this }, 'leading') === false) {
+ return false;
+ }
+
+ if ('walk' in this.leading) {
+ if (this.leading.walk(cb) === false) {
+ return false;
+ }
+ }
+
+ let aborted = false;
+
+ this.list.forEach((child, index) => {
+ if (aborted) {
+ return;
+ }
+
+ if (cb({ node: child, parent: this }, index) === false) {
+ aborted = true;
+ return;
+ }
+
+ if ('walk' in child) {
+ if (child.walk(cb) === false) {
+ aborted = true;
+ return;
+ }
+ }
+ });
+
+ if (aborted) {
+ return false;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ leading: this.leading.toJSON(),
+ list: this.list.map((x) => x.toJSON()),
+ before: this.before,
+ after: this.after,
+ };
+ }
+
+ isMediaConditionListWithOr(): this is MediaConditionListWithOr {
+ return MediaConditionListWithOr.isMediaConditionListWithOr(this);
+ }
+
+ static isMediaConditionListWithOr(x: unknown): x is MediaConditionListWithOr {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaConditionListWithOr)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaConditionListWithOr;
+ }
+}
+
+export type MediaConditionListWithOrWalkerEntry = MediaOrWalkerEntry | MediaOr;
+export type MediaConditionListWithOrWalkerParent = MediaOrWalkerParent | MediaConditionListWithOr;
diff --git a/packages/media-query-list-parser/src/nodes/media-condition.ts b/packages/media-query-list-parser/src/nodes/media-condition.ts
new file mode 100644
index 000000000..766c39eaf
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-condition.ts
@@ -0,0 +1,71 @@
+import { CSSToken } from '@csstools/css-tokenizer';
+import { MediaConditionListWithAnd, MediaConditionListWithAndWalkerEntry, MediaConditionListWithAndWalkerParent, MediaConditionListWithOr, MediaConditionListWithOrWalkerEntry, MediaConditionListWithOrWalkerParent } from './media-condition-list';
+import { MediaInParens } from './media-in-parens';
+import { MediaNot, MediaNotWalkerEntry, MediaNotWalkerParent } from './media-not';
+import { NodeType } from '../util/node-type';
+
+export class MediaCondition {
+ type = NodeType.MediaCondition;
+
+ media: MediaNot | MediaInParens | MediaConditionListWithAnd | MediaConditionListWithOr;
+
+ constructor(media: MediaNot | MediaInParens |MediaConditionListWithAnd | MediaConditionListWithOr) {
+ this.media = media;
+ }
+
+ tokens(): Array {
+ return this.media.tokens();
+ }
+
+ toString(): string {
+ return this.media.toString();
+ }
+
+ indexOf(item: MediaNot | MediaInParens | MediaConditionListWithAnd | MediaConditionListWithOr): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaConditionWalkerEntry, parent: MediaConditionWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ media: this.media.toJSON(),
+ };
+ }
+
+ isMediaCondition(): this is MediaCondition {
+ return MediaCondition.isMediaCondition(this);
+ }
+
+ static isMediaCondition(x: unknown): x is MediaCondition {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaCondition)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaCondition;
+ }
+}
+
+export type MediaConditionWalkerEntry = MediaNotWalkerEntry | MediaConditionListWithAndWalkerEntry | MediaConditionListWithOrWalkerEntry | MediaNot | MediaConditionListWithAnd | MediaConditionListWithOr;
+export type MediaConditionWalkerParent = MediaNotWalkerParent | MediaConditionListWithAndWalkerParent | MediaConditionListWithOrWalkerParent | MediaCondition;
diff --git a/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts b/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts
new file mode 100644
index 000000000..f53344500
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts
@@ -0,0 +1,82 @@
+import { ComponentValue, TokenNode } from '@csstools/css-parser-algorithms';
+import { parseMediaFeatureName } from './media-feature-name';
+import { NodeType } from '../util/node-type';
+import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer';
+
+export class MediaFeatureBoolean {
+ type = NodeType.MediaFeatureBoolean;
+
+ name: ComponentValue;
+ before: Array;
+ after: Array;
+
+ constructor(name: ComponentValue, before: Array = [], after: Array = []) {
+ this.name = name;
+ this.before = before;
+ this.after = after;
+ }
+
+ getName() {
+ const token = (((this.name as TokenNode).value as CSSToken) as TokenIdent);
+ return token[4].value;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.before,
+ ...this.name.tokens(),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.before) + this.name.toString() + stringify(...this.after);
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.getName(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeatureBoolean(): this is MediaFeatureBoolean {
+ return MediaFeatureBoolean.isMediaFeatureBoolean(this);
+ }
+
+ static isMediaFeatureBoolean(x: unknown): x is MediaFeatureBoolean {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeatureBoolean)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeatureBoolean;
+ }
+}
+
+export function parseMediaFeatureBoolean(componentValues: Array) {
+ const mediaFeatureName = parseMediaFeatureName(componentValues);
+ if (mediaFeatureName === false) {
+ return mediaFeatureName;
+ }
+
+ return new MediaFeatureBoolean(mediaFeatureName.name, mediaFeatureName.before, mediaFeatureName.after);
+}
diff --git a/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
new file mode 100644
index 000000000..60520ca74
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
@@ -0,0 +1,114 @@
+import { ComponentValue, ComponentValueType } from '@csstools/css-parser-algorithms';
+import { CSSToken, TokenDelim, TokenType } from '@csstools/css-tokenizer';
+
+export enum MediaFeatureLT {
+ LT = '<',
+ LT_OR_EQ = '<=',
+}
+
+export enum MediaFeatureGT {
+ GT = '>',
+ GT_OR_EQ = '>=',
+}
+
+export enum MediaFeatureEQ {
+ EQ = '=',
+}
+
+export type MediaFeatureComparison = MediaFeatureLT | MediaFeatureGT | MediaFeatureEQ
+
+export function matchesComparison(componentValues: Array): false | [number, number] {
+ let firstTokenIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Token) {
+ const token = componentValue.value as CSSToken;
+ if (token[0] === TokenType.Delim) {
+ if (token[4].value === MediaFeatureEQ.EQ) {
+ if (firstTokenIndex !== -1) {
+ return [firstTokenIndex, i];
+ }
+
+ firstTokenIndex = i;
+ continue;
+ }
+ if (token[4].value === MediaFeatureLT.LT) {
+ firstTokenIndex = i;
+ continue;
+ }
+ if (token[4].value === MediaFeatureGT.GT) {
+ firstTokenIndex = i;
+ continue;
+ }
+ }
+ }
+
+ break;
+ }
+
+ if (firstTokenIndex !== -1) {
+ return [firstTokenIndex, firstTokenIndex];
+ }
+
+ return false;
+}
+
+export function comparisonFromTokens(tokens: [TokenDelim, TokenDelim] | [TokenDelim]): MediaFeatureComparison | false {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ /* @ts-ignore */
+ if (tokens.length === 0 || tokens.length > 2) {
+ return false;
+ }
+
+ if (tokens[0][0] !== TokenType.Delim) {
+ return false;
+ }
+
+ if (tokens.length === 1) {
+ switch (tokens[0][4].value) {
+ case MediaFeatureEQ.EQ:
+ return MediaFeatureEQ.EQ;
+ case MediaFeatureLT.LT:
+ return MediaFeatureLT.LT;
+ case MediaFeatureGT.GT:
+ return MediaFeatureGT.GT;
+ default:
+ return false;
+ }
+ }
+
+ if (tokens[1][0] !== TokenType.Delim) {
+ return false;
+ }
+
+ if (tokens[1][4].value !== MediaFeatureEQ.EQ) {
+ return false;
+ }
+
+ switch (tokens[0][4].value) {
+ case MediaFeatureLT.LT:
+ return MediaFeatureLT.LT_OR_EQ;
+ case MediaFeatureGT.GT:
+ return MediaFeatureGT.GT_OR_EQ;
+ default:
+ return false;
+ }
+}
+
+export function invertComparison(operator: MediaFeatureComparison): MediaFeatureComparison | false {
+ switch (operator) {
+ case MediaFeatureEQ.EQ:
+ return MediaFeatureEQ.EQ;
+ case MediaFeatureLT.LT:
+ return MediaFeatureGT.GT;
+ case MediaFeatureLT.LT_OR_EQ:
+ return MediaFeatureGT.GT_OR_EQ;
+ case MediaFeatureGT.GT:
+ return MediaFeatureLT.LT;
+ case MediaFeatureGT.GT_OR_EQ:
+ return MediaFeatureLT.LT_OR_EQ;
+ default:
+ return false;
+ }
+}
diff --git a/packages/media-query-list-parser/src/nodes/media-feature-name.ts b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
new file mode 100644
index 000000000..85bd0e592
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -0,0 +1,113 @@
+import { ComponentValue, ComponentValueType, TokenNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer';
+import { isIdent } from '../util/component-value-is';
+import { NodeType } from '../util/node-type';
+
+export class MediaFeatureName {
+ type = NodeType.MediaFeatureName;
+
+ name: ComponentValue;
+ before: Array;
+ after: Array;
+
+ constructor(name: ComponentValue, before: Array = [], after: Array = []) {
+ this.name = name;
+ this.before = before;
+ this.after = after;
+ }
+
+ getName() {
+ const token = (((this.name as TokenNode).value as CSSToken) as TokenIdent);
+ return token[4].value;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.before,
+ ...this.name.tokens(),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.before) + this.name.toString() + stringify(...this.after);
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.getName(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeatureName(): this is MediaFeatureName {
+ return MediaFeatureName.isMediaFeatureName(this);
+ }
+
+ static isMediaFeatureName(x: unknown): x is MediaFeatureName {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeatureName)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeatureName;
+ }
+}
+
+export function parseMediaFeatureName(componentValues: Array) {
+ let singleIdentTokenIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (isIdent(componentValue)) {
+ if (singleIdentTokenIndex !== -1) {
+ return false;
+ }
+
+ singleIdentTokenIndex = i;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (singleIdentTokenIndex === -1) {
+ return false;
+ }
+
+ return new MediaFeatureName(
+ componentValues[singleIdentTokenIndex],
+ componentValues.slice(0, singleIdentTokenIndex).flatMap((x) => {
+ return x.tokens();
+ }),
+ componentValues.slice(singleIdentTokenIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ );
+}
diff --git a/packages/media-query-list-parser/src/nodes/media-feature-plain.ts b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
new file mode 100644
index 000000000..4d1a32d5f
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -0,0 +1,124 @@
+import { ComponentValue, ComponentValueType } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenColon, TokenType } from '@csstools/css-tokenizer';
+import { parseMediaFeatureName, MediaFeatureName } from './media-feature-name';
+import { parseMediaFeatureValue, MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
+import { NodeType } from '../util/node-type';
+
+export class MediaFeaturePlain {
+ type = NodeType.MediaFeaturePlain;
+
+ name: MediaFeatureName;
+ colon: TokenColon;
+ value: MediaFeatureValue;
+
+ constructor(name: MediaFeatureName, colon: TokenColon, value: MediaFeatureValue) {
+ this.name = name;
+ this.colon = colon;
+ this.value = value;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.name.tokens(),
+ this.colon,
+ ...this.value.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return this.name.toString() + stringify(this.colon) + this.value.toString();
+ }
+
+ indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ if (item === this.value) {
+ return 'value';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+
+ if (index === 'value') {
+ return this.value;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeaturePlainWalkerEntry, parent: MediaFeaturePlainWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
+ return false;
+ }
+
+ return this.value.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.name.toJSON(),
+ value: this.value.toJSON(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeaturePlain(): this is MediaFeaturePlain {
+ return MediaFeaturePlain.isMediaFeaturePlain(this);
+ }
+
+ static isMediaFeaturePlain(x: unknown): x is MediaFeaturePlain {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeaturePlain)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeaturePlain;
+ }
+}
+
+export type MediaFeaturePlainWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
+export type MediaFeaturePlainWalkerParent = MediaFeatureValueWalkerParent | MediaFeaturePlain;
+
+export function parseMediaFeaturePlain(componentValues: Array) {
+ let a: Array = [];
+ let b: Array = [];
+ let colon: TokenColon | null = null;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Token) {
+ const token = componentValue.value as CSSToken;
+ if (token[0] === TokenType.Colon) {
+ a = componentValues.slice(0, i);
+ b = componentValues.slice(i + 1);
+ colon = token;
+ break;
+ }
+ }
+ }
+
+ if (!a.length || !b.length) {
+ return false;
+ }
+
+ const name = parseMediaFeatureName(a);
+ if (name === false) {
+ return false;
+ }
+
+ const value = parseMediaFeatureValue(b);
+ if (value === false) {
+ return false;
+ }
+
+ return new MediaFeaturePlain(name, colon , value);
+}
diff --git a/packages/media-query-list-parser/src/nodes/media-feature-range.ts b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
new file mode 100644
index 000000000..7f9f5aff4
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -0,0 +1,502 @@
+import { ComponentValue, ComponentValueType, TokenNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenDelim, TokenType } from '@csstools/css-tokenizer';
+import { comparisonFromTokens, matchesComparison, MediaFeatureEQ, MediaFeatureGT, MediaFeatureLT } from './media-feature-comparison';
+import { MediaFeatureName, parseMediaFeatureName } from './media-feature-name';
+import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent, parseMediaFeatureValue } from './media-feature-value';
+import { NodeType } from '../util/node-type';
+
+export type MediaFeatureRange = MediaFeatureRangeNameValue |
+ MediaFeatureRangeValueName |
+ MediaFeatureRangeValueNameValue;
+
+export class MediaFeatureRangeNameValue {
+ type = NodeType.MediaFeatureRangeNameValue;
+
+ name: MediaFeatureName;
+ operator: [TokenDelim, TokenDelim] | [TokenDelim];
+ value: MediaFeatureValue;
+
+ constructor(name: MediaFeatureName, operator: [TokenDelim, TokenDelim] | [TokenDelim], value: MediaFeatureValue) {
+ this.name = name;
+ this.operator = operator;
+ this.value = value;
+ }
+
+ operatorKind() {
+ return comparisonFromTokens(this.operator);
+ }
+
+ tokens(): Array {
+ return [
+ ...this.name.tokens(),
+ ...this.operator,
+ ...this.value.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return this.name.toString() + stringify(...this.operator) + this.value.toString();
+ }
+
+ indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ if (item === this.value) {
+ return 'value';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+
+ if (index === 'value') {
+ return this.value;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.name.toJSON(),
+ value: this.value.toJSON(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeatureRangeNameValue(): this is MediaFeatureRangeNameValue {
+ return MediaFeatureRangeNameValue.isMediaFeatureRangeNameValue(this);
+ }
+
+ static isMediaFeatureRangeNameValue(x: unknown): x is MediaFeatureRangeNameValue {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeatureRangeNameValue)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeatureRangeNameValue;
+ }
+}
+
+export class MediaFeatureRangeValueName {
+ type = NodeType.MediaFeatureRangeValueName;
+
+ name: MediaFeatureName;
+ operator: [TokenDelim, TokenDelim] | [TokenDelim];
+ value: MediaFeatureValue;
+
+ constructor(name: MediaFeatureName, operator: [TokenDelim, TokenDelim] | [TokenDelim], value: MediaFeatureValue) {
+ this.name = name;
+ this.operator = operator;
+ this.value = value;
+ }
+
+ operatorKind() {
+ return comparisonFromTokens(this.operator);
+ }
+
+ tokens(): Array {
+ return [
+ ...this.value.tokens(),
+ ...this.operator,
+ ...this.name.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return this.value.toString() + stringify(...this.operator) + this.name.toString();
+ }
+
+ indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ if (item === this.value) {
+ return 'value';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+
+ if (index === 'value') {
+ return this.value;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.name.toJSON(),
+ value: this.value.toJSON(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeatureRangeValueName(): this is MediaFeatureRangeValueName {
+ return MediaFeatureRangeValueName.isMediaFeatureRangeValueName(this);
+ }
+
+ static isMediaFeatureRangeValueName(x: unknown): x is MediaFeatureRangeValueName {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeatureRangeValueName)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeatureRangeValueName;
+ }
+}
+
+export class MediaFeatureRangeValueNameValue {
+ type = NodeType.MediaFeatureRangeValueNameValue;
+
+ name: MediaFeatureName;
+ valueOne: MediaFeatureValue;
+ valueOneOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+ valueTwo: MediaFeatureValue;
+ valueTwoOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+
+ constructor(name: MediaFeatureName, valueOne: MediaFeatureValue, valueOneOperator: [TokenDelim, TokenDelim] | [TokenDelim], valueTwo: MediaFeatureValue, valueTwoOperator: [TokenDelim, TokenDelim] | [TokenDelim]) {
+ this.name = name;
+ this.valueOne = valueOne;
+ this.valueOneOperator = valueOneOperator;
+ this.valueTwo = valueTwo;
+ this.valueTwoOperator = valueTwoOperator;
+ }
+
+ valueOneOperatorKind() {
+ return comparisonFromTokens(this.valueOneOperator);
+ }
+
+ valueTwoOperatorKind() {
+ return comparisonFromTokens(this.valueTwoOperator);
+ }
+
+ tokens(): Array {
+ return [
+ ...this.valueOne.tokens(),
+ ...this.valueOneOperator,
+ ...this.name.tokens(),
+ ...this.valueTwoOperator,
+ ...this.valueTwo.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return this.valueOne.toString() + stringify(...this.valueOneOperator) + this.name.toString() + stringify(...this.valueTwoOperator) + this.valueTwo.toString();
+ }
+
+ indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ if (item === this.valueOne) {
+ return 'valueOne';
+ }
+
+ if (item === this.valueTwo) {
+ return 'valueTwo';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+
+ if (index === 'valueOne') {
+ return this.valueOne;
+ }
+
+ if (index === 'valueTwo') {
+ return this.valueTwo;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.valueOne, parent: this }, 'valueOne') === false) {
+ return false;
+ }
+
+ if ('walk' in this.valueOne) {
+ if (this.valueOne.walk(cb) === false) {
+ return false;
+ }
+ }
+
+ if (cb({ node: this.valueTwo, parent: this }, 'valueTwo') === false) {
+ return false;
+ }
+
+ if ('walk' in this.valueTwo) {
+ if (this.valueTwo.walk(cb) === false) {
+ return false;
+ }
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ name: this.name.toJSON(),
+ valueOne: this.valueOne.toJSON(),
+ valueTwo: this.valueTwo.toJSON(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeatureRangeValueNameValue(): this is MediaFeatureRangeValueNameValue {
+ return MediaFeatureRangeValueNameValue.isMediaFeatureRangeValueNameValue(this);
+ }
+
+ static isMediaFeatureRangeValueNameValue(x: unknown): x is MediaFeatureRangeValueNameValue {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeatureRangeValueNameValue)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeatureRangeValueNameValue;
+ }
+}
+
+export type MediaFeatureRangeWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
+export type MediaFeatureRangeWalkerParent = MediaFeatureValueWalkerParent | MediaFeatureRange;
+
+export function parseMediaFeatureRange(componentValues: Array) {
+ let comparisonOne: false | [number, number] = false;
+ let comparisonTwo: false | [number, number] = false;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Token) {
+ const token = componentValue.value as CSSToken;
+ if (token[0] === TokenType.Delim) {
+ const comparison = matchesComparison(componentValues.slice(i));
+ if (comparison !== false) {
+ if (comparisonOne === false) {
+ comparisonOne = [
+ comparison[0] + i,
+ comparison[1] + i,
+ ];
+ i += comparison[1];
+ } else {
+ comparisonTwo = [
+ comparison[0] + i,
+ comparison[1] + i,
+ ];
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (comparisonOne === false) {
+ return false;
+ }
+
+ const comparisonTokensOne: [TokenDelim, TokenDelim] | [TokenDelim] = [
+ (componentValues[comparisonOne[0]] as TokenNode).value as TokenDelim,
+ ];
+ if (comparisonOne[0] !== comparisonOne[1]) {
+ comparisonTokensOne.push(
+ (componentValues[comparisonOne[1]] as TokenNode).value as TokenDelim,
+ );
+ }
+
+ if (comparisonTwo === false) {
+ const a = componentValues.slice(0, comparisonOne[0]);
+ const b = componentValues.slice(comparisonOne[1] + 1);
+
+ const nameA = parseMediaFeatureName(a);
+ const nameB = parseMediaFeatureName(b);
+
+ if (!nameA && !nameB) {
+ return false;
+ }
+
+ if (
+ (nameA && !nameB) ||
+ nameA && mediaDescriptors.has(nameA.getName().toLowerCase())
+ ) {
+ const value = parseMediaFeatureValue(b);
+ if (!value) {
+ return false;
+ }
+
+ return new MediaFeatureRangeNameValue(nameA, comparisonTokensOne, value);
+ }
+
+ if (
+ (!nameA && nameB) ||
+ nameB && mediaDescriptors.has(nameB.getName().toLowerCase())
+ ) {
+ const value = parseMediaFeatureValue(a);
+ if (!value) {
+ return false;
+ }
+
+ return new MediaFeatureRangeValueName(nameB, comparisonTokensOne, value);
+ }
+
+ return false;
+ }
+
+ const comparisonTokensTwo: [TokenDelim, TokenDelim] | [TokenDelim] = [
+ (componentValues[comparisonTwo[0]] as TokenNode).value as TokenDelim,
+ ];
+ if (comparisonTwo[0] !== comparisonTwo[1]) {
+ comparisonTokensTwo.push(
+ (componentValues[comparisonTwo[1]] as TokenNode).value as TokenDelim,
+ );
+ }
+
+ const a = componentValues.slice(0, comparisonOne[0]);
+ const b = componentValues.slice(comparisonOne[1] + 1, comparisonTwo[0]);
+ const c = componentValues.slice(comparisonTwo[1] + 1);
+
+ const valueA = parseMediaFeatureValue(a);
+ const nameB = parseMediaFeatureName(b);
+ const valueC = parseMediaFeatureValue(c);
+
+ if (!valueA || !nameB || !valueC) {
+ return false;
+ }
+
+ // https://www.w3.org/TR/mediaqueries-5/#mq-range-context
+ // Only certain comparison operators are allowed and only in certain combinations.
+ {
+ const comparisonOneCheck = comparisonFromTokens(comparisonTokensOne);
+ if (
+ comparisonOneCheck === false ||
+ comparisonOneCheck === MediaFeatureEQ.EQ
+ ) {
+ return false;
+ }
+ const comparisonTwoCheck = comparisonFromTokens(comparisonTokensTwo);
+ if (
+ comparisonTwoCheck === false ||
+ comparisonTwoCheck === MediaFeatureEQ.EQ
+ ) {
+ return false;
+ }
+
+ if (
+ (
+ comparisonOneCheck === MediaFeatureLT.LT ||
+ comparisonOneCheck === MediaFeatureLT.LT_OR_EQ
+ )
+ &&
+ (
+ comparisonTwoCheck === MediaFeatureGT.GT ||
+ comparisonTwoCheck === MediaFeatureGT.GT_OR_EQ
+ )
+ ) {
+ return false;
+ }
+
+ if (
+ (
+ comparisonOneCheck === MediaFeatureGT.GT ||
+ comparisonOneCheck === MediaFeatureGT.GT_OR_EQ
+ )
+ &&
+ (
+ comparisonTwoCheck === MediaFeatureLT.LT ||
+ comparisonTwoCheck === MediaFeatureLT.LT_OR_EQ
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return new MediaFeatureRangeValueNameValue(
+ nameB,
+ valueA,
+ comparisonTokensOne,
+ valueC,
+ comparisonTokensTwo,
+ );
+}
+
+export const mediaDescriptors = new Set([
+ 'any-hover',
+ 'any-pointer',
+ 'aspect-ratio',
+ 'color',
+ 'color-gamut',
+ 'color-index',
+ 'device-aspect-ratio',
+ 'device-height',
+ 'device-width',
+ 'display-mode',
+ 'dynamic-range',
+ 'environment-blending',
+ 'forced-colors',
+ 'grid',
+ 'height',
+ 'horizontal-viewport-segments',
+ 'hover',
+ 'inverted-colors',
+ 'monochrome',
+ 'nav-controls',
+ 'orientation',
+ 'overflow-block',
+ 'overflow-inline',
+ 'pointer',
+ 'prefers-color-scheme',
+ 'prefers-contrast',
+ 'prefers-reduced-data',
+ 'prefers-reduced-motion',
+ 'prefers-reduced-transparency',
+ 'resolution',
+ 'scan',
+ 'scripting',
+ 'update',
+ 'vertical-viewport-segments',
+ 'video-color-gamut',
+ 'video-dynamic-range',
+ 'width',
+]);
diff --git a/packages/media-query-list-parser/src/nodes/media-feature-value.ts b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
new file mode 100644
index 000000000..642a7487d
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
@@ -0,0 +1,244 @@
+import { ComponentValue, ComponentValueType, ContainerNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
+import { isDimension, isIdent, isNumber } from '../util/component-value-is';
+import { NodeType } from '../util/node-type';
+
+export class MediaFeatureValue {
+ type = NodeType.MediaFeatureValue;
+
+ value: ComponentValue | Array;
+ before: Array;
+ after: Array;
+
+ constructor(value: ComponentValue | Array, before: Array = [], after: Array = []) {
+ if (Array.isArray(value) && value.length === 1) {
+ this.value = value[0];
+ } else {
+ this.value = value;
+ }
+
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens(): Array {
+ if (Array.isArray(this.value)) {
+ return [
+ ...this.before,
+ ...this.value.flatMap((x) => x.tokens()),
+ ...this.after,
+ ];
+ }
+
+ return [
+ ...this.before,
+ ...this.value.tokens(),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ if (Array.isArray(this.value)) {
+ return stringify(...this.before) + this.value.map((x) => x.toString()).join('') + stringify(...this.after);
+ }
+
+ return stringify(...this.before) + this.value.toString() + stringify(...this.after);
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ if (item === this.value) {
+ return 'value';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'value') {
+ return this.value;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureValueWalkerEntry, parent: MediaFeatureValueWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+
+ toJSON() {
+ if (Array.isArray(this.value)) {
+ return {
+ type: this.type,
+ value: this.value.map((x) => x.toJSON()),
+ tokens: this.tokens(),
+ };
+ }
+
+ return {
+ type: this.type,
+ value: this.value.toJSON(),
+ tokens: this.tokens(),
+ };
+ }
+
+ isMediaFeatureValue(): this is MediaFeatureValue {
+ return MediaFeatureValue.isMediaFeatureValue(this);
+ }
+
+ static isMediaFeatureValue(x: unknown): x is MediaFeatureValue {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeatureValue)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeatureValue;
+ }
+}
+
+export type MediaFeatureValueWalkerEntry = ComponentValue | Array;
+export type MediaFeatureValueWalkerParent = ContainerNode | MediaFeatureValue;
+
+export function parseMediaFeatureValue(componentValues: Array) {
+ let candidateIndexStart = -1;
+ let candidateIndexEnd = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (candidateIndexStart !== -1) {
+ return false;
+ }
+
+ if (isNumber(componentValue)) {
+ const maybeRatio = matchesRatioExactly(componentValues.slice(i));
+ if (maybeRatio !== -1) {
+ candidateIndexStart = maybeRatio[0] + i;
+ candidateIndexEnd = maybeRatio[1] + i;
+ i += maybeRatio[1] - maybeRatio[0];
+ continue;
+ }
+
+ candidateIndexStart = i;
+ candidateIndexEnd = i;
+ continue;
+ }
+
+ if (isDimension(componentValue)) {
+ candidateIndexStart = i;
+ candidateIndexEnd = i;
+ continue;
+ }
+
+ if (isIdent(componentValue)) {
+ candidateIndexStart = i;
+ candidateIndexEnd = i;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (candidateIndexStart === -1) {
+ return false;
+ }
+
+ return new MediaFeatureValue(
+ componentValues.slice(candidateIndexStart, candidateIndexEnd + 1),
+ componentValues.slice(0, candidateIndexStart).flatMap((x) => {
+ return x.tokens();
+ }),
+ componentValues.slice(candidateIndexEnd + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ );
+}
+
+export function matchesRatioExactly(componentValues: Array) {
+ let firstNumber = -1;
+ let secondNumber = -1;
+
+ const result = matchesRatio(componentValues);
+ if (result === -1) {
+ return -1;
+ }
+
+ firstNumber = result[0];
+ secondNumber = result[1];
+
+ for (let i = secondNumber+1; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === 'whitespace') {
+ continue;
+ }
+
+ if (componentValue.type === 'comment') {
+ continue;
+ }
+
+ return -1;
+ }
+
+ return [firstNumber, secondNumber];
+}
+
+export function matchesRatio(componentValues: Array) {
+ let firstNumber = -1;
+ let delim = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === 'whitespace') {
+ continue;
+ }
+
+ if (componentValue.type === 'comment') {
+ continue;
+ }
+
+ if (componentValue.type === 'token') {
+ const token = componentValue.value as CSSToken;
+ if (token[0] === TokenType.Delim && token[4].value === '/') {
+ if (firstNumber === -1) {
+ return -1;
+ }
+
+ if (delim !== -1) {
+ return -1;
+ }
+
+ delim = i;
+ continue;
+ }
+ }
+
+ if (isNumber(componentValue)) {
+ if (delim !== -1) {
+ return [firstNumber, i];
+ } else if (firstNumber !== -1) {
+ return -1;
+ } else {
+ firstNumber = i;
+ continue;
+ }
+ }
+
+ return -1;
+ }
+
+ return -1;
+}
+
diff --git a/packages/media-query-list-parser/src/nodes/media-feature.ts b/packages/media-query-list-parser/src/nodes/media-feature.ts
new file mode 100644
index 000000000..d9a9a4137
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature.ts
@@ -0,0 +1,143 @@
+import { SimpleBlockNode, TokenNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
+import { MediaFeatureBoolean, parseMediaFeatureBoolean } from './media-feature-boolean';
+import { MediaFeatureName } from './media-feature-name';
+import { MediaFeaturePlain, MediaFeaturePlainWalkerEntry, MediaFeaturePlainWalkerParent, parseMediaFeaturePlain } from './media-feature-plain';
+import { MediaFeatureRange, MediaFeatureRangeWalkerEntry, MediaFeatureRangeWalkerParent, parseMediaFeatureRange } from './media-feature-range';
+import { MediaFeatureValue } from './media-feature-value';
+import { NodeType } from '../util/node-type';
+
+export class MediaFeature {
+ type = NodeType.MediaFeature;
+
+ feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
+ before: Array;
+ after: Array;
+
+ constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange, before: Array = [], after: Array = []) {
+ this.feature = feature;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.before,
+ ...this.feature.tokens(),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.before) + this.feature.toString() + stringify(...this.after);
+ }
+
+ indexOf(item: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange): number | string {
+ if (item === this.feature) {
+ return 'feature';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'feature') {
+ return this.feature;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureWalkerEntry, parent: MediaFeatureWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.feature, parent: this }, 'feature') === false) {
+ return false;
+ }
+
+ if ('walk' in this.feature) {
+ return this.feature.walk(cb);
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ feature: this.feature.toJSON(),
+ before: this.before,
+ after: this.after,
+ };
+ }
+
+ isMediaFeature(): this is MediaFeature {
+ return MediaFeature.isMediaFeature(this);
+ }
+
+ static isMediaFeature(x: unknown): x is MediaFeature {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaFeature)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaFeature;
+ }
+}
+
+export type MediaFeatureWalkerEntry = MediaFeaturePlainWalkerEntry | MediaFeatureRangeWalkerEntry | MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
+export type MediaFeatureWalkerParent = MediaFeaturePlainWalkerParent | MediaFeatureRangeWalkerParent | MediaFeature;
+
+export function parseMediaFeature(simpleBlock: SimpleBlockNode, before: Array = [], after: Array = []) {
+ if (simpleBlock.startToken[0] !== TokenType.OpenParen) {
+ return false;
+ }
+
+ const boolean = parseMediaFeatureBoolean(simpleBlock.value);
+ if (boolean !== false) {
+ return new MediaFeature(boolean, before, after);
+ }
+
+ const plain = parseMediaFeaturePlain(simpleBlock.value);
+ if (plain !== false) {
+ return new MediaFeature(plain, before, after);
+ }
+
+ const range = parseMediaFeatureRange(simpleBlock.value);
+ if (range !== false) {
+ return new MediaFeature(range, before, after);
+ }
+
+ return false;
+}
+
+export function newMediaFeatureBoolean(name: string) {
+ return new MediaFeature(
+ new MediaFeatureBoolean(
+ new TokenNode([TokenType.Ident, name, -1, -1, { value: name }]),
+ ),
+ [
+ [TokenType.OpenParen, '(', -1, -1, undefined],
+ ],
+ [
+ [TokenType.CloseParen, ')', -1, -1, undefined],
+ ],
+ );
+}
+
+export function newMediaFeaturePlain(name: string, ...value: Array) {
+ return new MediaFeature(
+ new MediaFeaturePlain(
+ new MediaFeatureName(
+ new TokenNode([TokenType.Ident, name, -1, -1, { value: name }]),
+ ),
+ [TokenType.Colon, ':', -1, -1, undefined],
+ new MediaFeatureValue(
+ value.map((x) => new TokenNode(x)),
+ ),
+ ),
+ [
+ [TokenType.OpenParen, '(', -1, -1, undefined],
+ ],
+ [
+ [TokenType.CloseParen, ')', -1, -1, undefined],
+ ],
+ );
+}
diff --git a/packages/media-query-list-parser/src/nodes/media-in-parens.ts b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
new file mode 100644
index 000000000..02aff59a3
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
@@ -0,0 +1,91 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { GeneralEnclosed } from './general-enclosed';
+import { MediaAnd } from './media-and';
+import { MediaCondition } from './media-condition';
+import { MediaConditionList } from './media-condition-list';
+import { MediaFeature } from './media-feature';
+import { MediaFeatureBoolean } from './media-feature-boolean';
+import { MediaFeatureName } from './media-feature-name';
+import { MediaFeaturePlain } from './media-feature-plain';
+import { MediaFeatureRange } from './media-feature-range';
+import { MediaFeatureValue } from './media-feature-value';
+import { NodeType } from '../util/node-type';
+
+export class MediaInParens {
+ type = NodeType.MediaInParens;
+
+ media: MediaCondition | MediaFeature | GeneralEnclosed;
+ before: Array;
+ after: Array;
+
+ constructor(media: MediaCondition | MediaFeature | GeneralEnclosed, before: Array = [], after: Array = []) {
+ this.media = media;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.before,
+ ...this.media.tokens(),
+ ...this.after,
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.before) + this.media.toString() + stringify(...this.after);
+ }
+
+ indexOf(item: MediaCondition | MediaFeature | GeneralEnclosed): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaInParensWalkerEntry, parent: MediaInParensWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ if ('walk' in this.media) {
+ return this.media.walk(cb);
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ media: this.media.toJSON(),
+ before: this.before,
+ after: this.after,
+ };
+ }
+
+ isMediaInParens(): this is MediaInParens {
+ return MediaInParens.isMediaInParens(this);
+ }
+
+ static isMediaInParens(x: unknown): x is MediaInParens {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaInParens)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaInParens;
+ }
+}
+
+export type MediaInParensWalkerEntry = ComponentValue | Array | GeneralEnclosed | MediaAnd | MediaConditionList | MediaCondition | MediaFeatureBoolean | MediaFeatureName | MediaFeaturePlain | MediaFeatureRange | MediaFeatureValue | MediaFeature | GeneralEnclosed | MediaInParens;
+export type MediaInParensWalkerParent = ContainerNode | GeneralEnclosed | MediaAnd | MediaConditionList | MediaCondition | MediaFeatureBoolean | MediaFeatureName | MediaFeaturePlain | MediaFeatureRange | MediaFeatureValue | MediaFeature | GeneralEnclosed | MediaInParens;
diff --git a/packages/media-query-list-parser/src/nodes/media-not.ts b/packages/media-query-list-parser/src/nodes/media-not.ts
new file mode 100644
index 000000000..1a81910e6
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-not.ts
@@ -0,0 +1,75 @@
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
+import { NodeType } from '../util/node-type';
+
+export class MediaNot {
+ type = NodeType.MediaNot;
+
+ modifier: Array;
+ media: MediaInParens;
+
+ constructor(modifier: Array, media: MediaInParens) {
+ this.modifier = modifier;
+ this.media = media;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.modifier,
+ ...this.media.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.modifier) + this.media.toString();
+ }
+
+ indexOf(item: MediaInParens): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaNotWalkerEntry, parent: MediaNotWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ modifier: this.modifier,
+ media: this.media.toJSON(),
+ };
+ }
+
+ isMediaNot(): this is MediaNot {
+ return MediaNot.isMediaNot(this);
+ }
+
+ static isMediaNot(x: unknown): x is MediaNot {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaNot)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaNot;
+ }
+}
+
+export type MediaNotWalkerEntry = MediaInParensWalkerEntry | MediaInParens;
+export type MediaNotWalkerParent = MediaInParensWalkerParent | MediaNot;
diff --git a/packages/media-query-list-parser/src/nodes/media-or.ts b/packages/media-query-list-parser/src/nodes/media-or.ts
new file mode 100644
index 000000000..9d45e629f
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-or.ts
@@ -0,0 +1,75 @@
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
+import { NodeType } from '../util/node-type';
+
+export class MediaOr {
+ type = NodeType.MediaOr;
+
+ modifier: Array;
+ media: MediaInParens;
+
+ constructor(modifier: Array, media: MediaInParens) {
+ this.modifier = modifier;
+ this.media = media;
+ }
+
+ tokens(): Array {
+ return [
+ ...this.modifier,
+ ...this.media.tokens(),
+ ];
+ }
+
+ toString(): string {
+ return stringify(...this.modifier) + this.media.toString();
+ }
+
+ indexOf(item: MediaInParens): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaOrWalkerEntry, parent: MediaOrWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ modifier: this.modifier,
+ media: this.media.toJSON(),
+ };
+ }
+
+ isMediaOr(): this is MediaOr {
+ return MediaOr.isMediaOr(this);
+ }
+
+ static isMediaOr(x: unknown): x is MediaOr {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaOr)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaOr;
+ }
+}
+
+export type MediaOrWalkerEntry = MediaInParensWalkerEntry | MediaInParens;
+export type MediaOrWalkerParent = MediaInParensWalkerParent | MediaOr;
diff --git a/packages/media-query-list-parser/src/nodes/media-query-modifier.ts b/packages/media-query-list-parser/src/nodes/media-query-modifier.ts
new file mode 100644
index 000000000..22d94d092
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-query-modifier.ts
@@ -0,0 +1,22 @@
+import { TokenIdent, TokenType } from '@csstools/css-tokenizer';
+
+export enum MediaQueryModifier {
+ Not = 'not',
+ Only = 'only'
+}
+
+export function modifierFromToken(token: TokenIdent): MediaQueryModifier | false {
+ if (token[0] !== TokenType.Ident) {
+ return false;
+ }
+
+ const matchingValue = token[4].value.toLowerCase();
+ switch (matchingValue) {
+ case MediaQueryModifier.Not:
+ return MediaQueryModifier.Not;
+ case MediaQueryModifier.Only:
+ return MediaQueryModifier.Only;
+ default:
+ return false;
+ }
+}
diff --git a/packages/media-query-list-parser/src/nodes/media-query.ts b/packages/media-query-list-parser/src/nodes/media-query.ts
new file mode 100644
index 000000000..e6a1a6531
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-query.ts
@@ -0,0 +1,350 @@
+import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
+import { NodeType } from '../util/node-type';
+import { MediaCondition, MediaConditionWalkerEntry, MediaConditionWalkerParent } from './media-condition';
+import { MediaInParens } from './media-in-parens';
+import { MediaNot } from './media-not';
+
+export type MediaQuery = MediaQueryWithType | MediaQueryWithoutType | MediaQueryInvalid;
+
+export class MediaQueryWithType {
+ type = NodeType.MediaQueryWithType;
+
+ modifier: Array;
+ mediaType: Array;
+ and: Array;
+ media: MediaCondition | null = null;
+
+ constructor(modifier: Array, mediaType: Array, and?: Array, media?: MediaCondition | null) {
+ this.modifier = modifier;
+ this.mediaType = mediaType;
+
+ if (and && media) {
+ this.and = and;
+ this.media = media;
+ }
+ }
+
+ getModifier() {
+ if (!this.modifier.length) {
+ return '';
+ }
+
+ for (let i = 0; i < this.modifier.length; i++) {
+ const token = this.modifier[i];
+ if (token[0] === TokenType.Ident) {
+ return token[4].value;
+ }
+ }
+
+ return '';
+ }
+
+ negateQuery(): MediaQuery {
+ const copy = new MediaQueryWithType([...this.modifier], [...this.mediaType], this.and, this.media);
+ if (copy.modifier.length === 0) {
+ copy.modifier = [
+ [TokenType.Ident, 'not', -1, -1, { value: 'not' }],
+ [TokenType.Whitespace, ' ', -1, -1, undefined],
+ ];
+
+ return copy;
+ }
+
+ for (let i = 0; i < copy.modifier.length; i++) {
+ const token = copy.modifier[i];
+ if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'not') {
+ copy.modifier.splice(i, 1);
+ break;
+ }
+
+ if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'only') {
+ copy.modifier[i][1] = 'not';
+ copy.modifier[i][4].value = 'not';
+ break;
+ }
+ }
+
+ return copy;
+ }
+
+ getMediaType() {
+ if (!this.mediaType.length) {
+ return '';
+ }
+
+ for (let i = 0; i < this.mediaType.length; i++) {
+ const token = this.mediaType[i];
+ if (token[0] === TokenType.Ident) {
+ return token[4].value;
+ }
+ }
+
+ return '';
+ }
+
+ tokens() {
+ if (this.and && this.media) {
+ return [
+ ...this.modifier,
+ ...this.mediaType,
+ ...this.and,
+ ...this.media.tokens(),
+ ];
+ }
+
+ return [
+ ...this.modifier,
+ ...this.mediaType,
+ ];
+ }
+
+ toString() {
+ if (this.and && this.media) {
+ return stringify(...this.modifier) + stringify(...this.mediaType) + stringify(...this.and) + this.media.toString();
+ }
+
+ return stringify(...this.modifier) + stringify(...this.mediaType);
+ }
+
+ indexOf(item: MediaCondition): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaQueryWithTypeWalkerEntry, parent: MediaQueryWithTypeWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ if (!this.media) {
+ return;
+ }
+
+ return this.media.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ string: this.toString(),
+ modifier: this.modifier,
+ mediaType: this.mediaType,
+ and: this.and,
+ media: this.media,
+ };
+ }
+
+ isMediaQueryWithType(): this is MediaQueryWithType {
+ return MediaQueryWithType.isMediaQueryWithType(this);
+ }
+
+ static isMediaQueryWithType(x: unknown): x is MediaQueryWithType {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaQueryWithType)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaQueryWithType;
+ }
+}
+
+export type MediaQueryWithTypeWalkerEntry = MediaConditionWalkerEntry | MediaCondition;
+export type MediaQueryWithTypeWalkerParent = MediaConditionWalkerParent | MediaQueryWithType;
+
+export class MediaQueryWithoutType {
+ type = NodeType.MediaQueryWithoutType;
+
+ media: MediaCondition;
+
+ constructor(media: MediaCondition) {
+ this.media = media;
+ }
+
+ negateQuery(): MediaQuery {
+ let mediaCondition = this.media;
+ if (mediaCondition.media.type === NodeType.MediaNot) {
+ return new MediaQueryWithoutType(
+ new MediaCondition(
+ (mediaCondition.media as MediaNot).media,
+ ),
+ );
+ }
+
+ if (mediaCondition.media.type === NodeType.MediaConditionListWithOr) {
+ mediaCondition = new MediaCondition(
+ new MediaInParens(
+ mediaCondition,
+ [
+ [TokenType.Whitespace, ' ', 0, 0, undefined],
+ [TokenType.OpenParen, '(', 0, 0, undefined],
+ ],
+ [
+ [TokenType.CloseParen, ')', 0, 0, undefined],
+ ],
+ ),
+ );
+ }
+
+ const query = new MediaQueryWithType(
+ [
+ [TokenType.Ident, 'not', 0, 0, { value: 'not' }],
+ [TokenType.Whitespace, ' ', 0, 0, undefined],
+ ],
+ [
+ [TokenType.Ident, 'all', 0, 0, { value: 'all' }],
+ [TokenType.Whitespace, ' ', 0, 0, undefined],
+ ],
+ [
+ [TokenType.Ident, 'and', 0, 0, { value: 'and' }],
+ ],
+ mediaCondition,
+ );
+
+ return query;
+ }
+
+ tokens(): Array {
+ return this.media.tokens();
+ }
+
+ toString(): string {
+ return this.media.toString();
+ }
+
+ indexOf(item: MediaCondition): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaQueryWithoutTypeWalkerEntry, parent: MediaQueryWithoutTypeWalkerParent }, index: number | string) => boolean | void) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ string: this.toString(),
+ media: this.media,
+ };
+ }
+
+ isMediaQueryWithoutType(): this is MediaQueryWithoutType {
+ return MediaQueryWithoutType.isMediaQueryWithoutType(this);
+ }
+
+ static isMediaQueryWithoutType(x: unknown): x is MediaQueryWithoutType {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaQueryWithoutType)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaQueryWithoutType;
+ }
+}
+
+export type MediaQueryWithoutTypeWalkerEntry = MediaConditionWalkerEntry | MediaCondition;
+export type MediaQueryWithoutTypeWalkerParent = MediaConditionWalkerParent | MediaQueryWithoutType;
+
+export class MediaQueryInvalid {
+ type = NodeType.MediaQueryInvalid;
+
+ media: Array;
+
+ constructor(media: Array) {
+ this.media = media;
+ }
+
+ negateQuery(): MediaQuery {
+ return new MediaQueryInvalid(this.media);
+ }
+
+ tokens(): Array {
+ return this.media.flatMap((x) => x.tokens());
+ }
+
+ toString(): string {
+ return this.media.map((x) => x.toString()).join('');
+ }
+
+ walk(cb: (entry: { node: MediaQueryInvalidWalkerEntry, parent: MediaQueryInvalidWalkerParent }, index: number | string) => boolean | void) {
+ let aborted = false;
+
+ this.media.forEach((child, index) => {
+ if (aborted) {
+ return;
+ }
+
+ if (cb({ node: child, parent: this }, index) === false) {
+ aborted = true;
+ return;
+ }
+
+ if ('walk' in child) {
+ if (child.walk(cb) === false) {
+ aborted = true;
+ return;
+ }
+ }
+ });
+
+ if (aborted) {
+ return false;
+ }
+ }
+
+ toJSON() {
+ return {
+ type: this.type,
+ string: this.toString(),
+ media: this.media,
+ };
+ }
+
+ isMediaQueryInvalid(): this is MediaQueryInvalid {
+ return MediaQueryInvalid.isMediaQueryInvalid(this);
+ }
+
+ static isMediaQueryInvalid(x: unknown): x is MediaQueryInvalid {
+ if (!x) {
+ return false;
+ }
+
+ if (!(x instanceof MediaQueryInvalid)) {
+ return false;
+ }
+
+ return x.type === NodeType.MediaQueryInvalid;
+ }
+}
+
+export type MediaQueryInvalidWalkerEntry = ComponentValue;
+export type MediaQueryInvalidWalkerParent = ComponentValue | MediaQueryInvalid;
diff --git a/packages/media-query-list-parser/src/nodes/media-type.ts b/packages/media-query-list-parser/src/nodes/media-type.ts
new file mode 100644
index 000000000..d30336bea
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-type.ts
@@ -0,0 +1,58 @@
+import { TokenIdent, TokenType } from '@csstools/css-tokenizer';
+
+export enum MediaType {
+ /** Always matches */
+ All = 'all',
+ Print = 'print',
+ Screen = 'screen',
+ /** Never matches */
+ Tty = 'tty',
+ /** Never matches */
+ Tv = 'tv',
+ /** Never matches */
+ Projection = 'projection',
+ /** Never matches */
+ Handheld = 'handheld',
+ /** Never matches */
+ Braille = 'braille',
+ /** Never matches */
+ Embossed = 'embossed',
+ /** Never matches */
+ Aural = 'aural',
+ /** Never matches */
+ Speech = 'speech',
+}
+
+export function typeFromToken(token: TokenIdent): MediaType | false {
+ if (token[0] !== TokenType.Ident) {
+ return false;
+ }
+
+ const matchingValue = token[4].value.toLowerCase();
+ switch (matchingValue) {
+ case MediaType.All:
+ return MediaType.All;
+ case MediaType.Print:
+ return MediaType.Print;
+ case MediaType.Screen:
+ return MediaType.Screen;
+ case MediaType.Tty:
+ return MediaType.Tty;
+ case MediaType.Tv:
+ return MediaType.Tv;
+ case MediaType.Projection:
+ return MediaType.Projection;
+ case MediaType.Handheld:
+ return MediaType.Handheld;
+ case MediaType.Braille:
+ return MediaType.Braille;
+ case MediaType.Embossed:
+ return MediaType.Embossed;
+ case MediaType.Aural:
+ return MediaType.Aural;
+ case MediaType.Speech:
+ return MediaType.Speech;
+ default:
+ return false;
+ }
+}
diff --git a/packages/media-query-list-parser/src/parser/parse-media-query.ts b/packages/media-query-list-parser/src/parser/parse-media-query.ts
new file mode 100644
index 000000000..9e95b638e
--- /dev/null
+++ b/packages/media-query-list-parser/src/parser/parse-media-query.ts
@@ -0,0 +1,510 @@
+import { ComponentValue, ComponentValueType, isCommentNode, isTokenNode, isWhitespaceNode, SimpleBlockNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, TokenIdent, TokenType } from '@csstools/css-tokenizer';
+import { GeneralEnclosed } from '../nodes/general-enclosed';
+import { MediaAnd } from '../nodes/media-and';
+import { MediaCondition } from '../nodes/media-condition';
+import { MediaConditionListWithAnd, MediaConditionListWithOr } from '../nodes/media-condition-list';
+import { parseMediaFeature } from '../nodes/media-feature';
+import { MediaInParens } from '../nodes/media-in-parens';
+import { MediaNot } from '../nodes/media-not';
+import { MediaOr } from '../nodes/media-or';
+import { MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query';
+import { modifierFromToken } from '../nodes/media-query-modifier';
+import { isIdent } from '../util/component-value-is';
+
+export function parseMediaQuery(componentValues: Array) {
+ {
+ const condition = parseMediaCondition(componentValues);
+ if (condition !== false) {
+ return new MediaQueryWithoutType(condition);
+ }
+ }
+
+ {
+ let modifierIndex = -1;
+ let typeIndex = -1;
+ let andIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i] as ComponentValue;
+ if (isWhitespaceNode(componentValue)) {
+ continue;
+ }
+
+ if (isCommentNode(componentValue)) {
+ continue;
+ }
+
+ if (isTokenNode(componentValue)) {
+ const token = componentValue.value;
+ if (modifierIndex === -1 && token[0] === TokenType.Ident && modifierFromToken(token)) {
+ modifierIndex = i;
+ continue;
+ }
+
+ if (typeIndex === -1 && token[0] === TokenType.Ident && !modifierFromToken(token)) {
+ typeIndex = i;
+ continue;
+ }
+
+ if (andIndex === -1 && token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'and') {
+ andIndex = i;
+ const condition = parseMediaConditionWithoutOr(componentValues.slice(i+1));
+ if (condition === false) {
+ return false;
+ }
+
+ break;
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
+ let modifierTokens: Array = [];
+ let typeTokens: Array = [];
+
+ if (modifierIndex !== -1) {
+ modifierTokens = componentValues.slice(0, modifierIndex + 1).flatMap((x) => {
+ return x.tokens();
+ });
+
+ if (typeIndex !== -1) {
+ typeTokens = componentValues.slice(modifierIndex + 1, typeIndex + 1).flatMap((x) => {
+ return x.tokens();
+ });
+ }
+ } else if (typeIndex !== -1) {
+ typeTokens = componentValues.slice(0, typeIndex + 1).flatMap((x) => {
+ return x.tokens();
+ });
+ }
+
+ const remainder = componentValues.slice(Math.max(modifierIndex, typeIndex, andIndex) + 1);
+ const condition = parseMediaConditionWithoutOr(remainder);
+ if (condition === false) {
+ return new MediaQueryWithType(
+ modifierTokens,
+ [
+ ...typeTokens,
+ ...componentValues.slice(typeIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ ],
+ );
+ }
+
+ return new MediaQueryWithType(
+ modifierTokens,
+ typeTokens,
+ componentValues.slice(typeIndex + 1, andIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ condition,
+ );
+ }
+}
+
+export function parseMediaConditionListWithOr(componentValues: Array) {
+ let leading: MediaInParens | false = false;
+ const list: Array = [];
+ let firstIndex = -1;
+ let lastIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ if (leading) {
+ const part = parseMediaOr(componentValues.slice(i));
+ if (part !== false) {
+ i += part.advance;
+ list.push(part.node);
+ lastIndex = i;
+ continue;
+ }
+ }
+
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (leading) {
+ return false;
+ }
+
+ if (leading === false && componentValue.type === ComponentValueType.SimpleBlock) {
+ leading = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode);
+ if (leading === false) {
+ return false;
+ }
+
+ firstIndex = i;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (leading && list.length) {
+ return new MediaConditionListWithOr(
+ leading,
+ list,
+ componentValues.slice(0, firstIndex).flatMap((x) => {
+ return x.tokens();
+ }),
+ componentValues.slice(lastIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ );
+ }
+
+ return false;
+}
+
+export function parseMediaConditionListWithAnd(componentValues: Array) {
+ let leading: MediaInParens | false = false;
+ const list: Array = [];
+ let firstIndex = -1;
+ let lastIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ if (leading) {
+ const part = parseMediaAnd(componentValues.slice(i));
+ if (part !== false) {
+ i += part.advance;
+ list.push(part.node);
+ lastIndex = i;
+ continue;
+ }
+ }
+
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (leading) {
+ return false;
+ }
+
+ if (leading === false && componentValue.type === ComponentValueType.SimpleBlock) {
+ leading = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode);
+ if (leading === false) {
+ return false;
+ }
+
+ firstIndex = i;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (leading && list.length) {
+ return new MediaConditionListWithAnd(
+ leading,
+ list,
+ componentValues.slice(0, firstIndex).flatMap((x) => {
+ return x.tokens();
+ }),
+ componentValues.slice(lastIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ );
+ }
+
+ return false;
+}
+
+export function parseMediaCondition(componentValues: Array) {
+ const mediaNot = parseMediaNot(componentValues);
+ if (mediaNot !== false) {
+ return new MediaCondition(mediaNot);
+ }
+
+ const mediaListAnd = parseMediaConditionListWithAnd(componentValues);
+ if (mediaListAnd !== false) {
+ return new MediaCondition(mediaListAnd);
+ }
+
+ const mediaListOr = parseMediaConditionListWithOr(componentValues);
+ if (mediaListOr !== false) {
+ return new MediaCondition(mediaListOr);
+ }
+
+ const mediaInParens = parseMediaInParens(componentValues);
+ if (mediaInParens !== false) {
+ return new MediaCondition(mediaInParens);
+ }
+
+ return false;
+}
+
+export function parseMediaConditionWithoutOr(componentValues: Array) {
+ const mediaNot = parseMediaNot(componentValues);
+ if (mediaNot !== false) {
+ return new MediaCondition(mediaNot);
+ }
+
+ const mediaListAnd = parseMediaConditionListWithAnd(componentValues);
+ if (mediaListAnd !== false) {
+ return new MediaCondition(mediaListAnd);
+ }
+
+ const mediaInParens = parseMediaInParens(componentValues);
+ if (mediaInParens !== false) {
+ return new MediaCondition(mediaInParens);
+ }
+
+ return false;
+}
+
+export function parseMediaInParens(componentValues: Array) {
+ let singleSimpleBlockIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.SimpleBlock) {
+ if (singleSimpleBlockIndex !== -1) {
+ return false;
+ }
+
+ singleSimpleBlockIndex = i;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (singleSimpleBlockIndex === -1) {
+ return false;
+ }
+
+ const simpleBlock = componentValues[singleSimpleBlockIndex] as SimpleBlockNode;
+ if (simpleBlock.startToken[0] !== TokenType.OpenParen) {
+ return false;
+ }
+
+ const before = [
+ ...componentValues.slice(0, singleSimpleBlockIndex).flatMap((x) => {
+ return x.tokens();
+ }),
+ simpleBlock.startToken,
+ ];
+
+ const after = [
+ simpleBlock.endToken,
+ ...componentValues.slice(singleSimpleBlockIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ ];
+
+ const feature = parseMediaFeature(simpleBlock, before, after);
+ if (feature !== false) {
+ return new MediaInParens(feature);
+ }
+
+ const condition = parseMediaCondition(simpleBlock.value);
+ if (condition !== false) {
+ return new MediaInParens(condition, before, after);
+ }
+
+ return new MediaInParens(
+ new GeneralEnclosed(simpleBlock),
+ componentValues.slice(0, singleSimpleBlockIndex).flatMap((x) => {
+ return x.tokens();
+ }),
+ componentValues.slice(singleSimpleBlockIndex + 1).flatMap((x) => {
+ return x.tokens();
+ }),
+ );
+}
+
+export function parseMediaInParensFromSimpleBlock(simpleBlock: SimpleBlockNode) {
+ if (simpleBlock.startToken[0] !== TokenType.OpenParen) {
+ return false;
+ }
+
+ const feature = parseMediaFeature(simpleBlock, [simpleBlock.startToken], [simpleBlock.endToken]);
+ if (feature !== false) {
+ return new MediaInParens(feature);
+ }
+
+ const condition = parseMediaCondition(simpleBlock.value);
+ if (condition !== false) {
+ return new MediaInParens(condition, [simpleBlock.startToken], [simpleBlock.endToken]);
+ }
+
+ return new MediaInParens(new GeneralEnclosed(simpleBlock));
+}
+
+export function parseMediaNot(componentValues: Array) {
+ let sawNot = false;
+ let node: MediaNot | null = null;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (isIdent(componentValue)) {
+ const token = (componentValue.value as TokenIdent);
+ if (token[4].value.toLowerCase() === 'not') {
+ if (sawNot) {
+ return false;
+ }
+
+ sawNot = true;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (sawNot && componentValue.type === ComponentValueType.SimpleBlock) {
+ const media = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode);
+ if (media === false) {
+ return false;
+ }
+
+ node = new MediaNot(
+ componentValues.slice(0, i).flatMap((x) => {
+ return x.tokens();
+ }),
+ media,
+ );
+
+ continue;
+ }
+
+ return false;
+ }
+
+ if (node) {
+ return node;
+ }
+
+ return false;
+}
+
+export function parseMediaOr(componentValues: Array) {
+ let sawOr = false;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (isIdent(componentValue)) {
+ const token = (componentValue.value as TokenIdent);
+ if (token[4].value.toLowerCase() === 'or') {
+ if (sawOr) {
+ return false;
+ }
+
+ sawOr = true;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (sawOr && componentValue.type === ComponentValueType.SimpleBlock) {
+ const media = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode);
+ if (media === false) {
+ return false;
+ }
+
+ return {
+ advance: i,
+ node: new MediaOr(
+ componentValues.slice(0, i).flatMap((x) => {
+ return x.tokens();
+ }),
+ media,
+ ),
+ };
+ }
+
+ return false;
+ }
+
+ return false;
+}
+
+export function parseMediaAnd(componentValues: Array) {
+ let sawAnd = false;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (isIdent(componentValue)) {
+ const token = (componentValue.value as TokenIdent);
+ if (token[4].value.toLowerCase() === 'and') {
+ if (sawAnd) {
+ return false;
+ }
+
+ sawAnd = true;
+ continue;
+ }
+
+ return false;
+ }
+
+ if (sawAnd && componentValue.type === ComponentValueType.SimpleBlock) {
+ const media = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode);
+ if (media === false) {
+ return false;
+ }
+
+ return {
+ advance: i,
+ node: new MediaAnd(
+ componentValues.slice(0, i).flatMap((x) => {
+ return x.tokens();
+ }),
+ media,
+ ),
+ };
+ }
+
+ return false;
+ }
+
+ return false;
+}
diff --git a/packages/media-query-list-parser/src/parser/parse.ts b/packages/media-query-list-parser/src/parser/parse.ts
new file mode 100644
index 000000000..950ed7153
--- /dev/null
+++ b/packages/media-query-list-parser/src/parser/parse.ts
@@ -0,0 +1,44 @@
+import { parseCommaSeparatedListOfComponentValues } from '@csstools/css-parser-algorithms';
+import { ParserError } from '@csstools/css-parser-algorithms/dist/interfaces/error';
+import { CSSToken, tokenizer } from '@csstools/css-tokenizer';
+import { MediaQuery, MediaQueryInvalid } from '../nodes/media-query';
+import { parseMediaQuery } from './parse-media-query';
+
+export type Options = {
+ preserveInvalidMediaQueries?: boolean,
+ onParseError?: (error: ParserError) => void
+}
+
+export function parseFromTokens(tokens: Array, options?: Options) {
+ const componentValuesLists = parseCommaSeparatedListOfComponentValues(tokens, {
+ onParseError: options?.onParseError,
+ });
+
+ return componentValuesLists.map((componentValuesList, index) => {
+ const mediaQuery = parseMediaQuery(componentValuesList);
+ if (mediaQuery == false && options?.preserveInvalidMediaQueries === true) {
+ return new MediaQueryInvalid(componentValuesLists[index]);
+ }
+
+ return mediaQuery;
+ }).filter((x) => !!x) as Array;
+}
+
+export function parse(source: string, options?: Options) {
+ const t = tokenizer({ css: source }, {
+ commentsAreTokens: true,
+ onParseError: options?.onParseError,
+ });
+
+ const tokens = [];
+
+ {
+ while (!t.endOfFile()) {
+ tokens.push(t.nextToken());
+ }
+
+ tokens.push(t.nextToken()); // EOF-token
+ }
+
+ return parseFromTokens(tokens, options);
+}
diff --git a/packages/media-query-list-parser/src/util/clone-media-query.ts b/packages/media-query-list-parser/src/util/clone-media-query.ts
new file mode 100644
index 000000000..ec8b69bde
--- /dev/null
+++ b/packages/media-query-list-parser/src/util/clone-media-query.ts
@@ -0,0 +1,27 @@
+import { cloneTokens, stringify } from '@csstools/css-tokenizer';
+import { MediaQueryInvalid, MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query';
+import { parseFromTokens } from '../parser/parse';
+import { isMediaQueryInvalid, isMediaQueryWithoutType, isMediaQueryWithType } from './type-predicates';
+
+export function cloneMediaQuery(x: T): T {
+ const tokens = cloneTokens(x.tokens());
+ const parsed = parseFromTokens(tokens, { preserveInvalidMediaQueries: true });
+ const firstQuery = parsed[0];
+ if (!firstQuery) {
+ throw new Error(`Failed to clone media query for : "${stringify(...tokens)}"`);
+ }
+
+ if (isMediaQueryInvalid(x) && isMediaQueryInvalid(firstQuery)) {
+ return firstQuery as T;
+ }
+
+ if (isMediaQueryWithType(x) && isMediaQueryWithType(firstQuery)) {
+ return firstQuery as T;
+ }
+
+ if (isMediaQueryWithoutType(x) && isMediaQueryWithoutType(firstQuery)) {
+ return firstQuery as T;
+ }
+
+ throw new Error(`Failed to clone media query for : "${stringify(...tokens)}"`);
+}
diff --git a/packages/media-query-list-parser/src/util/component-value-is.ts b/packages/media-query-list-parser/src/util/component-value-is.ts
new file mode 100644
index 000000000..1088cc72b
--- /dev/null
+++ b/packages/media-query-list-parser/src/util/component-value-is.ts
@@ -0,0 +1,29 @@
+import { ComponentValue, ComponentValueType, FunctionNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, TokenFunction, TokenType } from '@csstools/css-tokenizer';
+
+export function isNumber(componentValue: ComponentValue) {
+ if (
+ (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Number) ||
+ (componentValue.type === ComponentValueType.Function && ((componentValue as FunctionNode).name as TokenFunction)[4].value === 'calc')
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+export function isDimension(componentValue: ComponentValue) {
+ if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Dimension) {
+ return true;
+ }
+
+ return false;
+}
+
+export function isIdent(componentValue: ComponentValue) {
+ if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/packages/media-query-list-parser/src/util/node-type.ts b/packages/media-query-list-parser/src/util/node-type.ts
new file mode 100644
index 000000000..dd8fd4374
--- /dev/null
+++ b/packages/media-query-list-parser/src/util/node-type.ts
@@ -0,0 +1,21 @@
+export enum NodeType {
+ GeneralEnclosed = 'general-enclosed',
+ MediaAnd = 'media-and',
+ MediaCondition = 'media-condition',
+ MediaConditionListWithAnd = 'media-condition-list-and',
+ MediaConditionListWithOr = 'media-condition-list-or',
+ MediaFeature = 'media-feature',
+ MediaFeatureBoolean = 'mf-boolean',
+ MediaFeatureName = 'mf-name',
+ MediaFeaturePlain = 'mf-plain',
+ MediaFeatureRangeNameValue = 'mf-range-name-value',
+ MediaFeatureRangeValueName = 'mf-range-value-name',
+ MediaFeatureRangeValueNameValue = 'mf-range-value-name-value',
+ MediaFeatureValue = 'mf-value',
+ MediaInParens = 'media-in-parens',
+ MediaNot = 'media-not',
+ MediaOr = 'media-or',
+ MediaQueryWithType = 'media-query-with-type',
+ MediaQueryWithoutType = 'media-query-without-type',
+ MediaQueryInvalid = 'media-query-invalid',
+}
diff --git a/packages/media-query-list-parser/src/util/type-predicates.ts b/packages/media-query-list-parser/src/util/type-predicates.ts
new file mode 100644
index 000000000..659f048b4
--- /dev/null
+++ b/packages/media-query-list-parser/src/util/type-predicates.ts
@@ -0,0 +1,102 @@
+import { GeneralEnclosed } from '../nodes/general-enclosed';
+import { MediaAnd } from '../nodes/media-and';
+import { MediaCondition } from '../nodes/media-condition';
+import { MediaConditionList, MediaConditionListWithAnd, MediaConditionListWithOr } from '../nodes/media-condition-list';
+import { MediaFeature } from '../nodes/media-feature';
+import { MediaFeatureBoolean } from '../nodes/media-feature-boolean';
+import { MediaFeatureName } from '../nodes/media-feature-name';
+import { MediaFeaturePlain } from '../nodes/media-feature-plain';
+import { MediaFeatureRange, MediaFeatureRangeNameValue, MediaFeatureRangeValueName, MediaFeatureRangeValueNameValue } from '../nodes/media-feature-range';
+import { MediaFeatureValue } from '../nodes/media-feature-value';
+import { MediaInParens } from '../nodes/media-in-parens';
+import { MediaNot } from '../nodes/media-not';
+import { MediaOr } from '../nodes/media-or';
+import { MediaQuery, MediaQueryInvalid, MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query';
+
+export function isGeneralEnclosed(x: unknown): x is GeneralEnclosed {
+ return GeneralEnclosed.isGeneralEnclosed(x);
+}
+
+export function isMediaAnd(x: unknown): x is MediaAnd {
+ return MediaAnd.isMediaAnd(x);
+}
+
+export function isMediaConditionList(x: unknown): x is MediaConditionList {
+ return isMediaConditionListWithAnd(x) || isMediaConditionListWithOr(x);
+}
+
+export function isMediaConditionListWithAnd(x: unknown): x is MediaConditionListWithAnd {
+ return MediaConditionListWithAnd.isMediaConditionListWithAnd(x);
+}
+
+export function isMediaConditionListWithOr(x: unknown): x is MediaConditionListWithOr {
+ return MediaConditionListWithOr.isMediaConditionListWithOr(x);
+}
+
+export function isMediaCondition(x: unknown): x is MediaCondition {
+ return MediaCondition.isMediaCondition(x);
+}
+
+export function isMediaFeatureBoolean(x: unknown): x is MediaFeatureBoolean {
+ return MediaFeatureBoolean.isMediaFeatureBoolean(x);
+}
+
+export function isMediaFeatureName(x: unknown): x is MediaFeatureName {
+ return MediaFeatureName.isMediaFeatureName(x);
+}
+
+export function isMediaFeatureValue(x: unknown): x is MediaFeatureValue {
+ return MediaFeatureValue.isMediaFeatureValue(x);
+}
+
+export function isMediaFeaturePlain(x: unknown): x is MediaFeaturePlain {
+ return MediaFeaturePlain.isMediaFeaturePlain(x);
+}
+
+export function isMediaFeatureRange(x: unknown): x is MediaFeatureRange {
+ return isMediaFeatureRangeNameValue(x) || isMediaFeatureRangeValueName(x) || isMediaFeatureRangeValueNameValue(x);
+}
+
+export function isMediaFeatureRangeNameValue(x: unknown): x is MediaFeatureRangeNameValue {
+ return MediaFeatureRangeNameValue.isMediaFeatureRangeNameValue(x);
+}
+
+export function isMediaFeatureRangeValueName(x: unknown): x is MediaFeatureRangeValueName {
+ return MediaFeatureRangeValueName.isMediaFeatureRangeValueName(x);
+}
+
+export function isMediaFeatureRangeValueNameValue(x: unknown): x is MediaFeatureRangeValueNameValue {
+ return MediaFeatureRangeValueNameValue.isMediaFeatureRangeValueNameValue(x);
+}
+
+export function isMediaFeature(x: unknown): x is MediaFeature {
+ return MediaFeature.isMediaFeature(x);
+}
+
+export function isMediaInParens(x: unknown): x is MediaInParens {
+ return MediaInParens.isMediaInParens(x);
+}
+
+export function isMediaNot(x: unknown): x is MediaNot {
+ return MediaNot.isMediaNot(x);
+}
+
+export function isMediaOr(x: unknown): x is MediaOr {
+ return MediaOr.isMediaOr(x);
+}
+
+export function isMediaQuery(x: unknown): x is MediaQuery {
+ return isMediaQueryWithType(x) || isMediaQueryWithoutType(x) || isMediaQueryInvalid(x);
+}
+
+export function isMediaQueryWithType(x: unknown): x is MediaQueryWithType {
+ return MediaQueryWithType.isMediaQueryWithType(x);
+}
+
+export function isMediaQueryWithoutType(x: unknown): x is MediaQueryWithoutType {
+ return MediaQueryWithoutType.isMediaQueryWithoutType(x);
+}
+
+export function isMediaQueryInvalid(x: unknown): x is MediaQueryInvalid {
+ return MediaQueryInvalid.isMediaQueryInvalid(x);
+}
diff --git a/packages/media-query-list-parser/stryker.conf.json b/packages/media-query-list-parser/stryker.conf.json
new file mode 100644
index 000000000..015ebbb73
--- /dev/null
+++ b/packages/media-query-list-parser/stryker.conf.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
+ "mutate": [
+ "src/**/*.ts"
+ ],
+ "buildCommand": "npm run build",
+ "testRunner": "command",
+ "coverageAnalysis": "perTest",
+ "tempDirName": "../../.stryker-tmp",
+ "commandRunner": {
+ "command": "npm run test"
+ },
+ "thresholds": {
+ "high": 100,
+ "low": 100,
+ "break": 100
+ },
+ "inPlace": true
+}
diff --git a/packages/media-query-list-parser/test/_import.mjs b/packages/media-query-list-parser/test/_import.mjs
new file mode 100644
index 000000000..3215dfbbb
--- /dev/null
+++ b/packages/media-query-list-parser/test/_import.mjs
@@ -0,0 +1,3 @@
+import { parse } from '@csstools/media-query-list-parser';
+
+parse('(min-width: 300px)');
diff --git a/packages/media-query-list-parser/test/_require.cjs b/packages/media-query-list-parser/test/_require.cjs
new file mode 100644
index 000000000..09406cf78
--- /dev/null
+++ b/packages/media-query-list-parser/test/_require.cjs
@@ -0,0 +1,3 @@
+const { parse } = require('@csstools/media-query-list-parser');
+
+parse('(min-width: 300px)');
diff --git a/packages/media-query-list-parser/test/api/options.mjs b/packages/media-query-list-parser/test/api/options.mjs
new file mode 100644
index 000000000..9268779f4
--- /dev/null
+++ b/packages/media-query-list-parser/test/api/options.mjs
@@ -0,0 +1,71 @@
+import assert from 'assert';
+import { parse } from '@csstools/media-query-list-parser';
+
+{
+ const resultAST = parse('[a, b], all', {
+ preserveInvalidMediaQueries: true,
+ });
+
+ assert.equal(
+ resultAST.length,
+ 2,
+ );
+
+ assert.equal(
+ resultAST[0].type,
+ 'media-query-invalid',
+ );
+}
+
+{
+ const resultAST = parse('[a, b], all', {
+ preserveInvalidMediaQueries: false,
+ });
+
+ assert.equal(
+ resultAST.length,
+ 1,
+ );
+}
+
+{
+ const resultAST = parse('[a, b], all');
+
+ assert.equal(
+ resultAST.length,
+ 1,
+ );
+}
+
+{
+ const resultAST = parse('(foo');
+
+ assert.equal(
+ resultAST.length,
+ 0,
+ );
+}
+
+{
+ let error;
+ const resultAST = parse('(foo', {
+ onParseError: (err) => {
+ error = err;
+ },
+ });
+
+ assert.equal(
+ resultAST.length,
+ 0,
+ );
+
+ assert.deepEqual(
+ error,
+ {
+ message: 'Unexpected EOF while consuming a simple block.',
+ start: 0,
+ end: -1,
+ state: ['5.4.8. Consume a simple block', 'Unexpected EOF'],
+ },
+ );
+}
diff --git a/packages/media-query-list-parser/test/cases/media-not/0001.expect.json b/packages/media-query-list-parser/test/cases/media-not/0001.expect.json
new file mode 100644
index 000000000..5a9906847
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/media-not/0001.expect.json
@@ -0,0 +1,71 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "not (color)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 5,
+ 9,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/media-not/0001.mjs b/packages/media-query-list-parser/test/cases/media-not/0001.mjs
new file mode 100644
index 000000000..5eb55f343
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/media-not/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not (color)',
+ 'media-not/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0001.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0001.expect.json
new file mode 100644
index 000000000..af1643cf2
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0001.expect.json
@@ -0,0 +1,50 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(color)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0001.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0001.mjs
new file mode 100644
index 000000000..2686abd60
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color)',
+ 'mf-boolean/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0002.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0002.expect.json
new file mode 100644
index 000000000..91c136e7d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0002.expect.json
@@ -0,0 +1,78 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "/* comment 1 */(/* comment 2 */color/* comment 3 */)/* comment 4 */",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 31,
+ 35,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 36,
+ 50,
+ null
+ ]
+ ]
+ },
+ "before": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 51,
+ 51,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 4 */",
+ 52,
+ 66,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0002.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0002.mjs
new file mode 100644
index 000000000..f8266d441
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '/* comment 1 */(/* comment 2 */color/* comment 3 */)/* comment 4 */',
+ 'mf-boolean/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0003.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0003.expect.json
new file mode 100644
index 000000000..af07020b7
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0003.expect.json
@@ -0,0 +1,50 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(true)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "true",
+ "tokens": [
+ [
+ "ident-token",
+ "true",
+ 1,
+ 4,
+ {
+ "value": "true"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0003.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0003.mjs
new file mode 100644
index 000000000..c0c9f5645
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(true)',
+ 'mf-boolean/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0004.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0004.expect.json
new file mode 100644
index 000000000..ec092ecf0
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0004.expect.json
@@ -0,0 +1,50 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(false)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "false",
+ "tokens": [
+ [
+ "ident-token",
+ "false",
+ 1,
+ 5,
+ {
+ "value": "false"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0004.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0004.mjs
new file mode 100644
index 000000000..0a34a2dc8
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(false)',
+ 'mf-boolean/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0005.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0005.expect.json
new file mode 100644
index 000000000..924b11622
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0005.expect.json
@@ -0,0 +1,49 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(color())",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "color(",
+ 1,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 8,
+ 8,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0005.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0005.mjs
new file mode 100644
index 000000000..fd8afbd15
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-boolean/0005.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color())',
+ 'mf-boolean/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0001.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0001.expect.json
new file mode 100644
index 000000000..7a0c3891e
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0001.expect.json
@@ -0,0 +1,128 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(min-width: 300px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "min-width",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 1,
+ 9,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 1,
+ 9,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 12,
+ 16,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0001.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0001.mjs
new file mode 100644
index 000000000..b674721b7
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(min-width: 300px)',
+ 'mf-plain/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0002.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0002.expect.json
new file mode 100644
index 000000000..dbcb81161
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0002.expect.json
@@ -0,0 +1,184 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "/* comment 1 */(/* comment 2 */min-width/* comment 3 */:/* comment 4 */300px/* comment 5 */)/* comment 6 */",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "min-width",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 31,
+ 39,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 40,
+ 54,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "comment",
+ "/* comment 4 */",
+ 56,
+ 70,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 5 */",
+ 76,
+ 90,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 31,
+ 39,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 40,
+ 54,
+ null
+ ],
+ [
+ "colon-token",
+ ":",
+ 55,
+ 55,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 4 */",
+ 56,
+ 70,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 71,
+ 75,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 5 */",
+ 76,
+ 90,
+ null
+ ]
+ ]
+ },
+ "before": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 91,
+ 91,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 6 */",
+ 92,
+ 106,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0002.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0002.mjs
new file mode 100644
index 000000000..90eb3e1f9
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '/* comment 1 */(/* comment 2 */min-width/* comment 3 */:/* comment 4 */300px/* comment 5 */)/* comment 6 */',
+ 'mf-plain/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0003.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0003.expect.json
new file mode 100644
index 000000000..1eb0b3ce3
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0003.expect.json
@@ -0,0 +1,122 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(resolution: infinite)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "resolution",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0003.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0003.mjs
new file mode 100644
index 000000000..ad7c3adf2
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(resolution: infinite)',
+ 'mf-plain/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0004.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0004.expect.json
new file mode 100644
index 000000000..5c69041e4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0004.expect.json
@@ -0,0 +1,81 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(resolution(foo): infinite)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "function-token",
+ "resolution(",
+ 1,
+ 11,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "ident-token",
+ "foo",
+ 12,
+ 14,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "colon-token",
+ ":",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 18,
+ 25,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0004.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0004.mjs
new file mode 100644
index 000000000..7c310ffb4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0004.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(resolution(foo): infinite)',
+ 'mf-plain/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0005.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0005.expect.json
new file mode 100644
index 000000000..81dc5863d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0005.expect.json
@@ -0,0 +1,120 @@
+[
+ {
+ "type": "media-query-invalid",
+ "string": "[resolution: infinite]",
+ "media": [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "[-token",
+ "[",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "]-token",
+ "]",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 13,
+ 20,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0005.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0005.mjs
new file mode 100644
index 000000000..946d31898
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-plain/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '[resolution: infinite]',
+ 'mf-plain/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0001.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0001.expect.json
new file mode 100644
index 000000000..11eadf3af
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0001.expect.json
@@ -0,0 +1,452 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(20px < width <= calc(20px * 5))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 8,
+ 12,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ]
+ ]
+ },
+ "valueOne": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "20px",
+ 1,
+ 4,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "20px",
+ 1,
+ 4,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ "valueTwo": {
+ "type": "mf-value",
+ "value": {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 17,
+ 21,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "20px",
+ 22,
+ 25,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "delim-token",
+ "*",
+ 27,
+ 27,
+ {
+ "value": "*"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "number-token",
+ "5",
+ 29,
+ 29,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "20px",
+ 22,
+ 25,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "*",
+ 27,
+ 27,
+ {
+ "value": "*"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "5",
+ 29,
+ 29,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 17,
+ 21,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "20px",
+ 22,
+ 25,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "delim-token",
+ "*",
+ 27,
+ 27,
+ {
+ "value": "*"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "number-token",
+ "5",
+ 29,
+ 29,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "20px",
+ 1,
+ 4,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 6,
+ 6,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 8,
+ 12,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 14,
+ 14,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 15,
+ 15,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 17,
+ 21,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "20px",
+ 22,
+ 25,
+ {
+ "value": 20,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "delim-token",
+ "*",
+ 27,
+ 27,
+ {
+ "value": "*"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "number-token",
+ "5",
+ 29,
+ 29,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 31,
+ 31,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0001.mjs b/packages/media-query-list-parser/test/cases/mf-range/0001.mjs
new file mode 100644
index 000000000..37aba33f4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(20px < width <= calc(20px * 5))',
+ 'mf-range/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0002.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0002.expect.json
new file mode 100644
index 000000000..00ce71e31
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0002.expect.json
@@ -0,0 +1,186 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "/* comment 1 */(/* comment 2 */30px/* comment 3 */* comment 4 */width/* comment 5 */)/* comment 6 */",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "comment",
+ "/* comment 4 */",
+ 51,
+ 65,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 66,
+ 70,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 5 */",
+ 71,
+ 85,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "30px",
+ 31,
+ 34,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "dimension-token",
+ "30px",
+ 31,
+ 34,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 35,
+ 49,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "comment",
+ "/* comment 2 */",
+ 16,
+ 30,
+ null
+ ],
+ [
+ "dimension-token",
+ "30px",
+ 31,
+ 34,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 3 */",
+ 35,
+ 49,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 50,
+ 50,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 4 */",
+ 51,
+ 65,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 66,
+ 70,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "comment",
+ "/* comment 5 */",
+ 71,
+ 85,
+ null
+ ]
+ ]
+ },
+ "before": [
+ [
+ "comment",
+ "/* comment 1 */",
+ 0,
+ 14,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 86,
+ 86,
+ null
+ ],
+ [
+ "comment",
+ "/* comment 6 */",
+ 87,
+ 101,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0002.mjs b/packages/media-query-list-parser/test/cases/mf-range/0002.mjs
new file mode 100644
index 000000000..81b097b60
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '/* comment 1 */(/* comment 2 */30px/* comment 3 */* comment 4 */width/* comment 5 */)/* comment 6 */',
+ 'mf-range/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0003.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0003.expect.json
new file mode 100644
index 000000000..b509b256c
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0003.expect.json
@@ -0,0 +1,488 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "( 1 / 5 < aspect-ratio < 3 / 2 )",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "aspect-ratio",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "aspect-ratio",
+ 10,
+ 21,
+ {
+ "value": "aspect-ratio"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ "valueOne": {
+ "type": "mf-value",
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "1",
+ 2,
+ 2,
+ {
+ "value": 1,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "/",
+ 4,
+ 4,
+ {
+ "value": "/"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "5",
+ 6,
+ 6,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ]
+ ]
+ }
+ ],
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "number-token",
+ "1",
+ 2,
+ 2,
+ {
+ "value": 1,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ],
+ [
+ "delim-token",
+ "/",
+ 4,
+ 4,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "number-token",
+ "5",
+ 6,
+ 6,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ "valueTwo": {
+ "type": "mf-value",
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "3",
+ 25,
+ 25,
+ {
+ "value": 3,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "/",
+ 27,
+ 27,
+ {
+ "value": "/"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "2",
+ 29,
+ 29,
+ {
+ "value": 2,
+ "type": "integer"
+ }
+ ]
+ ]
+ }
+ ],
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ],
+ [
+ "number-token",
+ "3",
+ 25,
+ 25,
+ {
+ "value": 3,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "delim-token",
+ "/",
+ 27,
+ 27,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "number-token",
+ "2",
+ 29,
+ 29,
+ {
+ "value": 2,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 30,
+ 30,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "number-token",
+ "1",
+ 2,
+ 2,
+ {
+ "value": 1,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ],
+ [
+ "delim-token",
+ "/",
+ 4,
+ 4,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "number-token",
+ "5",
+ 6,
+ 6,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 8,
+ 8,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "aspect-ratio",
+ 10,
+ 21,
+ {
+ "value": "aspect-ratio"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 23,
+ 23,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ],
+ [
+ "number-token",
+ "3",
+ 25,
+ 25,
+ {
+ "value": 3,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "delim-token",
+ "/",
+ 27,
+ 27,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 28,
+ 28,
+ null
+ ],
+ [
+ "number-token",
+ "2",
+ 29,
+ 29,
+ {
+ "value": 2,
+ "type": "integer"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 30,
+ 30,
+ null
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 31,
+ 31,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0003.mjs b/packages/media-query-list-parser/test/cases/mf-range/0003.mjs
new file mode 100644
index 000000000..5626fc00a
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '( 1 / 5 < aspect-ratio < 3 / 2 )',
+ 'mf-range/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0004.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0004.expect.json
new file mode 100644
index 000000000..8990ac58b
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0004.expect.json
@@ -0,0 +1,300 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(1/5<3/2)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "aspect-ratio",
+ "tokens": [
+ [
+ "ident-token",
+ "aspect-ratio",
+ 5,
+ 16,
+ {
+ "value": "aspect-ratio"
+ }
+ ]
+ ]
+ },
+ "valueOne": {
+ "type": "mf-value",
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "1",
+ 1,
+ 1,
+ {
+ "value": 1,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "/",
+ 2,
+ 2,
+ {
+ "value": "/"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "5",
+ 3,
+ 3,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ]
+ ]
+ }
+ ],
+ "tokens": [
+ [
+ "number-token",
+ "1",
+ 1,
+ 1,
+ {
+ "value": 1,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "/",
+ 2,
+ 2,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "number-token",
+ "5",
+ 3,
+ 3,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ "valueTwo": {
+ "type": "mf-value",
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "3",
+ 18,
+ 18,
+ {
+ "value": 3,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "/",
+ 19,
+ 19,
+ {
+ "value": "/"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "2",
+ 20,
+ 20,
+ {
+ "value": 2,
+ "type": "integer"
+ }
+ ]
+ ]
+ }
+ ],
+ "tokens": [
+ [
+ "number-token",
+ "3",
+ 18,
+ 18,
+ {
+ "value": 3,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "/",
+ 19,
+ 19,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "number-token",
+ "2",
+ 20,
+ 20,
+ {
+ "value": 2,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "number-token",
+ "1",
+ 1,
+ 1,
+ {
+ "value": 1,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "/",
+ 2,
+ 2,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "number-token",
+ "5",
+ 3,
+ 3,
+ {
+ "value": 5,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "<",
+ 4,
+ 4,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "ident-token",
+ "aspect-ratio",
+ 5,
+ 16,
+ {
+ "value": "aspect-ratio"
+ }
+ ],
+ [
+ "delim-token",
+ "<",
+ 17,
+ 17,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "number-token",
+ "3",
+ 18,
+ 18,
+ {
+ "value": 3,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "/",
+ 19,
+ 19,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "number-token",
+ "2",
+ 20,
+ 20,
+ {
+ "value": 2,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0004.mjs b/packages/media-query-list-parser/test/cases/mf-range/0004.mjs
new file mode 100644
index 000000000..3863dfccc
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(1/5<3/2)',
+ 'mf-range/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0005.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0005.expect.json
new file mode 100644
index 000000000..58e7ee127
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0005.expect.json
@@ -0,0 +1,144 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(width = 50px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50px",
+ 9,
+ 12,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "dimension-token",
+ "50px",
+ 9,
+ 12,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "delim-token",
+ "=",
+ 7,
+ 7,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "dimension-token",
+ "50px",
+ 9,
+ 12,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 13,
+ 13,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0005.mjs b/packages/media-query-list-parser/test/cases/mf-range/0005.mjs
new file mode 100644
index 000000000..fbaf31925
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(width = 50px)',
+ 'mf-range/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0006.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0006.expect.json
new file mode 100644
index 000000000..dc8669cc7
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0006.expect.json
@@ -0,0 +1,144 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(50px = width)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 8,
+ 12,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50px",
+ 1,
+ 4,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "50px",
+ 1,
+ 4,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "50px",
+ 1,
+ 4,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 5,
+ 5,
+ null
+ ],
+ [
+ "delim-token",
+ "=",
+ 6,
+ 6,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 8,
+ 12,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 13,
+ 13,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0006.mjs b/packages/media-query-list-parser/test/cases/mf-range/0006.mjs
new file mode 100644
index 000000000..cc1c77c20
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0006.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(50px = width)',
+ 'mf-range/0006',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0007.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0007.expect.json
new file mode 100644
index 000000000..2f4ab2f30
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0007.expect.json
@@ -0,0 +1,224 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(1000px > height > 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "height",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 10,
+ 15,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ "valueOne": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ "valueTwo": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "100px",
+ 19,
+ 23,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 19,
+ 23,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 10,
+ 15,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 17,
+ 17,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 19,
+ 23,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0007.mjs b/packages/media-query-list-parser/test/cases/mf-range/0007.mjs
new file mode 100644
index 000000000..0db8883c7
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0007.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(1000px > height > 100px)',
+ 'mf-range/0007',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0008.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0008.expect.json
new file mode 100644
index 000000000..7609cc506
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0008.expect.json
@@ -0,0 +1,110 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(1000px > height < 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 10,
+ 15,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 17,
+ 17,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 19,
+ 23,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0008.mjs b/packages/media-query-list-parser/test/cases/mf-range/0008.mjs
new file mode 100644
index 000000000..7b9fc7609
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0008.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(1000px > height < 100px)',
+ 'mf-range/0008',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0009.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0009.expect.json
new file mode 100644
index 000000000..67cfa7111
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0009.expect.json
@@ -0,0 +1,119 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(1000px > height <= 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 10,
+ 15,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 17,
+ 17,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 18,
+ 18,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 19,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 20,
+ 24,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0009.mjs b/packages/media-query-list-parser/test/cases/mf-range/0009.mjs
new file mode 100644
index 000000000..95098d1ca
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0009.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(1000px > height <= 100px)',
+ 'mf-range/0009',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0010.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0010.expect.json
new file mode 100644
index 000000000..ca2e475cb
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0010.expect.json
@@ -0,0 +1,110 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(1000px > height = 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 10,
+ 15,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "delim-token",
+ "=",
+ 17,
+ 17,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 19,
+ 23,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0010.mjs b/packages/media-query-list-parser/test/cases/mf-range/0010.mjs
new file mode 100644
index 000000000..6ff9a2ba0
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0010.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(1000px > height = 100px)',
+ 'mf-range/0010',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0011.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0011.expect.json
new file mode 100644
index 000000000..d78ab5dcd
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0011.expect.json
@@ -0,0 +1,126 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(1000px > height > = 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "dimension-token",
+ "1000px",
+ 1,
+ 6,
+ {
+ "value": 1000,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 10,
+ 15,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 17,
+ 17,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "=",
+ 19,
+ 19,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 21,
+ 25,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0011.mjs b/packages/media-query-list-parser/test/cases/mf-range/0011.mjs
new file mode 100644
index 000000000..5d1a49b55
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0011.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(1000px > height > = 100px)',
+ 'mf-range/0011',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0012.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0012.expect.json
new file mode 100644
index 000000000..945713f41
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0012.expect.json
@@ -0,0 +1,92 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(height > = 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "height",
+ 1,
+ 6,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "delim-token",
+ "=",
+ 10,
+ 10,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 12,
+ 16,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0012.mjs b/packages/media-query-list-parser/test/cases/mf-range/0012.mjs
new file mode 100644
index 000000000..01cd32f50
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0012.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(height > = 100px)',
+ 'mf-range/0012',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0013.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0013.expect.json
new file mode 100644
index 000000000..2cf2497d3
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0013.expect.json
@@ -0,0 +1,153 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(height >= 100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "height",
+ "tokens": [
+ [
+ "ident-token",
+ "height",
+ 1,
+ 6,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "100px",
+ 11,
+ 15,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 11,
+ 15,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "height",
+ 1,
+ 6,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "delim-token",
+ ">",
+ 8,
+ 8,
+ {
+ "value": ">"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 9,
+ 9,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "dimension-token",
+ "100px",
+ 11,
+ 15,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0013.mjs b/packages/media-query-list-parser/test/cases/mf-range/0013.mjs
new file mode 100644
index 000000000..ea88732f7
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0013.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(height >= 100px)',
+ 'mf-range/0013',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0014.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0014.expect.json
new file mode 100644
index 000000000..4f0b3df0f
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0014.expect.json
@@ -0,0 +1,153 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(100px <= width)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 10,
+ 14,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "100px",
+ 1,
+ 5,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "100px",
+ 1,
+ 5,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "100px",
+ 1,
+ 5,
+ {
+ "value": 100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 8,
+ 8,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 10,
+ 14,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 15,
+ 15,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/mf-range/0014.mjs b/packages/media-query-list-parser/test/cases/mf-range/0014.mjs
new file mode 100644
index 000000000..07057e069
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/mf-range/0014.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(100px <= width)',
+ 'mf-range/0014',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0001.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0001.expect.json
new file mode 100644
index 000000000..ca5c59492
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0001.expect.json
@@ -0,0 +1,19 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "screen",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0001.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0001.mjs
new file mode 100644
index 000000000..b8d1c3f6b
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'screen',
+ 'query-with-type/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0002.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0002.expect.json
new file mode 100644
index 000000000..f8f8ec84d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0002.expect.json
@@ -0,0 +1,36 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "only screen",
+ "modifier": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ],
+ "mediaType": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ],
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0002.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0002.mjs
new file mode 100644
index 000000000..d9da6523a
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'only screen',
+ 'query-with-type/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0003.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0003.expect.json
new file mode 100644
index 000000000..428d65884
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0003.expect.json
@@ -0,0 +1,36 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "not screen",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ],
+ "mediaType": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ],
+ [
+ "ident-token",
+ "screen",
+ 4,
+ 9,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0003.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0003.mjs
new file mode 100644
index 000000000..1684cf679
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not screen',
+ 'query-with-type/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0004.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0004.expect.json
new file mode 100644
index 000000000..c1793f870
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0004.expect.json
@@ -0,0 +1,182 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "only screen and (min-width: 300px)",
+ "modifier": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ],
+ "mediaType": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ],
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 12,
+ 14,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "min-width",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 17,
+ 25,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 28,
+ 32,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 28,
+ 32,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 17,
+ 25,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 28,
+ 32,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "whitespace-token",
+ " ",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 16,
+ 16,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0004.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0004.mjs
new file mode 100644
index 000000000..e056d5497
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'only screen and (min-width: 300px)',
+ 'query-with-type/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0005.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0005.expect.json
new file mode 100644
index 000000000..2beac0b28
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0005.expect.json
@@ -0,0 +1,203 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "/* a comment 0 */only/* a comment 1 */screen/* a comment 2 */ /* a comment 3 */and/* a comment 4 */(min-width:/* a comment 5 */300px)",
+ "modifier": [
+ [
+ "comment",
+ "/* a comment 0 */",
+ 0,
+ 16,
+ null
+ ],
+ [
+ "ident-token",
+ "only",
+ 17,
+ 20,
+ {
+ "value": "only"
+ }
+ ]
+ ],
+ "mediaType": [
+ [
+ "comment",
+ "/* a comment 1 */",
+ 21,
+ 37,
+ null
+ ],
+ [
+ "ident-token",
+ "screen",
+ 38,
+ 43,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "comment",
+ "/* a comment 2 */",
+ 44,
+ 60,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 61,
+ 61,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment 3 */",
+ 62,
+ 78,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 79,
+ 81,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "min-width",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 100,
+ 108,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 127,
+ 131,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "comment",
+ "/* a comment 5 */",
+ 110,
+ 126,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 127,
+ 131,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 100,
+ 108,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 109,
+ 109,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment 5 */",
+ 110,
+ 126,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 127,
+ 131,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "comment",
+ "/* a comment 4 */",
+ 82,
+ 98,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 99,
+ 99,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 132,
+ 132,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0005.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0005.mjs
new file mode 100644
index 000000000..7dffdd9d5
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/query-with-type/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '/* a comment 0 */only/* a comment 1 */screen/* a comment 2 */ /* a comment 3 */and/* a comment 4 */(min-width:/* a comment 5 */300px)',
+ 'query-with-type/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0001.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0001.expect.json
new file mode 100644
index 000000000..ca5c59492
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0001.expect.json
@@ -0,0 +1,19 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "screen",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0001.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0001.mjs
new file mode 100644
index 000000000..9b78044a6
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'screen',
+ 'specification-examples/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0002.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0002.expect.json
new file mode 100644
index 000000000..a4c943d65
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0002.expect.json
@@ -0,0 +1,19 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "print",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "print",
+ 0,
+ 4,
+ {
+ "value": "print"
+ }
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0002.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0002.mjs
new file mode 100644
index 000000000..fc91ac129
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'print',
+ 'specification-examples/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0003.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0003.expect.json
new file mode 100644
index 000000000..632d6888f
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0003.expect.json
@@ -0,0 +1,179 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "screen and (color)",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 7,
+ 9,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 12,
+ 16,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 17,
+ 17,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ },
+ {
+ "type": "media-query-with-type",
+ "string": " projection and (color)",
+ "modifier": [],
+ "mediaType": [
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 19,
+ null
+ ],
+ [
+ "ident-token",
+ "projection",
+ 20,
+ 29,
+ {
+ "value": "projection"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "whitespace-token",
+ " ",
+ 30,
+ 30,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 31,
+ 33,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 36,
+ 40,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "whitespace-token",
+ " ",
+ 34,
+ 34,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 35,
+ 35,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 41,
+ 41,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0003.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0003.mjs
new file mode 100644
index 000000000..70f1591ed
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'screen and (color), projection and (color)',
+ 'specification-examples/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0004.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0004.expect.json
new file mode 100644
index 000000000..0637a088a
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0004.expect.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0004.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0004.mjs
new file mode 100644
index 000000000..531984b8b
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '',
+ 'specification-examples/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0005.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0005.expect.json
new file mode 100644
index 000000000..4a93fb3d2
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0005.expect.json
@@ -0,0 +1,19 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "all",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "all",
+ 0,
+ 2,
+ {
+ "value": "all"
+ }
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0005.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0005.mjs
new file mode 100644
index 000000000..f9a7370f3
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'all',
+ 'specification-examples/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0006.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0006.expect.json
new file mode 100644
index 000000000..0a1810226
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0006.expect.json
@@ -0,0 +1,231 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "speech and (device-aspect-ratio: 16/9)",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "speech",
+ 0,
+ 5,
+ {
+ "value": "speech"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 7,
+ 9,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "device-aspect-ratio",
+ "tokens": [
+ [
+ "ident-token",
+ "device-aspect-ratio",
+ 12,
+ 30,
+ {
+ "value": "device-aspect-ratio"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "16",
+ 33,
+ 34,
+ {
+ "value": 16,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "/",
+ 35,
+ 35,
+ {
+ "value": "/"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "number-token",
+ "9",
+ 36,
+ 36,
+ {
+ "value": 9,
+ "type": "integer"
+ }
+ ]
+ ]
+ }
+ ],
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 32,
+ 32,
+ null
+ ],
+ [
+ "number-token",
+ "16",
+ 33,
+ 34,
+ {
+ "value": 16,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "/",
+ 35,
+ 35,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "number-token",
+ "9",
+ 36,
+ 36,
+ {
+ "value": 9,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "device-aspect-ratio",
+ 12,
+ 30,
+ {
+ "value": "device-aspect-ratio"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 31,
+ 31,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 32,
+ 32,
+ null
+ ],
+ [
+ "number-token",
+ "16",
+ 33,
+ 34,
+ {
+ "value": 16,
+ "type": "integer"
+ }
+ ],
+ [
+ "delim-token",
+ "/",
+ 35,
+ 35,
+ {
+ "value": "/"
+ }
+ ],
+ [
+ "number-token",
+ "9",
+ 36,
+ 36,
+ {
+ "value": 9,
+ "type": "integer"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 37,
+ 37,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0006.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0006.mjs
new file mode 100644
index 000000000..2a9da88e0
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0006.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'speech and (device-aspect-ratio: 16/9)',
+ 'specification-examples/0006',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0007.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0007.expect.json
new file mode 100644
index 000000000..d3955b3fd
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0007.expect.json
@@ -0,0 +1,174 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "not (width <= -100px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 5,
+ 9,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "-100px",
+ 14,
+ 19,
+ {
+ "value": -100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "dimension-token",
+ "-100px",
+ 14,
+ 19,
+ {
+ "value": -100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 5,
+ 9,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 11,
+ 11,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 12,
+ 12,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "dimension-token",
+ "-100px",
+ 14,
+ 19,
+ {
+ "value": -100,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 20,
+ 20,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0007.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0007.mjs
new file mode 100644
index 000000000..910e53f13
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0007.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not (width <= -100px)',
+ 'specification-examples/0007',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0008.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0008.expect.json
new file mode 100644
index 000000000..3ba2afa4c
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0008.expect.json
@@ -0,0 +1,314 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(width < 600px) and (height < 600px)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "600px",
+ 9,
+ 13,
+ {
+ "value": 600,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "dimension-token",
+ "600px",
+ 9,
+ 13,
+ {
+ "value": 600,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "dimension-token",
+ "600px",
+ 9,
+ 13,
+ {
+ "value": 600,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 14,
+ 14,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 15,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 16,
+ 18,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 19,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "height",
+ "tokens": [
+ [
+ "ident-token",
+ "height",
+ 21,
+ 26,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "600px",
+ 30,
+ 34,
+ {
+ "value": 600,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 29,
+ 29,
+ null
+ ],
+ [
+ "dimension-token",
+ "600px",
+ 30,
+ 34,
+ {
+ "value": 600,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "height",
+ 21,
+ 26,
+ {
+ "value": "height"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 28,
+ 28,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 29,
+ 29,
+ null
+ ],
+ [
+ "dimension-token",
+ "600px",
+ 30,
+ 34,
+ {
+ "value": 600,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 20,
+ 20,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 35,
+ 35,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0008.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0008.mjs
new file mode 100644
index 000000000..a29e3a438
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0008.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(width < 600px) and (height < 600px)',
+ 'specification-examples/0008',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0009.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0009.expect.json
new file mode 100644
index 000000000..d55ed0c15
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0009.expect.json
@@ -0,0 +1,270 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(update: slow) or (hover: none)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-or",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "update",
+ "tokens": [
+ [
+ "ident-token",
+ "update",
+ 1,
+ 6,
+ {
+ "value": "update"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "slow",
+ 9,
+ 12,
+ {
+ "value": "slow"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "ident-token",
+ "slow",
+ 9,
+ 12,
+ {
+ "value": "slow"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "update",
+ 1,
+ 6,
+ {
+ "value": "update"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "ident-token",
+ "slow",
+ 9,
+ 12,
+ {
+ "value": "slow"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 13,
+ 13,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-or",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 15,
+ 16,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "hover",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 19,
+ 23,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "none",
+ 26,
+ 29,
+ {
+ "value": "none"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "ident-token",
+ "none",
+ 26,
+ 29,
+ {
+ "value": "none"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 19,
+ 23,
+ {
+ "value": "hover"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 24,
+ 24,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "ident-token",
+ "none",
+ 26,
+ 29,
+ {
+ "value": "none"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 18,
+ 18,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0009.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0009.mjs
new file mode 100644
index 000000000..35c04d1af
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0009.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(update: slow) or (hover: none)',
+ 'specification-examples/0009',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0010.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0010.expect.json
new file mode 100644
index 000000000..86d043cf0
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0010.expect.json
@@ -0,0 +1,171 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(not (color)) or (hover)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-or",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 1,
+ 3,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 6,
+ 10,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ "list": [
+ {
+ "type": "media-or",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 14,
+ 15,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "hover",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 18,
+ 22,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 23,
+ 23,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0010.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0010.mjs
new file mode 100644
index 000000000..490eb23b4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0010.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(not (color)) or (hover)',
+ 'specification-examples/0010',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0011.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0011.expect.json
new file mode 100644
index 000000000..388ba1017
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0011.expect.json
@@ -0,0 +1,171 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "not ((color) or (hover))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-or",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 6,
+ 10,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-or",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 13,
+ 14,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "hover",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 17,
+ 21,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 16,
+ 16,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 4,
+ 4,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 23,
+ 23,
+ null
+ ]
+ ]
+ }
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0011.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0011.mjs
new file mode 100644
index 000000000..4e6c07660
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0011.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not ((color) or (hover))',
+ 'specification-examples/0011',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0012.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0012.expect.json
new file mode 100644
index 000000000..0ff982293
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0012.expect.json
@@ -0,0 +1,216 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(not (color)) and (not (hover))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 1,
+ 3,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 6,
+ 10,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 5,
+ 5,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 12,
+ 12,
+ null
+ ]
+ ]
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 14,
+ 16,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 17,
+ 17,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 19,
+ 21,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "hover",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 24,
+ 28,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 23,
+ 23,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 29,
+ 29,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 18,
+ 18,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 30,
+ 30,
+ null
+ ]
+ ]
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0012.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0012.mjs
new file mode 100644
index 000000000..c5e04a01d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0012.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(not (color)) and (not (hover))',
+ 'specification-examples/0012',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0013.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0013.expect.json
new file mode 100644
index 000000000..24a092e18
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0013.expect.json
@@ -0,0 +1,226 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(color) and ((pointer) or (hover))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 8,
+ 10,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-or",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "pointer",
+ "tokens": [
+ [
+ "ident-token",
+ "pointer",
+ 14,
+ 20,
+ {
+ "value": "pointer"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 13,
+ 13,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-or",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 23,
+ 24,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 25,
+ 25,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "hover",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 27,
+ 31,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0013.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0013.mjs
new file mode 100644
index 000000000..29fb57b1c
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0013.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color) and ((pointer) or (hover))',
+ 'specification-examples/0013',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0014.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0014.expect.json
new file mode 100644
index 000000000..26292910c
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0014.expect.json
@@ -0,0 +1,226 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "((color) and (pointer)) or (hover)",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-or",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 2,
+ 6,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 9,
+ 11,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "pointer",
+ "tokens": [
+ [
+ "ident-token",
+ "pointer",
+ 14,
+ 20,
+ {
+ "value": "pointer"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 13,
+ 13,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ "list": [
+ {
+ "type": "media-or",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 23,
+ 23,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 24,
+ 25,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "hover",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 28,
+ 32,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 27,
+ 27,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0014.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0014.mjs
new file mode 100644
index 000000000..1426a6960
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0014.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '((color) and (pointer)) or (hover)',
+ 'specification-examples/0014',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0015.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0015.expect.json
new file mode 100644
index 000000000..c20caa59e
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0015.expect.json
@@ -0,0 +1,237 @@
+[
+ {
+ "type": "media-query-invalid",
+ "string": "(color) and (pointer) or (hover)",
+ "media": [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 8,
+ 10,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "pointer",
+ 13,
+ 19,
+ {
+ "value": "pointer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 20,
+ 20,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "pointer",
+ 13,
+ 19,
+ {
+ "value": "pointer"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 21,
+ 21,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "or",
+ 22,
+ 23,
+ {
+ "value": "or"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 25,
+ 25,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "ident-token",
+ "hover",
+ 26,
+ 30,
+ {
+ "value": "hover"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 31,
+ 31,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 26,
+ 30,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0015.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0015.mjs
new file mode 100644
index 000000000..befcbc3a4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0015.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color) and (pointer) or (hover)',
+ 'specification-examples/0015',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0016.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0016.expect.json
new file mode 100644
index 000000000..0a0968a3d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0016.expect.json
@@ -0,0 +1,237 @@
+[
+ {
+ "type": "media-query-invalid",
+ "string": "(color) not (pointer) or (hover)",
+ "media": [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 6,
+ 6,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 1,
+ 5,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 8,
+ 10,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "ident-token",
+ "pointer",
+ 13,
+ 19,
+ {
+ "value": "pointer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 20,
+ 20,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "pointer",
+ 13,
+ 19,
+ {
+ "value": "pointer"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 21,
+ 21,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "or",
+ 22,
+ 23,
+ {
+ "value": "or"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 25,
+ 25,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "ident-token",
+ "hover",
+ 26,
+ 30,
+ {
+ "value": "hover"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 31,
+ 31,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "hover",
+ 26,
+ 30,
+ {
+ "value": "hover"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0016.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0016.mjs
new file mode 100644
index 000000000..a45f0c406
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0016.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(color) not (pointer) or (hover)',
+ 'specification-examples/0016',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0017.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0017.expect.json
new file mode 100644
index 000000000..85ad37800
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0017.expect.json
@@ -0,0 +1,148 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "((color) and (pointer) or (hover))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 2,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 9,
+ 11,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "pointer",
+ 14,
+ 20,
+ {
+ "value": "pointer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 23,
+ 24,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "ident-token",
+ "hover",
+ 27,
+ 31,
+ {
+ "value": "hover"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0017.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0017.mjs
new file mode 100644
index 000000000..37ded5cb8
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0017.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '((color) and (pointer) or (hover))',
+ 'specification-examples/0017',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0018.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0018.expect.json
new file mode 100644
index 000000000..fd2eda502
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0018.expect.json
@@ -0,0 +1,148 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "((color) not (pointer) or (hover))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "general-enclosed",
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 2,
+ 6,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "ident-token",
+ "not",
+ 9,
+ 11,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "pointer",
+ 14,
+ 20,
+ {
+ "value": "pointer"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ],
+ [
+ "ident-token",
+ "or",
+ 23,
+ 24,
+ {
+ "value": "or"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "(-token",
+ "(",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "ident-token",
+ "hover",
+ 27,
+ 31,
+ {
+ "value": "hover"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0018.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0018.mjs
new file mode 100644
index 000000000..46e80a9a7
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0018.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '((color) not (pointer) or (hover))',
+ 'specification-examples/0018',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+ 1,
+);
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0019.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0019.expect.json
new file mode 100644
index 000000000..67bb1b659
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0019.expect.json
@@ -0,0 +1,150 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "((color) and (width))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "color",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 2,
+ 6,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 1,
+ 1,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 7,
+ 7,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 9,
+ 11,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 12,
+ 12,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-boolean",
+ "name": "width",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 14,
+ 18,
+ {
+ "value": "width"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 13,
+ 13,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 19,
+ 19,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": []
+ }
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 20,
+ 20,
+ null
+ ]
+ ]
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0019.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0019.mjs
new file mode 100644
index 000000000..80b9aa5e4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/specification-examples/0019.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '((color) and (width))',
+ 'specification-examples/0019',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0001.expect.json b/packages/media-query-list-parser/test/cases/various/0001.expect.json
new file mode 100644
index 000000000..36d0caf8d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0001.expect.json
@@ -0,0 +1,149 @@
+[
+ {
+ "type": "media-query-invalid",
+ "string": "(/* a comment */foo ) something else",
+ "media": [
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 1,
+ 15,
+ null
+ ],
+ [
+ "ident-token",
+ "foo",
+ 16,
+ 18,
+ {
+ "value": "foo"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 20,
+ null
+ ],
+ [
+ ")-token",
+ ")",
+ 21,
+ 21,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "comment",
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 1,
+ 15,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "foo",
+ 16,
+ 18,
+ {
+ "value": "foo"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 19,
+ 20,
+ null
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "something",
+ 23,
+ 31,
+ {
+ "value": "something"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 32,
+ 32,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "else",
+ 33,
+ 36,
+ {
+ "value": "else"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0001.mjs b/packages/media-query-list-parser/test/cases/various/0001.mjs
new file mode 100644
index 000000000..c5764560e
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(/* a comment */foo ) something else',
+ 'various/0001',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0002.expect.json b/packages/media-query-list-parser/test/cases/various/0002.expect.json
new file mode 100644
index 000000000..7d8dd3926
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0002.expect.json
@@ -0,0 +1,737 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)",
+ "modifier": [
+ [
+ "ident-token",
+ "not",
+ 0,
+ 2,
+ {
+ "value": "not"
+ }
+ ]
+ ],
+ "mediaType": [
+ [
+ "whitespace-token",
+ " ",
+ 3,
+ 3,
+ null
+ ],
+ [
+ "ident-token",
+ "screen",
+ 4,
+ 9,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 11,
+ 13,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "min-width",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 33,
+ 33,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 34,
+ 36,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "prefers-color-scheme",
+ "tokens": [
+ [
+ "ident-token",
+ "prefers-color-scheme",
+ 39,
+ 58,
+ {
+ "value": "prefers-color-scheme"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "comment",
+ "/* a comment */",
+ 60,
+ 74,
+ null
+ ],
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "prefers-color-scheme",
+ 39,
+ 58,
+ {
+ "value": "prefers-color-scheme"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 59,
+ 59,
+ null
+ ],
+ [
+ "comment",
+ "/* a comment */",
+ 60,
+ 74,
+ null
+ ],
+ [
+ "ident-token",
+ "dark",
+ 75,
+ 78,
+ {
+ "value": "dark"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 38,
+ 38,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 79,
+ 79,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ },
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 80,
+ 80,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 81,
+ 83,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 84,
+ 84,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 86,
+ 90,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 91,
+ 91,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 93,
+ 93,
+ null
+ ],
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 86,
+ 90,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 91,
+ 91,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 92,
+ 92,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 93,
+ 93,
+ null
+ ],
+ [
+ "dimension-token",
+ "40vw",
+ 94,
+ 97,
+ {
+ "value": 40,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 85,
+ 85,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 98,
+ 98,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ },
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 99,
+ 99,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 100,
+ 102,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 103,
+ 103,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 111,
+ 111,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 112,
+ 116,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 117,
+ 117,
+ null
+ ]
+ ]
+ },
+ "valueOne": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 109,
+ 109,
+ null
+ ]
+ ]
+ },
+ "valueTwo": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 119,
+ 119,
+ null
+ ],
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "dimension-token",
+ "30px",
+ 105,
+ 108,
+ {
+ "value": 30,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 109,
+ 109,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 110,
+ 110,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 111,
+ 111,
+ null
+ ],
+ [
+ "ident-token",
+ "width",
+ 112,
+ 116,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 117,
+ 117,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 118,
+ 118,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 119,
+ 119,
+ null
+ ],
+ [
+ "dimension-token",
+ "50rem",
+ 120,
+ 124,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 104,
+ 104,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 125,
+ 125,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ]
+ ],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0002.mjs b/packages/media-query-list-parser/test/cases/various/0002.mjs
new file mode 100644
index 000000000..964b4d9b9
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0002.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)',
+ 'various/0002',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0003.expect.json b/packages/media-query-list-parser/test/cases/various/0003.expect.json
new file mode 100644
index 000000000..16e294933
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0003.expect.json
@@ -0,0 +1,319 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(resolution < infinite) and (infinite <= resolution) ",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-condition-list-and",
+ "leading": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "resolution",
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "resolution",
+ 1,
+ 10,
+ {
+ "value": "resolution"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 12,
+ 12,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 13,
+ 13,
+ null
+ ],
+ [
+ "ident-token",
+ "infinite",
+ 14,
+ 21,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 22,
+ 22,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ },
+ "list": [
+ {
+ "type": "media-and",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 23,
+ 23,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 24,
+ 26,
+ {
+ "value": "and"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 27,
+ 27,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-value-name",
+ "name": {
+ "type": "mf-name",
+ "name": "resolution",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 40,
+ 40,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 41,
+ 50,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "infinite",
+ 29,
+ 36,
+ {
+ "value": "infinite"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 37,
+ 37,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 38,
+ 38,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "delim-token",
+ "=",
+ 39,
+ 39,
+ {
+ "value": "="
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 40,
+ 40,
+ null
+ ],
+ [
+ "ident-token",
+ "resolution",
+ 41,
+ 50,
+ {
+ "value": "resolution"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 28,
+ 28,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 51,
+ 51,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ ],
+ "before": [],
+ "after": [
+ [
+ "whitespace-token",
+ " ",
+ 52,
+ 52,
+ null
+ ]
+ ]
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0003.mjs b/packages/media-query-list-parser/test/cases/various/0003.mjs
new file mode 100644
index 000000000..2af154b33
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0003.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(resolution < infinite) and (infinite <= resolution) ',
+ 'various/0003',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0004.expect.json b/packages/media-query-list-parser/test/cases/various/0004.expect.json
new file mode 100644
index 000000000..5288c94e4
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0004.expect.json
@@ -0,0 +1,367 @@
+[
+ {
+ "type": "media-query-without-type",
+ "string": "(width < calc(50vw - 3rem))",
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-range-name-value",
+ "name": {
+ "type": "mf-name",
+ "name": "width",
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "function",
+ "name": "calc",
+ "tokens": [
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "width",
+ 1,
+ 5,
+ {
+ "value": "width"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "delim-token",
+ "<",
+ 7,
+ 7,
+ {
+ "value": "<"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 8,
+ 8,
+ null
+ ],
+ [
+ "function-token",
+ "calc(",
+ 9,
+ 13,
+ {
+ "value": "calc"
+ }
+ ],
+ [
+ "dimension-token",
+ "50vw",
+ 14,
+ 17,
+ {
+ "value": 50,
+ "type": "integer",
+ "unit": "vw"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 18,
+ 18,
+ null
+ ],
+ [
+ "delim-token",
+ "-",
+ 19,
+ 19,
+ {
+ "value": "-"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ],
+ [
+ "dimension-token",
+ "3rem",
+ 21,
+ 24,
+ {
+ "value": 3,
+ "type": "integer",
+ "unit": "rem"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 25,
+ 25,
+ null
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 0,
+ 0,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 26,
+ 26,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0004.mjs b/packages/media-query-list-parser/test/cases/various/0004.mjs
new file mode 100644
index 000000000..62f73bf0a
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0004.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ '(width < calc(50vw - 3rem))',
+ 'various/0004',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0005.expect.json b/packages/media-query-list-parser/test/cases/various/0005.expect.json
new file mode 100644
index 000000000..a954c0d33
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0005.expect.json
@@ -0,0 +1,186 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "screen and not (min-width: 300px)",
+ "modifier": [],
+ "mediaType": [
+ [
+ "ident-token",
+ "screen",
+ 0,
+ 5,
+ {
+ "value": "screen"
+ }
+ ]
+ ],
+ "and": [
+ [
+ "whitespace-token",
+ " ",
+ 6,
+ 6,
+ null
+ ],
+ [
+ "ident-token",
+ "and",
+ 7,
+ 9,
+ {
+ "value": "and"
+ }
+ ]
+ ],
+ "media": {
+ "type": "media-condition",
+ "media": {
+ "type": "media-not",
+ "modifier": [
+ [
+ "whitespace-token",
+ " ",
+ 10,
+ 10,
+ null
+ ],
+ [
+ "ident-token",
+ "not",
+ 11,
+ 13,
+ {
+ "value": "not"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 14,
+ 14,
+ null
+ ]
+ ],
+ "media": {
+ "type": "media-in-parens",
+ "media": {
+ "type": "media-feature",
+ "feature": {
+ "type": "mf-plain",
+ "name": {
+ "type": "mf-name",
+ "name": "min-width",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ "value": {
+ "type": "mf-value",
+ "value": {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 16,
+ 24,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 26,
+ 26,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 27,
+ 31,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ },
+ "before": [
+ [
+ "(-token",
+ "(",
+ 15,
+ 15,
+ null
+ ]
+ ],
+ "after": [
+ [
+ ")-token",
+ ")",
+ 32,
+ 32,
+ null
+ ]
+ ]
+ },
+ "before": [],
+ "after": []
+ }
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0005.mjs b/packages/media-query-list-parser/test/cases/various/0005.mjs
new file mode 100644
index 000000000..7c55c608d
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0005.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'screen and not (min-width: 300px)',
+ 'various/0005',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0006.expect.json b/packages/media-query-list-parser/test/cases/various/0006.expect.json
new file mode 100644
index 000000000..4e7482d4b
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0006.expect.json
@@ -0,0 +1,43 @@
+[
+ {
+ "type": "media-query-with-type",
+ "string": "only screen ",
+ "modifier": [
+ [
+ "ident-token",
+ "only",
+ 0,
+ 3,
+ {
+ "value": "only"
+ }
+ ]
+ ],
+ "mediaType": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ],
+ [
+ "ident-token",
+ "screen",
+ 5,
+ 10,
+ {
+ "value": "screen"
+ }
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 11,
+ 11,
+ null
+ ]
+ ],
+ "media": null
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0006.mjs b/packages/media-query-list-parser/test/cases/various/0006.mjs
new file mode 100644
index 000000000..2d9faf758
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0006.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+runTest(
+ 'only screen ',
+ 'various/0006',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/cases/various/0007.expect.json b/packages/media-query-list-parser/test/cases/various/0007.expect.json
new file mode 100644
index 000000000..18df98287
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0007.expect.json
@@ -0,0 +1,740 @@
+[
+ {
+ "type": "media-query-invalid",
+ "string": " and and",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 0,
+ 0,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 1,
+ 3,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 4,
+ 4,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 5,
+ 7,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "media-query-invalid",
+ "string": " screen and and (color)",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 9,
+ 9,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 10,
+ 15,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 16,
+ 16,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 17,
+ 19,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 20,
+ 20,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 21,
+ 23,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 24,
+ 24,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 25,
+ 25,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 25,
+ 25,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 26,
+ 30,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 31,
+ 31,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 26,
+ 30,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "media-query-invalid",
+ "string": " only screen print",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 33,
+ 33,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 34,
+ 37,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 38,
+ 38,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 39,
+ 44,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 45,
+ 45,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "print",
+ 46,
+ 50,
+ {
+ "value": "print"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "media-query-invalid",
+ "string": " only only",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 52,
+ 52,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 53,
+ 56,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 57,
+ 57,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 58,
+ 61,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "media-query-invalid",
+ "string": " not print or",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 63,
+ 63,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "not",
+ 64,
+ 66,
+ {
+ "value": "not"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 67,
+ 67,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "print",
+ 68,
+ 72,
+ {
+ "value": "print"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 73,
+ 73,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "or",
+ 74,
+ 75,
+ {
+ "value": "or"
+ }
+ ]
+ ]
+ }
+ ]
+ },
+ {
+ "type": "media-query-invalid",
+ "string": " and (color)",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 77,
+ 77,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "and",
+ 78,
+ 80,
+ {
+ "value": "and"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 81,
+ 81,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 82,
+ 82,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 82,
+ 82,
+ null
+ ],
+ [
+ "ident-token",
+ "color",
+ 83,
+ 87,
+ {
+ "value": "color"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 88,
+ 88,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "color",
+ 83,
+ 87,
+ {
+ "value": "color"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "media-query-invalid",
+ "string": " only screen foo (min-width: 300px)",
+ "media": [
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 90,
+ 90,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "only",
+ 91,
+ 94,
+ {
+ "value": "only"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 95,
+ 95,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "screen",
+ 96,
+ 101,
+ {
+ "value": "screen"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 102,
+ 102,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "foo",
+ 103,
+ 105,
+ {
+ "value": "foo"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 106,
+ 106,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "simple-block",
+ "startToken": [
+ "(-token",
+ "(",
+ 107,
+ 107,
+ null
+ ],
+ "tokens": [
+ [
+ "(-token",
+ "(",
+ 107,
+ 107,
+ null
+ ],
+ [
+ "ident-token",
+ "min-width",
+ 108,
+ 116,
+ {
+ "value": "min-width"
+ }
+ ],
+ [
+ "colon-token",
+ ":",
+ 117,
+ 117,
+ null
+ ],
+ [
+ "whitespace-token",
+ " ",
+ 118,
+ 118,
+ null
+ ],
+ [
+ "dimension-token",
+ "300px",
+ 119,
+ 123,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ],
+ [
+ ")-token",
+ ")",
+ 124,
+ 124,
+ null
+ ]
+ ],
+ "value": [
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "ident-token",
+ "min-width",
+ 108,
+ 116,
+ {
+ "value": "min-width"
+ }
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "colon-token",
+ ":",
+ 117,
+ 117,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "whitespace",
+ "tokens": [
+ [
+ "whitespace-token",
+ " ",
+ 118,
+ 118,
+ null
+ ]
+ ]
+ },
+ {
+ "type": "token",
+ "tokens": [
+ [
+ "dimension-token",
+ "300px",
+ 119,
+ 123,
+ {
+ "value": 300,
+ "type": "integer",
+ "unit": "px"
+ }
+ ]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/media-query-list-parser/test/cases/various/0007.mjs b/packages/media-query-list-parser/test/cases/various/0007.mjs
new file mode 100644
index 000000000..1b5de51ff
--- /dev/null
+++ b/packages/media-query-list-parser/test/cases/various/0007.mjs
@@ -0,0 +1,14 @@
+import assert from 'assert';
+import { runTest } from '../../util/run-test.mjs';
+
+// MUST all be invalid:
+runTest(
+ ' and and, screen and and (color), only screen print, only only, not print or, and (color), only screen foo (min-width: 300px)',
+ 'various/0007',
+ (actual, expected) => {
+ assert.deepStrictEqual(
+ actual,
+ expected,
+ );
+ },
+);
diff --git a/packages/media-query-list-parser/test/serialize/0001.mjs b/packages/media-query-list-parser/test/serialize/0001.mjs
new file mode 100644
index 000000000..d1f41f3fe
--- /dev/null
+++ b/packages/media-query-list-parser/test/serialize/0001.mjs
@@ -0,0 +1,13 @@
+import assert from 'assert';
+import { newMediaFeatureBoolean, newMediaFeaturePlain } from '@csstools/media-query-list-parser';
+import { TokenType } from '@csstools/css-tokenizer';
+
+assert.strictEqual(
+ newMediaFeaturePlain('min-width', [TokenType.Dimension, '300px', 0, 0, { value: 300, unit: 'px' }]).toString(),
+ '(min-width:300px)',
+);
+
+assert.strictEqual(
+ newMediaFeatureBoolean('color').toString(),
+ '(color)',
+);
diff --git a/packages/media-query-list-parser/test/test.mjs b/packages/media-query-list-parser/test/test.mjs
new file mode 100644
index 000000000..58b7e395e
--- /dev/null
+++ b/packages/media-query-list-parser/test/test.mjs
@@ -0,0 +1,66 @@
+import './api/options.mjs';
+
+import './cases/media-not/0001.mjs';
+
+import './cases/mf-boolean/0001.mjs';
+import './cases/mf-boolean/0002.mjs';
+import './cases/mf-boolean/0003.mjs';
+import './cases/mf-boolean/0004.mjs';
+import './cases/mf-boolean/0005.mjs';
+
+import './cases/mf-plain/0001.mjs';
+import './cases/mf-plain/0002.mjs';
+import './cases/mf-plain/0003.mjs';
+import './cases/mf-plain/0004.mjs';
+import './cases/mf-plain/0005.mjs';
+
+import './cases/mf-range/0001.mjs';
+import './cases/mf-range/0002.mjs';
+import './cases/mf-range/0003.mjs';
+import './cases/mf-range/0004.mjs';
+import './cases/mf-range/0005.mjs';
+import './cases/mf-range/0006.mjs';
+import './cases/mf-range/0007.mjs';
+import './cases/mf-range/0008.mjs';
+import './cases/mf-range/0009.mjs';
+import './cases/mf-range/0010.mjs';
+import './cases/mf-range/0011.mjs';
+import './cases/mf-range/0012.mjs';
+import './cases/mf-range/0013.mjs';
+import './cases/mf-range/0014.mjs';
+
+import './cases/query-with-type/0001.mjs';
+import './cases/query-with-type/0002.mjs';
+import './cases/query-with-type/0003.mjs';
+import './cases/query-with-type/0004.mjs';
+import './cases/query-with-type/0005.mjs';
+
+import './cases/specification-examples/0001.mjs';
+import './cases/specification-examples/0002.mjs';
+import './cases/specification-examples/0003.mjs';
+import './cases/specification-examples/0004.mjs';
+import './cases/specification-examples/0005.mjs';
+import './cases/specification-examples/0006.mjs';
+import './cases/specification-examples/0007.mjs';
+import './cases/specification-examples/0008.mjs';
+import './cases/specification-examples/0009.mjs';
+import './cases/specification-examples/0010.mjs';
+import './cases/specification-examples/0011.mjs';
+import './cases/specification-examples/0012.mjs';
+import './cases/specification-examples/0013.mjs';
+import './cases/specification-examples/0014.mjs';
+import './cases/specification-examples/0015.mjs';
+import './cases/specification-examples/0016.mjs';
+import './cases/specification-examples/0017.mjs';
+import './cases/specification-examples/0018.mjs';
+import './cases/specification-examples/0019.mjs';
+
+import './cases/various/0001.mjs';
+import './cases/various/0002.mjs';
+import './cases/various/0003.mjs';
+import './cases/various/0004.mjs';
+import './cases/various/0005.mjs';
+import './cases/various/0006.mjs';
+import './cases/various/0007.mjs';
+
+import './serialize/0001.mjs';
diff --git a/packages/media-query-list-parser/test/util/run-test.mjs b/packages/media-query-list-parser/test/util/run-test.mjs
new file mode 100644
index 000000000..f96dc172e
--- /dev/null
+++ b/packages/media-query-list-parser/test/util/run-test.mjs
@@ -0,0 +1,42 @@
+import fs from 'fs';
+import path from 'path';
+import { isGeneralEnclosed, parse } from '@csstools/media-query-list-parser';
+
+export function runTest(source, testPath, assertEqual, expectGeneralEnclosed = 0) {
+ const resultAST = parse(source, {
+ preserveInvalidMediaQueries: true,
+ });
+
+ 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`)).toString());
+
+ assertEqual(
+ resultAST.map((x) => x.toString()).join(','),
+ expectData.map((x) => x.string).join(','),
+ );
+
+ assertEqual(
+ JSON.parse(resultAST_JSON),
+ expectData,
+ );
+
+ let generalEnclosedCounter = 0;
+ resultAST.map((x) => {
+ x.walk((entry) => {
+ if (isGeneralEnclosed(entry.node)) {
+ generalEnclosedCounter++;
+ }
+ });
+ });
+
+ assertEqual(
+ generalEnclosedCounter,
+ expectGeneralEnclosed,
+ );
+ }
+}
diff --git a/packages/media-query-list-parser/tsconfig.json b/packages/media-query-list-parser/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/packages/media-query-list-parser/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/plugins/postcss-custom-media/.tape.cjs b/plugins/postcss-custom-media/.tape.cjs
deleted file mode 100644
index 60fff7ba9..000000000
--- a/plugins/postcss-custom-media/.tape.cjs
+++ /dev/null
@@ -1,255 +0,0 @@
-const postcssTape = require('../../packages/postcss-tape/dist/index.cjs');
-const plugin = require('postcss-custom-media');
-const fs = require('fs');
-
-postcssTape(plugin)({
- 'basic': {
- message: 'supports basic usage'
- },
- 'basic:preserve': {
- message: 'supports { preserve: true } usage',
- options: {
- preserve: true
- }
- },
- 'examples/example': {
- message: 'minimal example',
- },
- 'examples/example:preserve': {
- message: 'minimal example',
- options: {
- preserve: true
- }
- },
- 'complex': {
- message: 'supports complex usage'
- },
- 'import': {
- message: 'supports { importFrom: { customMedia: { ... } } } usage',
- options: {
- importFrom: {
- customMedia: {
- '--mq-a': '(max-width: 30em), (max-height: 30em)',
- '--not-mq-a': 'not all and (--mq-a)'
- }
- }
- }
- },
- 'import:import-fn': {
- message: 'supports { importFrom() } usage',
- options: {
- importFrom() {
- return {
- customMedia: {
- '--mq-a': '(max-width: 30em), (max-height: 30em)',
- '--not-mq-a': 'not all and (--mq-a)'
- }
- };
- }
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:import-fn-promise': {
- message: 'supports { async importFrom() } usage',
- options: {
- importFrom() {
- return new Promise(resolve => {
- resolve({
- customMedia: {
- '--mq-a': '(max-width: 30em), (max-height: 30em)',
- '--not-mq-a': 'not all and (--mq-a)'
- }
- })
- });
- }
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:json': {
- message: 'supports { importFrom: "test/import-media.json" } usage',
- options: {
- importFrom: 'test/import-media.json'
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:js': {
- message: 'supports { importFrom: "test/import-media.js" } usage',
- options: {
- importFrom: 'test/import-media.js'
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:css': {
- message: 'supports { importFrom: "test/import-media.css" } usage',
- options: {
- importFrom: 'test/import-media.css'
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:css-from': {
- message: 'supports { importFrom: { from: "test/import-media.css" } } usage',
- options: {
- importFrom: { from: 'test/import-media.css' }
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:css-from-type': {
- message: 'supports { importFrom: [ { from: "test/import-media.css", type: "css" } ] } usage',
- options: {
- importFrom: [{ from: 'test/import-media.css', type: 'css' }]
- },
- expect: 'import.expect.css',
- result: 'import.result.css'
- },
- 'import:empty': {
- message: 'supports { importFrom: {} } usage',
- options: {
- importFrom: {}
- }
- },
- 'basic:export': {
- message: 'supports { exportTo: { customMedia: { ... } } } usage',
- options: {
- exportTo: (global.__exportMediaObject = global.__exportMediaObject || {
- customMedia: null
- })
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- after() {
- if (__exportMediaObject.customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') {
- throw new Error('The exportTo function failed');
- }
- }
- },
- 'basic:export-fn': {
- message: 'supports { exportTo() } usage',
- options: {
- exportTo(customMedia) {
- if (customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') {
- throw new Error('The exportTo function failed');
- }
- }
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css'
- },
- 'basic:export-fn-promise': {
- message: 'supports { async exportTo() } usage',
- options: {
- exportTo(customMedia) {
- return new Promise((resolve, reject) => {
- if (customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') {
- reject('The exportTo function failed');
- } else {
- resolve();
- }
- });
- }
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css'
- },
- 'basic:export-json': {
- message: 'supports { exportTo: "test/export-media.json" } usage',
- options: {
- exportTo: 'test/export-media.json'
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- before() {
- global.__exportMediaString = fs.readFileSync('test/export-media.json', 'utf8');
- },
- after() {
- if (global.__exportMediaString !== fs.readFileSync('test/export-media.json', 'utf8')) {
- throw new Error('The original file did not match the freshly exported copy');
- }
- }
- },
- 'basic:export-js': {
- message: 'supports { exportTo: "test/export-media.js" } usage',
- options: {
- exportTo: 'test/export-media.js'
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- before() {
- global.__exportMediaString = fs.readFileSync('test/export-media.js', 'utf8');
- },
- after() {
- if (global.__exportMediaString !== fs.readFileSync('test/export-media.js', 'utf8')) {
- throw new Error('The original file did not match the freshly exported copy');
- }
- }
- },
- 'basic:export-mjs': {
- message: 'supports { exportTo: "test/export-media.mjs" } usage',
- options: {
- exportTo: 'test/export-media.mjs'
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- before() {
- global.__exportMediaString = fs.readFileSync('test/export-media.mjs', 'utf8');
- },
- after() {
- if (global.__exportMediaString !== fs.readFileSync('test/export-media.mjs', 'utf8')) {
- throw new Error('The original file did not match the freshly exported copy');
- }
- }
- },
- 'basic:export-css': {
- message: 'supports { exportTo: "test/export-media.css" } usage',
- options: {
- exportTo: 'test/export-media.css'
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- before() {
- global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8');
- },
- after() {
- if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) {
- throw new Error('The original file did not match the freshly exported copy');
- }
- }
- },
- 'basic:export-css-to': {
- message: 'supports { exportTo: { to: "test/export-media.css" } } usage',
- options: {
- exportTo: { to: 'test/export-media.css' }
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- before() {
- global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8');
- },
- after() {
- if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) {
- throw new Error('The original file did not match the freshly exported copy');
- }
- }
- },
- 'basic:export-css-to-type': {
- message: 'supports { exportTo: { to: "test/export-media.css", type: "css" } } usage',
- options: {
- exportTo: { to: 'test/export-media.css', type: 'css' }
- },
- expect: 'basic.expect.css',
- result: 'basic.result.css',
- before() {
- global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8');
- },
- after() {
- if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) {
- throw new Error('The original file did not match the freshly exported copy');
- }
- }
- }
-});
diff --git a/plugins/postcss-custom-media/.tape.mjs b/plugins/postcss-custom-media/.tape.mjs
new file mode 100644
index 000000000..7dac6d271
--- /dev/null
+++ b/plugins/postcss-custom-media/.tape.mjs
@@ -0,0 +1,86 @@
+import postcssTape from '../../packages/postcss-tape/dist/index.cjs';
+import plugin from 'postcss-custom-media';
+
+postcssTape(plugin)({
+ 'basic-after-v9': {
+ message: 'supports basic usage'
+ },
+ 'basic-after-v9:preserve': {
+ message: 'supports { preserve: true } usage',
+ options: {
+ preserve: true
+ }
+ },
+ 'basic': {
+ message: 'supports basic usage (old)',
+ warnings: 1,
+ },
+ 'examples/example': {
+ message: 'minimal example',
+ },
+ 'examples/example:preserve': {
+ message: 'minimal example',
+ options: {
+ preserve: true
+ }
+ },
+ 'nesting': {
+ message: 'works when nested'
+ },
+ 'not-processable': {
+ message: 'only handles processable @custom-media rules'
+ },
+ 'parser-checks': {
+ message: 'supports more obscure CSS'
+ },
+ 'eof-1': {
+ message: 'handles EOF correctly (1)',
+ warnings: 1,
+ },
+ 'eof-2': {
+ message: 'handles EOF correctly (2)',
+ warnings: 1,
+ },
+ 'eof-3': {
+ message: 'handles EOF correctly (3)',
+ warnings: 1,
+ },
+ 'eof-4': {
+ message: 'handles EOF correctly (4)',
+ warnings: 1,
+ },
+ 'complex': {
+ message: 'supports complex usage'
+ },
+ 'cyclic': {
+ message: 'handles cyclic references',
+ warnings: 3,
+ },
+ 'override': {
+ message: 'handles reference overrides'
+ },
+ 'modifiers': {
+ message: 'supports media query modifiers'
+ },
+ 'list': {
+ message: 'supports media query lists'
+ },
+ 'true-false': {
+ message: 'supports true|false keywords'
+ },
+ 'comma-1': {
+ message: 'can correctly split media query lists'
+ },
+ 'comma-2': {
+ message: 'can correctly split media query lists'
+ },
+ 'and': {
+ message: 'supports media queries with "and"'
+ },
+ 'not': {
+ message: 'supports media queries with "not"'
+ },
+ 'or': {
+ message: 'supports media queries with "or"'
+ }
+});
diff --git a/plugins/postcss-custom-media/CHANGELOG.md b/plugins/postcss-custom-media/CHANGELOG.md
index 7ded46ba5..4e8143f47 100644
--- a/plugins/postcss-custom-media/CHANGELOG.md
+++ b/plugins/postcss-custom-media/CHANGELOG.md
@@ -3,6 +3,10 @@
### Unreleased (major)
- Updated: Support for Node v14+ (major).
+- Removed: `importFrom` feature (breaking).
+- Removed: `exportTo` feature (breaking).
+- Fixed: implement logical evaluation of custom media queries.
+- Added: Support for `true` and `false` keywords in `@custom-media`.
### 8.0.2 (June 4, 2022)
diff --git a/plugins/postcss-custom-media/README.md b/plugins/postcss-custom-media/README.md
index 73437387f..e847e2fba 100644
--- a/plugins/postcss-custom-media/README.md
+++ b/plugins/postcss-custom-media/README.md
@@ -74,90 +74,6 @@ postcssCustomMedia({ preserve: true })
}
```
-
-### importFrom
-
-The `importFrom` option specifies sources where custom media can be imported
-from, which might be CSS, JS, and JSON files, functions, and directly passed
-objects.
-
-```js
-postcssCustomMedia({
- importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em);
-});
-```
-
-```pcss
-@media (max-width: 30em) {
- /* styles for small viewport */
-}
-
-@media (--small-viewport) {
- /* styles for small viewport */
-}
-```
-
-Multiple sources can be passed into this option, and they will be parsed in the
-order they are received. JavaScript files, JSON files, functions, and objects
-will need to namespace custom media using the `customMedia` or
-`custom-media` key.
-
-```js
-postcssCustomMedia({
- importFrom: [
- 'path/to/file.css',
- 'and/then/this.js',
- 'and/then/that.json',
- {
- customMedia: { '--small-viewport': '(max-width: 30em)' }
- },
- () => {
- const customMedia = { '--small-viewport': '(max-width: 30em)' };
-
- return { customMedia };
- }
- ]
-});
-```
-
-### exportTo
-
-The `exportTo` option specifies destinations where custom media can be exported
-to, which might be CSS, JS, and JSON files, functions, and directly passed
-objects.
-
-```js
-postcssCustomMedia({
- exportTo: 'path/to/file.css' // @custom-media --small-viewport (max-width: 30em);
-});
-```
-
-Multiple destinations can be passed into this option, and they will be parsed
-in the order they are received. JavaScript files, JSON files, and objects will
-need to namespace custom media using the `customMedia` or
-`custom-media` key.
-
-```js
-const cachedObject = { customMedia: {} };
-
-postcssCustomMedia({
- exportTo: [
- 'path/to/file.css', // @custom-media --small-viewport (max-width: 30em);
- 'and/then/this.js', // module.exports = { customMedia: { '--small-viewport': '(max-width: 30em)' } }
- 'and/then/this.mjs', // export const customMedia = { '--small-viewport': '(max-width: 30em)' } }
- 'and/then/that.json', // { "custom-media": { "--small-viewport": "(max-width: 30em)" } }
- cachedObject,
- customMedia => {
- customMedia // { '--small-viewport': '(max-width: 30em)' }
- }
- ]
-});
-```
-
-See example exports written to [CSS](test/export-media.css),
-[JS](test/export-media.js), [MJS](test/export-media.mjs), and
-[JSON](test/export-media.json).
-
[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
[css-url]: https://cssdb.org/#custom-media-queries
[discord]: https://discord.gg/bUadyRwkJS
diff --git a/plugins/postcss-custom-media/docs/README.md b/plugins/postcss-custom-media/docs/README.md
index 9961d39fd..6d3d8dc76 100644
--- a/plugins/postcss-custom-media/docs/README.md
+++ b/plugins/postcss-custom-media/docs/README.md
@@ -49,89 +49,5 @@ is preserved. By default, it is not preserved.
```
-
-### importFrom
-
-The `importFrom` option specifies sources where custom media can be imported
-from, which might be CSS, JS, and JSON files, functions, and directly passed
-objects.
-
-```js
-({
- importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em);
-});
-```
-
-```pcss
-@media (max-width: 30em) {
- /* styles for small viewport */
-}
-
-@media (--small-viewport) {
- /* styles for small viewport */
-}
-```
-
-Multiple sources can be passed into this option, and they will be parsed in the
-order they are received. JavaScript files, JSON files, functions, and objects
-will need to namespace custom media using the `customMedia` or
-`custom-media` key.
-
-```js
-({
- importFrom: [
- 'path/to/file.css',
- 'and/then/this.js',
- 'and/then/that.json',
- {
- customMedia: { '--small-viewport': '(max-width: 30em)' }
- },
- () => {
- const customMedia = { '--small-viewport': '(max-width: 30em)' };
-
- return { customMedia };
- }
- ]
-});
-```
-
-### exportTo
-
-The `exportTo` option specifies destinations where custom media can be exported
-to, which might be CSS, JS, and JSON files, functions, and directly passed
-objects.
-
-```js
-({
- exportTo: 'path/to/file.css' // @custom-media --small-viewport (max-width: 30em);
-});
-```
-
-Multiple destinations can be passed into this option, and they will be parsed
-in the order they are received. JavaScript files, JSON files, and objects will
-need to namespace custom media using the `customMedia` or
-`custom-media` key.
-
-```js
-const cachedObject = { customMedia: {} };
-
-({
- exportTo: [
- 'path/to/file.css', // @custom-media --small-viewport (max-width: 30em);
- 'and/then/this.js', // module.exports = { customMedia: { '--small-viewport': '(max-width: 30em)' } }
- 'and/then/this.mjs', // export const customMedia = { '--small-viewport': '(max-width: 30em)' } }
- 'and/then/that.json', // { "custom-media": { "--small-viewport": "(max-width: 30em)" } }
- cachedObject,
- customMedia => {
- customMedia // { '--small-viewport': '(max-width: 30em)' }
- }
- ]
-});
-```
-
-See example exports written to [CSS](test/export-media.css),
-[JS](test/export-media.js), [MJS](test/export-media.mjs), and
-[JSON](test/export-media.json).
-
[Custom Media Specification]:
diff --git a/plugins/postcss-custom-media/package.json b/plugins/postcss-custom-media/package.json
index 1f760d0dd..39dc809f5 100644
--- a/plugins/postcss-custom-media/package.json
+++ b/plugins/postcss-custom-media/package.json
@@ -45,7 +45,8 @@
"dist"
],
"dependencies": {
- "postcss-value-parser": "^4.2.0"
+ "@csstools/css-tokenizer": "^1.0.0",
+ "@csstools/media-query-list-parser": "^1.0.0"
},
"peerDependencies": {
"postcss": "^8.4"
@@ -58,9 +59,9 @@
"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.cjs && npm run test:exports",
+ "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.cjs"
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs"
},
"homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media#readme",
"repository": {
diff --git a/plugins/postcss-custom-media/src/custom-media-from-root.js b/plugins/postcss-custom-media/src/custom-media-from-root.js
deleted file mode 100644
index d2ddbef6a..000000000
--- a/plugins/postcss-custom-media/src/custom-media-from-root.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import mediaASTFromString from './media-ast-from-string';
-import valueParser from 'postcss-value-parser';
-
-// return custom selectors from the css root, conditionally removing them
-export default (root, opts) => {
- // initialize custom selectors
- const customMedias = {};
-
- // for each custom selector atrule that is a child of the css root
- root.nodes.slice().forEach(node => {
- if (node.type !== 'atrule') {
- return;
- }
-
- if (node.name.toLowerCase() !== 'custom-media') {
- return;
- }
-
- let paramsAst = null;
- try {
- paramsAst = valueParser(node.params);
- } catch (_) {
- return;
- }
-
- if (!paramsAst || !paramsAst.nodes || !paramsAst.nodes.length) {
- return;
- }
-
- let nameNodeIndex = -1;
- for (let i = 0; i < paramsAst.nodes.length; i++) {
- const node = paramsAst.nodes[i];
- if (node.type === 'space' || node.type === 'comment') {
- continue;
- }
-
- if (node.type === 'word' && node.value.startsWith('--')) {
- nameNodeIndex = i;
- break;
- }
-
- return; /* invalid starting node */
- }
-
- if (nameNodeIndex < 0) {
- return;
- }
-
- const name = paramsAst.nodes[nameNodeIndex].value.trim();
- const selectors = valueParser.stringify(paramsAst.nodes.slice(nameNodeIndex + 1)).trim();
-
- // write the parsed selectors to the custom selector
- customMedias[name] = mediaASTFromString(selectors);
-
- // conditionally remove the custom selector atrule
- if (!Object(opts).preserve) {
- node.remove();
- }
- });
-
- return customMedias;
-};
diff --git a/plugins/postcss-custom-media/src/custom-media-from-root.ts b/plugins/postcss-custom-media/src/custom-media-from-root.ts
new file mode 100644
index 000000000..964e69bc0
--- /dev/null
+++ b/plugins/postcss-custom-media/src/custom-media-from-root.ts
@@ -0,0 +1,62 @@
+import { MediaQuery } from '@csstools/media-query-list-parser';
+import type { ChildNode, Container, Document, Root as PostCSSRoot } from 'postcss';
+import { isProcessableCustomMediaRule } from './is-processable-custom-media-rule';
+import { removeCyclicReferences } from './toposort';
+import { parseCustomMedia } from './transform-at-media/custom-media';
+
+// return custom media from the css root, conditionally removing them
+export default function getCustomMedia(root: PostCSSRoot, result, opts: { preserve?: boolean }): Map, falsy: Array }> {
+ // initialize custom media
+ const customMedia: Map, falsy: Array }> = new Map();
+ const customMediaGraph: Array<[string, string]> = [];
+
+ root.walkAtRules((atRule) => {
+ if (!isProcessableCustomMediaRule(atRule)) {
+ return;
+ }
+
+ const parsed = parseCustomMedia(atRule.params);
+ if (!parsed) {
+ return;
+ }
+
+ if (parsed.truthy.length === 0) {
+ return;
+ }
+
+ customMedia.set(parsed.name, {
+ truthy: parsed.truthy,
+ falsy: parsed.falsy,
+ });
+
+ customMediaGraph.push(...parsed.dependsOn);
+
+ if (!opts.preserve) {
+ const parent = atRule.parent;
+ atRule.remove();
+ removeEmptyAncestorBlocks(parent);
+ }
+ });
+
+ const cyclicReferences = removeCyclicReferences(customMedia, customMediaGraph);
+ for (const cyclicReference of cyclicReferences.values()) {
+ root.warn(result, `@custom-media rules have cyclic dependencies for "${cyclicReference}"`);
+ }
+
+ return customMedia;
+}
+
+function removeEmptyAncestorBlocks(block: Container) {
+ let currentNode: Document | Container = block;
+
+ while (currentNode) {
+ if (currentNode.nodes && currentNode.nodes.length > 0) {
+ return;
+ }
+
+ const parent = currentNode.parent;
+ currentNode.remove();
+ currentNode = parent;
+ }
+}
+
diff --git a/plugins/postcss-custom-media/src/custom-media-name.js b/plugins/postcss-custom-media/src/custom-media-name.js
deleted file mode 100644
index f9a7aaa5f..000000000
--- a/plugins/postcss-custom-media/src/custom-media-name.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import valueParser from 'postcss-value-parser';
-
-export function getCustomMediaNameReference(source) {
- if (!source) {
- return;
- }
-
- let paramsAst = null;
- try {
- paramsAst = valueParser(source);
- } catch (_) {
- return;
- }
-
- if (!paramsAst || !paramsAst.nodes || !paramsAst.nodes.length) {
- return;
- }
-
- if (paramsAst.nodes.length !== 1) {
- return;
- }
-
- while (paramsAst.nodes[0].type === 'function' && paramsAst.nodes[0].value === '') {
- paramsAst = paramsAst.nodes[0];
- }
-
- let nameNodeIndex = -1;
- for (let i = 0; i < paramsAst.nodes.length; i++) {
- const node = paramsAst.nodes[i];
- if (node.type === 'space' || node.type === 'comment') {
- continue;
- }
-
- if (node.type === 'word' && node.value.startsWith('--')) {
- nameNodeIndex = i;
- break;
- }
-
- return; /* invalid starting node */
- }
-
- if (nameNodeIndex < 0) {
- return;
- }
-
- return paramsAst.nodes[nameNodeIndex].value.trim();
-}
diff --git a/plugins/postcss-custom-media/src/get-custom-media-from-imports.js b/plugins/postcss-custom-media/src/get-custom-media-from-imports.js
deleted file mode 100644
index 944795ac5..000000000
--- a/plugins/postcss-custom-media/src/get-custom-media-from-imports.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-import { parse } from 'postcss';
-import getMediaAstFromMediaString from './media-ast-from-string';
-import getCustomMedia from './custom-media-from-root';
-
-/* Get Custom Media from CSS File
-/* ========================================================================== */
-
-async function getCustomMediaFromCSSFile(from) {
- const css = await readFile(from);
- const root = parse(css, { from });
-
- return getCustomMedia(root, { preserve: true });
-}
-
-/* Get Custom Media from Object
-/* ========================================================================== */
-
-function getCustomMediaFromObject(object) {
- const customMedia = Object.assign(
- {},
- Object(object).customMedia,
- Object(object)['custom-media'],
- );
-
- for (const key in customMedia) {
- customMedia[key] = getMediaAstFromMediaString(customMedia[key]);
- }
-
- return customMedia;
-}
-
-/* Get Custom Media from JSON file
-/* ========================================================================== */
-
-async function getCustomMediaFromJSONFile(from) {
- const object = await readJSON(from);
-
- return getCustomMediaFromObject(object);
-}
-
-/* Get Custom Media from JS file
-/* ========================================================================== */
-
-async function getCustomMediaFromJSFile(from) {
- const object = await import(from);
-
- return getCustomMediaFromObject(object);
-}
-
-/* Get Custom Media from Sources
-/* ========================================================================== */
-
-export default function getCustomMediaFromSources(sources) {
- return sources.map(source => {
- if (source instanceof Promise) {
- return source;
- } else if (source instanceof Function) {
- return source();
- }
-
- // read the source as an object
- const opts = source === Object(source) ? source : { from: String(source) };
-
- // skip objects with custom media
- if (Object(opts).customMedia || Object(opts)['custom-media']) {
- return opts;
- }
-
- // source pathname
- const from = path.resolve(String(opts.from || ''));
-
- // type of file being read from
- const type = (opts.type || path.extname(from).slice(1)).toLowerCase();
-
- return { type, from };
- }).reduce(async (customMedia, source) => {
- const { type, from } = await source;
-
- if (type === 'css' || type === 'pcss') {
- return Object.assign(await customMedia, await getCustomMediaFromCSSFile(from));
- }
-
- if (type === 'js') {
- return Object.assign(await customMedia, await getCustomMediaFromJSFile(from));
- }
-
- if (type === 'json') {
- return Object.assign(await customMedia, await getCustomMediaFromJSONFile(from));
- }
-
- return Object.assign(await customMedia, getCustomMediaFromObject(await source));
- }, {});
-}
-
-/* Helper utilities
-/* ========================================================================== */
-
-const readFile = from => new Promise((resolve, reject) => {
- fs.readFile(from, 'utf8', (error, result) => {
- if (error) {
- reject(error);
- } else {
- resolve(result);
- }
- });
-});
-
-const readJSON = async from => JSON.parse(await readFile(from));
diff --git a/plugins/postcss-custom-media/src/index.js b/plugins/postcss-custom-media/src/index.js
deleted file mode 100644
index 17d08a2ed..000000000
--- a/plugins/postcss-custom-media/src/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import getCustomMediaFromRoot from './custom-media-from-root';
-import getCustomMediaFromImports from './get-custom-media-from-imports';
-import transformAtrules from './transform-atrules';
-import writeCustomMediaToExports from './write-custom-media-to-exports';
-
-const creator = opts => {
- // whether to preserve custom media and at-rules using them
- const preserve = 'preserve' in Object(opts) ? Boolean(opts.preserve) : false;
-
- // sources to import custom media from
- const importFrom = [].concat(Object(opts).importFrom || []);
-
-
- // destinations to export custom media to
- const exportTo = [].concat(Object(opts).exportTo || []);
-
- // promise any custom media are imported
- const customMediaImportsPromise = getCustomMediaFromImports(importFrom);
-
- const customMediaHelperKey = Symbol('customMediaHelper');
-
- return {
- postcssPlugin: 'postcss-custom-media',
- Once: async (root, helpers) => {
-
- // combine rules from root and from imports
- helpers[customMediaHelperKey] = Object.assign(
- await customMediaImportsPromise,
- getCustomMediaFromRoot(root, { preserve }),
- );
-
- await writeCustomMediaToExports(helpers[customMediaHelperKey], exportTo);
- },
- AtRule: (atrule, helpers) => {
- if (atrule.name !== 'media') {
- return;
- }
-
- transformAtrules(atrule, helpers[customMediaHelperKey], { preserve });
- },
- };
-};
-
-creator.postcss = true;
-
-export default creator;
diff --git a/plugins/postcss-custom-media/src/index.ts b/plugins/postcss-custom-media/src/index.ts
new file mode 100644
index 000000000..9ab51864f
--- /dev/null
+++ b/plugins/postcss-custom-media/src/index.ts
@@ -0,0 +1,111 @@
+import { MediaQuery } from '@csstools/media-query-list-parser';
+import type { PluginCreator } from 'postcss';
+import getCustomMedia from './custom-media-from-root';
+import { transformAtMediaListTokens } from './transform-at-media/transform-at-media';
+
+export interface PluginOptions {
+ /** Determines whether Custom Media and media queries using custom media should be preserved in their original form. */
+ preserve?: boolean
+}
+
+const creator: PluginCreator = (opts?: PluginOptions) => {
+ // whether to preserve custom media and rules using them
+ const preserve = Boolean(Object(opts).preserve);
+
+ if ('importFrom' in Object(opts)) {
+ throw new Error('[postcss-custom-media] "importFrom" is no longer supported');
+ }
+
+ if ('exportTo' in Object(opts)) {
+ throw new Error('[postcss-custom-media] "exportTo" is no longer supported');
+ }
+
+ return {
+ postcssPlugin: 'postcss-custom-media',
+ prepare() {
+ let customMedia: Map, falsy: Array }> = new Map();
+
+ return {
+ Once: (root, { result }) => {
+ customMedia = getCustomMedia(root, result, { preserve: preserve });
+ },
+ AtRule: (atRule, { result }) => {
+ if (!atRule.params) {
+ return;
+ }
+
+ if (!atRule.params.includes('--')) {
+ return;
+ }
+
+ let transformedParams: Array<{ replaceWith: string, encapsulateWith?: string }> = [];
+
+ try {
+ transformedParams = transformAtMediaListTokens(atRule.params, customMedia);
+ } catch (err) {
+ atRule.warn(result, `Failed to parse @custom-media params with error message: "${err.message}"`);
+ return;
+ }
+
+ if (!transformedParams || transformedParams.length === 0) {
+ return;
+ }
+
+ if (transformedParams.length === 1) {
+ if (atRule.params.trim() === transformedParams[0].replaceWith.trim()) {
+ return;
+ }
+
+ atRule.cloneBefore({ params: transformedParams[0].replaceWith.trim() });
+
+ if (!preserve) {
+ atRule.remove();
+ return;
+ }
+
+ return;
+ }
+
+ const needsEncapsulation = !!(transformedParams.find((x) => {
+ return !!x.encapsulateWith;
+ }));
+
+ if (!needsEncapsulation) {
+ atRule.cloneBefore({ params: transformedParams.map((x) => x.replaceWith).join(',').trim() });
+
+ if (!preserve) {
+ atRule.remove();
+ }
+
+ return;
+ }
+
+ transformedParams.forEach((transformed) => {
+ if (!transformed.encapsulateWith) {
+ atRule.cloneBefore({ params: transformed.replaceWith.trim() });
+ return;
+ }
+
+ const clone = atRule.clone({ params: transformed.replaceWith });
+ const encapsulate = atRule.clone({ params: transformed.encapsulateWith.trim(), nodes: [] });
+
+ clone.parent = null;
+ encapsulate.parent = null;
+
+ encapsulate.append(clone);
+ atRule.before(encapsulate);
+ });
+
+ if (!preserve) {
+ atRule.remove();
+ return;
+ }
+ },
+ };
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
diff --git a/plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts b/plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts
new file mode 100644
index 000000000..ba3947ccd
--- /dev/null
+++ b/plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts
@@ -0,0 +1,28 @@
+import type { AtRule, ChildNode, Container, Document } from 'postcss';
+
+const allowedParentAtRules = new Set(['scope', 'container']);
+
+export function isProcessableCustomMediaRule(atRule: AtRule): boolean {
+ if (atRule.name.toLowerCase() !== 'custom-media') {
+ return false;
+ }
+
+ if (!atRule.params || !atRule.params.includes('--')) {
+ return false;
+ }
+
+ if (atRule.nodes && atRule.nodes.length > 0) {
+ return false;
+ }
+
+ let parent: Container | Document = 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-media/src/media-ast-from-string.js b/plugins/postcss-custom-media/src/media-ast-from-string.js
deleted file mode 100644
index 3821a319c..000000000
--- a/plugins/postcss-custom-media/src/media-ast-from-string.js
+++ /dev/null
@@ -1,134 +0,0 @@
-function parse(string, splitByAnd) {
- const array = [];
- let buffer = '';
- let split = false;
- let func = 0;
- let i = -1;
-
- while (++i < string.length) {
- const char = string[i];
-
- if (char === '(') {
- func += 1;
- } else if (char === ')') {
- if (func > 0) {
- func -= 1;
- }
- } else if (func === 0) {
- if (splitByAnd && andRegExp.test(buffer + char)) {
- split = true;
- } else if (!splitByAnd && char === ',') {
- split = true;
- }
- }
-
- if (split) {
- array.push(splitByAnd ? new MediaExpression(buffer + char) : new MediaQuery(buffer));
-
- buffer = '';
- split = false;
- } else {
- buffer += char;
- }
- }
-
- if (buffer !== '') {
- array.push(splitByAnd ? new MediaExpression(buffer) : new MediaQuery(buffer));
- }
-
- return array;
-}
-
-class MediaQueryList {
- constructor(string) {
- this.nodes = parse(string);
- }
-
- invert() {
- this.nodes.forEach(node => {
- node.invert();
- });
-
- return this;
- }
-
- clone() {
- return new MediaQueryList(String(this));
- }
-
- toString() {
- return this.nodes.join(',');
- }
-}
-
-class MediaQuery {
- constructor(string) {
- const [, before, media, after ] = string.match(spaceWrapRegExp);
- const [, modifier = '', afterModifier = ' ', type = '', beforeAnd = '', and = '', beforeExpression = '', expression1 = '', expression2 = ''] = media.match(mediaRegExp) || [];
- const raws = { before, after, afterModifier, originalModifier: modifier || '', beforeAnd, and, beforeExpression };
- const nodes = parse(expression1 || expression2, true);
-
- Object.assign(this, {
- modifier,
- type,
- raws,
- nodes,
- });
- }
-
- clone(overrides) {
- const instance = new MediaQuery(String(this));
-
- Object.assign(instance, overrides);
-
- return instance;
- }
-
- invert() {
- this.modifier = this.modifier ? '' : this.raws.originalModifier;
-
- return this;
- }
-
- toString() {
- const { raws } = this;
-
- return `${raws.before}${this.modifier}${this.modifier ? `${raws.afterModifier}` : ''}${this.type}${raws.beforeAnd}${raws.and}${raws.beforeExpression}${this.nodes.join('')}${this.raws.after}`;
- }
-}
-
-class MediaExpression {
- constructor(string) {
- const [, value, after = '', and = '', afterAnd = '' ] = string.match(andRegExp) || [null, string];
- const raws = { after, and, afterAnd };
-
- Object.assign(this, { value, raws });
- }
-
- clone(overrides) {
- const instance = new MediaExpression(String(this));
-
- Object.assign(instance, overrides);
-
- return instance;
- }
-
- toString() {
- const { raws } = this;
-
- return `${this.value}${raws.after}${raws.and}${raws.afterAnd}`;
- }
-}
-
-const modifierRE = '(not|only)';
-const typeRE = '(all|print|screen|speech)';
-const noExpressionRE = '([\\W\\w]*)';
-const expressionRE = '([\\W\\w]+)';
-const noSpaceRE = '(\\s*)';
-const spaceRE = '(\\s+)';
-const andRE = '(?:(\\s+)(and))';
-const andRegExp = new RegExp(`^${expressionRE}(?:${andRE}${spaceRE})$`, 'i');
-const spaceWrapRegExp = new RegExp(`^${noSpaceRE}${noExpressionRE}${noSpaceRE}$`);
-const mediaRegExp = new RegExp(`^(?:${modifierRE}${spaceRE})?(?:${typeRE}(?:${andRE}${spaceRE}${expressionRE})?|${expressionRE})$`, 'i');
-
-export default string => new MediaQueryList(string);
diff --git a/plugins/postcss-custom-media/src/toposort.ts b/plugins/postcss-custom-media/src/toposort.ts
new file mode 100644
index 000000000..f3166f2f3
--- /dev/null
+++ b/plugins/postcss-custom-media/src/toposort.ts
@@ -0,0 +1,134 @@
+// 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.
+
+// We (ab)use `toposort` to find cyclic references.
+// At the moment this is not optimized and uses a brute force approach.
+//
+// while there are cyclic ref errors
+// - remove node from graph
+// - re-do toposort
+export function removeCyclicReferences(nodes: Map, edges: Array>): Set {
+ const cyclicReferences: Set = new Set();
+
+ // eslint-disable-next-line no-constant-condition
+ let _edges = edges;
+ while (nodes.size > 0) {
+ try {
+ toposort(Array.from(nodes.keys()), _edges);
+ break;
+ } catch (e) {
+ /* see the hack below */
+ if (e['_graphNode']) {
+
+ nodes.delete(e['_graphNode']);
+ cyclicReferences.add(e['_graphNode']);
+ _edges = _edges.filter((x) => {
+ return x.indexOf(e['_graphNode']) === -1;
+ });
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ return cyclicReferences;
+}
+
+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)) {
+ const err = new Error('Cyclic dependency' + JSON.stringify(node));
+ err['_graphNode'] = node; /* a hack to communicate which node is causing the cyclic error */
+
+ throw err;
+ }
+
+ 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-custom-media/src/transform-at-media/always-true-or-false.ts b/plugins/postcss-custom-media/src/transform-at-media/always-true-or-false.ts
new file mode 100644
index 000000000..ec03674d1
--- /dev/null
+++ b/plugins/postcss-custom-media/src/transform-at-media/always-true-or-false.ts
@@ -0,0 +1,14 @@
+import { NumberType, TokenType } from '@csstools/css-tokenizer';
+import type { CSSToken } from '@csstools/css-tokenizer';
+
+export const alwaysTrue: Array = [
+ [TokenType.Ident, 'max-color', 0, 0, { value: 'max-color' }],
+ [TokenType.Colon, ':', 0, 0, undefined],
+ [TokenType.Number, '2147477350', 0, 0, { value: 2147477350, type: NumberType.Integer }],
+];
+
+export const neverTrue: Array = [
+ [TokenType.Ident, 'color', 0, 0, { value: 'color' }],
+ [TokenType.Colon, ':', 0, 0, undefined],
+ [TokenType.Number, '2147477350', 0, 0, { value: 2147477350, type: NumberType.Integer }],
+];
diff --git a/plugins/postcss-custom-media/src/transform-at-media/at-media-params-tokens.ts b/plugins/postcss-custom-media/src/transform-at-media/at-media-params-tokens.ts
new file mode 100644
index 000000000..3b96058ad
--- /dev/null
+++ b/plugins/postcss-custom-media/src/transform-at-media/at-media-params-tokens.ts
@@ -0,0 +1,20 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import type { CSSToken } from '@csstools/css-tokenizer';
+
+export function atMediaParamsTokens(params: string): Array {
+ const t = tokenizer({
+ css: params,
+ }, {
+ commentsAreTokens: true,
+ onParseError: () => {
+ throw new Error(`Unable to parse media query "${params}"`);
+ },
+ });
+
+ const tokens: Array = [];
+ while (!t.endOfFile()) {
+ tokens.push(t.nextToken());
+ }
+
+ return tokens;
+}
diff --git a/plugins/postcss-custom-media/src/transform-at-media/custom-media.ts b/plugins/postcss-custom-media/src/transform-at-media/custom-media.ts
new file mode 100644
index 000000000..bd9de7806
--- /dev/null
+++ b/plugins/postcss-custom-media/src/transform-at-media/custom-media.ts
@@ -0,0 +1,69 @@
+import { cloneTokens, stringify, TokenIdent, TokenType } from '@csstools/css-tokenizer';
+import { parseFromTokens, MediaQuery } from '@csstools/media-query-list-parser';
+import { atMediaParamsTokens } from '../transform-at-media/at-media-params-tokens';
+import { replaceTrueAndFalseTokens } from './true-and-false';
+
+export function parseCustomMedia(params: string): { name: string, truthy: Array, falsy: Array, dependsOn: Array<[string, string]> } | false {
+ const tokens = atMediaParamsTokens(params);
+
+ const customMediaReferences: Set = new Set();
+
+ let name = '';
+ let remainder = tokens;
+ for (let i = 0; i < tokens.length; i++) {
+ if (tokens[i][0] === TokenType.Comment) {
+ continue;
+ }
+ if (tokens[i][0] === TokenType.Whitespace) {
+ continue;
+ }
+
+ if (tokens[i][0] === TokenType.Ident) {
+ const identToken = tokens[i] as TokenIdent;
+ if (identToken[4].value.startsWith('--')) {
+ name = identToken[4].value;
+ remainder = tokens.slice(i + 1);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ for (let i = 0; i < remainder.length; i++) {
+ if (remainder[i][0] === TokenType.Ident) {
+ const identToken = remainder[i] as TokenIdent;
+ if (identToken[4].value.startsWith('--')) {
+ customMediaReferences.add(identToken[4].value);
+ }
+ }
+ }
+
+ remainder = replaceTrueAndFalseTokens(remainder);
+
+ const mediaQueryListTruthy = parseFromTokens(cloneTokens(remainder), {
+ preserveInvalidMediaQueries: true,
+ onParseError: () => {
+ throw new Error(`Unable to parse media query "${stringify(...remainder)}"`);
+ },
+ });
+ const mediaQueryListFalsy = parseFromTokens(cloneTokens(remainder), {
+ preserveInvalidMediaQueries: true,
+ onParseError: () => {
+ throw new Error(`Unable to parse media query "${stringify(...remainder) }"`);
+ },
+ });
+
+ for (let i = 0; i < mediaQueryListFalsy.length; i++) {
+ mediaQueryListFalsy[i] = mediaQueryListFalsy[i].negateQuery();
+ }
+
+ return {
+ name: name,
+ truthy: mediaQueryListTruthy,
+ falsy: mediaQueryListFalsy,
+ dependsOn: Array.from(customMediaReferences).map((x) => {
+ return [x, name];
+ }),
+ };
+}
diff --git a/plugins/postcss-custom-media/src/transform-at-media/transform-at-media.ts b/plugins/postcss-custom-media/src/transform-at-media/transform-at-media.ts
new file mode 100644
index 000000000..65bea992a
--- /dev/null
+++ b/plugins/postcss-custom-media/src/transform-at-media/transform-at-media.ts
@@ -0,0 +1,165 @@
+import { alwaysTrue, neverTrue } from './always-true-or-false';
+import { isGeneralEnclosed, isMediaAnd, isMediaConditionList, isMediaFeature, isMediaFeatureBoolean, isMediaNot, isMediaOr, isMediaQueryInvalid, isMediaQueryWithType, MediaQuery, newMediaFeaturePlain, parse } from '@csstools/media-query-list-parser';
+
+export function transformAtMediaListTokens(params: string, replacements: Map, falsy: Array }>): Array<{ replaceWith: string, encapsulateWith?: string }> {
+ const mediaQueries = parse(params, {
+ preserveInvalidMediaQueries: true, onParseError: () => {
+ throw new Error(`Unable to parse media query "${params}"`);
+ },
+ });
+
+ const stringQueries = mediaQueries.map((x) => x.toString());
+
+ for (let i = 0; i < mediaQueries.length; i++) {
+ const mediaQuery = mediaQueries[i];
+ const original = stringQueries[i];
+
+ {
+ const transformedQuery = transformSimpleMediaQuery(mediaQuery, replacements);
+ if (transformedQuery && transformedQuery.replaceWith !== original) {
+ return stringQueries.map((query, index) => {
+ if (index === i) {
+ return transformedQuery;
+ }
+
+ return {
+ replaceWith: query,
+ };
+ });
+ }
+ }
+
+ const transformedQuery = transformComplexMediaQuery(mediaQuery, replacements);
+ if (!transformedQuery || transformedQuery.length === 0) {
+ continue;
+ }
+
+ if (transformedQuery[0].replaceWith === original) {
+ continue;
+ }
+
+ return stringQueries.flatMap((query, index) => {
+ if (index === i) {
+ return transformedQuery;
+ }
+
+ return [{
+ replaceWith: query,
+ }];
+ });
+ }
+
+ return [];
+}
+
+export function transformSimpleMediaQuery(mediaQuery: MediaQuery, replacements: Map, falsy: Array }>): { replaceWith: string, encapsulateWith?: string } | null {
+ if (!mediaQueryIsSimple(mediaQuery)) {
+ return null;
+ }
+
+ let candidate: { replaceWith: string, encapsulateWith?: string } | null = null;
+
+ mediaQuery.walk((entry) => {
+ const node = entry.node;
+ if (!isMediaFeatureBoolean(node)) {
+ return;
+ }
+
+ const name = node.getName();
+ if (!name.startsWith('--')) {
+ return false;
+ }
+
+ const replacement = replacements.get(name);
+ if (replacement) {
+ candidate = {
+ replaceWith: replacement.truthy.map((x) => x.toString().trim()).join(','),
+ };
+
+ return false;
+ }
+ });
+
+ return candidate;
+}
+
+export function transformComplexMediaQuery(mediaQuery: MediaQuery, replacements: Map, falsy: Array }>): Array<{ replaceWith: string, encapsulateWith?: string }> {
+ let candidate: Array<{ replaceWith: string, encapsulateWith?: string }> = [];
+
+ mediaQuery.walk((entry) => {
+ const node = entry.node;
+ if (!isMediaFeatureBoolean(node)) {
+ return;
+ }
+
+ const parent = entry.parent;
+ if (!isMediaFeature(parent)) {
+ return;
+ }
+
+ const name = node.getName();
+ if (!name.startsWith('--')) {
+ return false;
+ }
+
+ const replacement = replacements.get(name);
+ if (replacement) {
+ const replaceWithTrue = newMediaFeaturePlain(
+ alwaysTrue[0][4].value as string,
+ alwaysTrue[2],
+ );
+
+ parent.feature = replaceWithTrue.feature;
+ const replaceWithTrueString = mediaQuery.toString();
+
+ const replaceWithFalse = newMediaFeaturePlain(
+ neverTrue[0][4].value as string,
+ neverTrue[2],
+ );
+
+ parent.feature = replaceWithFalse.feature;
+ const replaceWithFalseString = mediaQuery.toString();
+
+ candidate = [
+ {
+ replaceWith: replaceWithTrueString,
+ encapsulateWith: replacement.truthy.map((x) => x.toString().trim()).join(','),
+ },
+ {
+ replaceWith: replaceWithFalseString,
+ encapsulateWith: replacement.falsy.map((x) => x.toString().trim()).join(','),
+ },
+ ];
+
+ return false;
+ }
+ });
+
+ return candidate;
+}
+
+function mediaQueryIsSimple(mediaQuery: MediaQuery): boolean {
+ if (isMediaQueryInvalid(mediaQuery)) {
+ return false;
+ }
+
+ if (isMediaQueryWithType(mediaQuery)) {
+ return false;
+ }
+
+ let isSimple = true;
+ mediaQuery.walk((entry) => {
+ if (
+ isMediaAnd(entry.node) ||
+ isMediaOr(entry.node) ||
+ isMediaNot(entry.node) ||
+ isMediaConditionList(entry.node) ||
+ isGeneralEnclosed(entry.node)
+ ) {
+ isSimple = false;
+ return false;
+ }
+ });
+
+ return isSimple;
+}
diff --git a/plugins/postcss-custom-media/src/transform-at-media/true-and-false.ts b/plugins/postcss-custom-media/src/transform-at-media/true-and-false.ts
new file mode 100644
index 000000000..ebb56a460
--- /dev/null
+++ b/plugins/postcss-custom-media/src/transform-at-media/true-and-false.ts
@@ -0,0 +1,68 @@
+import { TokenType, TokenIdent } from '@csstools/css-tokenizer';
+import type { CSSToken } from '@csstools/css-tokenizer';
+import { alwaysTrue, neverTrue } from './always-true-or-false';
+
+export function replaceTrueAndFalseTokens(tokens: Array): Array {
+ let booleanToken;
+ let remainder;
+
+ for (let i = 0; i < tokens.length; i++) {
+ if (tokens[i][0] === TokenType.Comment) {
+ continue;
+ }
+ if (tokens[i][0] === TokenType.Whitespace) {
+ continue;
+ }
+
+ if (tokens[i][0] === TokenType.Ident) {
+ const identToken = tokens[i] as TokenIdent;
+ if (identToken[4].value.toLowerCase() === 'true') {
+ booleanToken = 'true';
+ remainder = tokens.slice(i + 1);
+ break;
+ }
+
+ if (identToken[4].value.toLowerCase() === 'false') {
+ booleanToken = 'false';
+ remainder = tokens.slice(i + 1);
+ break;
+ }
+ }
+
+ return tokens;
+ }
+
+ if (!booleanToken) {
+ return tokens;
+ }
+
+ {
+ // Nothing is allowed after true|false except for comments and whitespace
+ for (let i = 0; i < remainder.length; i++) {
+ if (remainder[i][0] === TokenType.Comment) {
+ continue;
+ }
+ if (remainder[i][0] === TokenType.Whitespace) {
+ continue;
+ }
+
+ return tokens;
+ }
+ }
+
+ if (booleanToken === 'true') {
+ return [
+ [TokenType.Whitespace, ' ', 0, 0, undefined],
+ [TokenType.OpenParen, '(', 0, 0, undefined],
+ ...alwaysTrue,
+ [TokenType.CloseParen, ')', 0, 0, undefined],
+ ];
+ }
+
+ return [
+ [TokenType.Whitespace, ' ', 0, 0, undefined],
+ [TokenType.OpenParen, '(', 0, 0, undefined],
+ ...neverTrue,
+ [TokenType.CloseParen, ')', 0, 0, undefined],
+ ];
+}
diff --git a/plugins/postcss-custom-media/src/transform-atrules.js b/plugins/postcss-custom-media/src/transform-atrules.js
deleted file mode 100644
index 130e0bbb9..000000000
--- a/plugins/postcss-custom-media/src/transform-atrules.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import transformMediaList from './transform-media-list';
-import mediaASTFromString from './media-ast-from-string';
-
-// transform custom pseudo selectors with custom selectors
-export default (atrule, customMedia, { preserve }) => {
- if (atrule.params.indexOf('--') > -1) {
- const mediaAST = mediaASTFromString(atrule.params);
- const params = String(transformMediaList(mediaAST, customMedia));
- if (params === null) {
- return;
- }
-
- if (params === atrule.params) {
- return;
- }
-
- atrule.cloneBefore({
- params: params,
- });
-
- if (!preserve) {
- atrule.remove();
- }
- }
-};
diff --git a/plugins/postcss-custom-media/src/transform-media-list.js b/plugins/postcss-custom-media/src/transform-media-list.js
deleted file mode 100644
index 2eed88fdb..000000000
--- a/plugins/postcss-custom-media/src/transform-media-list.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { getCustomMediaNameReference } from './custom-media-name';
-
-// return transformed medias, replacing custom pseudo medias with custom medias
-export default function transformMediaList(mediaList, customMedias) {
- let index = mediaList.nodes.length - 1;
-
- while (index >= 0) {
- const transformedMedias = transformMedia(mediaList.nodes[index], customMedias);
-
- if (transformedMedias.length) {
- mediaList.nodes.splice(index, 1, ...transformedMedias);
- }
-
- --index;
- }
-
- return mediaList;
-}
-
-// return custom pseudo medias replaced with custom medias
-function transformMedia(media, customMedias) {
- const transpiledMedias = [];
-
- for (const index in media.nodes) {
- const { value, nodes } = media.nodes[index];
- const key = getCustomMediaNameReference(value);
- if (key && (key in customMedias)) {
- for (const replacementMedia of customMedias[key].nodes) {
- // use the first available modifier unless they cancel each other out
- const modifier = media.modifier !== replacementMedia.modifier
- ? media.modifier || replacementMedia.modifier
- : '';
- const mediaClone = media.clone({
- modifier,
- // conditionally use the raws from the first available modifier
- raws: !modifier || media.modifier
- ? { ...media.raws }
- : { ...replacementMedia.raws },
- type: media.type || replacementMedia.type,
- });
-
- // conditionally include more replacement raws when the type is present
- if (mediaClone.type === replacementMedia.type) {
- Object.assign(mediaClone.raws, {
- and: replacementMedia.raws.and,
- beforeAnd: replacementMedia.raws.beforeAnd,
- beforeExpression: replacementMedia.raws.beforeExpression,
- });
- }
-
- mediaClone.nodes.splice(index, 1, ...replacementMedia.clone().nodes.map(node => {
- // use raws and spacing from the current usage
- if (media.nodes[index].raws.and) {
- node.raws = { ...media.nodes[index].raws };
- }
-
- node.spaces = { ...media.nodes[index].spaces };
-
- return node;
- }));
-
- // remove the currently transformed key to prevent recursion
- const nextCustomMedia = getCustomMediasWithoutKey(customMedias, key);
- const retranspiledMedias = transformMedia(mediaClone, nextCustomMedia);
-
- if (retranspiledMedias.length) {
- transpiledMedias.push(...retranspiledMedias);
- } else {
- transpiledMedias.push(mediaClone);
- }
- }
-
- return transpiledMedias;
- } else if (nodes && nodes.length) {
- transformMediaList(media.nodes[index], customMedias);
- }
- }
-
- return transpiledMedias;
-}
-
-const getCustomMediasWithoutKey = (customMedias, key) => {
- const nextCustomMedias = Object.assign({}, customMedias);
-
- delete nextCustomMedias[key];
-
- return nextCustomMedias;
-};
diff --git a/plugins/postcss-custom-media/src/write-custom-media-to-exports.js b/plugins/postcss-custom-media/src/write-custom-media-to-exports.js
deleted file mode 100644
index 52754f3f5..000000000
--- a/plugins/postcss-custom-media/src/write-custom-media-to-exports.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-
-/* Write Custom Media from CSS File
-/* ========================================================================== */
-
-async function writeCustomMediaToCssFile(to, customMedia) {
- const cssContent = Object.keys(customMedia).reduce((cssLines, name) => {
- cssLines.push(`@custom-media ${name} ${customMedia[name]};`);
-
- return cssLines;
- }, []).join('\n');
- const css = `${cssContent}\n`;
-
- await writeFile(to, css);
-}
-
-/* Write Custom Media from JSON file
-/* ========================================================================== */
-
-async function writeCustomMediaToJsonFile(to, customMedia) {
- const jsonContent = JSON.stringify({
- 'custom-media': customMedia,
- }, null, '\t');
- const json = `${jsonContent}\n`;
-
- await writeFile(to, json);
-}
-
-/* Write Custom Media from Common JS file
-/* ========================================================================== */
-
-async function writeCustomMediaToCjsFile(to, customMedia) {
- const jsContents = Object.keys(customMedia).reduce((jsLines, name) => {
- jsLines.push(`\t\t'${escapeForJS(name)}': '${escapeForJS(customMedia[name])}'`);
-
- return jsLines;
- }, []).join(',\n');
- const js = `module.exports = {\n\tcustomMedia: {\n${jsContents}\n\t}\n};\n`;
-
- await writeFile(to, js);
-}
-
-/* Write Custom Media from Module JS file
-/* ========================================================================== */
-
-async function writeCustomMediaToMjsFile(to, customMedia) {
- const mjsContents = Object.keys(customMedia).reduce((mjsLines, name) => {
- mjsLines.push(`\t'${escapeForJS(name)}': '${escapeForJS(customMedia[name])}'`);
-
- return mjsLines;
- }, []).join(',\n');
- const mjs = `export const customMedia = {\n${mjsContents}\n};\n`;
-
- await writeFile(to, mjs);
-}
-
-/* Write Custom Media to Exports
-/* ========================================================================== */
-
-export default function writeCustomMediaToExports(customMedia, destinations) {
- return Promise.all(destinations.map(async destination => {
- if (destination instanceof Function) {
- await destination(defaultCustomMediaToJSON(customMedia));
- } else {
- // read the destination as an object
- const opts = destination === Object(destination) ? destination : { to: String(destination) };
-
- // transformer for custom media into a JSON-compatible object
- const toJSON = opts.toJSON || defaultCustomMediaToJSON;
-
- if ('customMedia' in opts) {
- // write directly to an object as customMedia
- opts.customMedia = toJSON(customMedia);
- } else if ('custom-media' in opts) {
- // write directly to an object as custom-media
- opts['custom-media'] = toJSON(customMedia);
- } else {
- // destination pathname
- const to = String(opts.to || '');
-
- // type of file being written to
- const type = (opts.type || path.extname(to).slice(1)).toLowerCase();
-
- // transformed custom media
- const customMediaJSON = toJSON(customMedia);
-
- if (type === 'css') {
- await writeCustomMediaToCssFile(to, customMediaJSON);
- }
-
- if (type === 'js') {
- await writeCustomMediaToCjsFile(to, customMediaJSON);
- }
-
- if (type === 'json') {
- await writeCustomMediaToJsonFile(to, customMediaJSON);
- }
-
- if (type === 'mjs') {
- await writeCustomMediaToMjsFile(to, customMediaJSON);
- }
- }
- }
- }));
-}
-
-/* Helper utilities
-/* ========================================================================== */
-
-const defaultCustomMediaToJSON = customMedia => {
- return Object.keys(customMedia).reduce((customMediaJSON, key) => {
- customMediaJSON[key] = String(customMedia[key]);
-
- return customMediaJSON;
- }, {});
-};
-
-const writeFile = (to, text) => new Promise((resolve, reject) => {
- fs.writeFile(to, text, error => {
- if (error) {
- reject(error);
- } else {
- resolve();
- }
- });
-});
-
-const escapeForJS = string => string.replace(/\\([\s\S])|(')/g, '\\$1$2').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
diff --git a/plugins/postcss-custom-media/test/and.css b/plugins/postcss-custom-media/test/and.css
new file mode 100644
index 000000000..4d40c3bcd
--- /dev/null
+++ b/plugins/postcss-custom-media/test/and.css
@@ -0,0 +1,17 @@
+/* Media queries with "and" */
+@custom-media --with-and (min-width: 300px) and (min-height: 300px);
+
+@media not screen and (--with-and) {
+ .a {
+ order: 1;
+ }
+}
+
+@custom-media --not-screen not screen;
+@custom-media --not-print not print;
+
+@media (--not-screen) and (--not-print) {
+ .a {
+ order: 2;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/and.expect.css b/plugins/postcss-custom-media/test/and.expect.css
new file mode 100644
index 000000000..9cbf54fd4
--- /dev/null
+++ b/plugins/postcss-custom-media/test/and.expect.css
@@ -0,0 +1,61 @@
+/* Media queries with "and" */
+
+@media (min-width: 300px) and (min-height: 300px) {
+
+@media not screen and (max-color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not all and (min-width: 300px) and (min-height: 300px) {
+
+@media not screen and (color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not screen {
+
+@media not print {
+
+@media (max-color:2147477350) and (max-color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media print {
+
+@media (max-color:2147477350) and (color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+}
+
+@media screen {
+
+@media not print {
+
+@media (color:2147477350) and (max-color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media print {
+
+@media (color:2147477350) and (color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+}
diff --git a/plugins/postcss-custom-media/test/basic-after-v9.css b/plugins/postcss-custom-media/test/basic-after-v9.css
new file mode 100644
index 000000000..01fb549e9
--- /dev/null
+++ b/plugins/postcss-custom-media/test/basic-after-v9.css
@@ -0,0 +1,37 @@
+@custom-media --simple-feature-test (min-width: 300px);
+
+/* Most basic case */
+@media (--simple-feature-test) {
+ .a {
+ order: 1;
+ }
+}
+
+@media ((--simple-feature-test)) {
+ .a {
+ order: 1.1;
+ }
+}
+
+/* Also a type condition */
+@media screen and (--simple-feature-test) {
+ .a {
+ order: 2;
+ }
+}
+
+/* Negation */
+@media not (--simple-feature-test) {
+ .a {
+ order: 3;
+ }
+}
+
+/* LightningCSS example */
+@custom-media --modern (color), (hover);
+
+@media (--modern) and (width > 1024px) {
+ .a {
+ color: green;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/basic-after-v9.expect.css b/plugins/postcss-custom-media/test/basic-after-v9.expect.css
new file mode 100644
index 000000000..6d01623fe
--- /dev/null
+++ b/plugins/postcss-custom-media/test/basic-after-v9.expect.css
@@ -0,0 +1,64 @@
+/* Most basic case */
+@media (min-width: 300px) {
+ .a {
+ order: 1;
+ }
+}
+
+@media (min-width: 300px) {
+ .a {
+ order: 1.1;
+ }
+}
+
+/* Also a type condition */
+@media (min-width: 300px) {
+@media screen and (max-color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+@media not all and (min-width: 300px) {
+@media screen and (color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+/* Negation */
+@media (min-width: 300px) {
+@media not (max-color:2147477350) {
+ .a {
+ order: 3;
+ }
+}
+}
+@media not all and (min-width: 300px) {
+@media not (color:2147477350) {
+ .a {
+ order: 3;
+ }
+}
+}
+
+/* LightningCSS example */
+
+@media (color),(hover) {
+
+@media (max-color:2147477350) and (width > 1024px) {
+ .a {
+ color: green;
+ }
+}
+}
+
+@media not all and (color),not all and (hover) {
+
+@media (color:2147477350) and (width > 1024px) {
+ .a {
+ color: green;
+ }
+}
+}
diff --git a/plugins/postcss-custom-media/test/basic-after-v9.preserve.expect.css b/plugins/postcss-custom-media/test/basic-after-v9.preserve.expect.css
new file mode 100644
index 000000000..5fa31e9fd
--- /dev/null
+++ b/plugins/postcss-custom-media/test/basic-after-v9.preserve.expect.css
@@ -0,0 +1,94 @@
+@custom-media --simple-feature-test (min-width: 300px);
+
+/* Most basic case */
+@media (min-width: 300px) {
+ .a {
+ order: 1;
+ }
+}
+@media (--simple-feature-test) {
+ .a {
+ order: 1;
+ }
+}
+
+@media (min-width: 300px) {
+ .a {
+ order: 1.1;
+ }
+}
+
+@media ((--simple-feature-test)) {
+ .a {
+ order: 1.1;
+ }
+}
+
+/* Also a type condition */
+@media (min-width: 300px) {
+@media screen and (max-color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+@media not all and (min-width: 300px) {
+@media screen and (color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+@media screen and (--simple-feature-test) {
+ .a {
+ order: 2;
+ }
+}
+
+/* Negation */
+@media (min-width: 300px) {
+@media not (max-color:2147477350) {
+ .a {
+ order: 3;
+ }
+}
+}
+@media not all and (min-width: 300px) {
+@media not (color:2147477350) {
+ .a {
+ order: 3;
+ }
+}
+}
+@media not (--simple-feature-test) {
+ .a {
+ order: 3;
+ }
+}
+
+/* LightningCSS example */
+@custom-media --modern (color), (hover);
+
+@media (color),(hover) {
+
+@media (max-color:2147477350) and (width > 1024px) {
+ .a {
+ color: green;
+ }
+}
+}
+
+@media not all and (color),not all and (hover) {
+
+@media (color:2147477350) and (width > 1024px) {
+ .a {
+ color: green;
+ }
+}
+}
+
+@media (--modern) and (width > 1024px) {
+ .a {
+ color: green;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/basic.css b/plugins/postcss-custom-media/test/basic.css
index a992cfe2f..2b80609fe 100644
--- a/plugins/postcss-custom-media/test/basic.css
+++ b/plugins/postcss-custom-media/test/basic.css
@@ -10,31 +10,31 @@
@media (--mq-b) {
body {
- order: 1;
+ order: 2;
}
}
@media (--mq-a), (--mq-a) {
body {
- order: 1;
+ order: 3;
}
}
@media not all and (--mq-a) {
body {
- order: 2;
+ order: 4;
}
}
@media (--not-mq-a) {
body {
- order: 1;
+ order: 5;
}
}
@media not all and (--not-mq-a) {
body {
- order: 2;
+ order: 6;
}
}
@@ -43,19 +43,19 @@
@media (--circular-mq-a) {
body {
- order: 3;
+ order: 7;
}
}
@media (--circular-mq-b) {
body {
- order: 4;
+ order: 8;
}
}
@media (--unresolved-mq) {
body {
- order: 5;
+ order: 9;
}
}
@@ -64,7 +64,7 @@
@media (--min) and (--max) {
body {
- order: 6;
+ order: 10;
}
}
@@ -72,13 +72,13 @@
@media (--concat) {
body {
- order: 7;
+ order: 11;
}
}
@media (--concat) and (min-aspect-ratio: 16/9) {
body {
- order: 8;
+ order: 12;
}
}
diff --git a/plugins/postcss-custom-media/test/basic.expect.css b/plugins/postcss-custom-media/test/basic.expect.css
index ed797941c..97d82e390 100644
--- a/plugins/postcss-custom-media/test/basic.expect.css
+++ b/plugins/postcss-custom-media/test/basic.expect.css
@@ -6,69 +6,183 @@
@media screen and (max-width: 30em) {
body {
- order: 1;
+ order: 2;
}
}
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
+@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) {
body {
- order: 1;
+ order: 3;
+ }
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media not all and (max-color:2147477350) {
+ body {
+ order: 4;
}
}
+}
@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media not all and (color:2147477350) {
body {
- order: 2;
+ order: 4;
}
}
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media not all and (max-color:2147477350) {
+ body {
+ order: 5;
+ }
+}
+}
@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media not all and (color:2147477350) {
body {
- order: 1;
+ order: 5;
}
}
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media not all and (max-color:2147477350) {
-@media all and (max-width: 30em),all and (max-height: 30em) {
+@media not all and (max-color:2147477350) {
body {
- order: 2;
+ order: 6;
}
}
+}
+}
+
+@media not all and (max-width: 30em),not all and (max-height: 30em) {
-@media (--circular-mq-a) {
+@media not all and (color:2147477350) {
+
+@media not all and (max-color:2147477350) {
body {
- order: 3;
+ order: 6;
+ }
+}
+}
+}
+
+@media (max-width: 30em),(max-height: 30em) {
+
+@media all and (max-color:2147477350) {
+
+@media not all and (color:2147477350) {
+ body {
+ order: 6;
+ }
+}
+}
+}
+
+@media not all and (max-width: 30em),not all and (max-height: 30em) {
+
+@media all and (color:2147477350) {
+
+@media not all and (color:2147477350) {
+ body {
+ order: 6;
}
}
+}
+}
@media (--circular-mq-b) {
body {
- order: 4;
+ order: 7;
+ }
+}
+
+@media (--circular-mq-b) {
+ body {
+ order: 8;
}
}
@media (--unresolved-mq) {
body {
- order: 5;
+ order: 9;
}
}
+@media (min-width: 320px) {
+
+@media (max-width: 640px) {
+
+@media (max-color:2147477350) and (max-color:2147477350) {
+ body {
+ order: 10;
+ }
+}
+}
+
+@media not all and (max-width: 640px) {
+
+@media (max-color:2147477350) and (color:2147477350) {
+ body {
+ order: 10;
+ }
+}
+}
+}
+
+@media not all and (min-width: 320px) {
+
+@media (max-width: 640px) {
+
+@media (color:2147477350) and (max-color:2147477350) {
+ body {
+ order: 10;
+ }
+}
+}
+
+@media not all and (max-width: 640px) {
+
+@media (color:2147477350) and (color:2147477350) {
+ body {
+ order: 10;
+ }
+}
+}
+}
+
@media (min-width: 320px) and (max-width: 640px) {
body {
- order: 6;
+ order: 11;
}
}
@media (min-width: 320px) and (max-width: 640px) {
+
+@media (max-color:2147477350) and (min-aspect-ratio: 16/9) {
body {
- order: 7;
+ order: 12;
}
}
+}
-@media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) {
+@media not all and (min-width: 320px) and (max-width: 640px) {
+
+@media (color:2147477350) and (min-aspect-ratio: 16/9) {
body {
- order: 8;
+ order: 12;
}
}
+}
@media (max-width: 30em),(max-height: 30em) {
body {
@@ -82,27 +196,25 @@
}
}
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
+@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) {
body {
order: 1002;
}
}
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
+@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) {
body {
order: 1003;
}
}
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
+@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) {
body {
order: 1004;
}
}
-@media (max-width: 30em),(max-height: 30em),
-(max-width: 30em),
-(max-height: 30em) {
+@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) {
body {
order: 1005;
}
diff --git a/plugins/postcss-custom-media/test/basic.import.expect.css b/plugins/postcss-custom-media/test/basic.import.expect.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/plugins/postcss-custom-media/test/basic.preserve.expect.css b/plugins/postcss-custom-media/test/basic.preserve.expect.css
deleted file mode 100644
index 4c8aefc63..000000000
--- a/plugins/postcss-custom-media/test/basic.preserve.expect.css
+++ /dev/null
@@ -1,224 +0,0 @@
-@custom-media --mq-a (max-width: 30em), (max-height: 30em);
-@custom-media --mq-b screen and (max-width: 30em);
-@custom-media --not-mq-a not all and (--mq-a);
-
-@media (max-width: 30em),(max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media (--mq-a) {
- body {
- order: 1;
- }
-}
-
-@media screen and (max-width: 30em) {
- body {
- order: 1;
- }
-}
-
-@media (--mq-b) {
- body {
- order: 1;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media (--mq-a), (--mq-a) {
- body {
- order: 1;
- }
-}
-
-@media not all and (max-width: 30em),not all and (max-height: 30em) {
- body {
- order: 2;
- }
-}
-
-@media not all and (--mq-a) {
- body {
- order: 2;
- }
-}
-
-@media not all and (max-width: 30em),not all and (max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media (--not-mq-a) {
- body {
- order: 1;
- }
-}
-
-@media all and (max-width: 30em),all and (max-height: 30em) {
- body {
- order: 2;
- }
-}
-
-@media not all and (--not-mq-a) {
- body {
- order: 2;
- }
-}
-
-@custom-media --circular-mq-a (--circular-mq-b);
-@custom-media --circular-mq-b (--circular-mq-a);
-
-@media (--circular-mq-a) {
- body {
- order: 3;
- }
-}
-
-@media (--circular-mq-b) {
- body {
- order: 4;
- }
-}
-
-@media (--unresolved-mq) {
- body {
- order: 5;
- }
-}
-
-@custom-media --min (min-width: 320px);
-@custom-media --max (max-width: 640px);
-
-@media (min-width: 320px) and (max-width: 640px) {
- body {
- order: 6;
- }
-}
-
-@media (--min) and (--max) {
- body {
- order: 6;
- }
-}
-
-@custom-media --concat (min-width: 320px) and (max-width: 640px);
-
-@media (min-width: 320px) and (max-width: 640px) {
- body {
- order: 7;
- }
-}
-
-@media (--concat) {
- body {
- order: 7;
- }
-}
-
-@media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) {
- body {
- order: 8;
- }
-}
-
-@media (--concat) and (min-aspect-ratio: 16/9) {
- body {
- order: 8;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em) {
- body {
- order: 1000;
- }
-}
-
-@media ( --mq-a ) {
- body {
- order: 1000;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em) {
- body {
- order: 1001;
- }
-}
-
-@media ( --mq-a ) {
- body {
- order: 1001;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
- body {
- order: 1002;
- }
-}
-
-@media ( --mq-a ), ( --mq-a ) {
- body {
- order: 1002;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
- body {
- order: 1003;
- }
-}
-
-@media ( --mq-a ), ( --mq-a ) {
- body {
- order: 1003;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
- body {
- order: 1004;
- }
-}
-
-@media ( --mq-a ), ( --mq-a ) {
- body {
- order: 1004;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em),
-(max-width: 30em),
-(max-height: 30em) {
- body {
- order: 1005;
- }
-}
-
-@media (
- --mq-a
-),
-(
- --mq-a
-) {
- body {
- order: 1005;
- }
-}
-
-@media (trailer--) {
- body {
- order: 1006;
- }
-}
-
-@custom-media trailer-- (min-width: 320px);
diff --git a/plugins/postcss-custom-media/test/comma-1.css b/plugins/postcss-custom-media/test/comma-1.css
new file mode 100644
index 000000000..962a50fed
--- /dev/null
+++ b/plugins/postcss-custom-media/test/comma-1.css
@@ -0,0 +1,39 @@
+@custom-media --list-1 screen, print;
+
+@media (--list-1) {
+ .a {
+ order: 1;
+ }
+}
+
+@custom-media --list-2 screen, (func(aa,bb,cc));
+
+@media (--list-2) {
+ .a {
+ order: 2;
+ }
+}
+
+@custom-media --list-3 screen, [aa,bb,cc];
+
+@media (--list-3) {
+ .a {
+ order: 3;
+ }
+}
+
+@custom-media --list-4 screen, ([aa,bb,cc]);
+
+@media (--list-4) {
+ .a {
+ order: 4;
+ }
+}
+
+@custom-media --list-5 screen, ({aa,bb,cc});
+
+@media (--list-5) {
+ .a {
+ order: 5;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/comma-1.expect.css b/plugins/postcss-custom-media/test/comma-1.expect.css
new file mode 100644
index 000000000..6a87b6d35
--- /dev/null
+++ b/plugins/postcss-custom-media/test/comma-1.expect.css
@@ -0,0 +1,29 @@
+@media screen,print {
+ .a {
+ order: 1;
+ }
+}
+
+@media screen,(func(aa,bb,cc)) {
+ .a {
+ order: 2;
+ }
+}
+
+@media screen,[aa,bb,cc] {
+ .a {
+ order: 3;
+ }
+}
+
+@media screen,([aa,bb,cc]) {
+ .a {
+ order: 4;
+ }
+}
+
+@media screen,({aa,bb,cc}) {
+ .a {
+ order: 5;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/comma-2.css b/plugins/postcss-custom-media/test/comma-2.css
new file mode 100644
index 000000000..6e7c20c54
--- /dev/null
+++ b/plugins/postcss-custom-media/test/comma-2.css
@@ -0,0 +1,50 @@
+@custom-media --list-1 screen;
+@custom-media --list-2 print;
+
+@media (--list-1),(--list-2) {
+ .a {
+ order: 1;
+ }
+}
+
+@media (--list-1), (func(aa,(--list-2),cc)) {
+ .a {
+ order: 2;
+ }
+}
+
+@media (--list-1), [aa,(--list-2),cc] {
+ .a {
+ order: 3;
+ }
+}
+
+@media (--list-1), ([aa,(--list-2),cc]) {
+ .a {
+ order: 4;
+ }
+}
+
+@media (--list-1), ([aa,[(--list-2)],cc]) {
+ .a {
+ order: 5;
+ }
+}
+
+@media (--list-1), ({aa,(--list-2),cc}) {
+ .a {
+ order: 6;
+ }
+}
+
+@media (--list-1), ({aa,{(--list-2)},cc}) {
+ .a {
+ order: 7;
+ }
+}
+
+@media ((--list-1) or (min-width: 300px)),((--list-2) and (min-height: 300px)) {
+ .a {
+ order: 8;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/comma-2.expect.css b/plugins/postcss-custom-media/test/comma-2.expect.css
new file mode 100644
index 000000000..820d9b2cb
--- /dev/null
+++ b/plugins/postcss-custom-media/test/comma-2.expect.css
@@ -0,0 +1,77 @@
+@media screen,print {
+ .a {
+ order: 1;
+ }
+}
+
+@media screen, (func(aa,(--list-2),cc)) {
+ .a {
+ order: 2;
+ }
+}
+
+@media screen, [aa,(--list-2),cc] {
+ .a {
+ order: 3;
+ }
+}
+
+@media screen, ([aa,(--list-2),cc]) {
+ .a {
+ order: 4;
+ }
+}
+
+@media screen, ([aa,[(--list-2)],cc]) {
+ .a {
+ order: 5;
+ }
+}
+
+@media screen, ({aa,(--list-2),cc}) {
+ .a {
+ order: 6;
+ }
+}
+
+@media screen, ({aa,{(--list-2)},cc}) {
+ .a {
+ order: 7;
+ }
+}
+
+@media screen {
+
+@media ((max-color:2147477350) or (min-width: 300px)) {
+ .a {
+ order: 8;
+ }
+}
+}
+
+@media not screen {
+
+@media ((color:2147477350) or (min-width: 300px)) {
+ .a {
+ order: 8;
+ }
+}
+}
+
+@media print {
+
+@media ((max-color:2147477350) and (min-height: 300px)) {
+ .a {
+ order: 8;
+ }
+}
+}
+
+@media not print {
+
+@media ((color:2147477350) and (min-height: 300px)) {
+ .a {
+ order: 8;
+ }
+}
+}
diff --git a/plugins/postcss-custom-media/test/complex.css b/plugins/postcss-custom-media/test/complex.css
index 4b427cbd0..ca21e8482 100644
--- a/plugins/postcss-custom-media/test/complex.css
+++ b/plugins/postcss-custom-media/test/complex.css
@@ -21,12 +21,11 @@
}
/* #region https://github.com/csstools/postcss-custom-media/issues/51 */
-/* TODO: This is broken at the moment */
@custom-media --screen only screen;
@custom-media --md-and-larger1 --screen and (width >= 570px);
@custom-media --md-and-larger2 (--screen) and (width >= 570px);
@custom-media --md-and-larger3 only screen and (width >= 570px);
-@custom-media --md-larger4 (width >=570px);
+@custom-media --md-larger4 (width >= 570px);
@custom-media --md-smaller4 (width < 1000px);
@media (--md-and-larger1) {
diff --git a/plugins/postcss-custom-media/test/complex.expect.css b/plugins/postcss-custom-media/test/complex.expect.css
index 486d7f515..792e80338 100644
--- a/plugins/postcss-custom-media/test/complex.expect.css
+++ b/plugins/postcss-custom-media/test/complex.expect.css
@@ -10,24 +10,45 @@
}
}
-@media (min-width: 3) and (width > 1024px),(min-width: 4) and (width > 1024px) {
+@media (min-width: 3),(min-width: 4) {
+
+@media (max-color:2147477350) and (width > 1024px) {
.a { order: 3; }
}
+}
+
+@media not all and (min-width: 3),not all and (min-width: 4) {
+
+@media (color:2147477350) and (width > 1024px) {
+ .a { order: 3; }
+}
+}
/* #region https://github.com/csstools/postcss-custom-media/issues/51 */
-/* TODO: This is broken at the moment */
-@media only screen(width >= 570px) {
+@media --screen and (width >= 570px) {
body {
background-color: red;
}
}
-@media only screen(width >= 570px) {
+@media only screen {
+
+@media (max-color:2147477350) and (width >= 570px) {
+ body {
+ background-color: yellow;
+ }
+}
+}
+
+@media not screen {
+
+@media (color:2147477350) and (width >= 570px) {
body {
background-color: yellow;
}
}
+}
@media only screen and (width >= 570px) {
body {
@@ -35,15 +56,87 @@
}
}
-@media only screen(width >=570px) {
+@media only screen {
+
+@media (width >= 570px) {
+
+@media (max-color:2147477350) and (max-color:2147477350) {
+ body {
+ background-color: green;
+ }
+}
+}
+
+@media not all and (width >= 570px) {
+
+@media (max-color:2147477350) and (color:2147477350) {
+ body {
+ background-color: green;
+ }
+}
+}
+}
+
+@media not screen {
+
+@media (width >= 570px) {
+
+@media (color:2147477350) and (max-color:2147477350) {
+ body {
+ background-color: green;
+ }
+}
+}
+
+@media not all and (width >= 570px) {
+
+@media (color:2147477350) and (color:2147477350) {
+ body {
+ background-color: green;
+ }
+}
+}
+}
+
+@media (width >= 570px) {
+
+@media (width < 1000px) {
+
+@media (max-color:2147477350) and (max-color:2147477350) {
body {
background-color: green;
}
}
+}
-@media (width >=570px) and (width < 1000px) {
+@media not all and (width < 1000px) {
+
+@media (max-color:2147477350) and (color:2147477350) {
body {
background-color: green;
}
}
+}
+}
+
+@media not all and (width >= 570px) {
+
+@media (width < 1000px) {
+
+@media (color:2147477350) and (max-color:2147477350) {
+ body {
+ background-color: green;
+ }
+}
+}
+
+@media not all and (width < 1000px) {
+
+@media (color:2147477350) and (color:2147477350) {
+ body {
+ background-color: green;
+ }
+}
+}
+}
/* #endregion https://github.com/csstools/postcss-custom-media/issues/51 */
diff --git a/plugins/postcss-custom-media/test/cyclic.css b/plugins/postcss-custom-media/test/cyclic.css
new file mode 100644
index 000000000..c22b3a4e9
--- /dev/null
+++ b/plugins/postcss-custom-media/test/cyclic.css
@@ -0,0 +1,29 @@
+/* Simple cyclic */
+@custom-media --mq-a (--mq-b);
+@custom-media --mq-b (--mq-a);
+
+@media (--mq-a) {
+ .a {
+ color: red;
+ }
+}
+
+/* Indirect cyclic */
+@custom-media --mq-x (--mq-z);
+@custom-media --mq-y (--mq-x);
+@custom-media --mq-z (--mq-y);
+
+@media (--mq-z) {
+ .b {
+ color: red;
+ }
+}
+
+/* Self referencing */
+@custom-media --mq-self (--mq-self);
+
+@media (--mq-self) {
+ .c {
+ color: red;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/cyclic.expect.css b/plugins/postcss-custom-media/test/cyclic.expect.css
new file mode 100644
index 000000000..6674fd4a8
--- /dev/null
+++ b/plugins/postcss-custom-media/test/cyclic.expect.css
@@ -0,0 +1,23 @@
+/* Simple cyclic */
+
+@media (--mq-b) {
+ .a {
+ color: red;
+ }
+}
+
+/* Indirect cyclic */
+
+@media (--mq-z) {
+ .b {
+ color: red;
+ }
+}
+
+/* Self referencing */
+
+@media (--mq-self) {
+ .c {
+ color: red;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/eof-1.css b/plugins/postcss-custom-media/test/eof-1.css
new file mode 100644
index 000000000..424bd22f0
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-1.css
@@ -0,0 +1,4 @@
+@custom-media --one screen;
+
+@media (--one
+
diff --git a/plugins/postcss-custom-media/test/eof-1.expect.css b/plugins/postcss-custom-media/test/eof-1.expect.css
new file mode 100644
index 000000000..f3ea0731e
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-1.expect.css
@@ -0,0 +1,2 @@
+@media (--one
+
diff --git a/plugins/postcss-custom-media/test/eof-2.css b/plugins/postcss-custom-media/test/eof-2.css
new file mode 100644
index 000000000..50cb010f7
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-2.css
@@ -0,0 +1,3 @@
+@custom-media --one screen;
+
+@media (calc(--one
diff --git a/plugins/postcss-custom-media/test/eof-2.expect.css b/plugins/postcss-custom-media/test/eof-2.expect.css
new file mode 100644
index 000000000..460b84e8e
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-2.expect.css
@@ -0,0 +1 @@
+@media (calc(--one
diff --git a/plugins/postcss-custom-media/test/eof-3.css b/plugins/postcss-custom-media/test/eof-3.css
new file mode 100644
index 000000000..849c7fecc
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-3.css
@@ -0,0 +1,3 @@
+@custom-media --one screen;
+
+@media ([--one
diff --git a/plugins/postcss-custom-media/test/eof-3.expect.css b/plugins/postcss-custom-media/test/eof-3.expect.css
new file mode 100644
index 000000000..f907f2f71
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-3.expect.css
@@ -0,0 +1 @@
+@media ([--one
diff --git a/plugins/postcss-custom-media/test/eof-4.css b/plugins/postcss-custom-media/test/eof-4.css
new file mode 100644
index 000000000..4ddee3de8
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-4.css
@@ -0,0 +1,3 @@
+@custom-media --one screen;
+
+@media ({--one
diff --git a/plugins/postcss-custom-media/test/eof-4.expect.css b/plugins/postcss-custom-media/test/eof-4.expect.css
new file mode 100644
index 000000000..20ae091c4
--- /dev/null
+++ b/plugins/postcss-custom-media/test/eof-4.expect.css
@@ -0,0 +1 @@
+@media ({--one
diff --git a/plugins/postcss-custom-media/test/export-media.css b/plugins/postcss-custom-media/test/export-media.css
deleted file mode 100644
index f51e88c82..000000000
--- a/plugins/postcss-custom-media/test/export-media.css
+++ /dev/null
@@ -1,8 +0,0 @@
-@custom-media --mq-a (max-width: 30em), (max-height: 30em);
-@custom-media --mq-b screen and (max-width: 30em);
-@custom-media --not-mq-a not all and (--mq-a);
-@custom-media --circular-mq-a (--circular-mq-b);
-@custom-media --circular-mq-b (--circular-mq-a);
-@custom-media --min (min-width: 320px);
-@custom-media --max (max-width: 640px);
-@custom-media --concat (min-width: 320px) and (max-width: 640px);
diff --git a/plugins/postcss-custom-media/test/export-media.js b/plugins/postcss-custom-media/test/export-media.js
deleted file mode 100644
index acccd8408..000000000
--- a/plugins/postcss-custom-media/test/export-media.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports = {
- customMedia: {
- '--mq-a': '(max-width: 30em), (max-height: 30em)',
- '--mq-b': 'screen and (max-width: 30em)',
- '--not-mq-a': 'not all and (--mq-a)',
- '--circular-mq-a': '(--circular-mq-b)',
- '--circular-mq-b': '(--circular-mq-a)',
- '--min': '(min-width: 320px)',
- '--max': '(max-width: 640px)',
- '--concat': '(min-width: 320px) and (max-width: 640px)'
- }
-};
diff --git a/plugins/postcss-custom-media/test/export-media.json b/plugins/postcss-custom-media/test/export-media.json
deleted file mode 100644
index 729bde28e..000000000
--- a/plugins/postcss-custom-media/test/export-media.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "custom-media": {
- "--mq-a": "(max-width: 30em), (max-height: 30em)",
- "--mq-b": "screen and (max-width: 30em)",
- "--not-mq-a": "not all and (--mq-a)",
- "--circular-mq-a": "(--circular-mq-b)",
- "--circular-mq-b": "(--circular-mq-a)",
- "--min": "(min-width: 320px)",
- "--max": "(max-width: 640px)",
- "--concat": "(min-width: 320px) and (max-width: 640px)"
- }
-}
diff --git a/plugins/postcss-custom-media/test/export-media.mjs b/plugins/postcss-custom-media/test/export-media.mjs
deleted file mode 100644
index ea36519d8..000000000
--- a/plugins/postcss-custom-media/test/export-media.mjs
+++ /dev/null
@@ -1,10 +0,0 @@
-export const customMedia = {
- '--mq-a': '(max-width: 30em), (max-height: 30em)',
- '--mq-b': 'screen and (max-width: 30em)',
- '--not-mq-a': 'not all and (--mq-a)',
- '--circular-mq-a': '(--circular-mq-b)',
- '--circular-mq-b': '(--circular-mq-a)',
- '--min': '(min-width: 320px)',
- '--max': '(max-width: 640px)',
- '--concat': '(min-width: 320px) and (max-width: 640px)'
-};
diff --git a/plugins/postcss-custom-media/test/import-css.css b/plugins/postcss-custom-media/test/import-css.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/plugins/postcss-custom-media/test/import-media.css b/plugins/postcss-custom-media/test/import-media.css
deleted file mode 100644
index e788f32ae..000000000
--- a/plugins/postcss-custom-media/test/import-media.css
+++ /dev/null
@@ -1,2 +0,0 @@
-@custom-media --mq-a (max-width: 30em), (max-height: 30em);
-@custom-media --not-mq-a not all and (--mq-a);
diff --git a/plugins/postcss-custom-media/test/import-media.js b/plugins/postcss-custom-media/test/import-media.js
deleted file mode 100644
index 3f2e0401a..000000000
--- a/plugins/postcss-custom-media/test/import-media.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- customMedia: {
- '--mq-a': '(max-width: 30em), (max-height: 30em)',
- '--not-mq-a': 'not all and (--mq-a)'
- }
-}
diff --git a/plugins/postcss-custom-media/test/import-media.json b/plugins/postcss-custom-media/test/import-media.json
deleted file mode 100644
index 807d8dfdd..000000000
--- a/plugins/postcss-custom-media/test/import-media.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "customMedia": {
- "--mq-a": "(max-width: 30em), (max-height: 30em)",
- "--not-mq-a": "not all and (--mq-a)"
- }
-}
diff --git a/plugins/postcss-custom-media/test/import.css b/plugins/postcss-custom-media/test/import.css
deleted file mode 100644
index f37220e74..000000000
--- a/plugins/postcss-custom-media/test/import.css
+++ /dev/null
@@ -1,29 +0,0 @@
-@media (--mq-a) {
- body {
- order: 1;
- }
-}
-
-@media (--mq-a), (--mq-a) {
- body {
- order: 1;
- }
-}
-
-@media not all and (--mq-a) {
- body {
- order: 2;
- }
-}
-
-@media (--not-mq-a) {
- body {
- order: 1;
- }
-}
-
-@media not all and (--not-mq-a) {
- body {
- order: 2;
- }
-}
diff --git a/plugins/postcss-custom-media/test/import.empty.expect.css b/plugins/postcss-custom-media/test/import.empty.expect.css
deleted file mode 100644
index f37220e74..000000000
--- a/plugins/postcss-custom-media/test/import.empty.expect.css
+++ /dev/null
@@ -1,29 +0,0 @@
-@media (--mq-a) {
- body {
- order: 1;
- }
-}
-
-@media (--mq-a), (--mq-a) {
- body {
- order: 1;
- }
-}
-
-@media not all and (--mq-a) {
- body {
- order: 2;
- }
-}
-
-@media (--not-mq-a) {
- body {
- order: 1;
- }
-}
-
-@media not all and (--not-mq-a) {
- body {
- order: 2;
- }
-}
diff --git a/plugins/postcss-custom-media/test/import.expect.css b/plugins/postcss-custom-media/test/import.expect.css
deleted file mode 100644
index 0bc2bbf3b..000000000
--- a/plugins/postcss-custom-media/test/import.expect.css
+++ /dev/null
@@ -1,29 +0,0 @@
-@media (max-width: 30em),(max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media not all and (max-width: 30em),not all and (max-height: 30em) {
- body {
- order: 2;
- }
-}
-
-@media not all and (max-width: 30em),not all and (max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media all and (max-width: 30em),all and (max-height: 30em) {
- body {
- order: 2;
- }
-}
diff --git a/plugins/postcss-custom-media/test/import.plugin.expect.css b/plugins/postcss-custom-media/test/import.plugin.expect.css
deleted file mode 100644
index 0bc2bbf3b..000000000
--- a/plugins/postcss-custom-media/test/import.plugin.expect.css
+++ /dev/null
@@ -1,29 +0,0 @@
-@media (max-width: 30em),(max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media not all and (max-width: 30em),not all and (max-height: 30em) {
- body {
- order: 2;
- }
-}
-
-@media not all and (max-width: 30em),not all and (max-height: 30em) {
- body {
- order: 1;
- }
-}
-
-@media all and (max-width: 30em),all and (max-height: 30em) {
- body {
- order: 2;
- }
-}
diff --git a/plugins/postcss-custom-media/test/list.css b/plugins/postcss-custom-media/test/list.css
new file mode 100644
index 000000000..cd0b8e893
--- /dev/null
+++ b/plugins/postcss-custom-media/test/list.css
@@ -0,0 +1,30 @@
+/* Custom media with a list */
+@custom-media --mq-with-list-a (min-width: 101px), (min-height: 102px);
+
+@media screen and (--mq-with-list-a) {
+ .a {
+ order: 1;
+ }
+}
+
+@media ((other: feature) and (--mq-with-list-a)) {
+ .a {
+ order: 2;
+ }
+}
+
+/* Custom media used in a list */
+@custom-media --mq-without-a-list-a (min-width: 201px);
+@custom-media --mq-without-a-list-b (min-width: 202px);
+
+@media (--mq-without-a-list-a), (--mq-without-a-list-b) {
+ .b {
+ order: 3;
+ }
+}
+
+@media screen and (--mq-without-a-list-a), not (--mq-without-a-list-b) {
+ .b {
+ order: 4;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/list.expect.css b/plugins/postcss-custom-media/test/list.expect.css
new file mode 100644
index 000000000..3bf1138d3
--- /dev/null
+++ b/plugins/postcss-custom-media/test/list.expect.css
@@ -0,0 +1,81 @@
+/* Custom media with a list */
+
+@media (min-width: 101px),(min-height: 102px) {
+
+@media screen and (max-color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not all and (min-width: 101px),not all and (min-height: 102px) {
+
+@media screen and (color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media (min-width: 101px),(min-height: 102px) {
+
+@media ((other: feature) and (max-color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media not all and (min-width: 101px),not all and (min-height: 102px) {
+
+@media ((other: feature) and (color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+/* Custom media used in a list */
+
+@media (min-width: 201px),(min-width: 202px) {
+ .b {
+ order: 3;
+ }
+}
+
+@media (min-width: 201px) {
+
+@media screen and (max-color:2147477350) {
+ .b {
+ order: 4;
+ }
+}
+}
+
+@media not all and (min-width: 201px) {
+
+@media screen and (color:2147477350) {
+ .b {
+ order: 4;
+ }
+}
+}
+
+@media (min-width: 202px) {
+
+@media not (max-color:2147477350) {
+ .b {
+ order: 4;
+ }
+}
+}
+
+@media not all and (min-width: 202px) {
+
+@media not (color:2147477350) {
+ .b {
+ order: 4;
+ }
+}
+}
diff --git a/plugins/postcss-custom-media/test/modifiers.css b/plugins/postcss-custom-media/test/modifiers.css
new file mode 100644
index 000000000..c8216fad5
--- /dev/null
+++ b/plugins/postcss-custom-media/test/modifiers.css
@@ -0,0 +1,28 @@
+/* Media queries with modifiers (not|only) */
+@custom-media --mq-not-screen not screen;
+
+@media screen and (--mq-not-screen) {
+ .a {
+ order: 1;
+ }
+}
+
+@media only print and (not (--mq-not-screen)) {
+ .a {
+ order: 2;
+ }
+}
+
+@custom-media --mq-only-screen-min-width only screen and (min-width: 200px);
+
+@media print and (--mq-only-screen-min-width) {
+ .a {
+ order: 3;
+ }
+}
+
+@media (--mq-only-screen-min-width), (not (--mq-only-screen-min-width)) {
+ .a {
+ order: 4;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/modifiers.expect.css b/plugins/postcss-custom-media/test/modifiers.expect.css
new file mode 100644
index 000000000..1b2574995
--- /dev/null
+++ b/plugins/postcss-custom-media/test/modifiers.expect.css
@@ -0,0 +1,79 @@
+/* Media queries with modifiers (not|only) */
+
+@media not screen {
+
+@media screen and (max-color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media screen {
+
+@media screen and (color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not screen {
+
+@media only print and (not (max-color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media screen {
+
+@media only print and (not (color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media only screen and (min-width: 200px) {
+
+@media print and (max-color:2147477350) {
+ .a {
+ order: 3;
+ }
+}
+}
+
+@media not screen and (min-width: 200px) {
+
+@media print and (color:2147477350) {
+ .a {
+ order: 3;
+ }
+}
+}
+
+@media only screen and (min-width: 200px) {
+ .a {
+ order: 4;
+ }
+}
+
+@media only screen and (min-width: 200px) {
+
+@media (not (max-color:2147477350)) {
+ .a {
+ order: 4;
+ }
+}
+}
+
+@media not screen and (min-width: 200px) {
+
+@media (not (color:2147477350)) {
+ .a {
+ order: 4;
+ }
+}
+}
diff --git a/plugins/postcss-custom-media/test/nesting.css b/plugins/postcss-custom-media/test/nesting.css
new file mode 100644
index 000000000..4d48d760d
--- /dev/null
+++ b/plugins/postcss-custom-media/test/nesting.css
@@ -0,0 +1,25 @@
+:root {
+ @custom-media --foo screen;
+}
+
+.foo {
+ .bar {
+ @custom-media --foo print;
+
+ @media screen {
+ @custom-media --foo not all;
+ }
+ }
+
+ @media screen {
+ .bar {
+ @custom-media --foo not all;
+ }
+ }
+}
+
+@media (--foo) {
+ .a {
+ order: 1;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/nesting.expect.css b/plugins/postcss-custom-media/test/nesting.expect.css
new file mode 100644
index 000000000..80d83ea0b
--- /dev/null
+++ b/plugins/postcss-custom-media/test/nesting.expect.css
@@ -0,0 +1,20 @@
+.foo {
+ .bar {
+
+ @media screen {
+ @custom-media --foo not all;
+ }
+ }
+
+ @media screen {
+ .bar {
+ @custom-media --foo not all;
+ }
+ }
+}
+
+@media print {
+ .a {
+ order: 1;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/not-processable.css b/plugins/postcss-custom-media/test/not-processable.css
new file mode 100644
index 000000000..9aa06e217
--- /dev/null
+++ b/plugins/postcss-custom-media/test/not-processable.css
@@ -0,0 +1,17 @@
+@media print {
+ @custom-media --foo screen;
+}
+
+@custom-media --foo screen {
+ /* has children */
+}
+
+@custom-media --foo;
+@custom-media screen;
+@custom-media ;
+
+@media (--foo) {
+ .a {
+ order: 1;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/not-processable.expect.css b/plugins/postcss-custom-media/test/not-processable.expect.css
new file mode 100644
index 000000000..9aa06e217
--- /dev/null
+++ b/plugins/postcss-custom-media/test/not-processable.expect.css
@@ -0,0 +1,17 @@
+@media print {
+ @custom-media --foo screen;
+}
+
+@custom-media --foo screen {
+ /* has children */
+}
+
+@custom-media --foo;
+@custom-media screen;
+@custom-media ;
+
+@media (--foo) {
+ .a {
+ order: 1;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/not.css b/plugins/postcss-custom-media/test/not.css
new file mode 100644
index 000000000..cb21c4a2e
--- /dev/null
+++ b/plugins/postcss-custom-media/test/not.css
@@ -0,0 +1,17 @@
+/* Media queries with "and" */
+@custom-media --with-not not (min-height: 300px);
+
+@media not (--with-not) {
+ .a {
+ order: 1;
+ }
+}
+
+@custom-media --not-screen not screen;
+@custom-media --not-print not print;
+
+@media (--not-screen) and (not (--not-print)) {
+ .a {
+ order: 2;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/not.expect.css b/plugins/postcss-custom-media/test/not.expect.css
new file mode 100644
index 000000000..e1d749ba4
--- /dev/null
+++ b/plugins/postcss-custom-media/test/not.expect.css
@@ -0,0 +1,61 @@
+/* Media queries with "and" */
+
+@media not (min-height: 300px) {
+
+@media not (max-color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media (min-height: 300px) {
+
+@media not (color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not screen {
+
+@media not print {
+
+@media (max-color:2147477350) and (not (max-color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media print {
+
+@media (max-color:2147477350) and (not (color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+}
+
+@media screen {
+
+@media not print {
+
+@media (color:2147477350) and (not (max-color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media print {
+
+@media (color:2147477350) and (not (color:2147477350)) {
+ .a {
+ order: 2;
+ }
+}
+}
+}
diff --git a/plugins/postcss-custom-media/test/or.css b/plugins/postcss-custom-media/test/or.css
new file mode 100644
index 000000000..c428d9c3e
--- /dev/null
+++ b/plugins/postcss-custom-media/test/or.css
@@ -0,0 +1,17 @@
+/* Media queries with "or" */
+@custom-media --with-or (min-width: 300px) or (min-height: 300px);
+
+@media not screen and (--with-or) {
+ .a {
+ order: 1;
+ }
+}
+
+@custom-media --not-screen not screen;
+@custom-media --not-print not print;
+
+@media (--not-screen) or (--not-print) {
+ .a {
+ order: 2;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/or.expect.css b/plugins/postcss-custom-media/test/or.expect.css
new file mode 100644
index 000000000..54b983ab5
--- /dev/null
+++ b/plugins/postcss-custom-media/test/or.expect.css
@@ -0,0 +1,61 @@
+/* Media queries with "or" */
+
+@media (min-width: 300px) or (min-height: 300px) {
+
+@media not screen and (max-color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not all and ( (min-width: 300px) or (min-height: 300px)) {
+
+@media not screen and (color:2147477350) {
+ .a {
+ order: 1;
+ }
+}
+}
+
+@media not screen {
+
+@media not print {
+
+@media (max-color:2147477350) or (max-color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media print {
+
+@media (max-color:2147477350) or (color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+}
+
+@media screen {
+
+@media not print {
+
+@media (color:2147477350) or (max-color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+
+@media print {
+
+@media (color:2147477350) or (color:2147477350) {
+ .a {
+ order: 2;
+ }
+}
+}
+}
diff --git a/plugins/postcss-custom-media/test/override.css b/plugins/postcss-custom-media/test/override.css
new file mode 100644
index 000000000..7b7fd09bc
--- /dev/null
+++ b/plugins/postcss-custom-media/test/override.css
@@ -0,0 +1,30 @@
+@custom-media --mq-a (min-width: 101px);
+
+@scope (.a-scope) {
+ @custom-media --mq-a (min-width: 102px);
+}
+
+@media (--mq-a) {
+ .a {
+ order: 1;
+ }
+}
+
+@custom-media --mq-b (min-width: 201px);
+
+@media (--mq-b) {
+ @custom-media --mq-b (min-width: 202px);
+
+ .b {
+ order: 2;
+ }
+}
+
+@custom-media --mq-c (min-width: 301px);
+@custom-media --mq-c (min-width: 302px);
+
+@media (--mq-c) {
+ .c {
+ order: 3;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/override.expect.css b/plugins/postcss-custom-media/test/override.expect.css
new file mode 100644
index 000000000..30f2040bd
--- /dev/null
+++ b/plugins/postcss-custom-media/test/override.expect.css
@@ -0,0 +1,19 @@
+@media (min-width: 102px) {
+ .a {
+ order: 1;
+ }
+}
+
+@media (min-width: 201px) {
+ @custom-media --mq-b (min-width: 202px);
+
+ .b {
+ order: 2;
+ }
+}
+
+@media (min-width: 302px) {
+ .c {
+ order: 3;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/parser-checks.css b/plugins/postcss-custom-media/test/parser-checks.css
new file mode 100644
index 000000000..5c5b625a5
--- /dev/null
+++ b/plugins/postcss-custom-media/test/parser-checks.css
@@ -0,0 +1,58 @@
+@custom-media/*a comment */--one/*a comment */screen;
+
+@media (--one) {
+ .one {
+ order: 1;
+ }
+}
+
+@custom-media --two only screen;
+
+@media/*a comment */((/*a comment */--two/*a comment */))/*a comment */{
+ .two {
+ order: 2;
+ }
+}
+
+@custom-media --๐จโ๐จโ๐งโ๐ฆ (min-width: 300px);
+
+@media (--๐จโ๐จโ๐งโ๐ฆ) {
+ .three {
+ order: 3;
+ }
+}
+
+/* Double space is needed */
+@custom-media --\66 (min-width: 300px);
+
+@media (--f) {
+ .four {
+ order: 4;
+ }
+}
+
+@custom-media --five (min-width: 300px);
+
+@media (width: calc(--five * 5)) {
+ .five {
+ order: 5.0;
+ }
+}
+
+@media calc(--five) {
+ .five {
+ order: 5.1;
+ }
+}
+
+@media (width: calc((--five) * 5)) {
+ .five {
+ order: 5.2;
+ }
+}
+
+@media (width: calc(env(--five) * 5)) {
+ .five {
+ order: 5.3;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/parser-checks.expect.css b/plugins/postcss-custom-media/test/parser-checks.expect.css
new file mode 100644
index 000000000..40af5f4d2
--- /dev/null
+++ b/plugins/postcss-custom-media/test/parser-checks.expect.css
@@ -0,0 +1,49 @@
+@media /*a comment */screen {
+ .one {
+ order: 1;
+ }
+}
+
+@media/*a comment */only screen/*a comment */{
+ .two {
+ order: 2;
+ }
+}
+
+@media (min-width: 300px) {
+ .three {
+ order: 3;
+ }
+}
+
+/* Double space is needed */
+
+@media (min-width: 300px) {
+ .four {
+ order: 4;
+ }
+}
+
+@media (width: calc(--five * 5)) {
+ .five {
+ order: 5.0;
+ }
+}
+
+@media calc(--five) {
+ .five {
+ order: 5.1;
+ }
+}
+
+@media (width: calc((--five) * 5)) {
+ .five {
+ order: 5.2;
+ }
+}
+
+@media (width: calc(env(--five) * 5)) {
+ .five {
+ order: 5.3;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/true-false.css b/plugins/postcss-custom-media/test/true-false.css
new file mode 100644
index 000000000..3c717c050
--- /dev/null
+++ b/plugins/postcss-custom-media/test/true-false.css
@@ -0,0 +1,44 @@
+@custom-media --truthy tRUe;
+@custom-media --falsy fAlsE;
+
+@media screen and (--truthy) {
+ .true {
+ order: 1;
+ }
+}
+
+@media screen and (--falsy) {
+ .false {
+ order: 2;
+ }
+}
+
+@custom-media --truthy-trailing-ws tRUe ;
+@custom-media --falsy-trailing-comment fAlsE/* a comment */ /* another comment */;
+
+@media (--truthy-trailing-ws) {
+ .true {
+ order: 3;
+ }
+}
+
+@media (--falsy-trailing-comment) {
+ .false {
+ order: 4;
+ }
+}
+
+@custom-media --truthy-broken tRUe and (min-width: 300px);
+@custom-media --falsy-broken fAlsE and (min-width: 300px);
+
+@media (--truthy-broken) {
+ .true {
+ order: 5;
+ }
+}
+
+@media (--falsy-broken) {
+ .false {
+ order: 6;
+ }
+}
diff --git a/plugins/postcss-custom-media/test/true-false.expect.css b/plugins/postcss-custom-media/test/true-false.expect.css
new file mode 100644
index 000000000..fdde69e1b
--- /dev/null
+++ b/plugins/postcss-custom-media/test/true-false.expect.css
@@ -0,0 +1,53 @@
+@media (max-color:2147477350) {@media screen and (max-color:2147477350) {
+ .true {
+ order: 1;
+ }
+}
+}@media not all and (max-color:2147477350) {@media screen and (color:2147477350) {
+ .true {
+ order: 1;
+ }
+}
+}
+
+@media (color:2147477350) {
+
+@media screen and (max-color:2147477350) {
+ .false {
+ order: 2;
+ }
+}
+}
+
+@media not all and (color:2147477350) {
+
+@media screen and (color:2147477350) {
+ .false {
+ order: 2;
+ }
+}
+}
+
+@media (max-color:2147477350) {
+ .true {
+ order: 3;
+ }
+}
+
+@media (color:2147477350) {
+ .false {
+ order: 4;
+ }
+}
+
+@media tRUe and (min-width: 300px) {
+ .true {
+ order: 5;
+ }
+}
+
+@media fAlsE and (min-width: 300px) {
+ .false {
+ order: 6;
+ }
+}
diff --git a/plugins/postcss-custom-media/tsconfig.json b/plugins/postcss-custom-media/tsconfig.json
new file mode 100644
index 000000000..2e428a8c2
--- /dev/null
+++ b/plugins/postcss-custom-media/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": ".",
+ "module": "es2020"
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/rollup/configs/externals.js b/rollup/configs/externals.js
index 44805e2c2..4c78033bb 100644
--- a/rollup/configs/externals.js
+++ b/rollup/configs/externals.js
@@ -4,6 +4,9 @@ export const externalsForCLI = [
'url',
'vm',
+ '@csstools/css-parser-algorithms',
+ '@csstools/css-tokenizer',
+ '@csstools/media-query-list-parser',
'@csstools/postcss-cascade-layers',
'@csstools/postcss-color-function',
'@csstools/postcss-font-format-keywords',
@@ -70,6 +73,9 @@ export const externalsForPlugin = [
/^postcss\/lib\/*/,
'postcss-html',
+ '@csstools/css-parser-algorithms',
+ '@csstools/css-tokenizer',
+ '@csstools/media-query-list-parser',
'@csstools/postcss-cascade-layers',
'@csstools/postcss-color-function',
'@csstools/postcss-font-format-keywords',
diff --git a/rollup/presets/package-typescript.js b/rollup/presets/package-typescript.js
index d765bf703..6242f32c5 100644
--- a/rollup/presets/package-typescript.js
+++ b/rollup/presets/package-typescript.js
@@ -21,7 +21,9 @@ export function packageTypescript() {
extensions: ['.js', '.ts'],
presets: packageBabelPreset,
}),
- terser(),
+ terser({
+ keep_classnames: true,
+ }),
],
},
];