From df439119373d0a59bb6de9757534d1dd1925ed84 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Thu, 13 Oct 2022 14:13:01 +0200
Subject: [PATCH 01/35] wip
---
package-lock.json | 36 ++++
package.json | 2 +
packages/css-parser/.gitignore | 6 +
packages/css-parser/.nvmrc | 1 +
packages/css-parser/CHANGELOG.md | 3 +
packages/css-parser/LICENSE.md | 20 ++
packages/css-parser/README.md | 129 ++++++++++++
packages/css-parser/package.json | 68 +++++++
.../consume-component-block-function.ts | 152 ++++++++++++++
packages/css-parser/src/index.ts | 0
packages/css-parser/stryker.conf.json | 19 ++
packages/css-parser/test/_import.mjs | 5 +
packages/css-parser/test/_require.cjs | 5 +
packages/css-parser/test/test.mjs | 13 ++
packages/css-parser/tsconfig.json | 9 +
packages/css-tokenizer/src/index.ts | 2 +-
.../css-tokenizer/src/interfaces/token.ts | 51 +++++
.../.gitignore | 6 +
.../postcss-media-query-list-parser/.nvmrc | 1 +
.../CHANGELOG.md | 3 +
.../LICENSE.md | 20 ++
.../postcss-media-query-list-parser/README.md | 1 +
.../package.json | 63 ++++++
.../src/index.ts | 1 +
.../src/nodes/general-enclosed.ts | 15 ++
.../src/nodes/media-and.ts | 15 ++
.../src/nodes/media-condition-list.ts | 45 +++++
.../nodes/media-condition-without-or-or.ts | 16 ++
.../src/nodes/media-condition.ts | 16 ++
.../src/nodes/media-feature-boolean.ts | 5 +
.../src/nodes/media-feature-comparison.ts | 49 +++++
.../src/nodes/media-feature-name.ts | 21 ++
.../src/nodes/media-feature-plain.ts | 18 ++
.../src/nodes/media-feature-range.ts | 138 +++++++++++++
.../src/nodes/media-feature-value.ts | 15 ++
.../src/nodes/media-feature.ts | 17 ++
.../src/nodes/media-in-parens.ts | 29 +++
.../src/nodes/media-not.ts | 15 ++
.../src/nodes/media-or.ts | 15 ++
.../src/nodes/media-query-modifier.ts | 4 +
.../src/nodes/media-query.ts | 50 +++++
.../src/nodes/media-type.ts | 22 ++
.../src/parser/advance/advance.ts | 58 ++++++
.../src/parser/consume/consume-boolean.ts | 46 +++++
.../src/parser/consume/consume-plain.ts | 44 ++++
.../src/parser/consume/consume-value.ts | 12 ++
.../src/parser/parse.ts | 97 +++++++++
.../test/test.mjs | 3 +
.../tsconfig.json | 9 +
packages/virtual-media/.gitignore | 6 +
packages/virtual-media/.nvmrc | 1 +
packages/virtual-media/CHANGELOG.md | 3 +
packages/virtual-media/LICENSE.md | 20 ++
packages/virtual-media/README.md | 1 +
packages/virtual-media/package.json | 63 ++++++
packages/virtual-media/src/index.ts | 188 ++++++++++++++++++
packages/virtual-media/src/range/add.ts | 67 +++++++
packages/virtual-media/src/range/compare.ts | 33 +++
packages/virtual-media/src/range/range.ts | 4 +
packages/virtual-media/test/test.mjs | 28 +++
packages/virtual-media/tsconfig.json | 9 +
61 files changed, 1812 insertions(+), 1 deletion(-)
create mode 100644 packages/css-parser/.gitignore
create mode 100644 packages/css-parser/.nvmrc
create mode 100644 packages/css-parser/CHANGELOG.md
create mode 100644 packages/css-parser/LICENSE.md
create mode 100644 packages/css-parser/README.md
create mode 100644 packages/css-parser/package.json
create mode 100644 packages/css-parser/src/consume/consume-component-block-function.ts
create mode 100644 packages/css-parser/src/index.ts
create mode 100644 packages/css-parser/stryker.conf.json
create mode 100644 packages/css-parser/test/_import.mjs
create mode 100644 packages/css-parser/test/_require.cjs
create mode 100644 packages/css-parser/test/test.mjs
create mode 100644 packages/css-parser/tsconfig.json
create mode 100644 packages/postcss-media-query-list-parser/.gitignore
create mode 100644 packages/postcss-media-query-list-parser/.nvmrc
create mode 100644 packages/postcss-media-query-list-parser/CHANGELOG.md
create mode 100644 packages/postcss-media-query-list-parser/LICENSE.md
create mode 100644 packages/postcss-media-query-list-parser/README.md
create mode 100644 packages/postcss-media-query-list-parser/package.json
create mode 100644 packages/postcss-media-query-list-parser/src/index.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-and.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-condition-list.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-condition-without-or-or.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-condition.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-boolean.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-not.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-or.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-query.ts
create mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-type.ts
create mode 100644 packages/postcss-media-query-list-parser/src/parser/advance/advance.ts
create mode 100644 packages/postcss-media-query-list-parser/src/parser/consume/consume-boolean.ts
create mode 100644 packages/postcss-media-query-list-parser/src/parser/consume/consume-plain.ts
create mode 100644 packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts
create mode 100644 packages/postcss-media-query-list-parser/src/parser/parse.ts
create mode 100644 packages/postcss-media-query-list-parser/test/test.mjs
create mode 100644 packages/postcss-media-query-list-parser/tsconfig.json
create mode 100644 packages/virtual-media/.gitignore
create mode 100644 packages/virtual-media/.nvmrc
create mode 100644 packages/virtual-media/CHANGELOG.md
create mode 100644 packages/virtual-media/LICENSE.md
create mode 100644 packages/virtual-media/README.md
create mode 100644 packages/virtual-media/package.json
create mode 100644 packages/virtual-media/src/index.ts
create mode 100644 packages/virtual-media/src/range/add.ts
create mode 100644 packages/virtual-media/src/range/compare.ts
create mode 100644 packages/virtual-media/src/range/range.ts
create mode 100644 packages/virtual-media/test/test.mjs
create mode 100644 packages/virtual-media/tsconfig.json
diff --git a/package-lock.json b/package-lock.json
index 6651725c2..c93065d75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1848,6 +1848,10 @@
"resolved": "plugins/postcss-is-pseudo-class",
"link": true
},
+ "node_modules/@csstools/postcss-media-query-list-parser": {
+ "resolved": "packages/postcss-media-query-list-parser",
+ "link": true
+ },
"node_modules/@csstools/postcss-nested-calc": {
"resolved": "plugins/postcss-nested-calc",
"link": true
@@ -1888,6 +1892,10 @@
"resolved": "packages/selector-specificity",
"link": true
},
+ "node_modules/@csstools/virtual-media": {
+ "resolved": "packages/virtual-media",
+ "link": true
+ },
"node_modules/@eslint/eslintrc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
@@ -6844,6 +6852,17 @@
"url": "https://opencollective.com/csstools"
}
},
+ "packages/postcss-media-query-list-parser": {
+ "version": "1.0.0",
+ "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",
@@ -6880,6 +6899,17 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "packages/virtual-media": {
+ "version": "1.0.0",
+ "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",
@@ -9007,6 +9037,9 @@
"puppeteer": "^18.0.0"
}
},
+ "@csstools/postcss-media-query-list-parser": {
+ "version": "file:packages/postcss-media-query-list-parser"
+ },
"@csstools/postcss-nested-calc": {
"version": "file:plugins/postcss-nested-calc",
"requires": {
@@ -9069,6 +9102,9 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "@csstools/virtual-media": {
+ "version": "file:packages/virtual-media"
+ },
"@eslint/eslintrc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
diff --git a/package.json b/package.json
index e3228ae16..ededd4c7b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
"node": "^14 || ^16 || >=18"
},
"workspaces": [
+ "packages/css-tokenizer",
+ "packages/css-parser",
"packages/*",
"plugins/postcss-progressive-custom-properties",
"plugins/*",
diff --git a/packages/css-parser/.gitignore b/packages/css-parser/.gitignore
new file mode 100644
index 000000000..7172b04f1
--- /dev/null
+++ b/packages/css-parser/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+dist/*
diff --git a/packages/css-parser/.nvmrc b/packages/css-parser/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/packages/css-parser/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/packages/css-parser/CHANGELOG.md b/packages/css-parser/CHANGELOG.md
new file mode 100644
index 000000000..b0ff6b082
--- /dev/null
+++ b/packages/css-parser/CHANGELOG.md
@@ -0,0 +1,3 @@
+### 1.0.0
+
+- Initial version
diff --git a/packages/css-parser/LICENSE.md b/packages/css-parser/LICENSE.md
new file mode 100644
index 000000000..af5411fa2
--- /dev/null
+++ b/packages/css-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/css-parser/README.md b/packages/css-parser/README.md
new file mode 100644
index 000000000..a9dc1b02e
--- /dev/null
+++ b/packages/css-parser/README.md
@@ -0,0 +1,129 @@
+# CSS Parser
+
+[
][npm-url]
+[
][cli-url]
+[
][discord]
+
+Implemented from : https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/
+
+## Usage
+
+Add [CSS Parser] to your project:
+
+```bash
+npm install postcss @csstools/css-parser --save-dev
+```
+
+```js
+import { tokenizer, TokenType } from '@csstools/css-parser';
+
+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 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
+
+[CSS Parser]: https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser
diff --git a/packages/css-parser/package.json b/packages/css-parser/package.json
new file mode 100644
index 000000000..d495fdd38
--- /dev/null
+++ b/packages/css-parser/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "@csstools/css-parser",
+ "description": "Parse 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"
+ ],
+ "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": "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-parser#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/css-parser"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "parser"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/css-parser/src/consume/consume-component-block-function.ts b/packages/css-parser/src/consume/consume-component-block-function.ts
new file mode 100644
index 000000000..e5d1dc408
--- /dev/null
+++ b/packages/css-parser/src/consume/consume-component-block-function.ts
@@ -0,0 +1,152 @@
+import { CSSToken, mirrorVariant, stringify, TokenType, isToken, TokenIdent, TokenFunction } from '@csstools/css-tokenizer';
+
+export type ComponentValue = FunctionNode | SimpleBlockNode | CSSToken;
+
+export class ComponentValueNode {
+ type = 'component-value';
+
+ value: ComponentValue;
+ before: Array;
+ after: Array;
+
+ constructor(value: ComponentValue, before: Array, after: Array) {
+ this.value = value;
+ this.before = before;
+ this.after = after;
+ }
+
+ toString() {
+ if (isToken(this.value)) {
+ return stringify(
+ ...this.before,
+ this.value,
+ ...this.after,
+ );
+ }
+
+ return stringify(...this.before) + this.value.toString() + stringify(...this.after);
+ }
+}
+
+// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
+export function consumeComponentValue(tokens: Array): { advance: number, node: ComponentValue } {
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+ if (
+ token[0] === TokenType.OpenParen ||
+ token[0] === TokenType.OpenCurly ||
+ token[0] === TokenType.OpenSquare
+ ) {
+ const r = consumeSimpleBlock(tokens.slice(i));
+ return {
+ advance: r.advance + i,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Function) {
+ const r = consumeFunction(tokens.slice(i));
+ return {
+ advance: r.advance + i,
+ node: r.node,
+ };
+ }
+
+ return {
+ advance: i,
+ node: token,
+ };
+ }
+}
+
+export class FunctionNode {
+ type = 'function';
+
+ name: TokenFunction;
+ value: Array;
+ after: Array;
+
+ constructor(name: TokenFunction, value: Array, after: Array) {
+ this.name = name;
+ this.value = value;
+ this.after = after;
+ }
+
+ get nameTokenValue(): string {
+ return this.name[4].value;
+ }
+
+ toString() {
+ return stringify(this.name) + this.value.map((x) => x.toString()).join('') + stringify(...this.after);
+ }
+}
+
+// https://www.w3.org/TR/css-syntax-3/#consume-function
+export function consumeFunction(tokens: Array): { advance: number, node: FunctionNode } {
+ const valueList: Array = [];
+ let lastValueIndex = -1;
+
+ for (let i = 1; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ if (token[0] === TokenType.CloseParen) {
+ return {
+ advance: i,
+ node: new FunctionNode(
+ tokens[0] as TokenFunction,
+ valueList,
+ tokens.slice(lastValueIndex, i+1),
+ ),
+ };
+ }
+
+ const r = consumeComponentValue(tokens.slice(i));
+ i += r.advance;
+ lastValueIndex = i;
+ valueList.push(r.node);
+ }
+
+ throw new Error('Failed to parse');
+}
+
+export class SimpleBlockNode {
+ type = 'simple-block';
+
+ name: Array;
+ value: Array;
+
+ constructor(name: Array, value: Array) {
+ this.name = name;
+ this.value = value;
+ }
+
+ get nameIdentIndex(): number {
+ return this.name.findIndex((x) => {
+ return x[0] === TokenType.Ident;
+ });
+ }
+
+ toString() {
+ return stringify(...this.name) + this.value.map((x) => x.toString()).join('');
+ }
+}
+
+/** https://www.w3.org/TR/css-syntax-3/#consume-simple-block */
+export function consumeSimpleBlock(tokens: Array): { advance: number, node: SimpleBlockNode } {
+ const endingToken = mirrorVariant(tokens[0][0]);
+ if (!endingToken) {
+ throw new Error('Failed to parse');
+ }
+
+ for (let i = 1; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ if (token[0] === endingToken) {
+ return i;
+ }
+
+ i += consumeComponentValue(tokens.slice(i));
+ }
+
+ throw new Error('Failed to parse');
+}
diff --git a/packages/css-parser/src/index.ts b/packages/css-parser/src/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/css-parser/stryker.conf.json b/packages/css-parser/stryker.conf.json
new file mode 100644
index 000000000..015ebbb73
--- /dev/null
+++ b/packages/css-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/css-parser/test/_import.mjs b/packages/css-parser/test/_import.mjs
new file mode 100644
index 000000000..c1c61bf2b
--- /dev/null
+++ b/packages/css-parser/test/_import.mjs
@@ -0,0 +1,5 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+
+tokenizer({
+ css: '.some { css: ""; }',
+});
diff --git a/packages/css-parser/test/_require.cjs b/packages/css-parser/test/_require.cjs
new file mode 100644
index 000000000..abc74a96e
--- /dev/null
+++ b/packages/css-parser/test/_require.cjs
@@ -0,0 +1,5 @@
+const { tokenizer } = require('@csstools/css-tokenizer');
+
+tokenizer({
+ css: '.some { css: ""; }',
+});
diff --git a/packages/css-parser/test/test.mjs b/packages/css-parser/test/test.mjs
new file mode 100644
index 000000000..71d21591a
--- /dev/null
+++ b/packages/css-parser/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-parser/tsconfig.json b/packages/css-parser/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/packages/css-parser/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/packages/css-tokenizer/src/index.ts b/packages/css-tokenizer/src/index.ts
index 54e28b122..992bdf3bc 100644
--- a/packages/css-tokenizer/src/index.ts
+++ b/packages/css-tokenizer/src/index.ts
@@ -1,6 +1,6 @@
export type { CSSToken } from './interfaces/token';
export { Reader } from './reader';
-export { TokenType, NumberType } from './interfaces/token';
+export { TokenType, NumberType, mirrorVariant, isToken } from './interfaces/token';
export { stringify } from './stringify';
export { tokenizer } from './tokenizer';
diff --git a/packages/css-tokenizer/src/interfaces/token.ts b/packages/css-tokenizer/src/interfaces/token.ts
index 184be7ce9..90d259f01 100644
--- a/packages/css-tokenizer/src/interfaces/token.ts
+++ b/packages/css-tokenizer/src/interfaces/token.ts
@@ -132,3 +132,54 @@ export type Token = [
/** Extra data */
U,
]
+
+export function mirrorVariant(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/postcss-media-query-list-parser/.gitignore b/packages/postcss-media-query-list-parser/.gitignore
new file mode 100644
index 000000000..7172b04f1
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+dist/*
diff --git a/packages/postcss-media-query-list-parser/.nvmrc b/packages/postcss-media-query-list-parser/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/packages/postcss-media-query-list-parser/CHANGELOG.md b/packages/postcss-media-query-list-parser/CHANGELOG.md
new file mode 100644
index 000000000..b0ff6b082
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/CHANGELOG.md
@@ -0,0 +1,3 @@
+### 1.0.0
+
+- Initial version
diff --git a/packages/postcss-media-query-list-parser/LICENSE.md b/packages/postcss-media-query-list-parser/LICENSE.md
new file mode 100644
index 000000000..af5411fa2
--- /dev/null
+++ b/packages/postcss-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/postcss-media-query-list-parser/README.md b/packages/postcss-media-query-list-parser/README.md
new file mode 100644
index 000000000..361096774
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/README.md
@@ -0,0 +1 @@
+# TODO
diff --git a/packages/postcss-media-query-list-parser/package.json b/packages/postcss-media-query-list-parser/package.json
new file mode 100644
index 000000000..c1f53a38c
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@csstools/postcss-media-query-list-parser",
+ "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",
+ "test": "node ./test/test.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/postcss-media-query-list-parser#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/postcss-media-query-list-parser"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "tokenizer"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/index.ts b/packages/postcss-media-query-list-parser/src/index.ts
new file mode 100644
index 000000000..6c0dde8d4
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/index.ts
@@ -0,0 +1 @@
+export { parse } from './parser/parse';
diff --git a/packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts b/packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts
new file mode 100644
index 000000000..ef15f9f37
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts
@@ -0,0 +1,15 @@
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+
+export class GeneralEnclosed {
+ type = 'general-enclosed';
+
+ raw: Array;
+
+ constructor(raw: Array) {
+ this.raw = raw;
+ }
+
+ toString() {
+ return stringify(...this.raw);
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-and.ts b/packages/postcss-media-query-list-parser/src/nodes/media-and.ts
new file mode 100644
index 000000000..c6fd5c87e
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-and.ts
@@ -0,0 +1,15 @@
+import { MediaInParens } from './media-in-parens';
+
+export class MediaAnd {
+ type = 'media-and';
+
+ media: MediaInParens;
+
+ constructor(media: MediaInParens) {
+ this.media = media;
+ }
+
+ toString() {
+ return 'and' + this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-condition-list.ts b/packages/postcss-media-query-list-parser/src/nodes/media-condition-list.ts
new file mode 100644
index 000000000..563eebae3
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-condition-list.ts
@@ -0,0 +1,45 @@
+import { MediaAnd } from './media-and';
+import { MediaInParens } from './media-in-parens';
+import { MediaOr } from './media-or';
+
+export type MediaConditionList = MediaConditionListWithAnd | MediaConditionListWithOr;
+
+export class MediaConditionListWithAnd {
+ type = 'media-condition-list-and';
+
+ leading: MediaInParens;
+ list: Array;
+
+ constructor(leading: MediaInParens, list: Array) {
+ this.leading = leading;
+ this.list = list;
+ }
+
+ toString() {
+ if (this.list.length === 0) {
+ return this.leading.toString();
+ }
+
+ return this.leading.toString() + ' ' + this.list.map((x) => x.toString()).join(' ');
+ }
+}
+
+export class MediaConditionListWithOr {
+ type = 'media-condition-list-or';
+
+ leading: MediaInParens;
+ list: Array;
+
+ constructor(leading: MediaInParens, list: Array) {
+ this.leading = leading;
+ this.list = list;
+ }
+
+ toString() {
+ if (this.list.length === 0) {
+ return this.leading.toString();
+ }
+
+ return this.leading.toString() + ' ' + this.list.map((x) => x.toString()).join(' ');
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-condition-without-or-or.ts b/packages/postcss-media-query-list-parser/src/nodes/media-condition-without-or-or.ts
new file mode 100644
index 000000000..8cb02ade0
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-condition-without-or-or.ts
@@ -0,0 +1,16 @@
+import { MediaConditionListWithAnd } from './media-condition-list';
+import { MediaNot } from './media-not';
+
+export class MediaConditionWithoutOr {
+ type = 'media-condition-without-or';
+
+ media: MediaNot | MediaConditionListWithAnd;
+
+ constructor(media: MediaNot | MediaConditionListWithAnd) {
+ this.media = media;
+ }
+
+ toString() {
+ return this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-condition.ts b/packages/postcss-media-query-list-parser/src/nodes/media-condition.ts
new file mode 100644
index 000000000..fa276ee92
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-condition.ts
@@ -0,0 +1,16 @@
+import { MediaConditionListWithAnd, MediaConditionListWithOr } from './media-condition-list';
+import { MediaNot } from './media-not';
+
+export class MediaCondition {
+ type = 'media-condition';
+
+ media: MediaNot | MediaConditionListWithAnd | MediaConditionListWithOr;
+
+ constructor(media: MediaNot | MediaConditionListWithAnd | MediaConditionListWithOr) {
+ this.media = media;
+ }
+
+ toString() {
+ return this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-boolean.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-boolean.ts
new file mode 100644
index 000000000..bb960da2a
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature-boolean.ts
@@ -0,0 +1,5 @@
+import { MediaFeatureName } from './media-feature-name';
+
+export class MediaFeatureBoolean extends MediaFeatureName {
+ type = 'mf-boolean';
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts
new file mode 100644
index 000000000..e172c73ef
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts
@@ -0,0 +1,49 @@
+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 invertComparison(operator: MediaFeatureComparison): MediaFeatureComparison {
+ 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:
+ throw new Error('Unknown range syntax operator');
+ }
+}
+
+export function comparisonToString(operator: MediaFeatureComparison): string {
+ switch (operator) {
+ case MediaFeatureEQ.EQ:
+ return '=';
+ case MediaFeatureLT.LT:
+ return '<';
+ case MediaFeatureLT.LT_OR_EQ:
+ return '<=';
+ case MediaFeatureGT.GT:
+ return '>';
+ case MediaFeatureGT.GT_OR_EQ:
+ return '>=';
+ default:
+ throw new Error('Unknown range syntax operator');
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts
new file mode 100644
index 000000000..86349b095
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts
@@ -0,0 +1,21 @@
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
+
+export class MediaFeatureName {
+ type = 'mf-name';
+
+ tokens: Array;
+
+ constructor(tokens: Array) {
+ this.tokens = tokens;
+ }
+
+ get nameIndex(): number {
+ return this.tokens.findIndex((x) => {
+ return x[0] === TokenType.Ident;
+ });
+ }
+
+ toString() {
+ return stringify(...this.tokens);
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts
new file mode 100644
index 000000000..f8d898b14
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -0,0 +1,18 @@
+import { MediaFeatureName } from './media-feature-name';
+import { MediaFeatureValue } from './media-feature-value';
+
+export class MediaFeaturePlain {
+ type = 'mf-plain';
+
+ name: MediaFeatureName;
+ value: MediaFeatureValue;
+
+ constructor(name: MediaFeatureName, value: MediaFeatureValue) {
+ this.name = name;
+ this.value = value;
+ }
+
+ toString() {
+ return this.name.toString() + ':' + this.value.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts
new file mode 100644
index 000000000..a804d26df
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts
@@ -0,0 +1,138 @@
+import { comparisonToString, MediaFeatureComparison, MediaFeatureGT, MediaFeatureLT } from './media-feature-comparison';
+import { MediaFeatureName } from './media-feature-name';
+import { MediaFeatureValue } from './media-feature-value';
+
+export type MediaFeatureRange = MediaFeatureRangeNameValue |
+ MediaFeatureRangeValueName |
+ MediaFeatureRangeLowHigh |
+ MediaFeatureRangeHighLow;
+
+export class MediaFeatureRangeNameValue {
+ type = 'mf-range-name-value';
+
+ name: MediaFeatureName;
+ operator: MediaFeatureComparison;
+ value: MediaFeatureValue;
+
+ constructor(name: MediaFeatureName, operator: MediaFeatureComparison, value: MediaFeatureValue) {
+ this.name = name;
+ this.operator = operator;
+ this.value = value;
+ }
+
+ toString() {
+ let result = '';
+
+ result += this.name.toString();
+ result += ' ';
+
+ result += comparisonToString(this.operator);
+
+ result += ' ';
+ result += this.value.toString();
+
+ return result;
+ }
+}
+
+export class MediaFeatureRangeValueName {
+ type = 'mf-range-value-range';
+
+ name: MediaFeatureName;
+ operator: MediaFeatureComparison;
+ value: MediaFeatureValue;
+
+ constructor(value: MediaFeatureValue, operator: MediaFeatureComparison, name: MediaFeatureName) {
+ this.name = name;
+ this.operator = operator;
+ this.value = value;
+ }
+
+ toString() {
+ let result = '';
+
+ result += this.value.toString();
+ result += ' ';
+
+ result += comparisonToString(this.operator);
+
+ result += ' ';
+ result += this.name.toString();
+
+ return result;
+ }
+}
+
+export class MediaFeatureRangeLowHigh {
+ type = 'mf-range-low-high';
+
+ name: MediaFeatureName;
+ low: MediaFeatureValue;
+ lowOperator: MediaFeatureLT;
+ high: MediaFeatureValue;
+ highOperator: MediaFeatureLT;
+
+ constructor(name: MediaFeatureName, low: MediaFeatureValue, lowOperator: MediaFeatureLT, high: MediaFeatureValue, highOperator: MediaFeatureLT) {
+ this.name = name;
+ this.low = low;
+ this.lowOperator = lowOperator;
+ this.high = high;
+ this.highOperator = highOperator;
+ }
+
+ toString() {
+ let result = '';
+
+ result += this.low.toString();
+
+ result += ' ';
+ result += comparisonToString(this.lowOperator);
+ result += ' ';
+
+ result += this.name.toString();
+
+ result += ' ';
+ result += comparisonToString(this.highOperator);
+ result += ' ';
+
+ result += this.high.toString();
+ return result;
+ }
+}
+
+export class MediaFeatureRangeHighLow {
+ type = 'mf-range-high-low';
+
+ name: MediaFeatureName;
+ low: MediaFeatureValue;
+ lowOperator: MediaFeatureGT;
+ high: MediaFeatureValue;
+ highOperator: MediaFeatureGT;
+
+ constructor(name: MediaFeatureName, high: MediaFeatureValue, highOperator: MediaFeatureGT, low: MediaFeatureValue, lowOperator: MediaFeatureGT) {
+ this.name = name;
+ this.low = low;
+ this.lowOperator = lowOperator;
+ this.high = high;
+ this.highOperator = highOperator;
+ }
+
+ toString() {
+ let result = '';
+
+ result += this.high.toString();
+
+ result += ' ';
+ result += comparisonToString(this.highOperator);
+ result += ' ';
+
+ result += this.name.toString();
+
+ result += ' ';
+ result += comparisonToString(this.lowOperator);
+ result += ' ';
+
+ result += this.low.toString();
+ return result;
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts
new file mode 100644
index 000000000..83302072c
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts
@@ -0,0 +1,15 @@
+import { CSSToken, stringify} from '@csstools/css-tokenizer';
+
+export class MediaFeatureValue {
+ type = 'mf-value';
+
+ tokens: Array;
+
+ constructor(tokens: Array) {
+ this.tokens = tokens;
+ }
+
+ toString() {
+ return stringify(...this.tokens);
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature.ts
new file mode 100644
index 000000000..28e55bb18
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-feature.ts
@@ -0,0 +1,17 @@
+import { MediaFeatureBoolean } from './media-feature-boolean';
+import { MediaFeaturePlain } from './media-feature-plain';
+import { MediaFeatureRange } from './media-feature-range';
+
+export class MediaFeature {
+ type = 'media-feature';
+
+ feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
+
+ constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange) {
+ this.feature = feature;
+ }
+
+ toString() {
+ return '(' + this.feature.toString() + ')';
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts b/packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts
new file mode 100644
index 000000000..609b5210c
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts
@@ -0,0 +1,29 @@
+import { GeneralEnclosed } from './general-enclosed';
+import { MediaCondition } from './media-condition';
+import { MediaFeature } from './media-feature';
+
+export class MediaInParens {
+ type = 'media-in-parens';
+
+ media: MediaCondition | MediaFeature | GeneralEnclosed;
+
+ constructor(media: MediaCondition | MediaFeature | GeneralEnclosed) {
+ this.media = media;
+ }
+
+ toString() {
+ if (this.media.type === 'general-enclosed') {
+ return this.media.toString();
+ }
+
+ if (this.media.type === 'media-feature') {
+ return this.media.toString();
+ }
+
+ if (this.media.type === 'media-condition') {
+ return '(' + this.media.toString() + ')';
+ }
+
+ return this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-not.ts b/packages/postcss-media-query-list-parser/src/nodes/media-not.ts
new file mode 100644
index 000000000..c6b8d9b88
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-not.ts
@@ -0,0 +1,15 @@
+import { MediaInParens } from './media-in-parens';
+
+export class MediaNot {
+ type = 'media-not';
+
+ media: MediaInParens;
+
+ constructor(media: MediaInParens) {
+ this.media = media;
+ }
+
+ toString() {
+ return 'not ' + this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-or.ts b/packages/postcss-media-query-list-parser/src/nodes/media-or.ts
new file mode 100644
index 000000000..f2b6779d3
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-or.ts
@@ -0,0 +1,15 @@
+import { MediaInParens } from './media-in-parens';
+
+export class MediaOr {
+ type = 'media-or';
+
+ media: MediaInParens;
+
+ constructor(media: MediaInParens) {
+ this.media = media;
+ }
+
+ toString() {
+ return 'or ' + this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts b/packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts
new file mode 100644
index 000000000..d30ae315d
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts
@@ -0,0 +1,4 @@
+export enum MediaQueryModifier {
+ Not = 'not',
+ Only = 'only'
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-query.ts b/packages/postcss-media-query-list-parser/src/nodes/media-query.ts
new file mode 100644
index 000000000..5ab50cfec
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-query.ts
@@ -0,0 +1,50 @@
+import { MediaCondition } from './media-condition';
+import { MediaConditionWithoutOr } from './media-condition-without-or-or';
+import { MediaQueryModifier } from './media-query-modifier';
+import { MediaType } from './media-type';
+
+export type MediaQuery = MediaQueryWithType | MediaQueryWithoutType;
+
+export class MediaQueryWithType {
+ type = 'media-query-with-type';
+
+ modifier?: MediaQueryModifier;
+ mediaType: MediaType;
+ media?: MediaConditionWithoutOr;
+
+ constructor(modifier: MediaQueryModifier | null, mediaType: MediaType, media: MediaConditionWithoutOr | null) {
+ this.modifier = modifier;
+ this.mediaType = mediaType;
+ this.media = media;
+ }
+
+ toString() {
+ if (this.modifier && this.media) {
+ return `${this.modifier} ${this.mediaType} and ${this.media.toString()}`;
+ }
+
+ if (this.modifier) {
+ return `${this.modifier} ${this.mediaType}`;
+ }
+
+ if (this.media) {
+ return `${this.mediaType} and ${this.media.toString()}`;
+ }
+
+ return this.mediaType;
+ }
+}
+
+export class MediaQueryWithoutType {
+ type = 'media-query-without-type';
+
+ media: MediaCondition;
+
+ constructor(media: MediaCondition) {
+ this.media = media;
+ }
+
+ toString() {
+ return this.media.toString();
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-type.ts b/packages/postcss-media-query-list-parser/src/nodes/media-type.ts
new file mode 100644
index 000000000..341a7e474
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/nodes/media-type.ts
@@ -0,0 +1,22 @@
+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',
+}
diff --git a/packages/postcss-media-query-list-parser/src/parser/advance/advance.ts b/packages/postcss-media-query-list-parser/src/parser/advance/advance.ts
new file mode 100644
index 000000000..b66dc51d4
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/parser/advance/advance.ts
@@ -0,0 +1,58 @@
+import { CSSToken, mirrorVariant, TokenType } from '@csstools/css-tokenizer';
+
+// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
+export function advanceComponentValue(tokens: Array): number {
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+ if (
+ token[0] === TokenType.OpenParen ||
+ token[0] === TokenType.OpenCurly ||
+ token[0] === TokenType.OpenSquare
+ ) {
+ i += advanceSimpleBlock(tokens.slice(i));
+ return i;
+ }
+
+ if (token[0] === TokenType.Function) {
+ i += advanceFunction(tokens.slice(i));
+ return i;
+ }
+
+ return i;
+ }
+}
+
+// https://www.w3.org/TR/css-syntax-3/#consume-function
+export function advanceFunction(tokens: Array): number | null {
+ for (let i = 1; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ if (token[0] === TokenType.CloseParen) {
+ return i;
+ }
+
+ i += advanceComponentValue(tokens.slice(i));
+ }
+
+ throw new Error('Failed to parse');
+}
+
+/** https://www.w3.org/TR/css-syntax-3/#consume-simple-block */
+export function advanceSimpleBlock(tokens: Array): number | null {
+ const endingToken = mirrorVariant(tokens[0][0]);
+ if (!endingToken) {
+ throw new Error('Failed to parse');
+ }
+
+ for (let i = 1; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ if (token[0] === endingToken) {
+ return i;
+ }
+
+ i += advanceComponentValue(tokens.slice(i));
+ }
+
+ throw new Error('Failed to parse');
+}
diff --git a/packages/postcss-media-query-list-parser/src/parser/consume/consume-boolean.ts b/packages/postcss-media-query-list-parser/src/parser/consume/consume-boolean.ts
new file mode 100644
index 000000000..fc7081d5d
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/parser/consume/consume-boolean.ts
@@ -0,0 +1,46 @@
+import { CSSToken, TokenIdent, TokenType } from '@csstools/css-tokenizer';
+import { MediaFeatureBoolean } from '../../nodes/media-feature-boolean';
+
+export function consumeBoolean(tokens: Array): { node: MediaFeatureBoolean, tokens: Array } | null {
+ let ident : TokenIdent|null = null;
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ if (i === 0) {
+ if (token[0] === TokenType.OpenParen) {
+ continue;
+ }
+
+ return null;
+ }
+
+ if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) {
+ continue;
+ }
+
+ if (token[0] === TokenType.Ident) {
+ if (!ident) {
+ ident = token as TokenIdent;
+ continue;
+ }
+
+ return null;
+ }
+
+ if (token[0] === TokenType.CloseParen) {
+ if (ident) {
+ const node = new MediaFeatureBoolean(tokens.slice(0, i + 1));
+
+ return {
+ node: node,
+ tokens: tokens.slice(i + 1),
+ };
+ }
+
+ return null;
+ }
+
+ return null;
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/parser/consume/consume-plain.ts b/packages/postcss-media-query-list-parser/src/parser/consume/consume-plain.ts
new file mode 100644
index 000000000..3a84cd380
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/parser/consume/consume-plain.ts
@@ -0,0 +1,44 @@
+import { CSSToken, TokenIdent, TokenType } from '@csstools/css-tokenizer';
+import { MediaFeatureName } from '../../nodes/media-feature-name';
+import { MediaFeaturePlain } from '../../nodes/media-feature-plain';
+import { consumeValue } from './consume-value';
+
+export function consumePlain(tokens: Array): { node: MediaFeaturePlain, tokens: Array } | null {
+ let name: MediaFeatureName | null = null;
+ const value: MediaFeatureValue | null = null;
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+
+ if (i === 0) {
+ if (token[0] === TokenType.OpenParen) {
+ continue;
+ } else {
+ return null;
+ }
+ }
+
+ if (token[0] === TokenType.CloseParen) {
+ continue;
+ }
+
+ if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) {
+ continue;
+ }
+
+ if (token[0] === TokenType.Ident) {
+ if (!name) {
+ name = new MediaFeatureName(tokens.slice(0, i + 1));
+ continue;
+ } else {
+ return null;
+ }
+ }
+
+ if (token[0] === TokenType.Delim && token[1] === ':' && name) {
+ const value = consumeValue(tokens.slice(i + 1));
+ }
+
+ return null;
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts b/packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts
new file mode 100644
index 000000000..6d1b21b88
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts
@@ -0,0 +1,12 @@
+import { CSSToken } from '@csstools/css-tokenizer';
+import { MediaFeatureValue } from '../../nodes/media-feature-value';
+import { advanceComponentValue } from '../advance/advance';
+
+export function consumeValue(tokens: Array): { node: MediaFeatureValue, tokens: Array } | null {
+ const result = advanceComponentValue(tokens);
+
+ return {
+ node: new MediaFeatureValue(tokens.slice(0, result + 1)),
+ tokens: tokens.slice(result + 1),
+ };
+}
diff --git a/packages/postcss-media-query-list-parser/src/parser/parse.ts b/packages/postcss-media-query-list-parser/src/parser/parse.ts
new file mode 100644
index 000000000..8283ae072
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/src/parser/parse.ts
@@ -0,0 +1,97 @@
+import { CSSToken, TokenType, tokenizer } from '@csstools/css-tokenizer';
+import { MediaFeatureBoolean } from '../nodes/media-feature-boolean';
+import { advanceSimpleBlock } from './advance/advance';
+import { consumeBoolean } from './consume/consume-boolean';
+
+type Tokenizer = {
+ nextToken: () => CSSToken | undefined,
+ endOfFile: () => boolean,
+}
+
+export function parse(source: string) {
+ const t = tokenizer({ css: source }, {
+ commentsAreTokens: true,
+ onParseError: (err) => {
+ console.warn(err);
+ throw new Error(`Unable to parse "${source}"`);
+ },
+ });
+
+ const tokenBuffer = [];
+ while (!t.endOfFile()) {
+ tokenBuffer.push(t.nextToken());
+ }
+
+ // console.log(tokenBuffer);
+
+ const result = consumeBoolean(tokenBuffer);
+ const remainder = result.tokens;
+ const node = result.node;
+ const tokenSlice = node.tokens;
+
+ console.log(tokenSlice);
+ console.log(node.nameIndex);
+ console.log(node.tokens[node.nameIndex][4].value);
+
+ console.log(remainder);
+}
+
+// function consumeMediaQuery(t: Tokenizer) {
+// let token = t.nextToken();
+// if (t.endOfFile()) {
+// return;
+// }
+
+// while (token[0] === TokenType.Whitespace || token[0] === TokenType.Comment) {
+// token = t.nextToken();
+// if (t.endOfFile()) {
+// return;
+// }
+// }
+
+// if (token[0] === TokenType.OpenParen) {
+// return consumeMediaQueryWithoutType(t);
+// }
+
+// if (token[0] !== TokenType.Ident) {
+// return;
+// }
+
+// if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'only') {
+// return consumeMediaQueryWithType(t);
+// }
+
+// if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'not') {
+// token = t.nextToken();
+// if (t.endOfFile()) {
+// return;
+// }
+
+// while (token[0] === TokenType.Whitespace || token[0] === TokenType.Comment) {
+// token = t.nextToken();
+// if (t.endOfFile()) {
+// return;
+// }
+// }
+
+// if (token[0] === TokenType.Comma) {
+// return;
+// }
+
+// if (token[0] === TokenType.OpenParen) {
+// return consumeMediaQueryWithoutType(t);
+// }
+
+// return consumeMediaQueryWithType(t);
+// }
+
+// const modifier = token[]
+// }
+
+// function consumeMediaQueryWithoutType(t: Tokenizer) {
+
+// }
+
+// function consumeMediaQueryWithType(t: Tokenizer) {
+
+// }
diff --git a/packages/postcss-media-query-list-parser/test/test.mjs b/packages/postcss-media-query-list-parser/test/test.mjs
new file mode 100644
index 000000000..44a4b335d
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/test/test.mjs
@@ -0,0 +1,3 @@
+import { parse } from '@csstools/postcss-media-query-list-parser';
+
+parse('(/* a comment */foo ) something else');
diff --git a/packages/postcss-media-query-list-parser/tsconfig.json b/packages/postcss-media-query-list-parser/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/packages/postcss-media-query-list-parser/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
diff --git a/packages/virtual-media/.gitignore b/packages/virtual-media/.gitignore
new file mode 100644
index 000000000..7172b04f1
--- /dev/null
+++ b/packages/virtual-media/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+dist/*
diff --git a/packages/virtual-media/.nvmrc b/packages/virtual-media/.nvmrc
new file mode 100644
index 000000000..f0b10f153
--- /dev/null
+++ b/packages/virtual-media/.nvmrc
@@ -0,0 +1 @@
+v16.13.1
diff --git a/packages/virtual-media/CHANGELOG.md b/packages/virtual-media/CHANGELOG.md
new file mode 100644
index 000000000..b0ff6b082
--- /dev/null
+++ b/packages/virtual-media/CHANGELOG.md
@@ -0,0 +1,3 @@
+### 1.0.0
+
+- Initial version
diff --git a/packages/virtual-media/LICENSE.md b/packages/virtual-media/LICENSE.md
new file mode 100644
index 000000000..af5411fa2
--- /dev/null
+++ b/packages/virtual-media/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/virtual-media/README.md b/packages/virtual-media/README.md
new file mode 100644
index 000000000..361096774
--- /dev/null
+++ b/packages/virtual-media/README.md
@@ -0,0 +1 @@
+# TODO
diff --git a/packages/virtual-media/package.json b/packages/virtual-media/package.json
new file mode 100644
index 000000000..84e6aa12d
--- /dev/null
+++ b/packages/virtual-media/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@csstools/virtual-media",
+ "description": "Virtualized media for media queries.",
+ "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",
+ "test": "node ./test/test.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/virtual-media#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/virtual-media"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "tokenizer"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/virtual-media/src/index.ts b/packages/virtual-media/src/index.ts
new file mode 100644
index 000000000..e2a94869a
--- /dev/null
+++ b/packages/virtual-media/src/index.ts
@@ -0,0 +1,188 @@
+import { addRange } from './range/add';
+import { Range } from './range/range';
+
+export type Unknown = 'unknown';
+export type Impossible = 'impossible';
+
+export enum Operator {
+ LT = '<',
+ LT_OR_EQ = '<=',
+ GT = '>',
+ GT_OR_EQ = '>=',
+ EQ = '=',
+}
+
+const fraction = 1 / Math.pow(2, 32);
+
+export class VirtualMedia {
+ #impossibleMedia = false;
+
+ #types = new Set([
+ 'print',
+ 'screen',
+ ]);
+
+ mustMatchType(type: string) {
+ if (type.toLowerCase() === 'all') {
+ return;
+ }
+
+ if (!this.#types.has(type.toLowerCase())) {
+ this.#types.clear();
+ return;
+ }
+
+ this.#types.clear();
+ this.#types.add(type.toLowerCase());
+ }
+
+ mustNotMatchType(type: string) {
+ if (type.toLowerCase() === 'all') {
+ this.#types.clear();
+ return;
+ }
+
+ this.#types.delete(type.toLowerCase());
+ }
+
+ mustMatchWidth(low: number, lowOperator: Operator, high: number, highOperator: Operator) {
+ if (this.#impossibleMedia) {
+ return;
+ }
+
+ const range = {
+ low: low,
+ high: high,
+ };
+
+ switch (lowOperator) {
+ case Operator.LT:
+ range.low = range.low - fraction;
+ break;
+ case Operator.GT:
+ range.low = range.low + fraction;
+ break;
+ }
+
+ switch (highOperator) {
+ case Operator.LT:
+ range.high = range.high - fraction;
+ break;
+ case Operator.GT:
+ range.high = range.high + fraction;
+ break;
+ }
+
+ this.#width = addRange(this.#width, {
+ low: range.low,
+ high: range.high,
+ });
+
+ if (this.#width.length === 0) {
+ this.#impossibleMedia = true;
+ }
+ }
+
+ #width: Array> = [
+ {
+ low: Number.MIN_SAFE_INTEGER,
+ high: Number.MAX_SAFE_INTEGER,
+ },
+ ];
+
+ /**
+ * https://www.w3.org/TR/mediaqueries-5/#width
+ *
+ */
+ get width(): number | Unknown | Impossible {
+ if (this.#impossibleMedia) {
+ return 'impossible';
+ }
+
+ if (this.#width.length !== 1) {
+ return 'unknown';
+ }
+
+ if (this.#width[0].low !== this.#width[0].high) {
+ return 'unknown';
+ }
+
+ return this.#width[0].low;
+ }
+
+ #height: Array> = [
+ {
+ low: Number.MIN_SAFE_INTEGER,
+ high: Number.MAX_SAFE_INTEGER,
+ },
+ ];
+
+ /**
+ * https://www.w3.org/TR/mediaqueries-5/#height
+ *
+ */
+ get height(): number | Unknown | Impossible {
+ if (this.#impossibleMedia) {
+ return 'impossible';
+ }
+
+ if (this.#height.length !== 1) {
+ return 'unknown';
+ }
+
+ if (this.#height[0].low !== this.#height[0].high) {
+ return 'unknown';
+ }
+
+ return this.#height[0].low;
+ }
+
+ #aspectRatio: Range<{
+ /** dividend / divisor */
+ dividend: number,
+ /** dividend / divisor */
+ divisor: number
+ }> | Unknown = 'unknown';
+
+ /**
+ * https://www.w3.org/TR/mediaqueries-5/#aspect-ratio
+ *
+ */
+ get aspectRatio() {
+ if (this.#impossibleMedia) {
+ return 'unknown';
+ }
+
+ if (this.#aspectRatio === 'unknown') {
+ const height = this.height;
+ const width = this.width;
+ if (height === 'impossible' || width === 'impossible') {
+ return 'impossible';
+ }
+
+ if (height !== 'unknown' && width !== 'unknown') {
+ return width / height;
+ }
+
+ return 'unknown';
+ }
+
+ if (this.#aspectRatio.low.dividend !== this.#aspectRatio.high.dividend) {
+ return 'unknown';
+ }
+
+ if (this.#aspectRatio.low.divisor !== this.#aspectRatio.high.divisor) {
+ return 'unknown';
+ }
+
+ if (this.#aspectRatio.low.dividend === 0) {
+ return 'unknown';
+ }
+
+ if (this.#aspectRatio.low.divisor === 0) {
+ return 'unknown';
+ }
+
+ return this.#aspectRatio.low.dividend / this.#aspectRatio.low.divisor;
+ }
+}
diff --git a/packages/virtual-media/src/range/add.ts b/packages/virtual-media/src/range/add.ts
new file mode 100644
index 000000000..3b0688c89
--- /dev/null
+++ b/packages/virtual-media/src/range/add.ts
@@ -0,0 +1,67 @@
+import { compare } from './compare';
+import { Range } from './range';
+
+export function addRange(existingRanges: Array>, add: Range): Array> {
+ if (add.low > add.high) {
+ throw new Error('Inversed range ' + JSON.stringify(add));
+ }
+
+ const updated: Array> = [];
+
+ for (let i = 0; i < existingRanges.length; i++) {
+ const existingRange = existingRanges[i];
+
+ // The "add" spans an equal range as is currently allowed.
+ // Return the current part of existing ranges.
+ if (compare(add.low, existingRange.low) === 0 && compare(existingRange.high, add.high) === 0) {
+ return [
+ existingRange,
+ ];
+ }
+
+ // The "add" spans a range without any overlap with the current part.
+ // Continue to the next part.
+ if (compare(add.low, existingRange.high) > 0) {
+ continue;
+ }
+
+ // The "add" spans a range without any overlap with the current part.
+ // Continue to the next part.
+ if (compare(add.high, existingRange.low) < 0) {
+ continue;
+ }
+
+ // The "add" spans a smaller range, but is fully enclosed withing the current range.
+ // Return the part to add.
+ if (compare(add.low, existingRange.low) > 0 && compare(existingRange.high, add.high) > 0) {
+ return [
+ add,
+ ];
+ }
+
+ // The "add" spans a larger range than is currently allowed, but it fully encloses the current range.
+ // Add the current part of the existing ranges to the updated slice.
+ if (compare(add.low, existingRange.low) < 0 && compare(existingRange.high, add.high) < 0) {
+ updated.push(existingRange);
+ continue;
+ }
+
+ if (compare(add.low, existingRange.low) > 0) {
+ updated.push({
+ low: add.low,
+ high: existingRange.high,
+ });
+ continue;
+ }
+
+ if (compare(add.high, existingRange.high) < 0) {
+ updated.push({
+ low: existingRange.low,
+ high: add.high,
+ });
+ continue;
+ }
+ }
+
+ return updated;
+}
diff --git a/packages/virtual-media/src/range/compare.ts b/packages/virtual-media/src/range/compare.ts
new file mode 100644
index 000000000..cb678e037
--- /dev/null
+++ b/packages/virtual-media/src/range/compare.ts
@@ -0,0 +1,33 @@
+export function compare(a: T, b: T): -1 | 0 | 1 {
+ if ((typeof a) !== (typeof b)) {
+ return 0;
+ }
+
+ if ((typeof a === 'number')) {
+ const r = a - (b as number);
+ if (r < 0) {
+ return -1;
+ }
+
+ if (r > 0) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ if ((typeof a === 'string')) {
+ const r = a.localeCompare(b as string);
+ if (r < 0) {
+ return -1;
+ }
+
+ if (r > 0) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ return 0;
+}
diff --git a/packages/virtual-media/src/range/range.ts b/packages/virtual-media/src/range/range.ts
new file mode 100644
index 000000000..5eeae7c75
--- /dev/null
+++ b/packages/virtual-media/src/range/range.ts
@@ -0,0 +1,4 @@
+export type Range = {
+ low: T,
+ high: T
+};
diff --git a/packages/virtual-media/test/test.mjs b/packages/virtual-media/test/test.mjs
new file mode 100644
index 000000000..1832b1a17
--- /dev/null
+++ b/packages/virtual-media/test/test.mjs
@@ -0,0 +1,28 @@
+import { VirtualMedia, Operator } from '@csstools/virtual-media';
+
+// What with units?
+// Maybe a map of facts per property
+// { px: { low, high }, rem: { low, high } }
+//
+// Or a map of VirtualMedia and a way to share unitless properties?
+//
+// How to deal with "not (width: 300px)"
+
+{
+ const media = new VirtualMedia();
+ console.log(media.width);
+
+ media.mustMatchWidth(10, Operator.GT, 100, Operator.LT);
+ console.log(media.width);
+
+ media.mustMatchWidth(1000, Operator.GT, 10000, Operator.LT);
+ console.log(media.width);
+}
+
+{
+ const media = new VirtualMedia();
+ console.log(media.width);
+
+ media.mustMatchWidth(10, Operator.EQ, 10, Operator.EQ);
+ console.log(media.width);
+}
diff --git a/packages/virtual-media/tsconfig.json b/packages/virtual-media/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/packages/virtual-media/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
From 412d6bee368d1613519acc6d4eabb65eab43a392 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Mon, 17 Oct 2022 19:27:03 +0200
Subject: [PATCH 02/35] wip
---
package-lock.json | 88 +++-
package.json | 2 +-
.../.gitignore | 0
.../.nvmrc | 0
.../CHANGELOG.md | 0
.../LICENSE.md | 0
packages/css-parser-algorithms/README.md | 102 ++++
.../package.json | 6 +-
.../consume-component-block-function.ts | 434 ++++++++++++++++++
packages/css-parser-algorithms/src/index.ts | 4 +
.../src/interfaces/context.ts | 5 +
.../src/interfaces/error.ts | 6 +
...omma-separated-list-of-component-values.ts | 60 +++
.../src/parse/parse-component-value.ts | 41 ++
.../parse/parse-list-of-component-values.ts | 41 ++
.../stryker.conf.json | 0
.../css-parser-algorithms/test/_import.mjs | 11 +
.../css-parser-algorithms/test/_require.cjs | 11 +
.../css-parser-algorithms/test/consume.mjs | 121 +++++
...omma-separated-list-of-component-value.mjs | 38 ++
.../test/parse-component-value.mjs | 32 ++
.../test/parse-list-of-component-value.mjs | 33 ++
packages/css-parser-algorithms/test/parse.mjs | 3 +
packages/css-parser-algorithms/test/test.mjs | 3 +
.../test/util/collect-tokens.mjs | 11 +
.../tsconfig.json | 0
packages/css-parser/README.md | 129 ------
.../consume-component-block-function.ts | 152 ------
packages/css-parser/src/index.ts | 0
packages/css-parser/test/_import.mjs | 5 -
packages/css-parser/test/_require.cjs | 5 -
packages/css-parser/test/test.mjs | 13 -
packages/css-tokenizer/src/index.ts | 2 +-
.../css-tokenizer/src/interfaces/token.ts | 2 +-
.../.gitignore | 0
.../.nvmrc | 0
.../CHANGELOG.md | 0
.../LICENSE.md | 0
.../README.md | 0
.../package.json | 10 +-
.../src/index.ts | 0
.../src/nodes/general-enclosed.ts | 38 ++
.../src/nodes/media-and.ts | 0
.../src/nodes/media-condition-list.ts | 0
.../nodes/media-condition-without-or-or.ts | 0
.../src/nodes/media-condition.ts | 0
.../src/nodes/media-feature-boolean.ts | 0
.../src/nodes/media-feature-comparison.ts | 76 +++
.../src/nodes/media-feature-name.ts | 28 ++
.../src/nodes/media-feature-plain.ts | 38 ++
.../src/nodes/media-feature-range.ts | 214 +++++++++
.../src/nodes/media-feature-value.ts | 38 ++
.../src/nodes/media-feature.ts | 10 +-
.../src/nodes/media-in-parens.ts | 73 +++
.../src/nodes/media-not.ts | 0
.../src/nodes/media-or.ts | 0
.../src/nodes/media-query-modifier.ts | 22 +
.../src/nodes/media-query.ts | 0
.../src/nodes/media-type.ts | 59 +++
.../src/parser/consume/consume-boolean.ts | 0
.../src/parser/consume/consume-plain.ts | 0
.../src/parser/consume/consume-value.ts | 16 +
.../src/parser/parse.ts | 29 ++
.../media-query-list-parser/test/test.mjs | 12 +
.../tsconfig.json | 0
.../src/nodes/general-enclosed.ts | 15 -
.../src/nodes/media-feature-comparison.ts | 49 --
.../src/nodes/media-feature-name.ts | 21 -
.../src/nodes/media-feature-plain.ts | 18 -
.../src/nodes/media-feature-range.ts | 138 ------
.../src/nodes/media-feature-value.ts | 15 -
.../src/nodes/media-in-parens.ts | 29 --
.../src/nodes/media-query-modifier.ts | 4 -
.../src/nodes/media-type.ts | 22 -
.../src/parser/advance/advance.ts | 58 ---
.../src/parser/consume/consume-value.ts | 12 -
.../src/parser/parse.ts | 97 ----
.../test/test.mjs | 3 -
packages/virtual-media/.gitignore | 6 -
packages/virtual-media/.nvmrc | 1 -
packages/virtual-media/CHANGELOG.md | 3 -
packages/virtual-media/LICENSE.md | 20 -
packages/virtual-media/README.md | 1 -
packages/virtual-media/package.json | 63 ---
packages/virtual-media/src/index.ts | 188 --------
packages/virtual-media/src/range/add.ts | 67 ---
packages/virtual-media/src/range/compare.ts | 33 --
packages/virtual-media/src/range/range.ts | 4 -
packages/virtual-media/test/test.mjs | 28 --
packages/virtual-media/tsconfig.json | 9 -
rollup/presets/package-typescript.js | 4 +-
91 files changed, 1697 insertions(+), 1234 deletions(-)
rename packages/{css-parser => css-parser-algorithms}/.gitignore (100%)
rename packages/{css-parser => css-parser-algorithms}/.nvmrc (100%)
rename packages/{css-parser => css-parser-algorithms}/CHANGELOG.md (100%)
rename packages/{css-parser => css-parser-algorithms}/LICENSE.md (100%)
create mode 100644 packages/css-parser-algorithms/README.md
rename packages/{css-parser => css-parser-algorithms}/package.json (92%)
create mode 100644 packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
create mode 100644 packages/css-parser-algorithms/src/index.ts
create mode 100644 packages/css-parser-algorithms/src/interfaces/context.ts
create mode 100644 packages/css-parser-algorithms/src/interfaces/error.ts
create mode 100644 packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts
create mode 100644 packages/css-parser-algorithms/src/parse/parse-component-value.ts
create mode 100644 packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts
rename packages/{css-parser => css-parser-algorithms}/stryker.conf.json (100%)
create mode 100644 packages/css-parser-algorithms/test/_import.mjs
create mode 100644 packages/css-parser-algorithms/test/_require.cjs
create mode 100644 packages/css-parser-algorithms/test/consume.mjs
create mode 100644 packages/css-parser-algorithms/test/parse-comma-separated-list-of-component-value.mjs
create mode 100644 packages/css-parser-algorithms/test/parse-component-value.mjs
create mode 100644 packages/css-parser-algorithms/test/parse-list-of-component-value.mjs
create mode 100644 packages/css-parser-algorithms/test/parse.mjs
create mode 100644 packages/css-parser-algorithms/test/test.mjs
create mode 100644 packages/css-parser-algorithms/test/util/collect-tokens.mjs
rename packages/{css-parser => css-parser-algorithms}/tsconfig.json (100%)
delete mode 100644 packages/css-parser/README.md
delete mode 100644 packages/css-parser/src/consume/consume-component-block-function.ts
delete mode 100644 packages/css-parser/src/index.ts
delete mode 100644 packages/css-parser/test/_import.mjs
delete mode 100644 packages/css-parser/test/_require.cjs
delete mode 100644 packages/css-parser/test/test.mjs
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/.gitignore (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/.nvmrc (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/CHANGELOG.md (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/LICENSE.md (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/README.md (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/package.json (85%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/index.ts (100%)
create mode 100644 packages/media-query-list-parser/src/nodes/general-enclosed.ts
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-and.ts (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-condition-list.ts (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-condition-without-or-or.ts (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-condition.ts (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-feature-boolean.ts (100%)
create mode 100644 packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
create mode 100644 packages/media-query-list-parser/src/nodes/media-feature-name.ts
create mode 100644 packages/media-query-list-parser/src/nodes/media-feature-plain.ts
create mode 100644 packages/media-query-list-parser/src/nodes/media-feature-range.ts
create mode 100644 packages/media-query-list-parser/src/nodes/media-feature-value.ts
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-feature.ts (56%)
create mode 100644 packages/media-query-list-parser/src/nodes/media-in-parens.ts
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-not.ts (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-or.ts (100%)
create mode 100644 packages/media-query-list-parser/src/nodes/media-query-modifier.ts
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/nodes/media-query.ts (100%)
create mode 100644 packages/media-query-list-parser/src/nodes/media-type.ts
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/parser/consume/consume-boolean.ts (100%)
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/src/parser/consume/consume-plain.ts (100%)
create mode 100644 packages/media-query-list-parser/src/parser/consume/consume-value.ts
create mode 100644 packages/media-query-list-parser/src/parser/parse.ts
create mode 100644 packages/media-query-list-parser/test/test.mjs
rename packages/{postcss-media-query-list-parser => media-query-list-parser}/tsconfig.json (100%)
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/nodes/media-type.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/parser/advance/advance.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts
delete mode 100644 packages/postcss-media-query-list-parser/src/parser/parse.ts
delete mode 100644 packages/postcss-media-query-list-parser/test/test.mjs
delete mode 100644 packages/virtual-media/.gitignore
delete mode 100644 packages/virtual-media/.nvmrc
delete mode 100644 packages/virtual-media/CHANGELOG.md
delete mode 100644 packages/virtual-media/LICENSE.md
delete mode 100644 packages/virtual-media/README.md
delete mode 100644 packages/virtual-media/package.json
delete mode 100644 packages/virtual-media/src/index.ts
delete mode 100644 packages/virtual-media/src/range/add.ts
delete mode 100644 packages/virtual-media/src/range/compare.ts
delete mode 100644 packages/virtual-media/src/range/range.ts
delete mode 100644 packages/virtual-media/test/test.mjs
delete mode 100644 packages/virtual-media/tsconfig.json
diff --git a/package-lock.json b/package-lock.json
index c93065d75..2cb713f40 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,8 @@
"version": "1.0.0",
"license": "CC0-1.0",
"workspaces": [
+ "packages/css-tokenizer",
+ "packages/css-parser-algorithms",
"packages/*",
"plugins/postcss-progressive-custom-properties",
"plugins/*",
@@ -1796,6 +1798,10 @@
"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
@@ -1808,6 +1814,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
@@ -1848,10 +1858,6 @@
"resolved": "plugins/postcss-is-pseudo-class",
"link": true
},
- "node_modules/@csstools/postcss-media-query-list-parser": {
- "resolved": "packages/postcss-media-query-list-parser",
- "link": true
- },
"node_modules/@csstools/postcss-nested-calc": {
"resolved": "plugins/postcss-nested-calc",
"link": true
@@ -1892,10 +1898,6 @@
"resolved": "packages/selector-specificity",
"link": true
},
- "node_modules/@csstools/virtual-media": {
- "resolved": "packages/virtual-media",
- "link": true
- },
"node_modules/@eslint/eslintrc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
@@ -6825,6 +6827,37 @@
"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",
@@ -6852,8 +6885,26 @@
"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"
@@ -6900,7 +6951,9 @@
}
},
"packages/virtual-media": {
+ "name": "@csstools/virtual-media",
"version": "1.0.0",
+ "extraneous": true,
"license": "MIT",
"engines": {
"node": "^14 || ^16 || >=18"
@@ -8917,6 +8970,12 @@
"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"
},
@@ -8968,6 +9027,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": {}
@@ -9037,9 +9103,6 @@
"puppeteer": "^18.0.0"
}
},
- "@csstools/postcss-media-query-list-parser": {
- "version": "file:packages/postcss-media-query-list-parser"
- },
"@csstools/postcss-nested-calc": {
"version": "file:plugins/postcss-nested-calc",
"requires": {
@@ -9102,9 +9165,6 @@
"postcss-selector-parser": "^6.0.10"
}
},
- "@csstools/virtual-media": {
- "version": "file:packages/virtual-media"
- },
"@eslint/eslintrc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
diff --git a/package.json b/package.json
index ededd4c7b..6f42b9ecf 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
},
"workspaces": [
"packages/css-tokenizer",
- "packages/css-parser",
+ "packages/css-parser-algorithms",
"packages/*",
"plugins/postcss-progressive-custom-properties",
"plugins/*",
diff --git a/packages/css-parser/.gitignore b/packages/css-parser-algorithms/.gitignore
similarity index 100%
rename from packages/css-parser/.gitignore
rename to packages/css-parser-algorithms/.gitignore
diff --git a/packages/css-parser/.nvmrc b/packages/css-parser-algorithms/.nvmrc
similarity index 100%
rename from packages/css-parser/.nvmrc
rename to packages/css-parser-algorithms/.nvmrc
diff --git a/packages/css-parser/CHANGELOG.md b/packages/css-parser-algorithms/CHANGELOG.md
similarity index 100%
rename from packages/css-parser/CHANGELOG.md
rename to packages/css-parser-algorithms/CHANGELOG.md
diff --git a/packages/css-parser/LICENSE.md b/packages/css-parser-algorithms/LICENSE.md
similarity index 100%
rename from packages/css-parser/LICENSE.md
rename to packages/css-parser-algorithms/LICENSE.md
diff --git a/packages/css-parser-algorithms/README.md b/packages/css-parser-algorithms/README.md
new file mode 100644
index 000000000..ff2753b4d
--- /dev/null
+++ b/packages/css-parser-algorithms/README.md
@@ -0,0 +1,102 @@
+# 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)
+
+### 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
diff --git a/packages/css-parser/package.json b/packages/css-parser-algorithms/package.json
similarity index 92%
rename from packages/css-parser/package.json
rename to packages/css-parser-algorithms/package.json
index d495fdd38..2db452189 100644
--- a/packages/css-parser/package.json
+++ b/packages/css-parser-algorithms/package.json
@@ -1,5 +1,5 @@
{
- "name": "@csstools/css-parser",
+ "name": "@csstools/css-parser-algorithms",
"description": "Parse CSS",
"version": "1.0.0",
"contributors": [
@@ -51,11 +51,11 @@
"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-parser#readme",
+ "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"
+ "directory": "packages/css-parser-algorithms"
},
"bugs": "https://github.com/csstools/postcss-plugins/issues",
"keywords": [
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..bb9276ba3
--- /dev/null
+++ b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
@@ -0,0 +1,434 @@
+import { CSSToken, mirrorVariantType, stringify, TokenType, isToken, TokenFunction } from '@csstools/css-tokenizer';
+import { Context } from '../interfaces/context';
+
+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 i = 0;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = tokens[i];
+ if (
+ token[0] === TokenType.OpenParen ||
+ token[0] === TokenType.OpenCurly ||
+ token[0] === TokenType.OpenSquare
+ ) {
+ const r = consumeSimpleBlock(ctx, tokens.slice(i));
+ return {
+ advance: r.advance + i,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Function) {
+ const r = consumeFunction(ctx, tokens.slice(i));
+ return {
+ advance: r.advance + i,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Whitespace) {
+ const r = consumeWhitespace(ctx, tokens.slice(i));
+ return {
+ advance: r.advance + i,
+ node: r.node,
+ };
+ }
+
+ if (token[0] === TokenType.Comment) {
+ const r = consumeComment(ctx, tokens.slice(i));
+ return {
+ advance: r.advance + i,
+ node: r.node,
+ };
+ }
+
+ return {
+ advance: i + 1,
+ node: new TokenNode(token),
+ };
+ }
+}
+
+export class FunctionNode {
+ type = 'function';
+
+ name: TokenFunction;
+ endToken: CSSToken;
+ value: Array;
+
+ constructor(name: TokenFunction, endToken: CSSToken, value: Array) {
+ this.name = name;
+ this.endToken = endToken;
+ this.value = value;
+ }
+
+ get nameTokenValue(): string {
+ return this.name[4].value;
+ }
+
+ tokens() {
+ return [
+ this.name,
+ ...this.value.flatMap((x) => {
+ if (isToken(x)) {
+ return x;
+ }
+
+ return x.tokens();
+ }),
+ this.endToken,
+ ];
+ }
+
+ toString() {
+ const valueString = this.value.map((x) => {
+ if (isToken(x)) {
+ return stringify(x);
+ }
+
+ return x.toString();
+ }).join('');
+
+ return stringify(this.name) + valueString + stringify(this.endToken);
+ }
+
+ walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number) => boolean) {
+ 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;
+ }
+ }
+}
+
+// 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 = 'simple-block';
+
+ startToken: CSSToken;
+ endToken: CSSToken;
+ value: Array;
+
+ constructor(startToken: CSSToken, endToken: CSSToken, value: Array) {
+ this.startToken = startToken;
+ this.endToken = endToken;
+ this.value = value;
+ }
+
+ tokens() {
+ return [
+ this.startToken,
+ ...this.value.flatMap((x) => {
+ if (isToken(x)) {
+ return x;
+ }
+
+ return x.tokens();
+ }),
+ this.endToken,
+ ];
+ }
+
+ toString() {
+ const valueString = this.value.map((x) => {
+ if (isToken(x)) {
+ return stringify(x);
+ }
+
+ return x.toString();
+ }).join('');
+
+ return stringify(this.startToken) + valueString + stringify(this.endToken);
+ }
+
+ walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number) => boolean) {
+ 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;
+ }
+ }
+}
+
+/** 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 = 'whitespace';
+
+ value: Array;
+
+ constructor(value: Array) {
+ this.value = value;
+ }
+
+ tokens() {
+ return this.value;
+ }
+
+ toString() {
+ return stringify(...this.value);
+ }
+}
+
+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 = 'comment';
+
+ value: CSSToken;
+
+ constructor(value: CSSToken) {
+ this.value = value;
+ }
+
+ tokens() {
+ return [
+ this.value,
+ ];
+ }
+
+ toString() {
+ return stringify(this.value);
+ }
+}
+
+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 = 'token';
+
+ value: CSSToken;
+
+ constructor(value: CSSToken) {
+ this.value = value;
+ }
+
+ tokens() {
+ return [
+ this.value,
+ ];
+ }
+
+ toString() {
+ return stringify(this.value);
+ }
+}
+
+export class UnclosedFunctionNode {
+ type = 'unclosed-function';
+
+ value: Array;
+
+ constructor(value: Array) {
+ this.value = value;
+ }
+
+ tokens() {
+ return this.value;
+ }
+
+ toString() {
+ return stringify(...this.value);
+ }
+}
+
+export class UnclosedSimpleBlockNode {
+ type = 'unclosed-simple-block';
+
+ value: Array;
+
+ constructor(value: Array) {
+ this.value = value;
+ }
+
+ tokens() {
+ return this.value;
+ }
+
+ toString() {
+ return stringify(...this.value);
+ }
+}
diff --git a/packages/css-parser-algorithms/src/index.ts b/packages/css-parser-algorithms/src/index.ts
new file mode 100644
index 000000000..b88beecea
--- /dev/null
+++ b/packages/css-parser-algorithms/src/index.ts
@@ -0,0 +1,4 @@
+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';
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..fd138691d
--- /dev/null
+++ b/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts
@@ -0,0 +1,60 @@
+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..b837b04cd
--- /dev/null
+++ b/packages/css-parser-algorithms/src/parse/parse-component-value.ts
@@ -0,0 +1,41 @@
+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..4c167ddc1
--- /dev/null
+++ b/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts
@@ -0,0 +1,41 @@
+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/stryker.conf.json b/packages/css-parser-algorithms/stryker.conf.json
similarity index 100%
rename from packages/css-parser/stryker.conf.json
rename to packages/css-parser-algorithms/stryker.conf.json
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/consume.mjs b/packages/css-parser-algorithms/test/consume.mjs
new file mode 100644
index 000000000..30aa0a8f6
--- /dev/null
+++ b/packages/css-parser-algorithms/test/consume.mjs
@@ -0,0 +1,121 @@
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from './util/collect-tokens.mjs';
+import { consumeFunction, consumeSimpleBlock } from '@csstools/css-parser-algorithms';
+
+{
+ const testCases = [
+ {
+ css: '[10px]',
+ advance: 3,
+ },
+ {
+ css: '(calc(10px))',
+ advance: 5,
+ },
+ {
+ css: '(calc(10px) )',
+ advance: 6,
+ },
+ {
+ css: '(calc(10px)/* a comment */)',
+ advance: 6,
+ },
+ {
+ css: '((calc(10px)/* a comment */) (other))',
+ advance: 12,
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const t = tokenizer({
+ css: testCase.css,
+ }, { commentsAreTokens: true });
+
+ const tokens = collectTokens(t);
+ const ctx = {
+ onParseError: ((err) => {
+ throw new Error(JSON.stringify(err));
+ }),
+ };
+
+ const result = consumeSimpleBlock(ctx, tokens);
+
+ assert.deepEqual(
+ result.advance,
+ testCase.advance,
+ );
+
+ assert.deepEqual(
+ tokens[result.advance][0],
+ TokenType.EOF,
+ );
+
+ assert.deepEqual(
+ result.node.toString(),
+ testCase.css,
+ );
+
+ assert.deepEqual(
+ result.node.tokens(),
+ tokens.slice(0, -1),
+ );
+ }
+}
+
+{
+ const testCases = [
+ {
+ css: 'calc(10px)',
+ advance: 3,
+ },
+ {
+ css: 'calc(10px )',
+ advance: 4,
+ },
+ {
+ css: 'calc(10px/* a comment */)',
+ advance: 4,
+ },
+ {
+ css: 'calc(10px, /* a comment */, (other calc(more)))',
+ advance: 15,
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const t = tokenizer({
+ css: testCase.css,
+ }, { commentsAreTokens: true });
+
+ const tokens = collectTokens(t);
+
+ const ctx = {
+ onParseError: ((err) => {
+ throw new Error(JSON.stringify(err));
+ }),
+ };
+
+ const result = consumeFunction(ctx, tokens);
+
+ assert.deepEqual(
+ result.advance,
+ testCase.advance,
+ );
+
+ assert.deepEqual(
+ tokens[result.advance][0],
+ TokenType.EOF,
+ );
+
+ assert.deepEqual(
+ result.node.toString(),
+ testCase.css,
+ );
+
+ assert.deepEqual(
+ result.node.tokens(),
+ tokens.slice(0, -1),
+ );
+ }
+}
diff --git a/packages/css-parser-algorithms/test/parse-comma-separated-list-of-component-value.mjs b/packages/css-parser-algorithms/test/parse-comma-separated-list-of-component-value.mjs
new file mode 100644
index 000000000..3c8417a11
--- /dev/null
+++ b/packages/css-parser-algorithms/test/parse-comma-separated-list-of-component-value.mjs
@@ -0,0 +1,38 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from './util/collect-tokens.mjs';
+import { parseCommaSeparatedListOfComponentValues } from '@csstools/css-parser-algorithms';
+
+{
+ const testCases = [
+ {
+ css: '(10px,12px),(10px)',
+ result: [['(10px,12px)'], ['(10px)']],
+ },
+
+ {
+ css: '(10px,12px),10px 10px',
+ result: [['(10px,12px)'], ['10px', ' ', '10px']],
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const t = tokenizer({
+ css: testCase.css,
+ }, { commentsAreTokens: true });
+
+ const tokens = collectTokens(t);
+ const options = {
+ onParseError: ((err) => {
+ throw new Error(JSON.stringify(err));
+ }),
+ };
+
+ const result = parseCommaSeparatedListOfComponentValues(tokens, options);
+
+ assert.deepEqual(
+ result.map((x) => x.map((y) => y.toString())),
+ testCase.result,
+ );
+ }
+}
diff --git a/packages/css-parser-algorithms/test/parse-component-value.mjs b/packages/css-parser-algorithms/test/parse-component-value.mjs
new file mode 100644
index 000000000..e9cd6aa2b
--- /dev/null
+++ b/packages/css-parser-algorithms/test/parse-component-value.mjs
@@ -0,0 +1,32 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from './util/collect-tokens.mjs';
+import { parseComponentValue } from '@csstools/css-parser-algorithms';
+
+{
+ const testCases = [
+ {
+ css: '(calc(10px */* a comment */5))',
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const t = tokenizer({
+ css: testCase.css,
+ }, { commentsAreTokens: true });
+
+ const tokens = collectTokens(t);
+ const options = {
+ onParseError: ((err) => {
+ throw new Error(JSON.stringify(err));
+ }),
+ };
+
+ const result = parseComponentValue(tokens, options);
+
+ assert.deepEqual(
+ result.toString(),
+ testCase.css,
+ );
+ }
+}
diff --git a/packages/css-parser-algorithms/test/parse-list-of-component-value.mjs b/packages/css-parser-algorithms/test/parse-list-of-component-value.mjs
new file mode 100644
index 000000000..d417d8490
--- /dev/null
+++ b/packages/css-parser-algorithms/test/parse-list-of-component-value.mjs
@@ -0,0 +1,33 @@
+import { tokenizer } from '@csstools/css-tokenizer';
+import assert from 'assert';
+import { collectTokens } from './util/collect-tokens.mjs';
+import { parseListOfComponentValues } from '@csstools/css-parser-algorithms';
+
+{
+ const testCases = [
+ {
+ css: '10px 15px',
+ result: ['10px', ' ', '15px'],
+ },
+ ];
+
+ for (const testCase of testCases) {
+ const t = tokenizer({
+ css: testCase.css,
+ }, { commentsAreTokens: true });
+
+ const tokens = collectTokens(t);
+ const options = {
+ onParseError: ((err) => {
+ throw new Error(JSON.stringify(err));
+ }),
+ };
+
+ const result = parseListOfComponentValues(tokens, options);
+
+ assert.deepEqual(
+ result.map((x) => x.toString()),
+ testCase.result,
+ );
+ }
+}
diff --git a/packages/css-parser-algorithms/test/parse.mjs b/packages/css-parser-algorithms/test/parse.mjs
new file mode 100644
index 000000000..c776b8120
--- /dev/null
+++ b/packages/css-parser-algorithms/test/parse.mjs
@@ -0,0 +1,3 @@
+import './parse-component-value.mjs';
+import './parse-list-of-component-value.mjs';
+import './parse-comma-separated-list-of-component-value.mjs';
diff --git a/packages/css-parser-algorithms/test/test.mjs b/packages/css-parser-algorithms/test/test.mjs
new file mode 100644
index 000000000..1f4f59b9c
--- /dev/null
+++ b/packages/css-parser-algorithms/test/test.mjs
@@ -0,0 +1,3 @@
+import './consume.mjs';
+import './parse.mjs';
+
diff --git a/packages/css-parser-algorithms/test/util/collect-tokens.mjs b/packages/css-parser-algorithms/test/util/collect-tokens.mjs
new file mode 100644
index 000000000..5ab1c998b
--- /dev/null
+++ b/packages/css-parser-algorithms/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-parser/tsconfig.json b/packages/css-parser-algorithms/tsconfig.json
similarity index 100%
rename from packages/css-parser/tsconfig.json
rename to packages/css-parser-algorithms/tsconfig.json
diff --git a/packages/css-parser/README.md b/packages/css-parser/README.md
deleted file mode 100644
index a9dc1b02e..000000000
--- a/packages/css-parser/README.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# CSS Parser
-
-[
][npm-url]
-[
][cli-url]
-[
][discord]
-
-Implemented from : https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/
-
-## Usage
-
-Add [CSS Parser] to your project:
-
-```bash
-npm install postcss @csstools/css-parser --save-dev
-```
-
-```js
-import { tokenizer, TokenType } from '@csstools/css-parser';
-
-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 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
-
-[CSS Parser]: https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser
diff --git a/packages/css-parser/src/consume/consume-component-block-function.ts b/packages/css-parser/src/consume/consume-component-block-function.ts
deleted file mode 100644
index e5d1dc408..000000000
--- a/packages/css-parser/src/consume/consume-component-block-function.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { CSSToken, mirrorVariant, stringify, TokenType, isToken, TokenIdent, TokenFunction } from '@csstools/css-tokenizer';
-
-export type ComponentValue = FunctionNode | SimpleBlockNode | CSSToken;
-
-export class ComponentValueNode {
- type = 'component-value';
-
- value: ComponentValue;
- before: Array;
- after: Array;
-
- constructor(value: ComponentValue, before: Array, after: Array) {
- this.value = value;
- this.before = before;
- this.after = after;
- }
-
- toString() {
- if (isToken(this.value)) {
- return stringify(
- ...this.before,
- this.value,
- ...this.after,
- );
- }
-
- return stringify(...this.before) + this.value.toString() + stringify(...this.after);
- }
-}
-
-// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
-export function consumeComponentValue(tokens: Array): { advance: number, node: ComponentValue } {
- for (let i = 0; i < tokens.length; i++) {
- const token = tokens[i];
- if (
- token[0] === TokenType.OpenParen ||
- token[0] === TokenType.OpenCurly ||
- token[0] === TokenType.OpenSquare
- ) {
- const r = consumeSimpleBlock(tokens.slice(i));
- return {
- advance: r.advance + i,
- node: r.node,
- };
- }
-
- if (token[0] === TokenType.Function) {
- const r = consumeFunction(tokens.slice(i));
- return {
- advance: r.advance + i,
- node: r.node,
- };
- }
-
- return {
- advance: i,
- node: token,
- };
- }
-}
-
-export class FunctionNode {
- type = 'function';
-
- name: TokenFunction;
- value: Array;
- after: Array;
-
- constructor(name: TokenFunction, value: Array, after: Array) {
- this.name = name;
- this.value = value;
- this.after = after;
- }
-
- get nameTokenValue(): string {
- return this.name[4].value;
- }
-
- toString() {
- return stringify(this.name) + this.value.map((x) => x.toString()).join('') + stringify(...this.after);
- }
-}
-
-// https://www.w3.org/TR/css-syntax-3/#consume-function
-export function consumeFunction(tokens: Array): { advance: number, node: FunctionNode } {
- const valueList: Array = [];
- let lastValueIndex = -1;
-
- for (let i = 1; i < tokens.length; i++) {
- const token = tokens[i];
-
- if (token[0] === TokenType.CloseParen) {
- return {
- advance: i,
- node: new FunctionNode(
- tokens[0] as TokenFunction,
- valueList,
- tokens.slice(lastValueIndex, i+1),
- ),
- };
- }
-
- const r = consumeComponentValue(tokens.slice(i));
- i += r.advance;
- lastValueIndex = i;
- valueList.push(r.node);
- }
-
- throw new Error('Failed to parse');
-}
-
-export class SimpleBlockNode {
- type = 'simple-block';
-
- name: Array;
- value: Array;
-
- constructor(name: Array, value: Array) {
- this.name = name;
- this.value = value;
- }
-
- get nameIdentIndex(): number {
- return this.name.findIndex((x) => {
- return x[0] === TokenType.Ident;
- });
- }
-
- toString() {
- return stringify(...this.name) + this.value.map((x) => x.toString()).join('');
- }
-}
-
-/** https://www.w3.org/TR/css-syntax-3/#consume-simple-block */
-export function consumeSimpleBlock(tokens: Array): { advance: number, node: SimpleBlockNode } {
- const endingToken = mirrorVariant(tokens[0][0]);
- if (!endingToken) {
- throw new Error('Failed to parse');
- }
-
- for (let i = 1; i < tokens.length; i++) {
- const token = tokens[i];
-
- if (token[0] === endingToken) {
- return i;
- }
-
- i += consumeComponentValue(tokens.slice(i));
- }
-
- throw new Error('Failed to parse');
-}
diff --git a/packages/css-parser/src/index.ts b/packages/css-parser/src/index.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/packages/css-parser/test/_import.mjs b/packages/css-parser/test/_import.mjs
deleted file mode 100644
index c1c61bf2b..000000000
--- a/packages/css-parser/test/_import.mjs
+++ /dev/null
@@ -1,5 +0,0 @@
-import { tokenizer } from '@csstools/css-tokenizer';
-
-tokenizer({
- css: '.some { css: ""; }',
-});
diff --git a/packages/css-parser/test/_require.cjs b/packages/css-parser/test/_require.cjs
deleted file mode 100644
index abc74a96e..000000000
--- a/packages/css-parser/test/_require.cjs
+++ /dev/null
@@ -1,5 +0,0 @@
-const { tokenizer } = require('@csstools/css-tokenizer');
-
-tokenizer({
- css: '.some { css: ""; }',
-});
diff --git a/packages/css-parser/test/test.mjs b/packages/css-parser/test/test.mjs
deleted file mode 100644
index 71d21591a..000000000
--- a/packages/css-parser/test/test.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-// 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/src/index.ts b/packages/css-tokenizer/src/index.ts
index 992bdf3bc..d199143ce 100644
--- a/packages/css-tokenizer/src/index.ts
+++ b/packages/css-tokenizer/src/index.ts
@@ -1,6 +1,6 @@
export type { CSSToken } from './interfaces/token';
export { Reader } from './reader';
-export { TokenType, NumberType, mirrorVariant, isToken } from './interfaces/token';
+export { TokenType, NumberType, mirrorVariantType, isToken } from './interfaces/token';
export { stringify } from './stringify';
export { tokenizer } from './tokenizer';
diff --git a/packages/css-tokenizer/src/interfaces/token.ts b/packages/css-tokenizer/src/interfaces/token.ts
index 90d259f01..81a916036 100644
--- a/packages/css-tokenizer/src/interfaces/token.ts
+++ b/packages/css-tokenizer/src/interfaces/token.ts
@@ -133,7 +133,7 @@ export type Token = [
U,
]
-export function mirrorVariant(type: TokenType): TokenType|null {
+export function mirrorVariantType(type: TokenType): TokenType|null {
switch (type) {
case TokenType.OpenParen:
return TokenType.CloseParen;
diff --git a/packages/postcss-media-query-list-parser/.gitignore b/packages/media-query-list-parser/.gitignore
similarity index 100%
rename from packages/postcss-media-query-list-parser/.gitignore
rename to packages/media-query-list-parser/.gitignore
diff --git a/packages/postcss-media-query-list-parser/.nvmrc b/packages/media-query-list-parser/.nvmrc
similarity index 100%
rename from packages/postcss-media-query-list-parser/.nvmrc
rename to packages/media-query-list-parser/.nvmrc
diff --git a/packages/postcss-media-query-list-parser/CHANGELOG.md b/packages/media-query-list-parser/CHANGELOG.md
similarity index 100%
rename from packages/postcss-media-query-list-parser/CHANGELOG.md
rename to packages/media-query-list-parser/CHANGELOG.md
diff --git a/packages/postcss-media-query-list-parser/LICENSE.md b/packages/media-query-list-parser/LICENSE.md
similarity index 100%
rename from packages/postcss-media-query-list-parser/LICENSE.md
rename to packages/media-query-list-parser/LICENSE.md
diff --git a/packages/postcss-media-query-list-parser/README.md b/packages/media-query-list-parser/README.md
similarity index 100%
rename from packages/postcss-media-query-list-parser/README.md
rename to packages/media-query-list-parser/README.md
diff --git a/packages/postcss-media-query-list-parser/package.json b/packages/media-query-list-parser/package.json
similarity index 85%
rename from packages/postcss-media-query-list-parser/package.json
rename to packages/media-query-list-parser/package.json
index c1f53a38c..c400c28d4 100644
--- a/packages/postcss-media-query-list-parser/package.json
+++ b/packages/media-query-list-parser/package.json
@@ -1,5 +1,5 @@
{
- "name": "@csstools/postcss-media-query-list-parser",
+ "name": "@csstools/media-query-list-parser",
"description": "Tokenize CSS",
"version": "1.0.0",
"contributors": [
@@ -46,17 +46,21 @@
"prepublishOnly": "npm run clean && npm run build && npm run test",
"test": "node ./test/test.mjs"
},
- "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/postcss-media-query-list-parser#readme",
+ "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/postcss-media-query-list-parser"
+ "directory": "packages/media-query-list-parser"
},
"bugs": "https://github.com/csstools/postcss-plugins/issues",
"keywords": [
"css",
"tokenizer"
],
+ "dependencies": {
+ "@csstools/css-tokenizer": "^1.0.0",
+ "@csstools/css-parser-algorithms": "^1.0.0"
+ },
"volta": {
"extends": "../../package.json"
}
diff --git a/packages/postcss-media-query-list-parser/src/index.ts b/packages/media-query-list-parser/src/index.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/index.ts
rename to packages/media-query-list-parser/src/index.ts
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..bc0c4ca81
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
@@ -0,0 +1,38 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { isToken, stringify } from '@csstools/css-tokenizer';
+
+export class GeneralEnclosed {
+ type = 'general-enclosed';
+
+ value: ComponentValue;
+
+ constructor(value: ComponentValue) {
+ this.value = value;
+ }
+
+ tokens() {
+ if (isToken(this.value)) {
+ return this.value;
+ }
+
+ return this.value.tokens();
+ }
+
+ toString() {
+ if (isToken(this.value)) {
+ return stringify(this.value);
+ }
+
+ return this.value.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue, parent: ContainerNode | GeneralEnclosed }, index: number) => boolean) {
+ if (cb({ node: this.value, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-and.ts b/packages/media-query-list-parser/src/nodes/media-and.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-and.ts
rename to packages/media-query-list-parser/src/nodes/media-and.ts
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-condition-list.ts b/packages/media-query-list-parser/src/nodes/media-condition-list.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-condition-list.ts
rename to packages/media-query-list-parser/src/nodes/media-condition-list.ts
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-condition-without-or-or.ts b/packages/media-query-list-parser/src/nodes/media-condition-without-or-or.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-condition-without-or-or.ts
rename to packages/media-query-list-parser/src/nodes/media-condition-without-or-or.ts
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-condition.ts b/packages/media-query-list-parser/src/nodes/media-condition.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-condition.ts
rename to packages/media-query-list-parser/src/nodes/media-condition.ts
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-boolean.ts b/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-feature-boolean.ts
rename to packages/media-query-list-parser/src/nodes/media-feature-boolean.ts
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..79a2ef9f4
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
@@ -0,0 +1,76 @@
+import { 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 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..f15ccbde0
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -0,0 +1,28 @@
+import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { isToken, stringify } from '@csstools/css-tokenizer';
+
+export class MediaFeatureName {
+ type = 'mf-name';
+
+ value: ComponentValue;
+
+ constructor(value: ComponentValue) {
+ this.value = value;
+ }
+
+ tokens() {
+ if (isToken(this.value)) {
+ return this.value;
+ }
+
+ return this.value.tokens();
+ }
+
+ toString() {
+ if (isToken(this.value)) {
+ return stringify(this.value);
+ }
+
+ return this.value.toString();
+ }
+}
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..90fd309e4
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -0,0 +1,38 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { stringify, TokenColon } from '@csstools/css-tokenizer';
+import { MediaFeatureName } from './media-feature-name';
+import { MediaFeatureValue } from './media-feature-value';
+
+export class MediaFeaturePlain {
+ type = 'mf-plain';
+
+ name: MediaFeatureName;
+ colon: TokenColon;
+ value: MediaFeatureValue;
+
+ constructor(name: MediaFeatureName, colon: TokenColon, value: MediaFeatureValue) {
+ this.name = name;
+ this.colon = colon;
+ this.value = value;
+ }
+
+ tokens() {
+ return [
+ ...this.name.tokens(),
+ this.colon,
+ ...this.value.tokens(),
+ ];
+ }
+
+ toString() {
+ return this.name.toString() + stringify(this.colon) + this.value.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeaturePlain | MediaFeatureValue }, index: number) => boolean) {
+ if (cb({ node: this.value, parent: this }, 0) === false) {
+ return false;
+ }
+
+ return this.value.walk(cb);
+ }
+}
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..df9cdf053
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -0,0 +1,214 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { stringify, TokenDelim } from '@csstools/css-tokenizer';
+import { comparisonFromTokens } from './media-feature-comparison';
+import { MediaFeatureName } from './media-feature-name';
+import { MediaFeatureValue } from './media-feature-value';
+
+export type MediaFeatureRange = MediaFeatureRangeNameValue |
+ MediaFeatureRangeValueName |
+ MediaFeatureRangeLowHigh |
+ MediaFeatureRangeHighLow;
+
+export class MediaFeatureRangeNameValue {
+ type = 'mf-range-name-value';
+
+ 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() {
+ return [
+ ...this.name.tokens(),
+ ...this.operator,
+ ...this.value.tokens(),
+ ];
+ }
+
+ toString() {
+ return this.name.toString() + stringify(...this.operator) + this.value.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
+ if (cb({ node: this.value, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+}
+
+export class MediaFeatureRangeValueName {
+ type = 'mf-range-value-range';
+
+ 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() {
+ return [
+ ...this.value.tokens(),
+ ...this.operator,
+ ...this.name.tokens(),
+ ];
+ }
+
+ toString() {
+ return this.value.toString() + stringify(...this.operator) + this.name.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
+ if (cb({ node: this.value, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+}
+
+export class MediaFeatureRangeLowHigh {
+ type = 'mf-range-low-high';
+
+ name: MediaFeatureName;
+ low: MediaFeatureValue;
+ lowOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+ high: MediaFeatureValue;
+ highOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+
+ constructor(name: MediaFeatureName, low: MediaFeatureValue, lowOperator: [TokenDelim, TokenDelim] | [TokenDelim], high: MediaFeatureValue, highOperator: [TokenDelim, TokenDelim] | [TokenDelim]) {
+ this.name = name;
+ this.low = low;
+ this.lowOperator = lowOperator;
+ this.high = high;
+ this.highOperator = highOperator;
+ }
+
+ lowOperatorKind() {
+ return comparisonFromTokens(this.lowOperator);
+ }
+
+ highOperatorKind() {
+ return comparisonFromTokens(this.highOperator);
+ }
+
+ tokens() {
+ return [
+ ...this.low.tokens(),
+ ...this.lowOperator,
+ ...this.name.tokens(),
+ ...this.highOperator,
+ ...this.high.tokens(),
+ ];
+ }
+
+ toString() {
+ return this.low.toString() + stringify(...this.lowOperator) + this.name.toString() + stringify(...this.highOperator) + this.high.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
+ if (cb({ node: this.low, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.low) {
+ if (this.low.walk(cb) === false) {
+ return false;
+ }
+ }
+
+ if (cb({ node: this.high, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.high) {
+ if (this.high.walk(cb) === false) {
+ return false;
+ }
+ }
+ }
+}
+
+export class MediaFeatureRangeHighLow {
+ type = 'mf-range-high-low';
+
+ name: MediaFeatureName;
+ low: MediaFeatureValue;
+ lowOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+ high: MediaFeatureValue;
+ highOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+
+ constructor(name: MediaFeatureName, high: MediaFeatureValue, highOperator: [TokenDelim, TokenDelim] | [TokenDelim], low: MediaFeatureValue, lowOperator: [TokenDelim, TokenDelim] | [TokenDelim]) {
+ this.name = name;
+ this.low = low;
+ this.lowOperator = lowOperator;
+ this.high = high;
+ this.highOperator = highOperator;
+ }
+
+ lowOperatorKind() {
+ return comparisonFromTokens(this.lowOperator);
+ }
+
+ highOperatorKind() {
+ return comparisonFromTokens(this.highOperator);
+ }
+
+ tokens() {
+ return [
+ ...this.high.tokens(),
+ ...this.highOperator,
+ ...this.name.tokens(),
+ ...this.lowOperator,
+ ...this.low.tokens(),
+ ];
+ }
+
+ toString() {
+ return this.high.toString() + stringify(...this.highOperator) + this.name.toString() + stringify(...this.lowOperator) + this.low.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
+ if (cb({ node: this.high, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.high) {
+ if (this.high.walk(cb) === false) {
+ return false;
+ }
+ }
+
+ if (cb({ node: this.low, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.low) {
+ if (this.low.walk(cb) === false) {
+ return false;
+ }
+ }
+ }
+}
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..61e05380d
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
@@ -0,0 +1,38 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { isToken, stringify } from '@csstools/css-tokenizer';
+
+export class MediaFeatureValue {
+ type = 'mf-value';
+
+ value: ComponentValue;
+
+ constructor(value: ComponentValue) {
+ this.value = value;
+ }
+
+ tokens() {
+ if (isToken(this.value)) {
+ return this.value;
+ }
+
+ return this.value.tokens();
+ }
+
+ toString() {
+ if (isToken(this.value)) {
+ return stringify(this.value);
+ }
+
+ return this.value.toString();
+ }
+
+ walk(cb: (entry: { node: ComponentValue, parent: ContainerNode | MediaFeatureValue }, index: number) => boolean) {
+ if (cb({ node: this.value, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.value) {
+ return this.value.walk(cb);
+ }
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature.ts b/packages/media-query-list-parser/src/nodes/media-feature.ts
similarity index 56%
rename from packages/postcss-media-query-list-parser/src/nodes/media-feature.ts
rename to packages/media-query-list-parser/src/nodes/media-feature.ts
index 28e55bb18..ae69aaa27 100644
--- a/packages/postcss-media-query-list-parser/src/nodes/media-feature.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature.ts
@@ -1,3 +1,4 @@
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
import { MediaFeatureBoolean } from './media-feature-boolean';
import { MediaFeaturePlain } from './media-feature-plain';
import { MediaFeatureRange } from './media-feature-range';
@@ -7,11 +8,16 @@ export class MediaFeature {
feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
- constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange) {
+ startToken: CSSToken;
+ endToken: CSSToken;
+
+ constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange, startToken: CSSToken, endToken: CSSToken) {
+ this.startToken = startToken;
+ this.endToken = endToken;
this.feature = feature;
}
toString() {
- return '(' + this.feature.toString() + ')';
+ return stringify(this.startToken) + this.feature.toString() + stringify(this.endToken);
}
}
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..495e89448
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
@@ -0,0 +1,73 @@
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { GeneralEnclosed } from './general-enclosed';
+import { MediaCondition } from './media-condition';
+import { MediaFeature } from './media-feature';
+
+export class MediaInParens {
+ type = 'media-in-parens';
+
+ startToken?: CSSToken;
+ endToken?: CSSToken;
+ media: MediaCondition | MediaFeature | GeneralEnclosed;
+
+ constructor(media: MediaCondition | MediaFeature | GeneralEnclosed, startToken?: CSSToken, endToken?: CSSToken) {
+ this.startToken = startToken;
+ this.endToken = endToken;
+ this.media = media;
+ }
+
+ tokens() {
+ if (this.media.type === 'general-enclosed') {
+ return this.media.tokens();
+ }
+
+ if (this.media.type === 'media-feature') {
+ return this.media.tokens();
+ }
+
+ if (this.media.type === 'media-condition') {
+ if (!this.startToken || !this.endToken) {
+ throw new Error('Failed to list tokens for "media-in-parens" with "media-condition"');
+ }
+
+ return [
+ this.startToken,
+ ...this.media.tokens(),
+ this.endToken,
+ ];
+ }
+
+ throw new Error('Failed to list tokens for "media-in-parens"');
+ }
+
+ toString() {
+ if (this.media.type === 'general-enclosed') {
+ return this.media.toString();
+ }
+
+ if (this.media.type === 'media-feature') {
+ return this.media.toString();
+ }
+
+ if (this.media.type === 'media-condition') {
+ if (!this.startToken || !this.endToken) {
+ throw new Error('Failed to stringify "media-in-parens" with "media-condition"');
+ }
+
+ return stringify(this.startToken) + this.media.toString() + stringify(this.endToken);
+ }
+
+ throw new Error('Failed to stringify "media-in-parens"');
+ }
+
+ walk(cb: (entry: { node: ComponentValue | MediaCondition | MediaFeature | GeneralEnclosed, parent: ContainerNode | MediaInParens | GeneralEnclosed }, index: number) => boolean) {
+ if (cb({ node: this.media, parent: this }, 0) === false) {
+ return false;
+ }
+
+ if ('walk' in this.media) {
+ return this.media.walk(cb);
+ }
+ }
+}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-not.ts b/packages/media-query-list-parser/src/nodes/media-not.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-not.ts
rename to packages/media-query-list-parser/src/nodes/media-not.ts
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-or.ts b/packages/media-query-list-parser/src/nodes/media-or.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-or.ts
rename to packages/media-query-list-parser/src/nodes/media-or.ts
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/postcss-media-query-list-parser/src/nodes/media-query.ts b/packages/media-query-list-parser/src/nodes/media-query.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/nodes/media-query.ts
rename to packages/media-query-list-parser/src/nodes/media-query.ts
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..2eeebf847
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-type.ts
@@ -0,0 +1,59 @@
+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/postcss-media-query-list-parser/src/parser/consume/consume-boolean.ts b/packages/media-query-list-parser/src/parser/consume/consume-boolean.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/parser/consume/consume-boolean.ts
rename to packages/media-query-list-parser/src/parser/consume/consume-boolean.ts
diff --git a/packages/postcss-media-query-list-parser/src/parser/consume/consume-plain.ts b/packages/media-query-list-parser/src/parser/consume/consume-plain.ts
similarity index 100%
rename from packages/postcss-media-query-list-parser/src/parser/consume/consume-plain.ts
rename to packages/media-query-list-parser/src/parser/consume/consume-plain.ts
diff --git a/packages/media-query-list-parser/src/parser/consume/consume-value.ts b/packages/media-query-list-parser/src/parser/consume/consume-value.ts
new file mode 100644
index 000000000..991bc9b63
--- /dev/null
+++ b/packages/media-query-list-parser/src/parser/consume/consume-value.ts
@@ -0,0 +1,16 @@
+import { CSSToken } from '@csstools/css-tokenizer';
+import { parseComponentValue } from '@csstools/css-parser-algorithms';
+import { MediaFeatureValue } from '../../nodes/media-feature-value';
+
+export function consumeValue(tokens: Array): { node: MediaFeatureValue, tokens: Array } | null {
+ const result = parseComponentValue(tokens, {
+ onParseError(err) {
+ throw new Error(JSON.stringify(err));
+ },
+ });
+
+ return {
+ node: new MediaFeatureValue(result),
+ tokens: tokens.slice(result.tokens().length + 1),
+ };
+}
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..e96aa49ef
--- /dev/null
+++ b/packages/media-query-list-parser/src/parser/parse.ts
@@ -0,0 +1,29 @@
+import { parseCommaSeparatedListOfComponentValues } from '@csstools/css-parser-algorithms';
+import { tokenizer } from '@csstools/css-tokenizer';
+
+export function parse(source: string) {
+ 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
+ }
+
+ const result = parseCommaSeparatedListOfComponentValues(tokens, {
+ onParseError: onParseError,
+ });
+
+ return result;
+}
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..0ddae5dc7
--- /dev/null
+++ b/packages/media-query-list-parser/test/test.mjs
@@ -0,0 +1,12 @@
+import { parse } from '@csstools/media-query-list-parser';
+
+parse('(/* a comment */foo ) something else');
+
+parse('((min-width: 300px) and (prefers-color-scheme:/* a comment */dark))').forEach((mediaQuery) => {
+ mediaQuery.forEach((args) => {
+ args.walk((a) => {
+ console.log(a.node.type);
+ console.log(a.node.toString());
+ });
+ });
+});
diff --git a/packages/postcss-media-query-list-parser/tsconfig.json b/packages/media-query-list-parser/tsconfig.json
similarity index 100%
rename from packages/postcss-media-query-list-parser/tsconfig.json
rename to packages/media-query-list-parser/tsconfig.json
diff --git a/packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts b/packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts
deleted file mode 100644
index ef15f9f37..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/general-enclosed.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { CSSToken, stringify } from '@csstools/css-tokenizer';
-
-export class GeneralEnclosed {
- type = 'general-enclosed';
-
- raw: Array;
-
- constructor(raw: Array) {
- this.raw = raw;
- }
-
- toString() {
- return stringify(...this.raw);
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts
deleted file mode 100644
index e172c73ef..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-feature-comparison.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-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 invertComparison(operator: MediaFeatureComparison): MediaFeatureComparison {
- 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:
- throw new Error('Unknown range syntax operator');
- }
-}
-
-export function comparisonToString(operator: MediaFeatureComparison): string {
- switch (operator) {
- case MediaFeatureEQ.EQ:
- return '=';
- case MediaFeatureLT.LT:
- return '<';
- case MediaFeatureLT.LT_OR_EQ:
- return '<=';
- case MediaFeatureGT.GT:
- return '>';
- case MediaFeatureGT.GT_OR_EQ:
- return '>=';
- default:
- throw new Error('Unknown range syntax operator');
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts
deleted file mode 100644
index 86349b095..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-feature-name.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
-
-export class MediaFeatureName {
- type = 'mf-name';
-
- tokens: Array;
-
- constructor(tokens: Array) {
- this.tokens = tokens;
- }
-
- get nameIndex(): number {
- return this.tokens.findIndex((x) => {
- return x[0] === TokenType.Ident;
- });
- }
-
- toString() {
- return stringify(...this.tokens);
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts
deleted file mode 100644
index f8d898b14..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-feature-plain.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { MediaFeatureName } from './media-feature-name';
-import { MediaFeatureValue } from './media-feature-value';
-
-export class MediaFeaturePlain {
- type = 'mf-plain';
-
- name: MediaFeatureName;
- value: MediaFeatureValue;
-
- constructor(name: MediaFeatureName, value: MediaFeatureValue) {
- this.name = name;
- this.value = value;
- }
-
- toString() {
- return this.name.toString() + ':' + this.value.toString();
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts
deleted file mode 100644
index a804d26df..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-feature-range.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { comparisonToString, MediaFeatureComparison, MediaFeatureGT, MediaFeatureLT } from './media-feature-comparison';
-import { MediaFeatureName } from './media-feature-name';
-import { MediaFeatureValue } from './media-feature-value';
-
-export type MediaFeatureRange = MediaFeatureRangeNameValue |
- MediaFeatureRangeValueName |
- MediaFeatureRangeLowHigh |
- MediaFeatureRangeHighLow;
-
-export class MediaFeatureRangeNameValue {
- type = 'mf-range-name-value';
-
- name: MediaFeatureName;
- operator: MediaFeatureComparison;
- value: MediaFeatureValue;
-
- constructor(name: MediaFeatureName, operator: MediaFeatureComparison, value: MediaFeatureValue) {
- this.name = name;
- this.operator = operator;
- this.value = value;
- }
-
- toString() {
- let result = '';
-
- result += this.name.toString();
- result += ' ';
-
- result += comparisonToString(this.operator);
-
- result += ' ';
- result += this.value.toString();
-
- return result;
- }
-}
-
-export class MediaFeatureRangeValueName {
- type = 'mf-range-value-range';
-
- name: MediaFeatureName;
- operator: MediaFeatureComparison;
- value: MediaFeatureValue;
-
- constructor(value: MediaFeatureValue, operator: MediaFeatureComparison, name: MediaFeatureName) {
- this.name = name;
- this.operator = operator;
- this.value = value;
- }
-
- toString() {
- let result = '';
-
- result += this.value.toString();
- result += ' ';
-
- result += comparisonToString(this.operator);
-
- result += ' ';
- result += this.name.toString();
-
- return result;
- }
-}
-
-export class MediaFeatureRangeLowHigh {
- type = 'mf-range-low-high';
-
- name: MediaFeatureName;
- low: MediaFeatureValue;
- lowOperator: MediaFeatureLT;
- high: MediaFeatureValue;
- highOperator: MediaFeatureLT;
-
- constructor(name: MediaFeatureName, low: MediaFeatureValue, lowOperator: MediaFeatureLT, high: MediaFeatureValue, highOperator: MediaFeatureLT) {
- this.name = name;
- this.low = low;
- this.lowOperator = lowOperator;
- this.high = high;
- this.highOperator = highOperator;
- }
-
- toString() {
- let result = '';
-
- result += this.low.toString();
-
- result += ' ';
- result += comparisonToString(this.lowOperator);
- result += ' ';
-
- result += this.name.toString();
-
- result += ' ';
- result += comparisonToString(this.highOperator);
- result += ' ';
-
- result += this.high.toString();
- return result;
- }
-}
-
-export class MediaFeatureRangeHighLow {
- type = 'mf-range-high-low';
-
- name: MediaFeatureName;
- low: MediaFeatureValue;
- lowOperator: MediaFeatureGT;
- high: MediaFeatureValue;
- highOperator: MediaFeatureGT;
-
- constructor(name: MediaFeatureName, high: MediaFeatureValue, highOperator: MediaFeatureGT, low: MediaFeatureValue, lowOperator: MediaFeatureGT) {
- this.name = name;
- this.low = low;
- this.lowOperator = lowOperator;
- this.high = high;
- this.highOperator = highOperator;
- }
-
- toString() {
- let result = '';
-
- result += this.high.toString();
-
- result += ' ';
- result += comparisonToString(this.highOperator);
- result += ' ';
-
- result += this.name.toString();
-
- result += ' ';
- result += comparisonToString(this.lowOperator);
- result += ' ';
-
- result += this.low.toString();
- return result;
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts b/packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts
deleted file mode 100644
index 83302072c..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-feature-value.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { CSSToken, stringify} from '@csstools/css-tokenizer';
-
-export class MediaFeatureValue {
- type = 'mf-value';
-
- tokens: Array;
-
- constructor(tokens: Array) {
- this.tokens = tokens;
- }
-
- toString() {
- return stringify(...this.tokens);
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts b/packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts
deleted file mode 100644
index 609b5210c..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-in-parens.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { GeneralEnclosed } from './general-enclosed';
-import { MediaCondition } from './media-condition';
-import { MediaFeature } from './media-feature';
-
-export class MediaInParens {
- type = 'media-in-parens';
-
- media: MediaCondition | MediaFeature | GeneralEnclosed;
-
- constructor(media: MediaCondition | MediaFeature | GeneralEnclosed) {
- this.media = media;
- }
-
- toString() {
- if (this.media.type === 'general-enclosed') {
- return this.media.toString();
- }
-
- if (this.media.type === 'media-feature') {
- return this.media.toString();
- }
-
- if (this.media.type === 'media-condition') {
- return '(' + this.media.toString() + ')';
- }
-
- return this.media.toString();
- }
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts b/packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts
deleted file mode 100644
index d30ae315d..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-query-modifier.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export enum MediaQueryModifier {
- Not = 'not',
- Only = 'only'
-}
diff --git a/packages/postcss-media-query-list-parser/src/nodes/media-type.ts b/packages/postcss-media-query-list-parser/src/nodes/media-type.ts
deleted file mode 100644
index 341a7e474..000000000
--- a/packages/postcss-media-query-list-parser/src/nodes/media-type.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-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',
-}
diff --git a/packages/postcss-media-query-list-parser/src/parser/advance/advance.ts b/packages/postcss-media-query-list-parser/src/parser/advance/advance.ts
deleted file mode 100644
index b66dc51d4..000000000
--- a/packages/postcss-media-query-list-parser/src/parser/advance/advance.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { CSSToken, mirrorVariant, TokenType } from '@csstools/css-tokenizer';
-
-// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
-export function advanceComponentValue(tokens: Array): number {
- for (let i = 0; i < tokens.length; i++) {
- const token = tokens[i];
- if (
- token[0] === TokenType.OpenParen ||
- token[0] === TokenType.OpenCurly ||
- token[0] === TokenType.OpenSquare
- ) {
- i += advanceSimpleBlock(tokens.slice(i));
- return i;
- }
-
- if (token[0] === TokenType.Function) {
- i += advanceFunction(tokens.slice(i));
- return i;
- }
-
- return i;
- }
-}
-
-// https://www.w3.org/TR/css-syntax-3/#consume-function
-export function advanceFunction(tokens: Array): number | null {
- for (let i = 1; i < tokens.length; i++) {
- const token = tokens[i];
-
- if (token[0] === TokenType.CloseParen) {
- return i;
- }
-
- i += advanceComponentValue(tokens.slice(i));
- }
-
- throw new Error('Failed to parse');
-}
-
-/** https://www.w3.org/TR/css-syntax-3/#consume-simple-block */
-export function advanceSimpleBlock(tokens: Array): number | null {
- const endingToken = mirrorVariant(tokens[0][0]);
- if (!endingToken) {
- throw new Error('Failed to parse');
- }
-
- for (let i = 1; i < tokens.length; i++) {
- const token = tokens[i];
-
- if (token[0] === endingToken) {
- return i;
- }
-
- i += advanceComponentValue(tokens.slice(i));
- }
-
- throw new Error('Failed to parse');
-}
diff --git a/packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts b/packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts
deleted file mode 100644
index 6d1b21b88..000000000
--- a/packages/postcss-media-query-list-parser/src/parser/consume/consume-value.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { CSSToken } from '@csstools/css-tokenizer';
-import { MediaFeatureValue } from '../../nodes/media-feature-value';
-import { advanceComponentValue } from '../advance/advance';
-
-export function consumeValue(tokens: Array): { node: MediaFeatureValue, tokens: Array } | null {
- const result = advanceComponentValue(tokens);
-
- return {
- node: new MediaFeatureValue(tokens.slice(0, result + 1)),
- tokens: tokens.slice(result + 1),
- };
-}
diff --git a/packages/postcss-media-query-list-parser/src/parser/parse.ts b/packages/postcss-media-query-list-parser/src/parser/parse.ts
deleted file mode 100644
index 8283ae072..000000000
--- a/packages/postcss-media-query-list-parser/src/parser/parse.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { CSSToken, TokenType, tokenizer } from '@csstools/css-tokenizer';
-import { MediaFeatureBoolean } from '../nodes/media-feature-boolean';
-import { advanceSimpleBlock } from './advance/advance';
-import { consumeBoolean } from './consume/consume-boolean';
-
-type Tokenizer = {
- nextToken: () => CSSToken | undefined,
- endOfFile: () => boolean,
-}
-
-export function parse(source: string) {
- const t = tokenizer({ css: source }, {
- commentsAreTokens: true,
- onParseError: (err) => {
- console.warn(err);
- throw new Error(`Unable to parse "${source}"`);
- },
- });
-
- const tokenBuffer = [];
- while (!t.endOfFile()) {
- tokenBuffer.push(t.nextToken());
- }
-
- // console.log(tokenBuffer);
-
- const result = consumeBoolean(tokenBuffer);
- const remainder = result.tokens;
- const node = result.node;
- const tokenSlice = node.tokens;
-
- console.log(tokenSlice);
- console.log(node.nameIndex);
- console.log(node.tokens[node.nameIndex][4].value);
-
- console.log(remainder);
-}
-
-// function consumeMediaQuery(t: Tokenizer) {
-// let token = t.nextToken();
-// if (t.endOfFile()) {
-// return;
-// }
-
-// while (token[0] === TokenType.Whitespace || token[0] === TokenType.Comment) {
-// token = t.nextToken();
-// if (t.endOfFile()) {
-// return;
-// }
-// }
-
-// if (token[0] === TokenType.OpenParen) {
-// return consumeMediaQueryWithoutType(t);
-// }
-
-// if (token[0] !== TokenType.Ident) {
-// return;
-// }
-
-// if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'only') {
-// return consumeMediaQueryWithType(t);
-// }
-
-// if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'not') {
-// token = t.nextToken();
-// if (t.endOfFile()) {
-// return;
-// }
-
-// while (token[0] === TokenType.Whitespace || token[0] === TokenType.Comment) {
-// token = t.nextToken();
-// if (t.endOfFile()) {
-// return;
-// }
-// }
-
-// if (token[0] === TokenType.Comma) {
-// return;
-// }
-
-// if (token[0] === TokenType.OpenParen) {
-// return consumeMediaQueryWithoutType(t);
-// }
-
-// return consumeMediaQueryWithType(t);
-// }
-
-// const modifier = token[]
-// }
-
-// function consumeMediaQueryWithoutType(t: Tokenizer) {
-
-// }
-
-// function consumeMediaQueryWithType(t: Tokenizer) {
-
-// }
diff --git a/packages/postcss-media-query-list-parser/test/test.mjs b/packages/postcss-media-query-list-parser/test/test.mjs
deleted file mode 100644
index 44a4b335d..000000000
--- a/packages/postcss-media-query-list-parser/test/test.mjs
+++ /dev/null
@@ -1,3 +0,0 @@
-import { parse } from '@csstools/postcss-media-query-list-parser';
-
-parse('(/* a comment */foo ) something else');
diff --git a/packages/virtual-media/.gitignore b/packages/virtual-media/.gitignore
deleted file mode 100644
index 7172b04f1..000000000
--- a/packages/virtual-media/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-node_modules
-package-lock.json
-yarn.lock
-*.result.css
-*.result.css.map
-dist/*
diff --git a/packages/virtual-media/.nvmrc b/packages/virtual-media/.nvmrc
deleted file mode 100644
index f0b10f153..000000000
--- a/packages/virtual-media/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-v16.13.1
diff --git a/packages/virtual-media/CHANGELOG.md b/packages/virtual-media/CHANGELOG.md
deleted file mode 100644
index b0ff6b082..000000000
--- a/packages/virtual-media/CHANGELOG.md
+++ /dev/null
@@ -1,3 +0,0 @@
-### 1.0.0
-
-- Initial version
diff --git a/packages/virtual-media/LICENSE.md b/packages/virtual-media/LICENSE.md
deleted file mode 100644
index af5411fa2..000000000
--- a/packages/virtual-media/LICENSE.md
+++ /dev/null
@@ -1,20 +0,0 @@
-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/virtual-media/README.md b/packages/virtual-media/README.md
deleted file mode 100644
index 361096774..000000000
--- a/packages/virtual-media/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# TODO
diff --git a/packages/virtual-media/package.json b/packages/virtual-media/package.json
deleted file mode 100644
index 84e6aa12d..000000000
--- a/packages/virtual-media/package.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "name": "@csstools/virtual-media",
- "description": "Virtualized media for media queries.",
- "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",
- "test": "node ./test/test.mjs"
- },
- "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/virtual-media#readme",
- "repository": {
- "type": "git",
- "url": "https://github.com/csstools/postcss-plugins.git",
- "directory": "packages/virtual-media"
- },
- "bugs": "https://github.com/csstools/postcss-plugins/issues",
- "keywords": [
- "css",
- "tokenizer"
- ],
- "volta": {
- "extends": "../../package.json"
- }
-}
diff --git a/packages/virtual-media/src/index.ts b/packages/virtual-media/src/index.ts
deleted file mode 100644
index e2a94869a..000000000
--- a/packages/virtual-media/src/index.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-import { addRange } from './range/add';
-import { Range } from './range/range';
-
-export type Unknown = 'unknown';
-export type Impossible = 'impossible';
-
-export enum Operator {
- LT = '<',
- LT_OR_EQ = '<=',
- GT = '>',
- GT_OR_EQ = '>=',
- EQ = '=',
-}
-
-const fraction = 1 / Math.pow(2, 32);
-
-export class VirtualMedia {
- #impossibleMedia = false;
-
- #types = new Set([
- 'print',
- 'screen',
- ]);
-
- mustMatchType(type: string) {
- if (type.toLowerCase() === 'all') {
- return;
- }
-
- if (!this.#types.has(type.toLowerCase())) {
- this.#types.clear();
- return;
- }
-
- this.#types.clear();
- this.#types.add(type.toLowerCase());
- }
-
- mustNotMatchType(type: string) {
- if (type.toLowerCase() === 'all') {
- this.#types.clear();
- return;
- }
-
- this.#types.delete(type.toLowerCase());
- }
-
- mustMatchWidth(low: number, lowOperator: Operator, high: number, highOperator: Operator) {
- if (this.#impossibleMedia) {
- return;
- }
-
- const range = {
- low: low,
- high: high,
- };
-
- switch (lowOperator) {
- case Operator.LT:
- range.low = range.low - fraction;
- break;
- case Operator.GT:
- range.low = range.low + fraction;
- break;
- }
-
- switch (highOperator) {
- case Operator.LT:
- range.high = range.high - fraction;
- break;
- case Operator.GT:
- range.high = range.high + fraction;
- break;
- }
-
- this.#width = addRange(this.#width, {
- low: range.low,
- high: range.high,
- });
-
- if (this.#width.length === 0) {
- this.#impossibleMedia = true;
- }
- }
-
- #width: Array> = [
- {
- low: Number.MIN_SAFE_INTEGER,
- high: Number.MAX_SAFE_INTEGER,
- },
- ];
-
- /**
- * https://www.w3.org/TR/mediaqueries-5/#width
- *
- */
- get width(): number | Unknown | Impossible {
- if (this.#impossibleMedia) {
- return 'impossible';
- }
-
- if (this.#width.length !== 1) {
- return 'unknown';
- }
-
- if (this.#width[0].low !== this.#width[0].high) {
- return 'unknown';
- }
-
- return this.#width[0].low;
- }
-
- #height: Array> = [
- {
- low: Number.MIN_SAFE_INTEGER,
- high: Number.MAX_SAFE_INTEGER,
- },
- ];
-
- /**
- * https://www.w3.org/TR/mediaqueries-5/#height
- *
- */
- get height(): number | Unknown | Impossible {
- if (this.#impossibleMedia) {
- return 'impossible';
- }
-
- if (this.#height.length !== 1) {
- return 'unknown';
- }
-
- if (this.#height[0].low !== this.#height[0].high) {
- return 'unknown';
- }
-
- return this.#height[0].low;
- }
-
- #aspectRatio: Range<{
- /** dividend / divisor */
- dividend: number,
- /** dividend / divisor */
- divisor: number
- }> | Unknown = 'unknown';
-
- /**
- * https://www.w3.org/TR/mediaqueries-5/#aspect-ratio
- *
- */
- get aspectRatio() {
- if (this.#impossibleMedia) {
- return 'unknown';
- }
-
- if (this.#aspectRatio === 'unknown') {
- const height = this.height;
- const width = this.width;
- if (height === 'impossible' || width === 'impossible') {
- return 'impossible';
- }
-
- if (height !== 'unknown' && width !== 'unknown') {
- return width / height;
- }
-
- return 'unknown';
- }
-
- if (this.#aspectRatio.low.dividend !== this.#aspectRatio.high.dividend) {
- return 'unknown';
- }
-
- if (this.#aspectRatio.low.divisor !== this.#aspectRatio.high.divisor) {
- return 'unknown';
- }
-
- if (this.#aspectRatio.low.dividend === 0) {
- return 'unknown';
- }
-
- if (this.#aspectRatio.low.divisor === 0) {
- return 'unknown';
- }
-
- return this.#aspectRatio.low.dividend / this.#aspectRatio.low.divisor;
- }
-}
diff --git a/packages/virtual-media/src/range/add.ts b/packages/virtual-media/src/range/add.ts
deleted file mode 100644
index 3b0688c89..000000000
--- a/packages/virtual-media/src/range/add.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { compare } from './compare';
-import { Range } from './range';
-
-export function addRange(existingRanges: Array>, add: Range): Array> {
- if (add.low > add.high) {
- throw new Error('Inversed range ' + JSON.stringify(add));
- }
-
- const updated: Array> = [];
-
- for (let i = 0; i < existingRanges.length; i++) {
- const existingRange = existingRanges[i];
-
- // The "add" spans an equal range as is currently allowed.
- // Return the current part of existing ranges.
- if (compare(add.low, existingRange.low) === 0 && compare(existingRange.high, add.high) === 0) {
- return [
- existingRange,
- ];
- }
-
- // The "add" spans a range without any overlap with the current part.
- // Continue to the next part.
- if (compare(add.low, existingRange.high) > 0) {
- continue;
- }
-
- // The "add" spans a range without any overlap with the current part.
- // Continue to the next part.
- if (compare(add.high, existingRange.low) < 0) {
- continue;
- }
-
- // The "add" spans a smaller range, but is fully enclosed withing the current range.
- // Return the part to add.
- if (compare(add.low, existingRange.low) > 0 && compare(existingRange.high, add.high) > 0) {
- return [
- add,
- ];
- }
-
- // The "add" spans a larger range than is currently allowed, but it fully encloses the current range.
- // Add the current part of the existing ranges to the updated slice.
- if (compare(add.low, existingRange.low) < 0 && compare(existingRange.high, add.high) < 0) {
- updated.push(existingRange);
- continue;
- }
-
- if (compare(add.low, existingRange.low) > 0) {
- updated.push({
- low: add.low,
- high: existingRange.high,
- });
- continue;
- }
-
- if (compare(add.high, existingRange.high) < 0) {
- updated.push({
- low: existingRange.low,
- high: add.high,
- });
- continue;
- }
- }
-
- return updated;
-}
diff --git a/packages/virtual-media/src/range/compare.ts b/packages/virtual-media/src/range/compare.ts
deleted file mode 100644
index cb678e037..000000000
--- a/packages/virtual-media/src/range/compare.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export function compare(a: T, b: T): -1 | 0 | 1 {
- if ((typeof a) !== (typeof b)) {
- return 0;
- }
-
- if ((typeof a === 'number')) {
- const r = a - (b as number);
- if (r < 0) {
- return -1;
- }
-
- if (r > 0) {
- return 1;
- }
-
- return 0;
- }
-
- if ((typeof a === 'string')) {
- const r = a.localeCompare(b as string);
- if (r < 0) {
- return -1;
- }
-
- if (r > 0) {
- return 1;
- }
-
- return 0;
- }
-
- return 0;
-}
diff --git a/packages/virtual-media/src/range/range.ts b/packages/virtual-media/src/range/range.ts
deleted file mode 100644
index 5eeae7c75..000000000
--- a/packages/virtual-media/src/range/range.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type Range = {
- low: T,
- high: T
-};
diff --git a/packages/virtual-media/test/test.mjs b/packages/virtual-media/test/test.mjs
deleted file mode 100644
index 1832b1a17..000000000
--- a/packages/virtual-media/test/test.mjs
+++ /dev/null
@@ -1,28 +0,0 @@
-import { VirtualMedia, Operator } from '@csstools/virtual-media';
-
-// What with units?
-// Maybe a map of facts per property
-// { px: { low, high }, rem: { low, high } }
-//
-// Or a map of VirtualMedia and a way to share unitless properties?
-//
-// How to deal with "not (width: 300px)"
-
-{
- const media = new VirtualMedia();
- console.log(media.width);
-
- media.mustMatchWidth(10, Operator.GT, 100, Operator.LT);
- console.log(media.width);
-
- media.mustMatchWidth(1000, Operator.GT, 10000, Operator.LT);
- console.log(media.width);
-}
-
-{
- const media = new VirtualMedia();
- console.log(media.width);
-
- media.mustMatchWidth(10, Operator.EQ, 10, Operator.EQ);
- console.log(media.width);
-}
diff --git a/packages/virtual-media/tsconfig.json b/packages/virtual-media/tsconfig.json
deleted file mode 100644
index e0d06239c..000000000
--- a/packages/virtual-media/tsconfig.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "outDir": "dist",
- "declarationDir": "."
- },
- "include": ["./src/**/*"],
- "exclude": ["dist"],
-}
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,
+ }),
],
},
];
From ccf0b594a90aeffde9fdadc00a2056dae0c47bc3 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Wed, 19 Oct 2022 22:44:50 +0200
Subject: [PATCH 03/35] wip
---
.../consume-component-block-function.ts | 30 +++-
.../src/nodes/general-enclosed.ts | 21 ++-
.../src/nodes/media-and.ts | 41 ++++-
.../src/nodes/media-condition-list.ts | 167 ++++++++++++++++--
.../nodes/media-condition-without-or-or.ts | 16 --
.../src/nodes/media-condition-without-or.ts | 45 +++++
.../src/nodes/media-condition.ts | 33 +++-
.../src/nodes/media-feature-name.ts | 32 +++-
.../src/nodes/media-feature-plain.ts | 32 +++-
.../src/nodes/media-feature-range.ts | 130 ++++++++++++--
.../src/nodes/media-feature-value.ts | 21 ++-
.../src/nodes/media-feature.ts | 52 +++++-
.../src/nodes/media-in-parens.ts | 65 +++----
.../src/nodes/media-not.ts | 41 ++++-
.../src/nodes/media-or.ts | 41 ++++-
.../src/nodes/media-query.ts | 90 ++++++++--
.../src/parser/parse.ts | 52 +++++-
.../media-query-list-parser/test/test.mjs | 7 +-
18 files changed, 779 insertions(+), 137 deletions(-)
delete mode 100644 packages/media-query-list-parser/src/nodes/media-condition-without-or-or.ts
create mode 100644 packages/media-query-list-parser/src/nodes/media-condition-without-or.ts
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
index bb9276ba3..20c618ec6 100644
--- a/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
+++ b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
@@ -98,7 +98,20 @@ export class FunctionNode {
return stringify(this.name) + valueString + stringify(this.endToken);
}
- walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number) => boolean) {
+ 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) {
let aborted = false;
this.value.forEach((child, index) => {
@@ -210,7 +223,20 @@ export class SimpleBlockNode {
return stringify(this.startToken) + valueString + stringify(this.endToken);
}
- walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number) => boolean) {
+ 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) {
let aborted = false;
this.value.forEach((child, index) => {
diff --git a/packages/media-query-list-parser/src/nodes/general-enclosed.ts b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
index bc0c4ca81..d2e019ee4 100644
--- a/packages/media-query-list-parser/src/nodes/general-enclosed.ts
+++ b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
@@ -26,8 +26,22 @@ export class GeneralEnclosed {
return this.value.toString();
}
- walk(cb: (entry: { node: ComponentValue, parent: ContainerNode | GeneralEnclosed }, index: number) => boolean) {
- if (cb({ node: this.value, parent: this }, 0) === false) {
+ 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) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
return false;
}
@@ -36,3 +50,6 @@ export class 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
index c6fd5c87e..b6695c07d 100644
--- a/packages/media-query-list-parser/src/nodes/media-and.ts
+++ b/packages/media-query-list-parser/src/nodes/media-and.ts
@@ -1,15 +1,50 @@
-import { MediaInParens } from './media-in-parens';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
export class MediaAnd {
type = 'media-and';
+ modifier: Array;
media: MediaInParens;
- constructor(media: MediaInParens) {
+ constructor(modifier: Array, media: MediaInParens) {
+ this.modifier = modifier;
this.media = media;
}
+ tokens() {
+ return [
+ ...this.modifier,
+ ...this.media.tokens(),
+ ];
+ }
+
toString() {
- return 'and' + this.media.toString();
+ 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) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
}
}
+
+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
index 563eebae3..588c3565a 100644
--- a/packages/media-query-list-parser/src/nodes/media-condition-list.ts
+++ b/packages/media-query-list-parser/src/nodes/media-condition-list.ts
@@ -1,6 +1,7 @@
-import { MediaAnd } from './media-and';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaAnd, MediaAndWalkerEntry, MediaAndWalkerParent } from './media-and';
import { MediaInParens } from './media-in-parens';
-import { MediaOr } from './media-or';
+import { MediaOr, MediaOrWalkerEntry, MediaOrWalkerParent } from './media-or';
export type MediaConditionList = MediaConditionListWithAnd | MediaConditionListWithOr;
@@ -9,37 +10,183 @@ export class MediaConditionListWithAnd {
leading: MediaInParens;
list: Array;
+ before: Array;
+ after: Array;
- constructor(leading: MediaInParens, list: Array) {
+ constructor(leading: MediaInParens, list: Array, before: Array = [], after: Array = []) {
this.leading = leading;
this.list = list;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens() {
+ return [
+ ...this.before,
+ this.leading.tokens(),
+ ...this.list.flatMap((item) => item.tokens()),
+ ...this.after,
+ ];
}
toString() {
- if (this.list.length === 0) {
- return this.leading.toString();
+ 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 this.leading.toString() + ' ' + this.list.map((x) => x.toString()).join(' ');
+ 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) {
+ 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;
+ }
}
}
+export type MediaConditionListWithAndWalkerEntry = MediaAndWalkerEntry | MediaAnd;
+export type MediaConditionListWithAndWalkerParent = MediaAndWalkerParent | MediaConditionListWithAnd;
+
export class MediaConditionListWithOr {
type = 'media-condition-list-or';
leading: MediaInParens;
list: Array;
+ before: Array;
+ after: Array;
- constructor(leading: MediaInParens, list: Array) {
+ constructor(leading: MediaInParens, list: Array, before: Array = [], after: Array = []) {
this.leading = leading;
this.list = list;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens() {
+ return [
+ ...this.before,
+ this.leading.tokens(),
+ ...this.list.flatMap((item) => item.tokens()),
+ ...this.after,
+ ];
}
toString() {
- if (this.list.length === 0) {
- return this.leading.toString();
+ 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 this.leading.toString() + ' ' + this.list.map((x) => x.toString()).join(' ');
+ 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) {
+ 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;
+ }
}
}
+
+export type MediaConditionListWithOrWalkerEntry = MediaOrWalkerEntry | MediaOr;
+export type MediaConditionListWithOrWalkerParent = MediaOrWalkerParent | MediaConditionListWithOr;
diff --git a/packages/media-query-list-parser/src/nodes/media-condition-without-or-or.ts b/packages/media-query-list-parser/src/nodes/media-condition-without-or-or.ts
deleted file mode 100644
index 8cb02ade0..000000000
--- a/packages/media-query-list-parser/src/nodes/media-condition-without-or-or.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { MediaConditionListWithAnd } from './media-condition-list';
-import { MediaNot } from './media-not';
-
-export class MediaConditionWithoutOr {
- type = 'media-condition-without-or';
-
- media: MediaNot | MediaConditionListWithAnd;
-
- constructor(media: MediaNot | MediaConditionListWithAnd) {
- this.media = media;
- }
-
- toString() {
- return this.media.toString();
- }
-}
diff --git a/packages/media-query-list-parser/src/nodes/media-condition-without-or.ts b/packages/media-query-list-parser/src/nodes/media-condition-without-or.ts
new file mode 100644
index 000000000..1834898dd
--- /dev/null
+++ b/packages/media-query-list-parser/src/nodes/media-condition-without-or.ts
@@ -0,0 +1,45 @@
+import { MediaConditionListWithAnd, MediaConditionListWithAndWalkerEntry, MediaConditionListWithAndWalkerParent } from './media-condition-list';
+import { MediaNot, MediaNotWalkerEntry, MediaNotWalkerParent } from './media-not';
+
+export class MediaConditionWithoutOr {
+ type = 'media-condition-without-or';
+
+ media: MediaNot | MediaConditionListWithAnd;
+
+ constructor(media: MediaNot | MediaConditionListWithAnd) {
+ this.media = media;
+ }
+
+ tokens() {
+ return this.media.tokens();
+ }
+
+ toString() {
+ return this.media.toString();
+ }
+
+ indexOf(item: MediaNot | MediaConditionListWithAnd): number | string {
+ if (item === this.media) {
+ return 'media';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaConditionWithoutOrWalkerEntry, parent: MediaConditionWithoutOrWalkerParent }, index: number | string) => boolean) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
+}
+
+export type MediaConditionWithoutOrWalkerEntry = MediaConditionListWithAndWalkerEntry | MediaNotWalkerEntry | MediaNot | MediaConditionListWithAnd;
+export type MediaConditionWithoutOrWalkerParent = MediaConditionListWithAndWalkerParent | MediaNotWalkerParent | MediaConditionWithoutOr;
diff --git a/packages/media-query-list-parser/src/nodes/media-condition.ts b/packages/media-query-list-parser/src/nodes/media-condition.ts
index fa276ee92..b29ccd613 100644
--- a/packages/media-query-list-parser/src/nodes/media-condition.ts
+++ b/packages/media-query-list-parser/src/nodes/media-condition.ts
@@ -1,5 +1,5 @@
-import { MediaConditionListWithAnd, MediaConditionListWithOr } from './media-condition-list';
-import { MediaNot } from './media-not';
+import { MediaConditionListWithAnd, MediaConditionListWithAndWalkerEntry, MediaConditionListWithAndWalkerParent, MediaConditionListWithOr, MediaConditionListWithOrWalkerEntry, MediaConditionListWithOrWalkerParent } from './media-condition-list';
+import { MediaNot, MediaNotWalkerEntry, MediaNotWalkerParent } from './media-not';
export class MediaCondition {
type = 'media-condition';
@@ -10,7 +10,36 @@ export class MediaCondition {
this.media = media;
}
+ tokens() {
+ return this.media.tokens();
+ }
+
toString() {
return this.media.toString();
}
+
+ indexOf(item: MediaNot | 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) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
}
+
+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-name.ts b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
index f15ccbde0..451adefd4 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-name.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -4,25 +4,39 @@ import { isToken, stringify } from '@csstools/css-tokenizer';
export class MediaFeatureName {
type = 'mf-name';
- value: ComponentValue;
+ name: ComponentValue;
- constructor(value: ComponentValue) {
- this.value = value;
+ constructor(name: ComponentValue) {
+ this.name = name;
}
tokens() {
- if (isToken(this.value)) {
- return this.value;
+ if (isToken(this.name)) {
+ return this.name;
}
- return this.value.tokens();
+ return this.name.tokens();
}
toString() {
- if (isToken(this.value)) {
- return stringify(this.value);
+ if (isToken(this.name)) {
+ return stringify(this.name);
}
- return this.value.toString();
+ return this.name.toString();
+ }
+
+ indexOf(item: ComponentValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
}
}
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
index 90fd309e4..104de5819 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -1,7 +1,6 @@
-import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
import { stringify, TokenColon } from '@csstools/css-tokenizer';
import { MediaFeatureName } from './media-feature-name';
-import { MediaFeatureValue } from './media-feature-value';
+import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
export class MediaFeaturePlain {
type = 'mf-plain';
@@ -28,11 +27,36 @@ export class MediaFeaturePlain {
return this.name.toString() + stringify(this.colon) + this.value.toString();
}
- walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeaturePlain | MediaFeatureValue }, index: number) => boolean) {
- if (cb({ node: this.value, parent: this }, 0) === false) {
+ 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) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
return false;
}
return this.value.walk(cb);
}
}
+
+export type MediaFeaturePlainWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
+export type MediaFeaturePlainWalkerParent = MediaFeatureValueWalkerParent | MediaFeaturePlain;
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
index df9cdf053..45a144df4 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-range.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -1,8 +1,7 @@
-import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
import { stringify, TokenDelim } from '@csstools/css-tokenizer';
import { comparisonFromTokens } from './media-feature-comparison';
import { MediaFeatureName } from './media-feature-name';
-import { MediaFeatureValue } from './media-feature-value';
+import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
export type MediaFeatureRange = MediaFeatureRangeNameValue |
MediaFeatureRangeValueName |
@@ -38,8 +37,30 @@ export class MediaFeatureRangeNameValue {
return this.name.toString() + stringify(...this.operator) + this.value.toString();
}
- walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
- if (cb({ node: this.value, parent: this }, 0) === false) {
+ 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) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
return false;
}
@@ -78,8 +99,30 @@ export class MediaFeatureRangeValueName {
return this.value.toString() + stringify(...this.operator) + this.name.toString();
}
- walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
- if (cb({ node: this.value, parent: this }, 0) === false) {
+ 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) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
return false;
}
@@ -128,8 +171,38 @@ export class MediaFeatureRangeLowHigh {
return this.low.toString() + stringify(...this.lowOperator) + this.name.toString() + stringify(...this.highOperator) + this.high.toString();
}
- walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
- if (cb({ node: this.low, parent: this }, 0) === false) {
+ indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ if (item === this.low) {
+ return 'low';
+ }
+
+ if (item === this.high) {
+ return 'high';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+
+ if (index === 'low') {
+ return this.low;
+ }
+
+ if (index === 'high') {
+ return this.high;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean) {
+ if (cb({ node: this.low, parent: this }, 'low') === false) {
return false;
}
@@ -139,7 +212,7 @@ export class MediaFeatureRangeLowHigh {
}
}
- if (cb({ node: this.high, parent: this }, 0) === false) {
+ if (cb({ node: this.high, parent: this }, 'high') === false) {
return false;
}
@@ -190,8 +263,38 @@ export class MediaFeatureRangeHighLow {
return this.high.toString() + stringify(...this.highOperator) + this.name.toString() + stringify(...this.lowOperator) + this.low.toString();
}
- walk(cb: (entry: { node: ComponentValue | MediaFeatureValue, parent: ContainerNode | MediaFeatureValue | MediaFeatureRange }, index: number) => boolean) {
- if (cb({ node: this.high, parent: this }, 0) === false) {
+ indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
+ if (item === this.name) {
+ return 'name';
+ }
+
+ if (item === this.low) {
+ return 'low';
+ }
+
+ if (item === this.high) {
+ return 'high';
+ }
+
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'name') {
+ return this.name;
+ }
+
+ if (index === 'low') {
+ return this.low;
+ }
+
+ if (index === 'high') {
+ return this.high;
+ }
+ }
+
+ walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean) {
+ if (cb({ node: this.high, parent: this }, 'high') === false) {
return false;
}
@@ -201,7 +304,7 @@ export class MediaFeatureRangeHighLow {
}
}
- if (cb({ node: this.low, parent: this }, 0) === false) {
+ if (cb({ node: this.low, parent: this }, 'low') === false) {
return false;
}
@@ -212,3 +315,6 @@ export class MediaFeatureRangeHighLow {
}
}
}
+
+export type MediaFeatureRangeWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
+export type MediaFeatureRangeWalkerParent = MediaFeatureValueWalkerParent | MediaFeatureRange;
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
index 61e05380d..bf07cbb3c 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-value.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
@@ -26,8 +26,22 @@ export class MediaFeatureValue {
return this.value.toString();
}
- walk(cb: (entry: { node: ComponentValue, parent: ContainerNode | MediaFeatureValue }, index: number) => boolean) {
- if (cb({ node: this.value, parent: this }, 0) === false) {
+ 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) {
+ if (cb({ node: this.value, parent: this }, 'value') === false) {
return false;
}
@@ -36,3 +50,6 @@ export class MediaFeatureValue {
}
}
}
+
+export type MediaFeatureValueWalkerEntry = ComponentValue;
+export type MediaFeatureValueWalkerParent = ContainerNode | MediaFeatureValue;
diff --git a/packages/media-query-list-parser/src/nodes/media-feature.ts b/packages/media-query-list-parser/src/nodes/media-feature.ts
index ae69aaa27..60fcc21eb 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature.ts
@@ -1,23 +1,57 @@
import { CSSToken, stringify } from '@csstools/css-tokenizer';
import { MediaFeatureBoolean } from './media-feature-boolean';
-import { MediaFeaturePlain } from './media-feature-plain';
-import { MediaFeatureRange } from './media-feature-range';
+import { MediaFeaturePlain, MediaFeaturePlainWalkerEntry, MediaFeaturePlainWalkerParent } from './media-feature-plain';
+import { MediaFeatureRange, MediaFeatureRangeWalkerEntry, MediaFeatureRangeWalkerParent } from './media-feature-range';
export class MediaFeature {
type = 'media-feature';
feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
+ before: Array;
+ after: Array;
- startToken: CSSToken;
- endToken: CSSToken;
-
- constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange, startToken: CSSToken, endToken: CSSToken) {
- this.startToken = startToken;
- this.endToken = endToken;
+ constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange, before: Array = [], after: Array = []) {
this.feature = feature;
+ this.before = before;
+ this.after = after;
+ }
+
+ tokens() {
+ return [
+ ...this.before,
+ ...this.feature.tokens(),
+ ...this.after,
+ ];
}
toString() {
- return stringify(this.startToken) + this.feature.toString() + stringify(this.endToken);
+ 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) {
+ if (cb({ node: this.feature, parent: this }, 'feature') === false) {
+ return false;
+ }
+
+ if ('walk' in this.feature) {
+ return this.feature.walk(cb);
+ }
}
}
+
+export type MediaFeatureWalkerEntry = MediaFeaturePlainWalkerEntry | MediaFeatureRangeWalkerEntry | MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
+export type MediaFeatureWalkerParent = MediaFeaturePlainWalkerParent | MediaFeatureRangeWalkerParent | MediaFeature;
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
index 495e89448..f937357ab 100644
--- a/packages/media-query-list-parser/src/nodes/media-in-parens.ts
+++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
@@ -7,62 +7,44 @@ import { MediaFeature } from './media-feature';
export class MediaInParens {
type = 'media-in-parens';
- startToken?: CSSToken;
- endToken?: CSSToken;
media: MediaCondition | MediaFeature | GeneralEnclosed;
+ before: Array;
+ after: Array;
- constructor(media: MediaCondition | MediaFeature | GeneralEnclosed, startToken?: CSSToken, endToken?: CSSToken) {
- this.startToken = startToken;
- this.endToken = endToken;
+ constructor(media: MediaCondition | MediaFeature | GeneralEnclosed, before: Array = [], after: Array = []) {
this.media = media;
+ this.before = before;
+ this.after = after;
}
tokens() {
- if (this.media.type === 'general-enclosed') {
- return this.media.tokens();
- }
-
- if (this.media.type === 'media-feature') {
- return this.media.tokens();
- }
-
- if (this.media.type === 'media-condition') {
- if (!this.startToken || !this.endToken) {
- throw new Error('Failed to list tokens for "media-in-parens" with "media-condition"');
- }
-
- return [
- this.startToken,
- ...this.media.tokens(),
- this.endToken,
- ];
- }
-
- throw new Error('Failed to list tokens for "media-in-parens"');
+ return [
+ ...this.before,
+ ...this.media.tokens(),
+ ...this.after,
+ ];
}
toString() {
- if (this.media.type === 'general-enclosed') {
- return this.media.toString();
- }
+ return stringify(...this.before) + this.media.toString() + stringify(...this.after);
+ }
- if (this.media.type === 'media-feature') {
- return this.media.toString();
+ indexOf(item: MediaCondition | MediaFeature | GeneralEnclosed): number | string {
+ if (item === this.media) {
+ return 'media';
}
- if (this.media.type === 'media-condition') {
- if (!this.startToken || !this.endToken) {
- throw new Error('Failed to stringify "media-in-parens" with "media-condition"');
- }
+ return -1;
+ }
- return stringify(this.startToken) + this.media.toString() + stringify(this.endToken);
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
}
-
- throw new Error('Failed to stringify "media-in-parens"');
}
- walk(cb: (entry: { node: ComponentValue | MediaCondition | MediaFeature | GeneralEnclosed, parent: ContainerNode | MediaInParens | GeneralEnclosed }, index: number) => boolean) {
- if (cb({ node: this.media, parent: this }, 0) === false) {
+ walk(cb: (entry: { node: MediaInParensWalkerEntry, parent: MediaInParensWalkerParent }, index: number | string) => boolean) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
return false;
}
@@ -71,3 +53,6 @@ export class MediaInParens {
}
}
}
+
+export type MediaInParensWalkerEntry = ComponentValue | MediaCondition | MediaFeature | GeneralEnclosed;
+export type MediaInParensWalkerParent = ContainerNode | MediaInParens | GeneralEnclosed;
diff --git a/packages/media-query-list-parser/src/nodes/media-not.ts b/packages/media-query-list-parser/src/nodes/media-not.ts
index c6b8d9b88..2a9dae586 100644
--- a/packages/media-query-list-parser/src/nodes/media-not.ts
+++ b/packages/media-query-list-parser/src/nodes/media-not.ts
@@ -1,15 +1,50 @@
-import { MediaInParens } from './media-in-parens';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
export class MediaNot {
type = 'media-not';
+ modifier: Array;
media: MediaInParens;
- constructor(media: MediaInParens) {
+ constructor(modifier: Array, media: MediaInParens) {
+ this.modifier = modifier;
this.media = media;
}
+ tokens() {
+ return [
+ ...this.modifier,
+ ...this.media.tokens(),
+ ];
+ }
+
toString() {
- return 'not ' + this.media.toString();
+ 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) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
}
}
+
+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
index f2b6779d3..c3945fe2f 100644
--- a/packages/media-query-list-parser/src/nodes/media-or.ts
+++ b/packages/media-query-list-parser/src/nodes/media-or.ts
@@ -1,15 +1,50 @@
-import { MediaInParens } from './media-in-parens';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
export class MediaOr {
type = 'media-or';
+ modifier: Array;
media: MediaInParens;
- constructor(media: MediaInParens) {
+ constructor(modifier: Array, media: MediaInParens) {
+ this.modifier = modifier;
this.media = media;
}
+ tokens() {
+ return [
+ ...this.modifier,
+ ...this.media.tokens(),
+ ];
+ }
+
toString() {
- return 'or ' + this.media.toString();
+ 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) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
}
}
+
+export type MediaOrWalkerEntry = MediaInParensWalkerEntry | MediaInParens;
+export type MediaOrWalkerParent = MediaInParensWalkerParent | MediaOr;
diff --git a/packages/media-query-list-parser/src/nodes/media-query.ts b/packages/media-query-list-parser/src/nodes/media-query.ts
index 5ab50cfec..ad2780997 100644
--- a/packages/media-query-list-parser/src/nodes/media-query.ts
+++ b/packages/media-query-list-parser/src/nodes/media-query.ts
@@ -1,40 +1,71 @@
-import { MediaCondition } from './media-condition';
-import { MediaConditionWithoutOr } from './media-condition-without-or-or';
-import { MediaQueryModifier } from './media-query-modifier';
-import { MediaType } from './media-type';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { MediaCondition, MediaConditionWalkerEntry, MediaConditionWalkerParent } from './media-condition';
+import { MediaConditionWithoutOr, MediaConditionWithoutOrWalkerEntry, MediaConditionWithoutOrWalkerParent } from './media-condition-without-or';
export type MediaQuery = MediaQueryWithType | MediaQueryWithoutType;
export class MediaQueryWithType {
type = 'media-query-with-type';
- modifier?: MediaQueryModifier;
- mediaType: MediaType;
- media?: MediaConditionWithoutOr;
+ modifier: Array;
+ mediaType: Array;
+ media: MediaConditionWithoutOr | null = null;
- constructor(modifier: MediaQueryModifier | null, mediaType: MediaType, media: MediaConditionWithoutOr | null) {
+ constructor(modifier: Array, mediaType: Array, media?: MediaConditionWithoutOr | null) {
this.modifier = modifier;
this.mediaType = mediaType;
this.media = media;
}
+ tokens() {
+ if (this.media) {
+ return [
+ ...this.modifier,
+ ...this.mediaType,
+ ...this.media.tokens(),
+ ];
+ }
+
+ return [
+ ...this.modifier,
+ ...this.mediaType,
+ ];
+ }
+
toString() {
- if (this.modifier && this.media) {
- return `${this.modifier} ${this.mediaType} and ${this.media.toString()}`;
+ if (this.media) {
+ return stringify(...this.modifier) + stringify(...this.mediaType) + this.media.toString();
}
- if (this.modifier) {
- return `${this.modifier} ${this.mediaType}`;
+ return stringify(...this.modifier) + stringify(...this.mediaType);
+ }
+
+ indexOf(item: MediaConditionWithoutOr): number | string {
+ if (item === this.media) {
+ return 'media';
}
- if (this.media) {
- return `${this.mediaType} and ${this.media.toString()}`;
+ return -1;
+ }
+
+ at(index: number | string) {
+ if (index === 'media') {
+ return this.media;
}
+ }
- return this.mediaType;
+ walk(cb: (entry: { node: MediaQueryWithTypeWalkerEntry, parent: MediaQueryWithTypeWalkerParent }, index: number | string) => boolean) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
}
}
+export type MediaQueryWithTypeWalkerEntry = MediaConditionWithoutOrWalkerEntry | MediaConditionWithoutOr;
+export type MediaQueryWithTypeWalkerParent = MediaConditionWithoutOrWalkerParent | MediaQueryWithType;
+
export class MediaQueryWithoutType {
type = 'media-query-without-type';
@@ -44,7 +75,36 @@ export class MediaQueryWithoutType {
this.media = media;
}
+ tokens() {
+ return this.media.tokens();
+ }
+
toString() {
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) {
+ if (cb({ node: this.media, parent: this }, 'media') === false) {
+ return false;
+ }
+
+ return this.media.walk(cb);
+ }
}
+
+export type MediaQueryWithoutTypeWalkerEntry = MediaConditionWalkerEntry | MediaCondition;
+export type MediaQueryWithoutTypeWalkerParent = MediaConditionWalkerParent | MediaQueryWithoutType;
diff --git a/packages/media-query-list-parser/src/parser/parse.ts b/packages/media-query-list-parser/src/parser/parse.ts
index e96aa49ef..c050d65f6 100644
--- a/packages/media-query-list-parser/src/parser/parse.ts
+++ b/packages/media-query-list-parser/src/parser/parse.ts
@@ -1,5 +1,6 @@
-import { parseCommaSeparatedListOfComponentValues } from '@csstools/css-parser-algorithms';
-import { tokenizer } from '@csstools/css-tokenizer';
+import { ComponentValue, parseCommaSeparatedListOfComponentValues, TokenNode } from '@csstools/css-parser-algorithms';
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+import { GeneralEnclosed } from '../nodes/general-enclosed';
export function parse(source: string) {
const onParseError = (err) => {
@@ -21,9 +22,52 @@ export function parse(source: string) {
tokens.push(t.nextToken()); // EOF-token
}
- const result = parseCommaSeparatedListOfComponentValues(tokens, {
+ const parsed = parseCommaSeparatedListOfComponentValues(tokens, {
onParseError: onParseError,
});
- return result;
+ const mediaQueryList = parsed.map((componentValuesList) => {
+ const result = [];
+
+ const lastSliceIndex = 0;
+ for (let i = 0; i < componentValuesList.length; i++) {
+ const componentValue = componentValuesList[i];
+ if (componentValue.type === 'whitespace' || componentValue.type === 'comment') {
+ continue;
+ }
+
+ if (componentValue.type === 'function') {
+ result.push(new GeneralEnclosed(componentValue));
+ }
+ }
+
+ });
+
+ return mediaQueryList;
+}
+
+function consumeMediaQueryWithType(componentValuesList: Array) {
+ const modifier: Array = [];
+ const mediaType: Array = [];
+
+ const lastSliceIndex = 0;
+ for (let i = 0; i < componentValuesList.length; i++) {
+ const componentValue = componentValuesList[i];
+ if (componentValue.type === 'whitespace' || componentValue.type === 'comment') {
+ continue;
+ }
+
+ if (componentValue.type === 'token') {
+ const token = (componentValue as TokenNode).value;
+
+ switch (token[0]) {
+ case TokenType.Ident:
+
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
}
diff --git a/packages/media-query-list-parser/test/test.mjs b/packages/media-query-list-parser/test/test.mjs
index 0ddae5dc7..cd5d20ff0 100644
--- a/packages/media-query-list-parser/test/test.mjs
+++ b/packages/media-query-list-parser/test/test.mjs
@@ -2,8 +2,13 @@ import { parse } from '@csstools/media-query-list-parser';
parse('(/* a comment */foo ) something else');
-parse('((min-width: 300px) and (prefers-color-scheme:/* a comment */dark))').forEach((mediaQuery) => {
+parse('not screen and ((min-width: 300px) and (prefers-color-scheme:/* a comment */dark))').forEach((mediaQuery) => {
mediaQuery.forEach((args) => {
+ if (!('walk' in args)) {
+ console.log(args);
+ return;
+ }
+
args.walk((a) => {
console.log(a.node.type);
console.log(a.node.toString());
From f327ff4c6745beab0f07b30ef1cef2b397abbd03 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Thu, 20 Oct 2022 17:26:35 +0200
Subject: [PATCH 04/35] wip
---
packages/css-tokenizer/test/token/numeric.mjs | 20 +++
.../src/nodes/general-enclosed.ts | 9 -
.../src/nodes/media-feature-name.ts | 53 ++++--
.../src/nodes/media-feature-plain.ts | 40 ++++-
.../src/nodes/media-feature-value.ts | 161 ++++++++++++++++--
.../src/nodes/media-in-parens.ts | 13 +-
.../src/parser/parse.ts | 31 +---
.../src/util/component-value-is.ts | 29 ++++
8 files changed, 290 insertions(+), 66 deletions(-)
create mode 100644 packages/media-query-list-parser/src/util/component-value-is.ts
diff --git a/packages/css-tokenizer/test/token/numeric.mjs b/packages/css-tokenizer/test/token/numeric.mjs
index 801564d6b..0ee6c1f0f 100644
--- a/packages/css-tokenizer/test/token/numeric.mjs
+++ b/packages/css-tokenizer/test/token/numeric.mjs
@@ -220,6 +220,26 @@ import { collectTokens } from '../util/collect-tokens.mjs';
);
}
+{
+ 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 ',
diff --git a/packages/media-query-list-parser/src/nodes/general-enclosed.ts b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
index d2e019ee4..eb7ca0774 100644
--- a/packages/media-query-list-parser/src/nodes/general-enclosed.ts
+++ b/packages/media-query-list-parser/src/nodes/general-enclosed.ts
@@ -1,5 +1,4 @@
import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
-import { isToken, stringify } from '@csstools/css-tokenizer';
export class GeneralEnclosed {
type = 'general-enclosed';
@@ -11,18 +10,10 @@ export class GeneralEnclosed {
}
tokens() {
- if (isToken(this.value)) {
- return this.value;
- }
-
return this.value.tokens();
}
toString() {
- if (isToken(this.value)) {
- return stringify(this.value);
- }
-
return this.value.toString();
}
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
index 451adefd4..70ec906c3 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-name.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -1,29 +1,30 @@
import { ComponentValue } from '@csstools/css-parser-algorithms';
-import { isToken, stringify } from '@csstools/css-tokenizer';
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
+import { isIdent } from '../util/component-value-is';
export class MediaFeatureName {
type = 'mf-name';
name: ComponentValue;
+ before: Array;
+ after: Array;
- constructor(name: ComponentValue) {
+ constructor(name: ComponentValue, before: Array = [], after: Array = []) {
this.name = name;
+ this.before = before;
+ this.after = after;
}
tokens() {
- if (isToken(this.name)) {
- return this.name;
- }
-
- return this.name.tokens();
+ return [
+ ...this.before,
+ ...this.name.tokens(),
+ ...this.after,
+ ];
}
toString() {
- if (isToken(this.name)) {
- return stringify(this.name);
- }
-
- return this.name.toString();
+ return stringify(...this.before) + this.name.toString() + stringify(...this.after);
}
indexOf(item: ComponentValue): number | string {
@@ -40,3 +41,31 @@ export class MediaFeatureName {
}
}
}
+
+export function matchesMediaFeatureName(componentValues: Array) {
+ let singleIdentTokenIndex = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === 'whitespace') {
+ continue;
+ }
+
+ if (componentValue.type === 'comment') {
+ continue;
+ }
+
+ if (isIdent(componentValue)) {
+ if (singleIdentTokenIndex !== -1) {
+ return -1;
+ }
+
+ singleIdentTokenIndex = i;
+ continue;
+ }
+
+ return -1;
+ }
+
+ return singleIdentTokenIndex;
+}
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
index 104de5819..c57b216f2 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -1,6 +1,7 @@
-import { stringify, TokenColon } from '@csstools/css-tokenizer';
-import { MediaFeatureName } from './media-feature-name';
-import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
+import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenColon, TokenType } from '@csstools/css-tokenizer';
+import { matchesMediaFeatureName, MediaFeatureName } from './media-feature-name';
+import { matchesMediaFeatureValue, MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
export class MediaFeaturePlain {
type = 'mf-plain';
@@ -60,3 +61,36 @@ export class MediaFeaturePlain {
export type MediaFeaturePlainWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
export type MediaFeaturePlainWalkerParent = MediaFeatureValueWalkerParent | MediaFeaturePlain;
+
+export function matchesMediaFeaturePlain(componentValues: Array) {
+ let a: Array = [];
+ let b: Array = [];
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === 'token') {
+ const token = componentValue.value as CSSToken;
+ if (token[0] === TokenType.Colon) {
+ a = componentValues.slice(0, i);
+ b = componentValues.slice(i + 1);
+ break;
+ }
+ }
+ }
+
+ if (!a.length || !b.length) {
+ return -1;
+ }
+
+ const aResult = matchesMediaFeatureName(a);
+ if (aResult === -1) {
+ return -1;
+ }
+
+ const bResult = matchesMediaFeatureValue(b);
+ if (bResult === -1) {
+ return -1;
+ }
+
+ return [aResult, bResult];
+}
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
index bf07cbb3c..ae0661866 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-value.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
@@ -1,29 +1,42 @@
-import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
-import { isToken, stringify } from '@csstools/css-tokenizer';
+import { ComponentValue, ContainerNode, FunctionNode, TokenNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenFunction, TokenType } from '@csstools/css-tokenizer';
+import { isDimension, isIdent, isNumber } from '../util/component-value-is';
export class MediaFeatureValue {
type = 'mf-value';
- value: ComponentValue;
+ value: ComponentValue | Array;
+ before: Array;
+ after: Array;
- constructor(value: ComponentValue) {
+ constructor(value: ComponentValue | Array, before: Array = [], after: Array = []) {
this.value = value;
+ this.before = before;
+ this.after = after;
}
tokens() {
- if (isToken(this.value)) {
- return this.value;
+ if (Array.isArray(this.value)) {
+ return [
+ ...this.before,
+ ...this.value.flatMap((x) => x.tokens()),
+ ...this.after,
+ ];
}
- return this.value.tokens();
+ return [
+ ...this.before,
+ ...this.value.tokens(),
+ ...this.after,
+ ];
}
toString() {
- if (isToken(this.value)) {
- return stringify(this.value);
+ if (Array.isArray(this.value)) {
+ return stringify(...this.before) + this.value.map((x) => x.toString()).join('') + stringify(...this.after);
}
- return this.value.toString();
+ return stringify(...this.before) + this.value.toString() + stringify(...this.after);
}
indexOf(item: ComponentValue): number | string {
@@ -51,5 +64,131 @@ export class MediaFeatureValue {
}
}
-export type MediaFeatureValueWalkerEntry = ComponentValue;
+export type MediaFeatureValueWalkerEntry = ComponentValue | Array;
export type MediaFeatureValueWalkerParent = ContainerNode | MediaFeatureValue;
+
+export function matchesMediaFeatureValue(componentValues: Array) {
+ let candidateIndexStart = -1;
+ let candidateIndexEnd = -1;
+
+ for (let i = 0; i < componentValues.length; i++) {
+ const componentValue = componentValues[i];
+ if (componentValue.type === 'whitespace') {
+ continue;
+ }
+
+ if (componentValue.type === 'comment') {
+ continue;
+ }
+
+ if (candidateIndexStart !== -1) {
+ return -1;
+ }
+
+ if (isNumber(componentValue)) {
+ const maybeRatio = matchesRatioExactly(componentValues.slice(i));
+ if (maybeRatio !== -1) {
+ candidateIndexStart = maybeRatio[0];
+ candidateIndexEnd = maybeRatio[1];
+ 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 -1;
+ }
+
+ return [candidateIndexStart, candidateIndexEnd];
+}
+
+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-in-parens.ts b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
index f937357ab..9d4d07626 100644
--- a/packages/media-query-list-parser/src/nodes/media-in-parens.ts
+++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
@@ -1,8 +1,15 @@
-import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { ComponentValue, ContainerNode, TokenNode } 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';
export class MediaInParens {
type = 'media-in-parens';
@@ -54,5 +61,5 @@ export class MediaInParens {
}
}
-export type MediaInParensWalkerEntry = ComponentValue | MediaCondition | MediaFeature | GeneralEnclosed;
-export type MediaInParensWalkerParent = ContainerNode | MediaInParens | GeneralEnclosed;
+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/parser/parse.ts b/packages/media-query-list-parser/src/parser/parse.ts
index c050d65f6..af7834d2e 100644
--- a/packages/media-query-list-parser/src/parser/parse.ts
+++ b/packages/media-query-list-parser/src/parser/parse.ts
@@ -1,6 +1,7 @@
-import { ComponentValue, parseCommaSeparatedListOfComponentValues, TokenNode } from '@csstools/css-parser-algorithms';
-import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+import { ComponentValue, parseCommaSeparatedListOfComponentValues, SimpleBlockNode, TokenNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, tokenizer, TokenType } from '@csstools/css-tokenizer';
import { GeneralEnclosed } from '../nodes/general-enclosed';
+import { MediaFeatureName } from '../nodes/media-feature-name';
export function parse(source: string) {
const onParseError = (err) => {
@@ -45,29 +46,3 @@ export function parse(source: string) {
return mediaQueryList;
}
-
-function consumeMediaQueryWithType(componentValuesList: Array) {
- const modifier: Array = [];
- const mediaType: Array = [];
-
- const lastSliceIndex = 0;
- for (let i = 0; i < componentValuesList.length; i++) {
- const componentValue = componentValuesList[i];
- if (componentValue.type === 'whitespace' || componentValue.type === 'comment') {
- continue;
- }
-
- if (componentValue.type === 'token') {
- const token = (componentValue as TokenNode).value;
-
- switch (token[0]) {
- case TokenType.Ident:
-
- break;
-
- default:
- break;
- }
- }
- }
-}
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..d94b5433f
--- /dev/null
+++ b/packages/media-query-list-parser/src/util/component-value-is.ts
@@ -0,0 +1,29 @@
+import { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, TokenFunction, TokenType } from '@csstools/css-tokenizer';
+
+export function isNumber(componentValue: ComponentValue) {
+ if (
+ (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Number) ||
+ (componentValue.type === 'function' && ((componentValue as FunctionNode).name as TokenFunction)[4].value === 'calc')
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+export function isDimension(componentValue: ComponentValue) {
+ if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Dimension) {
+ return true;
+ }
+
+ return false;
+}
+
+export function isIdent(componentValue: ComponentValue) {
+ if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
+ return true;
+ }
+
+ return false;
+}
From 343a2abf4af100100a2035850fa1e83b65ac860d Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Fri, 21 Oct 2022 19:42:21 +0200
Subject: [PATCH 05/35] wip
---
packages/css-tokenizer/test/token/basic.mjs | 25 ++++++++
.../src/nodes/media-feature-comparison.ts | 40 ++++++++++++-
.../src/nodes/media-feature-name.ts | 22 +++++--
.../src/nodes/media-feature-plain.ts | 24 ++++----
.../src/nodes/media-feature-range.ts | 58 ++++++++++++++++++-
.../src/nodes/media-feature-value.ts | 31 +++++++---
.../src/nodes/media-in-parens.ts | 2 +-
.../src/util/component-value-is.ts | 26 ++++++++-
8 files changed, 200 insertions(+), 28 deletions(-)
diff --git a/packages/css-tokenizer/test/token/basic.mjs b/packages/css-tokenizer/test/token/basic.mjs
index 609e80713..46033d7d5 100644
--- a/packages/css-tokenizer/test/token/basic.mjs
+++ b/packages/css-tokenizer/test/token/basic.mjs
@@ -19,6 +19,31 @@ import { collectTokens } from '../util/collect-tokens.mjs';
);
}
+{
+ 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));',
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
index 79a2ef9f4..cd02d772f 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
@@ -1,4 +1,5 @@
-import { TokenDelim, TokenType } from '@csstools/css-tokenizer';
+import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { CSSToken, TokenDelim, TokenType } from '@csstools/css-tokenizer';
export enum MediaFeatureLT {
LT = '<',
@@ -16,6 +17,43 @@ export enum MediaFeatureEQ {
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 === 'token') {
+ const token = componentValue.value as CSSToken;
+ if (token[0] === TokenType.Delim) {
+ if (token[4].value === MediaFeatureEQ.EQ) {
+ if (firstTokenIndex) {
+ 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 */
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
index 70ec906c3..b700959b0 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-name.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -1,5 +1,5 @@
import { ComponentValue } from '@csstools/css-parser-algorithms';
-import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
+import { CSSToken, stringify } from '@csstools/css-tokenizer';
import { isIdent } from '../util/component-value-is';
export class MediaFeatureName {
@@ -42,7 +42,7 @@ export class MediaFeatureName {
}
}
-export function matchesMediaFeatureName(componentValues: Array) {
+export function parseMediaFeatureName(componentValues: Array) {
let singleIdentTokenIndex = -1;
for (let i = 0; i < componentValues.length; i++) {
@@ -57,15 +57,27 @@ export function matchesMediaFeatureName(componentValues: Array)
if (isIdent(componentValue)) {
if (singleIdentTokenIndex !== -1) {
- return -1;
+ return false;
}
singleIdentTokenIndex = i;
continue;
}
- return -1;
+ return false;
+ }
+
+ if (singleIdentTokenIndex === -1) {
+ return false;
}
- return singleIdentTokenIndex;
+ 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
index c57b216f2..2672270fb 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -1,7 +1,7 @@
import { ComponentValue } from '@csstools/css-parser-algorithms';
import { CSSToken, stringify, TokenColon, TokenType } from '@csstools/css-tokenizer';
-import { matchesMediaFeatureName, MediaFeatureName } from './media-feature-name';
-import { matchesMediaFeatureValue, MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
+import { parseMediaFeatureName, MediaFeatureName } from './media-feature-name';
+import { parseMediaFeatureValue, MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
export class MediaFeaturePlain {
type = 'mf-plain';
@@ -62,9 +62,10 @@ export class MediaFeaturePlain {
export type MediaFeaturePlainWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
export type MediaFeaturePlainWalkerParent = MediaFeatureValueWalkerParent | MediaFeaturePlain;
-export function matchesMediaFeaturePlain(componentValues: Array) {
+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];
@@ -73,24 +74,25 @@ export function matchesMediaFeaturePlain(componentValues: Array)
if (token[0] === TokenType.Colon) {
a = componentValues.slice(0, i);
b = componentValues.slice(i + 1);
+ colon = token;
break;
}
}
}
if (!a.length || !b.length) {
- return -1;
+ return false;
}
- const aResult = matchesMediaFeatureName(a);
- if (aResult === -1) {
- return -1;
+ const name = parseMediaFeatureName(a);
+ if (name === false) {
+ return false;
}
- const bResult = matchesMediaFeatureValue(b);
- if (bResult === -1) {
- return -1;
+ const value = parseMediaFeatureValue(b);
+ if (value === false) {
+ return false;
}
- return [aResult, bResult];
+ 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
index 45a144df4..363906105 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-range.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -1,5 +1,6 @@
-import { stringify, TokenDelim } from '@csstools/css-tokenizer';
-import { comparisonFromTokens } from './media-feature-comparison';
+import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenDelim, TokenType } from '@csstools/css-tokenizer';
+import { comparisonFromTokens, matchesComparison } from './media-feature-comparison';
import { MediaFeatureName } from './media-feature-name';
import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
@@ -318,3 +319,56 @@ export class MediaFeatureRangeHighLow {
export type MediaFeatureRangeWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
export type MediaFeatureRangeWalkerParent = MediaFeatureValueWalkerParent | MediaFeatureRange;
+
+export function matchesMediaFeaturePlain(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 === '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[i] + i,
+ ];
+ } else {
+ comparisonTwo = [
+ comparison[0] + i,
+ comparison[i] + i,
+ ];
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (comparisonOne === -1) {
+ return false;
+ }
+
+ if (comparisonTwo === -1) {
+ return false;
+ }
+
+ if (!a.length || !b.length) {
+ return -1;
+ }
+
+ const aResult = matchesMediaFeatureName(a);
+ if (aResult === -1) {
+ return -1;
+ }
+
+ const bResult = matchesMediaFeatureValue(b);
+ if (bResult === -1) {
+ return -1;
+ }
+
+ return [aResult, bResult];
+}
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
index ae0661866..25acb0ec2 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-value.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
@@ -1,5 +1,5 @@
-import { ComponentValue, ContainerNode, FunctionNode, TokenNode } from '@csstools/css-parser-algorithms';
-import { CSSToken, stringify, TokenFunction, TokenType } from '@csstools/css-tokenizer';
+import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
import { isDimension, isIdent, isNumber } from '../util/component-value-is';
export class MediaFeatureValue {
@@ -10,7 +10,12 @@ export class MediaFeatureValue {
after: Array;
constructor(value: ComponentValue | Array, before: Array = [], after: Array = []) {
- this.value = value;
+ if (Array.isArray(value) && value.length === 1) {
+ this.value = value[0];
+ } else {
+ this.value = value;
+ }
+
this.before = before;
this.after = after;
}
@@ -67,7 +72,7 @@ export class MediaFeatureValue {
export type MediaFeatureValueWalkerEntry = ComponentValue | Array;
export type MediaFeatureValueWalkerParent = ContainerNode | MediaFeatureValue;
-export function matchesMediaFeatureValue(componentValues: Array) {
+export function parseMediaFeatureValue(componentValues: Array) {
let candidateIndexStart = -1;
let candidateIndexEnd = -1;
@@ -82,7 +87,7 @@ export function matchesMediaFeatureValue(componentValues: Array)
}
if (candidateIndexStart !== -1) {
- return -1;
+ return false;
}
if (isNumber(componentValue)) {
@@ -111,10 +116,22 @@ export function matchesMediaFeatureValue(componentValues: Array)
continue;
}
- return -1;
+ return false;
+ }
+
+ if (candidateIndexStart === -1) {
+ return false;
}
- return [candidateIndexStart, candidateIndexEnd];
+ 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) {
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
index 9d4d07626..5b351b81d 100644
--- a/packages/media-query-list-parser/src/nodes/media-in-parens.ts
+++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
@@ -1,4 +1,4 @@
-import { ComponentValue, ContainerNode, TokenNode } from '@csstools/css-parser-algorithms';
+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';
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
index d94b5433f..c71b94ab5 100644
--- a/packages/media-query-list-parser/src/util/component-value-is.ts
+++ b/packages/media-query-list-parser/src/util/component-value-is.ts
@@ -1,5 +1,5 @@
import { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms';
-import { CSSToken, TokenFunction, TokenType } from '@csstools/css-tokenizer';
+import { CSSToken, TokenFunction, TokenIdent, TokenType } from '@csstools/css-tokenizer';
export function isNumber(componentValue: ComponentValue) {
if (
@@ -12,6 +12,30 @@ export function isNumber(componentValue: ComponentValue) {
return false;
}
+export function isNumericConstant(componentValue: ComponentValue) {
+ if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
+ const token = componentValue.value as TokenIdent;
+ const tokenValue = token[4].value.toLowerCase();
+ if (tokenValue === 'infinity') {
+ return true;
+ }
+ if (tokenValue === '-infinity') {
+ return true;
+ }
+ if (tokenValue === 'nan') {
+ return true;
+ }
+ if (tokenValue === 'e') {
+ return true;
+ }
+ if (tokenValue === 'pi') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
export function isDimension(componentValue: ComponentValue) {
if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Dimension) {
return true;
From 1c82f160fe54f997857e946e95e53dbc1f162049 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sat, 22 Oct 2022 23:08:55 +0200
Subject: [PATCH 06/35] finish range
---
.../src/nodes/media-feature-name.ts | 9 +-
.../src/nodes/media-feature-range.ts | 289 +++++++++---------
2 files changed, 153 insertions(+), 145 deletions(-)
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
index b700959b0..b7f6612d6 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-name.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -1,5 +1,5 @@
-import { ComponentValue } from '@csstools/css-parser-algorithms';
-import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { ComponentValue, TokenNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer';
import { isIdent } from '../util/component-value-is';
export class MediaFeatureName {
@@ -15,6 +15,11 @@ export class MediaFeatureName {
this.after = after;
}
+ getName() {
+ const token = (((this.name as TokenNode).value as CSSToken) as TokenIdent);
+ return token[4].value;
+ }
+
tokens() {
return [
...this.before,
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
index 363906105..c1e957add 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-range.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -1,13 +1,12 @@
-import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { ComponentValue, TokenNode } from '@csstools/css-parser-algorithms';
import { CSSToken, stringify, TokenDelim, TokenType } from '@csstools/css-tokenizer';
import { comparisonFromTokens, matchesComparison } from './media-feature-comparison';
-import { MediaFeatureName } from './media-feature-name';
-import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value';
+import { MediaFeatureName, parseMediaFeatureName } from './media-feature-name';
+import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent, parseMediaFeatureValue } from './media-feature-value';
export type MediaFeatureRange = MediaFeatureRangeNameValue |
MediaFeatureRangeValueName |
- MediaFeatureRangeLowHigh |
- MediaFeatureRangeHighLow;
+ MediaFeatureRangeValueNameValue;
export class MediaFeatureRangeNameValue {
type = 'mf-range-name-value';
@@ -133,43 +132,43 @@ export class MediaFeatureRangeValueName {
}
}
-export class MediaFeatureRangeLowHigh {
- type = 'mf-range-low-high';
+export class MediaFeatureRangeValueNameValue {
+ type = 'mf-range-value-name-value';
name: MediaFeatureName;
- low: MediaFeatureValue;
- lowOperator: [TokenDelim, TokenDelim] | [TokenDelim];
- high: MediaFeatureValue;
- highOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+ valueOne: MediaFeatureValue;
+ valueOneOperator: [TokenDelim, TokenDelim] | [TokenDelim];
+ valueTwo: MediaFeatureValue;
+ valueTwoOperator: [TokenDelim, TokenDelim] | [TokenDelim];
- constructor(name: MediaFeatureName, low: MediaFeatureValue, lowOperator: [TokenDelim, TokenDelim] | [TokenDelim], high: MediaFeatureValue, highOperator: [TokenDelim, TokenDelim] | [TokenDelim]) {
+ constructor(name: MediaFeatureName, valueOne: MediaFeatureValue, valueOneOperator: [TokenDelim, TokenDelim] | [TokenDelim], valueTwo: MediaFeatureValue, valueTwoOperator: [TokenDelim, TokenDelim] | [TokenDelim]) {
this.name = name;
- this.low = low;
- this.lowOperator = lowOperator;
- this.high = high;
- this.highOperator = highOperator;
+ this.valueOne = valueOne;
+ this.valueOneOperator = valueOneOperator;
+ this.valueTwo = valueTwo;
+ this.valueTwoOperator = valueTwoOperator;
}
- lowOperatorKind() {
- return comparisonFromTokens(this.lowOperator);
+ valueOneOperatorKind() {
+ return comparisonFromTokens(this.valueOneOperator);
}
- highOperatorKind() {
- return comparisonFromTokens(this.highOperator);
+ valueTwoOperatorKind() {
+ return comparisonFromTokens(this.valueTwoOperator);
}
tokens() {
return [
- ...this.low.tokens(),
- ...this.lowOperator,
+ ...this.valueOne.tokens(),
+ ...this.valueOneOperator,
...this.name.tokens(),
- ...this.highOperator,
- ...this.high.tokens(),
+ ...this.valueTwoOperator,
+ ...this.valueTwo.tokens(),
];
}
toString() {
- return this.low.toString() + stringify(...this.lowOperator) + this.name.toString() + stringify(...this.highOperator) + this.high.toString();
+ return this.valueOne.toString() + stringify(...this.valueOneOperator) + this.name.toString() + stringify(...this.valueTwoOperator) + this.valueTwo.toString();
}
indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
@@ -177,12 +176,12 @@ export class MediaFeatureRangeLowHigh {
return 'name';
}
- if (item === this.low) {
- return 'low';
+ if (item === this.valueOne) {
+ return 'valueOne';
}
- if (item === this.high) {
- return 'high';
+ if (item === this.valueTwo) {
+ return 'valueTwo';
}
return -1;
@@ -193,124 +192,32 @@ export class MediaFeatureRangeLowHigh {
return this.name;
}
- if (index === 'low') {
- return this.low;
+ if (index === 'valueOne') {
+ return this.valueOne;
}
- if (index === 'high') {
- return this.high;
+ if (index === 'valueTwo') {
+ return this.valueTwo;
}
}
walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean) {
- if (cb({ node: this.low, parent: this }, 'low') === false) {
+ if (cb({ node: this.valueOne, parent: this }, 'valueOne') === false) {
return false;
}
- if ('walk' in this.low) {
- if (this.low.walk(cb) === false) {
+ if ('walk' in this.valueOne) {
+ if (this.valueOne.walk(cb) === false) {
return false;
}
}
- if (cb({ node: this.high, parent: this }, 'high') === false) {
+ if (cb({ node: this.valueTwo, parent: this }, 'valueTwo') === false) {
return false;
}
- if ('walk' in this.high) {
- if (this.high.walk(cb) === false) {
- return false;
- }
- }
- }
-}
-
-export class MediaFeatureRangeHighLow {
- type = 'mf-range-high-low';
-
- name: MediaFeatureName;
- low: MediaFeatureValue;
- lowOperator: [TokenDelim, TokenDelim] | [TokenDelim];
- high: MediaFeatureValue;
- highOperator: [TokenDelim, TokenDelim] | [TokenDelim];
-
- constructor(name: MediaFeatureName, high: MediaFeatureValue, highOperator: [TokenDelim, TokenDelim] | [TokenDelim], low: MediaFeatureValue, lowOperator: [TokenDelim, TokenDelim] | [TokenDelim]) {
- this.name = name;
- this.low = low;
- this.lowOperator = lowOperator;
- this.high = high;
- this.highOperator = highOperator;
- }
-
- lowOperatorKind() {
- return comparisonFromTokens(this.lowOperator);
- }
-
- highOperatorKind() {
- return comparisonFromTokens(this.highOperator);
- }
-
- tokens() {
- return [
- ...this.high.tokens(),
- ...this.highOperator,
- ...this.name.tokens(),
- ...this.lowOperator,
- ...this.low.tokens(),
- ];
- }
-
- toString() {
- return this.high.toString() + stringify(...this.highOperator) + this.name.toString() + stringify(...this.lowOperator) + this.low.toString();
- }
-
- indexOf(item: MediaFeatureName | MediaFeatureValue): number | string {
- if (item === this.name) {
- return 'name';
- }
-
- if (item === this.low) {
- return 'low';
- }
-
- if (item === this.high) {
- return 'high';
- }
-
- return -1;
- }
-
- at(index: number | string) {
- if (index === 'name') {
- return this.name;
- }
-
- if (index === 'low') {
- return this.low;
- }
-
- if (index === 'high') {
- return this.high;
- }
- }
-
- walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean) {
- if (cb({ node: this.high, parent: this }, 'high') === false) {
- return false;
- }
-
- if ('walk' in this.high) {
- if (this.high.walk(cb) === false) {
- return false;
- }
- }
-
- if (cb({ node: this.low, parent: this }, 'low') === false) {
- return false;
- }
-
- if ('walk' in this.low) {
- if (this.low.walk(cb) === false) {
+ if ('walk' in this.valueTwo) {
+ if (this.valueTwo.walk(cb) === false) {
return false;
}
}
@@ -348,27 +255,123 @@ export function matchesMediaFeaturePlain(componentValues: Array)
}
}
- if (comparisonOne === -1) {
+ if (comparisonOne === false) {
return false;
}
- if (comparisonTwo === -1) {
- 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 (!a.length || !b.length) {
- return -1;
+ if (comparisonTwo === false) {
+ const a = componentValues.slice(0, comparisonOne[0] - 1);
+ 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 aResult = matchesMediaFeatureName(a);
- if (aResult === -1) {
- return -1;
+ 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 bResult = matchesMediaFeatureValue(b);
- if (bResult === -1) {
- return -1;
+ const a = componentValues.slice(0, comparisonOne[0] - 1);
+ const b = componentValues.slice(comparisonOne[1] + 1, comparisonTwo[0] - 1);
+ 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;
}
- return [aResult, bResult];
+ 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',
+]);
From 23fd5684bd698b0b26b3923e88193a1f466bc774 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sun, 23 Oct 2022 10:29:58 +0200
Subject: [PATCH 07/35] more work
---
.../consume-component-block-function.ts | 24 +++++++---
.../src/nodes/media-condition.ts | 5 ++
.../src/nodes/media-feature-boolean.ts | 12 ++++-
.../src/nodes/media-feature-comparison.ts | 4 +-
.../src/nodes/media-feature-name.ts | 6 +--
.../src/nodes/media-feature-plain.ts | 4 +-
.../src/nodes/media-feature-range.ts | 6 +--
.../src/nodes/media-feature-value.ts | 6 +--
.../src/nodes/media-feature.ts | 46 +++++++++++++------
.../src/nodes/media-in-parens.ts | 26 +++++++++--
.../src/parser/consume/consume-boolean.ts | 46 -------------------
.../src/parser/consume/consume-plain.ts | 44 ------------------
.../src/parser/consume/consume-value.ts | 16 -------
.../src/parser/parse.ts | 17 +++----
.../src/util/component-value-is.ts | 12 ++---
.../media-query-list-parser/test/test.mjs | 4 +-
rollup/configs/externals.js | 2 +
17 files changed, 115 insertions(+), 165 deletions(-)
delete mode 100644 packages/media-query-list-parser/src/parser/consume/consume-boolean.ts
delete mode 100644 packages/media-query-list-parser/src/parser/consume/consume-plain.ts
delete mode 100644 packages/media-query-list-parser/src/parser/consume/consume-value.ts
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
index 20c618ec6..525b852b6 100644
--- a/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
+++ b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts
@@ -5,6 +5,16 @@ export type ContainerNode = FunctionNode | SimpleBlockNode;
export type ComponentValue = FunctionNode | SimpleBlockNode | WhitespaceNode | CommentNode | TokenNode | UnclosedSimpleBlockNode | UnclosedFunctionNode;
+export enum ComponentValueType {
+ Function = 'function',
+ SimpleBlock = 'simple-block',
+ Whitespace = 'whitespace',
+ Comment = 'comment',
+ Token = 'token',
+ UnclosedFunction = 'unclosed-function',
+ UnclosedSimpleBlock = 'unclosed-simple-block'
+}
+
// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value
export function consumeComponentValue(ctx: Context, tokens: Array): { advance: number, node: ComponentValue } {
const i = 0;
@@ -56,7 +66,7 @@ export function consumeComponentValue(ctx: Context, tokens: Array): {
}
export class FunctionNode {
- type = 'function';
+ type: ComponentValueType = ComponentValueType.Function;
name: TokenFunction;
endToken: CSSToken;
@@ -185,7 +195,7 @@ export function consumeFunction(ctx: Context, tokens: Array): { advanc
}
export class SimpleBlockNode {
- type = 'simple-block';
+ type: ComponentValueType = ComponentValueType.SimpleBlock;
startToken: CSSToken;
endToken: CSSToken;
@@ -315,7 +325,7 @@ export function consumeSimpleBlock(ctx: Context, tokens: Array): { adv
}
export class WhitespaceNode {
- type = 'whitespace';
+ type: ComponentValueType = ComponentValueType.Whitespace;
value: Array;
@@ -350,7 +360,7 @@ export function consumeWhitespace(ctx: Context, tokens: Array): { adva
}
export class CommentNode {
- type = 'comment';
+ type: ComponentValueType = ComponentValueType.Comment;
value: CSSToken;
@@ -404,7 +414,7 @@ export function consumeAllCommentsAndWhitespace(ctx: Context, tokens: Array;
@@ -442,7 +452,7 @@ export class UnclosedFunctionNode {
}
export class UnclosedSimpleBlockNode {
- type = 'unclosed-simple-block';
+ type: ComponentValueType = ComponentValueType.UnclosedSimpleBlock;
value: Array;
diff --git a/packages/media-query-list-parser/src/nodes/media-condition.ts b/packages/media-query-list-parser/src/nodes/media-condition.ts
index b29ccd613..d3e4d9881 100644
--- a/packages/media-query-list-parser/src/nodes/media-condition.ts
+++ b/packages/media-query-list-parser/src/nodes/media-condition.ts
@@ -1,3 +1,4 @@
+import { ComponentValue } from '@csstools/css-parser-algorithms';
import { MediaConditionListWithAnd, MediaConditionListWithAndWalkerEntry, MediaConditionListWithAndWalkerParent, MediaConditionListWithOr, MediaConditionListWithOrWalkerEntry, MediaConditionListWithOrWalkerParent } from './media-condition-list';
import { MediaNot, MediaNotWalkerEntry, MediaNotWalkerParent } from './media-not';
@@ -43,3 +44,7 @@ export class MediaCondition {
export type MediaConditionWalkerEntry = MediaNotWalkerEntry | MediaConditionListWithAndWalkerEntry | MediaConditionListWithOrWalkerEntry | MediaNot | MediaConditionListWithAnd | MediaConditionListWithOr;
export type MediaConditionWalkerParent = MediaNotWalkerParent | MediaConditionListWithAndWalkerParent | MediaConditionListWithOrWalkerParent | MediaCondition;
+
+export function parseMediaCondition(componentValues: Array): false | MediaCondition {
+ return false;
+}
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
index bb960da2a..ad3ab9e65 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts
@@ -1,5 +1,15 @@
-import { MediaFeatureName } from './media-feature-name';
+import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { MediaFeatureName, parseMediaFeatureName } from './media-feature-name';
export class MediaFeatureBoolean extends MediaFeatureName {
type = 'mf-boolean';
}
+
+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
index cd02d772f..580a5dce2 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts
@@ -1,4 +1,4 @@
-import { ComponentValue } from '@csstools/css-parser-algorithms';
+import { ComponentValue, ComponentValueType } from '@csstools/css-parser-algorithms';
import { CSSToken, TokenDelim, TokenType } from '@csstools/css-tokenizer';
export enum MediaFeatureLT {
@@ -22,7 +22,7 @@ export function matchesComparison(componentValues: Array): false
for (let i = 0; i < componentValues.length; i++) {
const componentValue = componentValues[i];
- if (componentValue.type === 'token') {
+ if (componentValue.type === ComponentValueType.Token) {
const token = componentValue.value as CSSToken;
if (token[0] === TokenType.Delim) {
if (token[4].value === MediaFeatureEQ.EQ) {
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
index b7f6612d6..102669bd4 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-name.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts
@@ -1,4 +1,4 @@
-import { ComponentValue, TokenNode } from '@csstools/css-parser-algorithms';
+import { ComponentValue, ComponentValueType, TokenNode } from '@csstools/css-parser-algorithms';
import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer';
import { isIdent } from '../util/component-value-is';
@@ -52,11 +52,11 @@ export function parseMediaFeatureName(componentValues: Array) {
for (let i = 0; i < componentValues.length; i++) {
const componentValue = componentValues[i];
- if (componentValue.type === 'whitespace') {
+ if (componentValue.type === ComponentValueType.Whitespace) {
continue;
}
- if (componentValue.type === 'comment') {
+ if (componentValue.type === ComponentValueType.Comment) {
continue;
}
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
index 2672270fb..1eaaff61c 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts
@@ -1,4 +1,4 @@
-import { ComponentValue } from '@csstools/css-parser-algorithms';
+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';
@@ -69,7 +69,7 @@ export function parseMediaFeaturePlain(componentValues: Array) {
for (let i = 0; i < componentValues.length; i++) {
const componentValue = componentValues[i];
- if (componentValue.type === 'token') {
+ if (componentValue.type === ComponentValueType.Token) {
const token = componentValue.value as CSSToken;
if (token[0] === TokenType.Colon) {
a = componentValues.slice(0, i);
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
index c1e957add..70d1ed1aa 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-range.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -1,4 +1,4 @@
-import { ComponentValue, TokenNode } from '@csstools/css-parser-algorithms';
+import { ComponentValue, ComponentValueType, TokenNode } from '@csstools/css-parser-algorithms';
import { CSSToken, stringify, TokenDelim, TokenType } from '@csstools/css-tokenizer';
import { comparisonFromTokens, matchesComparison } from './media-feature-comparison';
import { MediaFeatureName, parseMediaFeatureName } from './media-feature-name';
@@ -227,13 +227,13 @@ export class MediaFeatureRangeValueNameValue {
export type MediaFeatureRangeWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue;
export type MediaFeatureRangeWalkerParent = MediaFeatureValueWalkerParent | MediaFeatureRange;
-export function matchesMediaFeaturePlain(componentValues: Array) {
+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 === 'token') {
+ if (componentValue.type === ComponentValueType.Token) {
const token = componentValue.value as CSSToken;
if (token[0] === TokenType.Delim) {
const comparison = matchesComparison(componentValues.slice(i));
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
index 25acb0ec2..89be76753 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-value.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts
@@ -1,4 +1,4 @@
-import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
+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';
@@ -78,11 +78,11 @@ export function parseMediaFeatureValue(componentValues: Array) {
for (let i = 0; i < componentValues.length; i++) {
const componentValue = componentValues[i];
- if (componentValue.type === 'whitespace') {
+ if (componentValue.type === ComponentValueType.Whitespace) {
continue;
}
- if (componentValue.type === 'comment') {
+ if (componentValue.type === ComponentValueType.Comment) {
continue;
}
diff --git a/packages/media-query-list-parser/src/nodes/media-feature.ts b/packages/media-query-list-parser/src/nodes/media-feature.ts
index 60fcc21eb..a9b026ce6 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature.ts
@@ -1,31 +1,24 @@
-import { CSSToken, stringify } from '@csstools/css-tokenizer';
-import { MediaFeatureBoolean } from './media-feature-boolean';
-import { MediaFeaturePlain, MediaFeaturePlainWalkerEntry, MediaFeaturePlainWalkerParent } from './media-feature-plain';
-import { MediaFeatureRange, MediaFeatureRangeWalkerEntry, MediaFeatureRangeWalkerParent } from './media-feature-range';
+import { SimpleBlockNode } from '@csstools/css-parser-algorithms';
+import { TokenType } from '@csstools/css-tokenizer';
+import { MediaFeatureBoolean, parseMediaFeatureBoolean } from './media-feature-boolean';
+import { MediaFeaturePlain, MediaFeaturePlainWalkerEntry, MediaFeaturePlainWalkerParent, parseMediaFeaturePlain } from './media-feature-plain';
+import { MediaFeatureRange, MediaFeatureRangeWalkerEntry, MediaFeatureRangeWalkerParent, parseMediaFeatureRange } from './media-feature-range';
export class MediaFeature {
type = 'media-feature';
feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
- before: Array;
- after: Array;
- constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange, before: Array = [], after: Array = []) {
+ constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange) {
this.feature = feature;
- this.before = before;
- this.after = after;
}
tokens() {
- return [
- ...this.before,
- ...this.feature.tokens(),
- ...this.after,
- ];
+ return this.feature.tokens();
}
toString() {
- return stringify(...this.before) + this.feature.toString() + stringify(...this.after);
+ return this.feature.toString();
}
indexOf(item: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange): number | string {
@@ -55,3 +48,26 @@ export class MediaFeature {
export type MediaFeatureWalkerEntry = MediaFeaturePlainWalkerEntry | MediaFeatureRangeWalkerEntry | MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange;
export type MediaFeatureWalkerParent = MediaFeaturePlainWalkerParent | MediaFeatureRangeWalkerParent | MediaFeature;
+
+export function parseMediaFeature(simpleBlock: SimpleBlockNode) {
+ if (simpleBlock.startToken[0] !== TokenType.OpenParen) {
+ return false;
+ }
+
+ const boolean = parseMediaFeatureBoolean(simpleBlock.value);
+ if (boolean !== false) {
+ return new MediaFeature(boolean);
+ }
+
+ const plain = parseMediaFeaturePlain(simpleBlock.value);
+ if (plain !== false) {
+ return new MediaFeature(plain);
+ }
+
+ const range = parseMediaFeatureRange(simpleBlock.value);
+ if (range !== false) {
+ return new MediaFeature(range);
+ }
+
+ return false;
+}
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
index 5b351b81d..b5da64cee 100644
--- a/packages/media-query-list-parser/src/nodes/media-in-parens.ts
+++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts
@@ -1,10 +1,10 @@
-import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms';
-import { CSSToken, stringify } from '@csstools/css-tokenizer';
+import { ComponentValue, ContainerNode, SimpleBlockNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer';
import { GeneralEnclosed } from './general-enclosed';
import { MediaAnd } from './media-and';
-import { MediaCondition } from './media-condition';
+import { MediaCondition, parseMediaCondition } from './media-condition';
import { MediaConditionList } from './media-condition-list';
-import { MediaFeature } from './media-feature';
+import { MediaFeature, parseMediaFeature } from './media-feature';
import { MediaFeatureBoolean } from './media-feature-boolean';
import { MediaFeatureName } from './media-feature-name';
import { MediaFeaturePlain } from './media-feature-plain';
@@ -63,3 +63,21 @@ export class 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;
+
+export function parseMediaInParens(simpleBlock: SimpleBlockNode) {
+ if (simpleBlock.startToken[0] !== TokenType.OpenParen) {
+ return false;
+ }
+
+ const feature = parseMediaFeature(simpleBlock);
+ 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 GeneralEnclosed(simpleBlock);
+}
diff --git a/packages/media-query-list-parser/src/parser/consume/consume-boolean.ts b/packages/media-query-list-parser/src/parser/consume/consume-boolean.ts
deleted file mode 100644
index fc7081d5d..000000000
--- a/packages/media-query-list-parser/src/parser/consume/consume-boolean.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { CSSToken, TokenIdent, TokenType } from '@csstools/css-tokenizer';
-import { MediaFeatureBoolean } from '../../nodes/media-feature-boolean';
-
-export function consumeBoolean(tokens: Array): { node: MediaFeatureBoolean, tokens: Array } | null {
- let ident : TokenIdent|null = null;
-
- for (let i = 0; i < tokens.length; i++) {
- const token = tokens[i];
-
- if (i === 0) {
- if (token[0] === TokenType.OpenParen) {
- continue;
- }
-
- return null;
- }
-
- if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) {
- continue;
- }
-
- if (token[0] === TokenType.Ident) {
- if (!ident) {
- ident = token as TokenIdent;
- continue;
- }
-
- return null;
- }
-
- if (token[0] === TokenType.CloseParen) {
- if (ident) {
- const node = new MediaFeatureBoolean(tokens.slice(0, i + 1));
-
- return {
- node: node,
- tokens: tokens.slice(i + 1),
- };
- }
-
- return null;
- }
-
- return null;
- }
-}
diff --git a/packages/media-query-list-parser/src/parser/consume/consume-plain.ts b/packages/media-query-list-parser/src/parser/consume/consume-plain.ts
deleted file mode 100644
index 3a84cd380..000000000
--- a/packages/media-query-list-parser/src/parser/consume/consume-plain.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { CSSToken, TokenIdent, TokenType } from '@csstools/css-tokenizer';
-import { MediaFeatureName } from '../../nodes/media-feature-name';
-import { MediaFeaturePlain } from '../../nodes/media-feature-plain';
-import { consumeValue } from './consume-value';
-
-export function consumePlain(tokens: Array): { node: MediaFeaturePlain, tokens: Array } | null {
- let name: MediaFeatureName | null = null;
- const value: MediaFeatureValue | null = null;
-
- for (let i = 0; i < tokens.length; i++) {
- const token = tokens[i];
-
- if (i === 0) {
- if (token[0] === TokenType.OpenParen) {
- continue;
- } else {
- return null;
- }
- }
-
- if (token[0] === TokenType.CloseParen) {
- continue;
- }
-
- if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) {
- continue;
- }
-
- if (token[0] === TokenType.Ident) {
- if (!name) {
- name = new MediaFeatureName(tokens.slice(0, i + 1));
- continue;
- } else {
- return null;
- }
- }
-
- if (token[0] === TokenType.Delim && token[1] === ':' && name) {
- const value = consumeValue(tokens.slice(i + 1));
- }
-
- return null;
- }
-}
diff --git a/packages/media-query-list-parser/src/parser/consume/consume-value.ts b/packages/media-query-list-parser/src/parser/consume/consume-value.ts
deleted file mode 100644
index 991bc9b63..000000000
--- a/packages/media-query-list-parser/src/parser/consume/consume-value.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { CSSToken } from '@csstools/css-tokenizer';
-import { parseComponentValue } from '@csstools/css-parser-algorithms';
-import { MediaFeatureValue } from '../../nodes/media-feature-value';
-
-export function consumeValue(tokens: Array): { node: MediaFeatureValue, tokens: Array } | null {
- const result = parseComponentValue(tokens, {
- onParseError(err) {
- throw new Error(JSON.stringify(err));
- },
- });
-
- return {
- node: new MediaFeatureValue(result),
- tokens: tokens.slice(result.tokens().length + 1),
- };
-}
diff --git a/packages/media-query-list-parser/src/parser/parse.ts b/packages/media-query-list-parser/src/parser/parse.ts
index af7834d2e..bbabc6c93 100644
--- a/packages/media-query-list-parser/src/parser/parse.ts
+++ b/packages/media-query-list-parser/src/parser/parse.ts
@@ -1,7 +1,6 @@
-import { ComponentValue, parseCommaSeparatedListOfComponentValues, SimpleBlockNode, TokenNode } from '@csstools/css-parser-algorithms';
-import { CSSToken, tokenizer, TokenType } from '@csstools/css-tokenizer';
-import { GeneralEnclosed } from '../nodes/general-enclosed';
-import { MediaFeatureName } from '../nodes/media-feature-name';
+import { ComponentValueType, parseCommaSeparatedListOfComponentValues, SimpleBlockNode } from '@csstools/css-parser-algorithms';
+import { tokenizer } from '@csstools/css-tokenizer';
+import { parseMediaInParens } from '../nodes/media-in-parens';
export function parse(source: string) {
const onParseError = (err) => {
@@ -30,18 +29,14 @@ export function parse(source: string) {
const mediaQueryList = parsed.map((componentValuesList) => {
const result = [];
- const lastSliceIndex = 0;
for (let i = 0; i < componentValuesList.length; i++) {
const componentValue = componentValuesList[i];
- if (componentValue.type === 'whitespace' || componentValue.type === 'comment') {
- continue;
- }
-
- if (componentValue.type === 'function') {
- result.push(new GeneralEnclosed(componentValue));
+ if (componentValue.type === ComponentValueType.SimpleBlock) {
+ result.push(parseMediaInParens(componentValue as SimpleBlockNode));
}
}
+ return result;
});
return mediaQueryList;
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
index c71b94ab5..1475eaeeb 100644
--- a/packages/media-query-list-parser/src/util/component-value-is.ts
+++ b/packages/media-query-list-parser/src/util/component-value-is.ts
@@ -1,10 +1,10 @@
-import { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms';
+import { ComponentValue, ComponentValueType, FunctionNode } from '@csstools/css-parser-algorithms';
import { CSSToken, TokenFunction, TokenIdent, TokenType } from '@csstools/css-tokenizer';
export function isNumber(componentValue: ComponentValue) {
if (
- (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Number) ||
- (componentValue.type === 'function' && ((componentValue as FunctionNode).name as TokenFunction)[4].value === 'calc')
+ (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;
}
@@ -13,7 +13,7 @@ export function isNumber(componentValue: ComponentValue) {
}
export function isNumericConstant(componentValue: ComponentValue) {
- if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
+ if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
const token = componentValue.value as TokenIdent;
const tokenValue = token[4].value.toLowerCase();
if (tokenValue === 'infinity') {
@@ -37,7 +37,7 @@ export function isNumericConstant(componentValue: ComponentValue) {
}
export function isDimension(componentValue: ComponentValue) {
- if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Dimension) {
+ if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Dimension) {
return true;
}
@@ -45,7 +45,7 @@ export function isDimension(componentValue: ComponentValue) {
}
export function isIdent(componentValue: ComponentValue) {
- if (componentValue.type === 'token' && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
+ if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Ident) {
return true;
}
diff --git a/packages/media-query-list-parser/test/test.mjs b/packages/media-query-list-parser/test/test.mjs
index cd5d20ff0..64a572311 100644
--- a/packages/media-query-list-parser/test/test.mjs
+++ b/packages/media-query-list-parser/test/test.mjs
@@ -2,7 +2,7 @@ import { parse } from '@csstools/media-query-list-parser';
parse('(/* a comment */foo ) something else');
-parse('not screen and ((min-width: 300px) and (prefers-color-scheme:/* a comment */dark))').forEach((mediaQuery) => {
+parse('not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark)').forEach((mediaQuery) => {
mediaQuery.forEach((args) => {
if (!('walk' in args)) {
console.log(args);
@@ -10,7 +10,7 @@ parse('not screen and ((min-width: 300px) and (prefers-color-scheme:/* a comment
}
args.walk((a) => {
- console.log(a.node.type);
+ console.log(a.node.type, a.parent.type);
console.log(a.node.toString());
});
});
diff --git a/rollup/configs/externals.js b/rollup/configs/externals.js
index aaf0a70e5..c3a6e57a3 100644
--- a/rollup/configs/externals.js
+++ b/rollup/configs/externals.js
@@ -4,6 +4,7 @@ export const externalsForCLI = [
'url',
'vm',
+ '@csstools/css-parser-algorithms',
'@csstools/css-tokenizer',
'@csstools/postcss-cascade-layers',
'@csstools/postcss-color-function',
@@ -70,6 +71,7 @@ export const externalsForPlugin = [
/^postcss\/lib\/*/,
'postcss-html',
+ '@csstools/css-parser-algorithms',
'@csstools/css-tokenizer',
'@csstools/postcss-cascade-layers',
'@csstools/postcss-color-function',
From c18208e427a7057c59b9888be0668f6c499f1ecb Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sun, 23 Oct 2022 10:39:16 +0200
Subject: [PATCH 08/35] lint
---
packages/media-query-list-parser/package.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/media-query-list-parser/package.json b/packages/media-query-list-parser/package.json
index c400c28d4..c12761f4c 100644
--- a/packages/media-query-list-parser/package.json
+++ b/packages/media-query-list-parser/package.json
@@ -37,6 +37,10 @@
"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 });\"",
@@ -57,10 +61,6 @@
"css",
"tokenizer"
],
- "dependencies": {
- "@csstools/css-tokenizer": "^1.0.0",
- "@csstools/css-parser-algorithms": "^1.0.0"
- },
"volta": {
"extends": "../../package.json"
}
From 2942574b1f4f78f326f94f664edc6f5f058c59f5 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sun, 23 Oct 2022 11:03:21 +0200
Subject: [PATCH 09/35] fix ranges
---
.../src/nodes/media-feature-range.ts | 10 +++++-----
packages/media-query-list-parser/test/test.mjs | 16 +++++++++++++++-
2 files changed, 20 insertions(+), 6 deletions(-)
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
index 70d1ed1aa..db08fc92e 100644
--- a/packages/media-query-list-parser/src/nodes/media-feature-range.ts
+++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts
@@ -241,12 +241,12 @@ export function parseMediaFeatureRange(componentValues: Array) {
if (comparisonOne === false) {
comparisonOne = [
comparison[0] + i,
- comparison[i] + i,
+ comparison[1] + i,
];
} else {
comparisonTwo = [
comparison[0] + i,
- comparison[i] + i,
+ comparison[1] + i,
];
break;
}
@@ -269,7 +269,7 @@ export function parseMediaFeatureRange(componentValues: Array) {
}
if (comparisonTwo === false) {
- const a = componentValues.slice(0, comparisonOne[0] - 1);
+ const a = componentValues.slice(0, comparisonOne[0]);
const b = componentValues.slice(comparisonOne[1] + 1);
const nameA = parseMediaFeatureName(a);
@@ -315,8 +315,8 @@ export function parseMediaFeatureRange(componentValues: Array) {
);
}
- const a = componentValues.slice(0, comparisonOne[0] - 1);
- const b = componentValues.slice(comparisonOne[1] + 1, comparisonTwo[0] - 1);
+ 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);
diff --git a/packages/media-query-list-parser/test/test.mjs b/packages/media-query-list-parser/test/test.mjs
index 64a572311..f13936c41 100644
--- a/packages/media-query-list-parser/test/test.mjs
+++ b/packages/media-query-list-parser/test/test.mjs
@@ -2,7 +2,21 @@ import { parse } from '@csstools/media-query-list-parser';
parse('(/* a comment */foo ) something else');
-parse('not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark)').forEach((mediaQuery) => {
+parse('not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)').forEach((mediaQuery) => {
+ mediaQuery.forEach((args) => {
+ if (!('walk' in args)) {
+ console.log(args);
+ return;
+ }
+
+ args.walk((a) => {
+ console.log(a.node.type, a.parent.type);
+ console.log(a.node.toString());
+ });
+ });
+});
+
+parse('(resolution < infinite) and (infinite < resolution)').forEach((mediaQuery) => {
mediaQuery.forEach((args) => {
if (!('walk' in args)) {
console.log(args);
From ff0fa6441efde37e887c15a89f8423f4976720af Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sun, 23 Oct 2022 17:02:34 +0200
Subject: [PATCH 10/35] more work
---
.../src/nodes/media-and.ts | 56 +++++++-
.../src/nodes/media-condition-list.ts | 122 +++++++++++++++++-
.../src/nodes/media-in-parens.ts | 4 +-
.../src/nodes/media-not.ts | 56 +++++++-
.../src/nodes/media-or.ts | 56 +++++++-
.../src/parser/parse.ts | 8 +-
.../media-query-list-parser/test/test.mjs | 51 +++++---
7 files changed, 320 insertions(+), 33 deletions(-)
diff --git a/packages/media-query-list-parser/src/nodes/media-and.ts b/packages/media-query-list-parser/src/nodes/media-and.ts
index b6695c07d..81d378461 100644
--- a/packages/media-query-list-parser/src/nodes/media-and.ts
+++ b/packages/media-query-list-parser/src/nodes/media-and.ts
@@ -1,5 +1,7 @@
-import { CSSToken, stringify } from '@csstools/css-tokenizer';
-import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens';
+import { ComponentValue, ComponentValueType, SimpleBlockNode } from '@csstools/css-parser-algorithms';
+import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer';
+import { isIdent } from '../util/component-value-is';
+import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent, parseMediaInParens } from './media-in-parens';
export class MediaAnd {
type = 'media-and';
@@ -48,3 +50,53 @@ export class MediaAnd {
export type MediaAndWalkerEntry = MediaInParensWalkerEntry | MediaInParens;
export type MediaAndWalkerParent = MediaInParensWalkerParent | MediaAnd;
+
+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 = parseMediaInParens(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/nodes/media-condition-list.ts b/packages/media-query-list-parser/src/nodes/media-condition-list.ts
index 588c3565a..56ca8ed0f 100644
--- a/packages/media-query-list-parser/src/nodes/media-condition-list.ts
+++ b/packages/media-query-list-parser/src/nodes/media-condition-list.ts
@@ -1,7 +1,8 @@
+import { ComponentValue, ComponentValueType, SimpleBlockNode } from '@csstools/css-parser-algorithms';
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 { MediaAnd, MediaAndWalkerEntry, MediaAndWalkerParent, parseMediaAnd } from './media-and';
+import { MediaInParens, parseMediaInParens } from './media-in-parens';
+import { MediaOr, MediaOrWalkerEntry, MediaOrWalkerParent, parseMediaOr } from './media-or';
export type MediaConditionList = MediaConditionListWithAnd | MediaConditionListWithOr;
@@ -98,6 +99,63 @@ export class MediaConditionListWithAnd {
export type MediaConditionListWithAndWalkerEntry = MediaAndWalkerEntry | MediaAnd;
export type MediaConditionListWithAndWalkerParent = MediaAndWalkerParent | MediaConditionListWithAnd;
+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;
+ }
+
+ return false;
+ }
+
+ const componentValue = componentValues[i];
+ if (componentValue.type === ComponentValueType.Whitespace) {
+ continue;
+ }
+
+ if (componentValue.type === ComponentValueType.Comment) {
+ continue;
+ }
+
+ if (leading === false && componentValue.type === ComponentValueType.SimpleBlock) {
+ leading = parseMediaInParens(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 class MediaConditionListWithOr {
type = 'media-condition-list-or';
@@ -190,3 +248,61 @@ export class MediaConditionListWithOr {
export type MediaConditionListWithOrWalkerEntry = MediaOrWalkerEntry | MediaOr;
export type MediaConditionListWithOrWalkerParent = MediaOrWalkerParent | MediaConditionListWithOr;
+
+
+export function parseMediaConditionListWithOr(componentValues: Array) {
+ let leading: MediaInParens | false = false;
+ const list: Array