diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d98bf10d0..71d9340e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,49 +47,24 @@ jobs: with: node-version: ${{ matrix.node }} - - name: env - run: | - echo 'DIST_RESTORE_KEYS<> $GITHUB_ENV - echo "$(git --no-pager log -9 --skip 1 --no-merges --pretty=format:'%H--test-dist-${{ matrix.node }}-${{ matrix.os }}')" >> $GITHUB_ENV - echo 'EOF' >> $GITHUB_ENV - echo "COMMIT_SHA=$(git --no-pager log -1 --no-merges --pretty=format:'%H')" >> $GITHUB_ENV - echo "BUILD_AND_TEST_ALL_PACKAGES=$(echo ${{ github.event.inputs.run_index }})" >> $GITHUB_ENV - - - name: previous build artifacts cache - uses: actions/cache@v3.0.11 - with: - path: | - .cached-commit - cli/*/dist/** - experimental/*/dist/** - packages/*/dist/** - plugin-packs/*/dist/** - plugins/*/dist/** - key: ${{ env.COMMIT_SHA }}--test-dist-${{ matrix.node }}-${{ matrix.os }} - restore-keys: ${{ env.DIST_RESTORE_KEYS }} - - name: npm ci run: | npm ci --ignore-scripts - - name: determine modified workspaces - run: | - echo "MODIFIED_WORKSPACES=$(node './.github/bin/modified-workspaces/log-modified-workspaces.mjs')" >> $GITHUB_ENV - # Build, lint and PostCSS Tape tests must all work and pass : # - with exact dependencies from package-lock.json # - without requiring postinstall scripts from dependencies to run - name: build run: | - npm run build --if-present $MODIFIED_WORKSPACES + npm run build --if-present - name: lint - run: npm run lint --if-present $MODIFIED_WORKSPACES + run: npm run lint --if-present if: matrix.is_base_node_version && matrix.is_base_os_version # Basic tests - name: test - run: npm run test --if-present $MODIFIED_WORKSPACES + run: npm run test --if-present env: ENABLE_ANNOTATIONS_FOR_NODE: ${{ matrix.is_base_node_version }} ENABLE_ANNOTATIONS_FOR_OS: ${{ matrix.is_base_os_version }} @@ -99,14 +74,14 @@ jobs: - name: test:cli run: | npm install --ignore-scripts - npm run test:cli --if-present $MODIFIED_WORKSPACES + npm run test:cli --if-present # Browser Tests # running "npm ci" again, but allowing scripts so that Chrome is installed - name: test:browser run: | npm ci - npm run test:browser --if-present $MODIFIED_WORKSPACES + npm run test:browser --if-present if: matrix.is_base_node_version && matrix.is_base_os_version # E2E Tests @@ -125,11 +100,5 @@ jobs: if: matrix.is_base_node_version && matrix.is_base_os_version - name: test:deno - run: npm run test:deno --if-present $MODIFIED_WORKSPACES + run: npm run test:deno --if-present if: matrix.is_base_node_version && matrix.is_base_os_version - - # record the current commit for the cache at the end of the job - # must be the last step - - name: record current commit - run: | - echo "$(git --no-pager log -1 --no-merges --pretty=format:'%H')" > .cached-commit diff --git a/package-lock.json b/package-lock.json index e14d9cce1..8ba40711a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "license": "CC0-1.0", "workspaces": [ + "packages/css-tokenizer", + "packages/css-parser-algorithms", + "packages/media-query-list-parser", "packages/*", "plugins/postcss-progressive-custom-properties", "plugins/*", @@ -115,14 +118,14 @@ "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": "^12 || ^14 || >=16" + "node": "^14 || ^16 || >=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.2" + "postcss": "^8.4" } }, "node_modules/@ampproject/remapping": { @@ -1812,6 +1815,14 @@ "resolved": "experimental/css-has-pseudo", "link": true }, + "node_modules/@csstools/css-parser-algorithms": { + "resolved": "packages/css-parser-algorithms", + "link": true + }, + "node_modules/@csstools/css-tokenizer": { + "resolved": "packages/css-tokenizer", + "link": true + }, "node_modules/@csstools/csstools-cli": { "resolved": "cli/csstools-cli", "link": true @@ -1820,6 +1831,10 @@ "resolved": "packages/generate-test-cases", "link": true }, + "node_modules/@csstools/media-query-list-parser": { + "resolved": "packages/media-query-list-parser", + "link": true + }, "node_modules/@csstools/postcss-base-plugin": { "resolved": "plugins/postcss-base-plugin", "link": true @@ -6911,6 +6926,49 @@ "url": "https://opencollective.com/csstools" } }, + "packages/css-parser": { + "name": "@csstools/css-parser", + "version": "1.0.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@csstools/css-tokenizer": "^1.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "packages/css-parser-algorithms": { + "name": "@csstools/css-parser-algorithms", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@csstools/css-tokenizer": "^1.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "packages/css-tokenizer": { + "name": "@csstools/css-tokenizer", + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, "packages/generate-test-cases": { "name": "@csstools/generate-test-cases", "version": "1.0.0", @@ -6926,6 +6984,35 @@ "url": "https://opencollective.com/csstools" } }, + "packages/media-query-list-parser": { + "name": "@csstools/media-query-list-parser", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^1.0.0", + "@csstools/css-tokenizer": "^1.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "packages/postcss-media-query-list-parser": { + "name": "@csstools/postcss-media-query-list-parser", + "version": "1.0.0", + "extraneous": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, "packages/postcss-tape": { "name": "@csstools/postcss-tape", "version": "1.0.0", @@ -6962,6 +7049,19 @@ "postcss-selector-parser": "^6.0.10" } }, + "packages/virtual-media": { + "name": "@csstools/virtual-media", + "version": "1.0.0", + "extraneous": true, + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, "plugin-packs/postcss-preset-env": { "version": "8.0.0-alpha.0", "license": "CC0-1.0", @@ -7243,7 +7343,8 @@ "version": "8.0.2", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^1.0.0", + "@csstools/media-query-list-parser": "^1.0.0" }, "engines": { "node": "^14 || ^16 || >=18" @@ -7731,14 +7832,14 @@ "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": "^12 || ^14 || >=16" + "node": "^14 || ^16 || >=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.2" + "postcss": "^8.4" } }, "plugins/postcss-selector-not": { @@ -9003,6 +9104,15 @@ "version": "file:experimental/css-has-pseudo", "requires": {} }, + "@csstools/css-parser-algorithms": { + "version": "file:packages/css-parser-algorithms", + "requires": { + "@csstools/css-tokenizer": "^1.0.0" + } + }, + "@csstools/css-tokenizer": { + "version": "file:packages/css-tokenizer" + }, "@csstools/csstools-cli": { "version": "file:cli/csstools-cli", "requires": { @@ -9051,6 +9161,13 @@ "mdn-data": "^2.0.28" } }, + "@csstools/media-query-list-parser": { + "version": "file:packages/media-query-list-parser", + "requires": { + "@csstools/css-parser-algorithms": "^1.0.0", + "@csstools/css-tokenizer": "^1.0.0" + } + }, "@csstools/postcss-base-plugin": { "version": "file:plugins/postcss-base-plugin", "requires": {} @@ -11703,7 +11820,8 @@ "postcss-custom-media": { "version": "file:plugins/postcss-custom-media", "requires": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^1.0.0", + "@csstools/media-query-list-parser": "^1.0.0" } }, "postcss-custom-properties": { diff --git a/package.json b/package.json index 63de9f4f5..92b4a687e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "node": "^14 || ^16 || >=18" }, "workspaces": [ + "packages/css-tokenizer", + "packages/css-parser-algorithms", + "packages/media-query-list-parser", "packages/*", "plugins/postcss-progressive-custom-properties", "plugins/*", diff --git a/packages/css-parser-algorithms/.gitignore b/packages/css-parser-algorithms/.gitignore new file mode 100644 index 000000000..f548255b0 --- /dev/null +++ b/packages/css-parser-algorithms/.gitignore @@ -0,0 +1,7 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.json +dist/* diff --git a/packages/css-parser-algorithms/.nvmrc b/packages/css-parser-algorithms/.nvmrc new file mode 100644 index 000000000..f0b10f153 --- /dev/null +++ b/packages/css-parser-algorithms/.nvmrc @@ -0,0 +1 @@ +v16.13.1 diff --git a/packages/css-parser-algorithms/CHANGELOG.md b/packages/css-parser-algorithms/CHANGELOG.md new file mode 100644 index 000000000..b0ff6b082 --- /dev/null +++ b/packages/css-parser-algorithms/CHANGELOG.md @@ -0,0 +1,3 @@ +### 1.0.0 + +- Initial version diff --git a/packages/css-parser-algorithms/LICENSE.md b/packages/css-parser-algorithms/LICENSE.md new file mode 100644 index 000000000..af5411fa2 --- /dev/null +++ b/packages/css-parser-algorithms/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/css-parser-algorithms/README.md b/packages/css-parser-algorithms/README.md new file mode 100644 index 000000000..aeda87344 --- /dev/null +++ b/packages/css-parser-algorithms/README.md @@ -0,0 +1,120 @@ +# CSS Parser Algorithms + +[npm version][npm-url] +[Build Status][cli-url] +[Discord][discord] + +Implemented from : https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/ + +## Usage + +Add [CSS Parser Algorithms] to your project: + +```bash +npm install postcss @csstools/css-parser-algorithms --save-dev +``` + +[CSS Parser Algorithms] only accepts tokenized CSS. +It must be used together with `@csstools/css-tokenizer`. + + +```js +import { tokenizer, TokenType } from '@csstools/css-tokenizer'; +import { parseComponentValue } from '@csstools/css-parser-algorithms'; + +const myCSS = `@media only screen and (min-width: 768rem) { + .foo { + content: 'Some content!' !important; + } +} +`; + +const t = tokenizer({ + css: myCSS, +}); + +const tokens = []; + +{ + while (!t.endOfFile()) { + tokens.push(t.nextToken()); + } + + tokens.push(t.nextToken()); // EOF-token +} + +const options = { + onParseError: ((err) => { + throw new Error(JSON.stringify(err)); + }), +}; + +const result = parseComponentValue(tokens, options); + +console.log(result); +``` + +### Available functions + +- [`parseComponentValue`](https://www.w3.org/TR/css-syntax-3/#parse-component-value) +- [`parseListOfComponentValues`](https://www.w3.org/TR/css-syntax-3/#parse-list-of-component-values) +- [`parseCommaSeparatedListOfComponentValues`](https://www.w3.org/TR/css-syntax-3/#parse-comma-separated-list-of-component-values) + +### Utilities + +#### `gatherNodeAncestry` + +The AST does not expose the entire ancestry of each node. +The walker methods do provide access to the current parent, but also not the entire ancestry. + +To gather the entire ancestry for a a given sub tree of the AST you can use `gatherNodeAncestry`. +The result is a `Map` with the child nodes as keys and the parents as values. +This allows you to lookup any ancestor of any node. + +```css +import { parseComponentValue } from '@csstools/css-parser-algorithms'; + +const result = parseComponentValue(tokens, options); +const ancestry = gatherNodeAncestry(result); +``` + +### Options + +```ts +{ + onParseError?: (error: ParserError) => void +} +``` + +#### `onParseError` + +The parser algorithms are forgiving and won't stop when a parse error is encountered. +Parse errors also aren't tokens. + +To receive parsing error information you can set a callback. + +Parser errors will try to inform you about the point in the parsing logic the error happened. +This tells you the kind of error. + +`start` and `end` are the location in your CSS source code. + +`UnclosedSimpleBlockNode` and `UnclosedFunctionNode` entries will be added to the output. +This allows you to recover from errors and/or show warnings. + +## Goals and non-goals + +Things this package aims to be: +- specification compliant CSS parser +- a reliable low level package to be used in CSS sub-grammars + +What it is not: +- opinionated +- fast +- small +- a replacement for PostCSS (PostCSS is fast and also an ecosystem) + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/css-parser-algorithms + +[CSS Parser Algorithms]: https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms diff --git a/packages/css-parser-algorithms/package.json b/packages/css-parser-algorithms/package.json new file mode 100644 index 000000000..35048273d --- /dev/null +++ b/packages/css-parser-algorithms/package.json @@ -0,0 +1,69 @@ +{ + "name": "@csstools/css-parser-algorithms", + "description": "Algorithms to help you parse CSS from an array of tokens.", + "version": "1.0.0", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke", + "email": "romainmenke@gmail.com" + } + ], + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "dependencies": { + "@csstools/css-tokenizer": "^1.0.0" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.js", + "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", + "lint": "npm run lint:eslint && npm run lint:package-json", + "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", + "lint:package-json": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run clean && npm run build && npm run test", + "stryker": "stryker run --logLevel error", + "test": "node ./test/test.mjs", + "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", + "test:rewrite-expects": "REWRITE_EXPECTS=true node ./test/test.mjs" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/css-parser-algorithms#readme", + "repository": { + "type": "git", + "url": "https://github.com/csstools/postcss-plugins.git", + "directory": "packages/css-parser-algorithms" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "css", + "parser" + ], + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts new file mode 100644 index 000000000..726b51843 --- /dev/null +++ b/packages/css-parser-algorithms/src/consume/consume-component-block-function.ts @@ -0,0 +1,621 @@ +import { CSSToken, mirrorVariantType, stringify, TokenType, isToken, TokenFunction } from '@csstools/css-tokenizer'; +import { Context } from '../interfaces/context'; +import { ComponentValueType } from '../util/component-value-type'; + +export type ContainerNode = FunctionNode | SimpleBlockNode; + +export type ComponentValue = FunctionNode | SimpleBlockNode | WhitespaceNode | CommentNode | TokenNode | UnclosedSimpleBlockNode | UnclosedFunctionNode; + +// https://www.w3.org/TR/css-syntax-3/#consume-a-component-value +export function consumeComponentValue(ctx: Context, tokens: Array): { advance: number, node: ComponentValue } { + const token = tokens[0]; + if ( + token[0] === TokenType.OpenParen || + token[0] === TokenType.OpenCurly || + token[0] === TokenType.OpenSquare + ) { + const r = consumeSimpleBlock(ctx, tokens); + return { + advance: r.advance, + node: r.node, + }; + } + + if (token[0] === TokenType.Function) { + const r = consumeFunction(ctx, tokens); + return { + advance: r.advance, + node: r.node, + }; + } + + if (token[0] === TokenType.Whitespace) { + const r = consumeWhitespace(ctx, tokens); + return { + advance: r.advance, + node: r.node, + }; + } + + if (token[0] === TokenType.Comment) { + const r = consumeComment(ctx, tokens); + return { + advance: r.advance, + node: r.node, + }; + } + + return { + advance: 1, + node: new TokenNode(token), + }; +} + +export class FunctionNode { + type: ComponentValueType = ComponentValueType.Function; + + name: TokenFunction; + endToken: CSSToken; + value: Array; + + constructor(name: TokenFunction, endToken: CSSToken, value: Array) { + this.name = name; + this.endToken = endToken; + this.value = value; + } + + nameTokenValue(): string { + return this.name[4].value; + } + + tokens(): Array { + return [ + this.name, + ...this.value.flatMap((x) => { + if (isToken(x)) { + return x; + } + + return x.tokens(); + }), + this.endToken, + ]; + } + + toString(): string { + const valueString = this.value.map((x) => { + if (isToken(x)) { + return stringify(x); + } + + return x.toString(); + }).join(''); + + return stringify(this.name) + valueString + stringify(this.endToken); + } + + indexOf(item: ComponentValue): number | string { + return this.value.indexOf(item); + } + + at(index: number | string) { + if (typeof index === 'number') { + if (index < 0) { + index = this.value.length + index; + } + return this.value[index]; + } + } + + walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number | string) => boolean | void) { + let aborted = false; + + this.value.forEach((child, index) => { + if (aborted) { + return; + } + + if (cb({ node: child, parent: this }, index) === false) { + aborted = true; + return; + } + + if ('walk' in child) { + if (child.walk(cb) === false) { + aborted = true; + return; + } + } + }); + + if (aborted) { + return false; + } + } + + toJSON() { + return { + type: this.type, + name: this.nameTokenValue(), + tokens: this.tokens(), + value: this.value.map((x) => x.toJSON()), + }; + } + + isFunctionNode(): this is FunctionNode { + return FunctionNode.isFunctionNode(this); + } + + static isFunctionNode(x: unknown): x is FunctionNode { + if (!x) { + return false; + } + + if (!(x instanceof FunctionNode)) { + return false; + } + + return x.type === ComponentValueType.Function; + } +} + +// https://www.w3.org/TR/css-syntax-3/#consume-function +export function consumeFunction(ctx: Context, tokens: Array): { advance: number, node: FunctionNode | UnclosedFunctionNode } { + const value: Array = []; + + let i = 1; + + // eslint-disable-next-line no-constant-condition + while (true) { + const token = tokens[i]; + if (!token || token[0] === TokenType.EOF) { + ctx.onParseError({ + message: 'Unexpected EOF while consuming a function.', + start: tokens[0][2], + end: tokens[tokens.length - 1][3], + state: [ + '5.4.9. Consume a function', + 'Unexpected EOF', + ], + }); + + return { + advance: tokens.length, + node: new UnclosedFunctionNode(tokens), + }; + } + + if (token[0] === TokenType.CloseParen) { + return { + advance: i + 1, + node: new FunctionNode(tokens[0] as TokenFunction, token, value), + }; + } + + if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) { + const result = consumeAllCommentsAndWhitespace(ctx, tokens.slice(i)); + i += result.advance; + value.push(...result.nodes); + continue; + } + + const result = consumeComponentValue(ctx, tokens.slice(i)); + i += result.advance; + value.push(result.node); + } +} + +export class SimpleBlockNode { + type: ComponentValueType = ComponentValueType.SimpleBlock; + + startToken: CSSToken; + endToken: CSSToken; + value: Array; + + constructor(startToken: CSSToken, endToken: CSSToken, value: Array) { + this.startToken = startToken; + this.endToken = endToken; + this.value = value; + } + + tokens(): Array { + return [ + this.startToken, + ...this.value.flatMap((x) => { + if (isToken(x)) { + return x; + } + + return x.tokens(); + }), + this.endToken, + ]; + } + + toString(): string { + const valueString = this.value.map((x) => { + if (isToken(x)) { + return stringify(x); + } + + return x.toString(); + }).join(''); + + return stringify(this.startToken) + valueString + stringify(this.endToken); + } + + indexOf(item: ComponentValue): number | string { + return this.value.indexOf(item); + } + + at(index: number | string) { + if (typeof index === 'number') { + if (index < 0) { + index = this.value.length + index; + } + return this.value[index]; + } + } + + walk(cb: (entry: { node: ComponentValue, parent: ContainerNode }, index: number | string) => boolean | void) { + let aborted = false; + + this.value.forEach((child, index) => { + if (aborted) { + return; + } + + if (cb({ node: child, parent: this }, index) === false) { + aborted = true; + return; + } + + if ('walk' in child) { + if (child.walk(cb) === false) { + aborted = true; + return; + } + } + }); + + if (aborted) { + return false; + } + } + + toJSON() { + return { + type: this.type, + startToken: this.startToken, + tokens: this.tokens(), + value: this.value.map((x) => x.toJSON()), + }; + } + + isSimpleBlockNode(): this is SimpleBlockNode { + return SimpleBlockNode.isSimpleBlockNode(this); + } + + static isSimpleBlockNode(x: unknown): x is SimpleBlockNode { + if (!x) { + return false; + } + + if (!(x instanceof SimpleBlockNode)) { + return false; + } + + return x.type === ComponentValueType.SimpleBlock; + } +} + +/** https://www.w3.org/TR/css-syntax-3/#consume-simple-block */ +export function consumeSimpleBlock(ctx: Context, tokens: Array): { advance: number, node: SimpleBlockNode | UnclosedSimpleBlockNode } { + const endingTokenType = mirrorVariantType(tokens[0][0]); + if (!endingTokenType) { + throw new Error('Failed to parse, a mirror variant must exist for all block open tokens.'); + } + + const value: Array = []; + + let i = 1; + + // eslint-disable-next-line no-constant-condition + while (true) { + const token = tokens[i]; + if (!token || token[0] === TokenType.EOF) { + ctx.onParseError({ + message: 'Unexpected EOF while consuming a simple block.', + start: tokens[0][2], + end: tokens[tokens.length - 1][3], + state: [ + '5.4.8. Consume a simple block', + 'Unexpected EOF', + ], + }); + + return { + advance: tokens.length, + node: new UnclosedSimpleBlockNode(tokens), + }; + } + + if (token[0] === endingTokenType) { + return { + advance: i + 1, + node: new SimpleBlockNode(tokens[0], token, value), + }; + } + + if (token[0] === TokenType.Comment || token[0] === TokenType.Whitespace) { + const result = consumeAllCommentsAndWhitespace(ctx, tokens.slice(i)); + i += result.advance; + value.push(...result.nodes); + continue; + } + + const result = consumeComponentValue(ctx, tokens.slice(i)); + i += result.advance; + value.push(result.node); + } +} + +export class WhitespaceNode { + type: ComponentValueType = ComponentValueType.Whitespace; + + value: Array; + + constructor(value: Array) { + this.value = value; + } + + tokens(): Array { + return this.value; + } + + toString(): string { + return stringify(...this.value); + } + + toJSON() { + return { + type: this.type, + tokens: this.tokens(), + }; + } + + isWhitespaceNode(): this is WhitespaceNode { + return WhitespaceNode.isWhitespaceNode(this); + } + + static isWhitespaceNode(x: unknown): x is WhitespaceNode { + if (!x) { + return false; + } + + if (!(x instanceof WhitespaceNode)) { + return false; + } + + return x.type === ComponentValueType.Whitespace; + } +} + +export function consumeWhitespace(ctx: Context, tokens: Array): { advance: number, node: WhitespaceNode } { + let i = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + const token = tokens[i]; + if (token[0] !== TokenType.Whitespace) { + return { + advance: i, + node: new WhitespaceNode(tokens.slice(0, i)), + }; + } + + i++; + } +} + +export class CommentNode { + type: ComponentValueType = ComponentValueType.Comment; + + value: CSSToken; + + constructor(value: CSSToken) { + this.value = value; + } + + tokens(): Array { + return [ + this.value, + ]; + } + + toString(): string { + return stringify(this.value); + } + + toJSON() { + return { + type: this.type, + tokens: this.tokens(), + }; + } + + isCommentNode(): this is CommentNode { + return CommentNode.isCommentNode(this); + } + + static isCommentNode(x: unknown): x is CommentNode { + if (!x) { + return false; + } + + if (!(x instanceof CommentNode)) { + return false; + } + + return x.type === ComponentValueType.Comment; + } +} + +export function consumeComment(ctx: Context, tokens: Array): { advance: number, node: CommentNode } { + return { + advance: 1, + node: new CommentNode(tokens[0]), + }; +} + +export function consumeAllCommentsAndWhitespace(ctx: Context, tokens: Array): { advance: number, nodes: Array } { + const nodes: Array = []; + + let i = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (tokens[i][0] === TokenType.Whitespace) { + const result = consumeWhitespace(ctx, tokens.slice(i)); + i += result.advance; + nodes.push(result.node); + continue; + } + + if (tokens[i][0] === TokenType.Comment) { + nodes.push(new CommentNode(tokens[i])); + i++; + continue; + } + + return { + advance: i, + nodes: nodes, + }; + } +} + +export class TokenNode { + type: ComponentValueType = ComponentValueType.Token; + + value: CSSToken; + + constructor(value: CSSToken) { + this.value = value; + } + + tokens(): Array { + return [ + this.value, + ]; + } + + toString(): string { + return stringify(this.value); + } + + toJSON() { + return { + type: this.type, + tokens: this.tokens(), + }; + } + + isTokenNode(): this is TokenNode { + return TokenNode.isTokenNode(this); + } + + static isTokenNode(x: unknown): x is TokenNode { + if (!x) { + return false; + } + + if (!(x instanceof TokenNode)) { + return false; + } + + return x.type === ComponentValueType.Token; + } +} + +export class UnclosedFunctionNode { + type: ComponentValueType = ComponentValueType.UnclosedFunction; + + value: Array; + + constructor(value: Array) { + this.value = value; + } + + tokens(): Array { + return this.value; + } + + toString(): string { + return stringify(...this.value); + } + + toJSON() { + return { + type: this.type, + tokens: this.tokens(), + }; + } + + isUnclosedFunctionNode(): this is UnclosedFunctionNode { + return UnclosedFunctionNode.isUnclosedFunctionNode(this); + } + + static isUnclosedFunctionNode(x: unknown): x is UnclosedFunctionNode { + if (!x) { + return false; + } + + if (!(x instanceof UnclosedFunctionNode)) { + return false; + } + + return x.type === ComponentValueType.UnclosedFunction; + } +} + +export class UnclosedSimpleBlockNode { + type: ComponentValueType = ComponentValueType.UnclosedSimpleBlock; + + value: Array; + + constructor(value: Array) { + this.value = value; + } + + tokens(): Array { + return this.value; + } + + toString(): string { + return stringify(...this.value); + } + + toJSON() { + return { + type: this.type, + tokens: this.tokens(), + }; + } + + isUnclosedSimpleBlockNode(): this is UnclosedSimpleBlockNode { + return UnclosedSimpleBlockNode.isUnclosedSimpleBlockNode(this); + } + + static isUnclosedSimpleBlockNode(x: unknown): x is UnclosedSimpleBlockNode { + if (!x) { + return false; + } + + if (!(x instanceof UnclosedSimpleBlockNode)) { + return false; + } + + return x.type === ComponentValueType.UnclosedSimpleBlock; + } +} diff --git a/packages/css-parser-algorithms/src/index.ts b/packages/css-parser-algorithms/src/index.ts new file mode 100644 index 000000000..5bb92c097 --- /dev/null +++ b/packages/css-parser-algorithms/src/index.ts @@ -0,0 +1,16 @@ +export * from './consume/consume-component-block-function'; +export { parseComponentValue } from './parse/parse-component-value'; +export { parseListOfComponentValues } from './parse/parse-list-of-component-values'; +export { parseCommaSeparatedListOfComponentValues } from './parse/parse-comma-separated-list-of-component-values'; +export { gatherNodeAncestry } from './util/node-ancestry'; +export { ParserError } from './interfaces/error'; +export { ComponentValueType } from './util/component-value-type'; +export { + isCommentNode, + isFunctionNode, + isSimpleBlockNode, + isTokenNode, + isUnclosedFunctionNode, + isUnclosedSimpleBlockNode, + isWhitespaceNode, +} from './util/type-predicates'; diff --git a/packages/css-parser-algorithms/src/interfaces/context.ts b/packages/css-parser-algorithms/src/interfaces/context.ts new file mode 100644 index 000000000..27eb77de6 --- /dev/null +++ b/packages/css-parser-algorithms/src/interfaces/context.ts @@ -0,0 +1,5 @@ +import { ParserError } from './error'; + +export type Context = { + onParseError: (error: ParserError) => void +} diff --git a/packages/css-parser-algorithms/src/interfaces/error.ts b/packages/css-parser-algorithms/src/interfaces/error.ts new file mode 100644 index 000000000..e8c677745 --- /dev/null +++ b/packages/css-parser-algorithms/src/interfaces/error.ts @@ -0,0 +1,6 @@ +export type ParserError = { + message: string, + start: number, + end: number, + state: Array +} diff --git a/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts b/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts new file mode 100644 index 000000000..a4136fcb8 --- /dev/null +++ b/packages/css-parser-algorithms/src/parse/parse-comma-separated-list-of-component-values.ts @@ -0,0 +1,59 @@ +import { ParserError } from '../interfaces/error'; +import { CSSToken, TokenType } from '@csstools/css-tokenizer'; +import { ComponentValue, consumeComponentValue } from '../consume/consume-component-block-function'; + +export function parseCommaSeparatedListOfComponentValues(tokens: Array, options?: { onParseError?: (error: ParserError) => void }) { + const ctx = { + onParseError: options?.onParseError ?? (() => { /* noop */ }), + }; + + const tokensCopy = [ + ...tokens, + ]; + + if (tokens.length === 0) { + return []; + } + + // We expect the last token to be an EOF token. + // Passing slices of tokens to this function can easily cause the EOF token to be missing. + if (tokensCopy[tokensCopy.length - 1][0] !== TokenType.EOF) { + tokensCopy.push([ + TokenType.EOF, + '', + tokensCopy[tokensCopy.length - 1][2], + tokensCopy[tokensCopy.length - 1][3], + undefined, + ]); + } + + const listOfCvls: Array> = []; + let list: Array = []; + + let i = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (!tokensCopy[i] || tokensCopy[i][0] === TokenType.EOF) { + if (list.length) { + listOfCvls.push(list); + } + + return listOfCvls; + } + + if (tokensCopy[i][0] === TokenType.Comma) { + if (list.length) { + listOfCvls.push(list); + } + + list = []; + i++; + continue; + } + + const result = consumeComponentValue(ctx, tokens.slice(i)); + list.push(result.node); + i += result.advance; + } +} diff --git a/packages/css-parser-algorithms/src/parse/parse-component-value.ts b/packages/css-parser-algorithms/src/parse/parse-component-value.ts new file mode 100644 index 000000000..1fbb8ca01 --- /dev/null +++ b/packages/css-parser-algorithms/src/parse/parse-component-value.ts @@ -0,0 +1,40 @@ +import { ParserError } from '../interfaces/error'; +import { CSSToken, TokenType } from '@csstools/css-tokenizer'; +import { consumeComponentValue } from '../consume/consume-component-block-function'; + +export function parseComponentValue(tokens: Array, options?: { onParseError?: (error: ParserError) => void }) { + const ctx = { + onParseError: options?.onParseError ?? (() => { /* noop */ }), + }; + + const tokensCopy = [ + ...tokens, + ]; + + // We expect the last token to be an EOF token. + // Passing slices of tokens to this function can easily cause the EOF token to be missing. + if (tokensCopy[tokensCopy.length - 1][0] !== TokenType.EOF) { + tokensCopy.push([ + TokenType.EOF, + '', + tokensCopy[tokensCopy.length - 1][2], + tokensCopy[tokensCopy.length - 1][3], + undefined, + ]); + } + + const result = consumeComponentValue(ctx, tokensCopy); + if (tokensCopy[Math.min(result.advance, tokensCopy.length - 1)][0] === TokenType.EOF) { + return result.node; + } + + ctx.onParseError({ + message: 'Expected EOF after parsing a component value.', + start: tokens[0][2], + end: tokens[tokens.length - 1][3], + state: [ + '5.3.9. Parse a component value', + 'Expected EOF', + ], + }); +} diff --git a/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts b/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts new file mode 100644 index 000000000..833a5d17a --- /dev/null +++ b/packages/css-parser-algorithms/src/parse/parse-list-of-component-values.ts @@ -0,0 +1,40 @@ +import { ParserError } from '../interfaces/error'; +import { CSSToken, TokenType } from '@csstools/css-tokenizer'; +import { ComponentValue, consumeComponentValue } from '../consume/consume-component-block-function'; + +export function parseListOfComponentValues(tokens: Array, options?: { onParseError?: (error: ParserError) => void }) { + const ctx = { + onParseError: options?.onParseError ?? (() => { /* noop */ }), + }; + + const tokensCopy = [ + ...tokens, + ]; + + // We expect the last token to be an EOF token. + // Passing slices of tokens to this function can easily cause the EOF token to be missing. + if (tokensCopy[tokensCopy.length - 1][0] !== TokenType.EOF) { + tokensCopy.push([ + TokenType.EOF, + '', + tokensCopy[tokensCopy.length - 1][2], + tokensCopy[tokensCopy.length - 1][3], + undefined, + ]); + } + + const list: Array = []; + + let i = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (!tokensCopy[i] || tokensCopy[i][0] === TokenType.EOF) { + return list; + } + + const result = consumeComponentValue(ctx, tokensCopy.slice(i)); + list.push(result.node); + i += result.advance; + } +} diff --git a/packages/css-parser-algorithms/src/util/component-value-type.ts b/packages/css-parser-algorithms/src/util/component-value-type.ts new file mode 100644 index 000000000..228ae5fc7 --- /dev/null +++ b/packages/css-parser-algorithms/src/util/component-value-type.ts @@ -0,0 +1,9 @@ +export enum ComponentValueType { + Function = 'function', + SimpleBlock = 'simple-block', + Whitespace = 'whitespace', + Comment = 'comment', + Token = 'token', + UnclosedFunction = 'unclosed-function', + UnclosedSimpleBlock = 'unclosed-simple-block' +} diff --git a/packages/css-parser-algorithms/src/util/node-ancestry.ts b/packages/css-parser-algorithms/src/util/node-ancestry.ts new file mode 100644 index 000000000..6a908ee6e --- /dev/null +++ b/packages/css-parser-algorithms/src/util/node-ancestry.ts @@ -0,0 +1,19 @@ +export interface walkable { + walk(cb: (entry: { node: Array | unknown, parent: unknown }, index: number | string) => boolean | void) +} + +export function gatherNodeAncestry(node: walkable) { + const ancestry: Map = new Map(); + + node.walk((entry) => { + if (Array.isArray(entry.node)) { + entry.node.forEach((x) => { + ancestry.set(x, entry.parent); + }); + } else { + ancestry.set(entry.node, entry.parent); + } + }); + + return ancestry; +} diff --git a/packages/css-parser-algorithms/src/util/type-predicates.ts b/packages/css-parser-algorithms/src/util/type-predicates.ts new file mode 100644 index 000000000..a5cb96eda --- /dev/null +++ b/packages/css-parser-algorithms/src/util/type-predicates.ts @@ -0,0 +1,29 @@ +import { CommentNode, FunctionNode, SimpleBlockNode, TokenNode, UnclosedFunctionNode, UnclosedSimpleBlockNode, WhitespaceNode } from '../consume/consume-component-block-function'; + +export function isSimpleBlockNode(x: unknown): x is SimpleBlockNode { + return SimpleBlockNode.isSimpleBlockNode(x); +} + +export function isFunctionNode(x: unknown): x is FunctionNode { + return FunctionNode.isFunctionNode(x); +} + +export function isUnclosedSimpleBlockNode(x: unknown): x is UnclosedSimpleBlockNode { + return UnclosedSimpleBlockNode.isUnclosedSimpleBlockNode(x); +} + +export function isUnclosedFunctionNode(x: unknown): x is UnclosedFunctionNode { + return UnclosedFunctionNode.isUnclosedFunctionNode(x); +} + +export function isWhitespaceNode(x: unknown): x is WhitespaceNode { + return WhitespaceNode.isWhitespaceNode(x); +} + +export function isCommentNode(x: unknown): x is CommentNode { + return CommentNode.isCommentNode(x); +} + +export function isTokenNode(x: unknown): x is TokenNode { + return TokenNode.isTokenNode(x); +} diff --git a/packages/css-parser-algorithms/stryker.conf.json b/packages/css-parser-algorithms/stryker.conf.json new file mode 100644 index 000000000..015ebbb73 --- /dev/null +++ b/packages/css-parser-algorithms/stryker.conf.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "mutate": [ + "src/**/*.ts" + ], + "buildCommand": "npm run build", + "testRunner": "command", + "coverageAnalysis": "perTest", + "tempDirName": "../../.stryker-tmp", + "commandRunner": { + "command": "npm run test" + }, + "thresholds": { + "high": 100, + "low": 100, + "break": 100 + }, + "inPlace": true +} diff --git a/packages/css-parser-algorithms/test/_import.mjs b/packages/css-parser-algorithms/test/_import.mjs new file mode 100644 index 000000000..44050c1ac --- /dev/null +++ b/packages/css-parser-algorithms/test/_import.mjs @@ -0,0 +1,11 @@ +import { parseComponentValue } from '@csstools/css-parser-algorithms'; +import { TokenType } from '@csstools/css-tokenizer'; + +parseComponentValue([ + [ + TokenType.Ident, 'any', 0, 0, undefined, + ], + [ + TokenType.EOF, '', 0, 0, undefined, + ], +]); diff --git a/packages/css-parser-algorithms/test/_require.cjs b/packages/css-parser-algorithms/test/_require.cjs new file mode 100644 index 000000000..1e519d1d4 --- /dev/null +++ b/packages/css-parser-algorithms/test/_require.cjs @@ -0,0 +1,11 @@ +const { parseComponentValue } = require('@csstools/css-parser-algorithms'); +const { TokenType } = require('@csstools/css-tokenizer'); + +parseComponentValue([ + [ + TokenType.Ident, 'any', 0, 0, undefined, + ], + [ + TokenType.EOF, '', 0, 0, undefined, + ], +]); diff --git a/packages/css-parser-algorithms/test/cases/media-not/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/media-not/0001.list-comma.expect.json new file mode 100644 index 000000000..33eb1719a --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/media-not/0001.list-comma.expect.json @@ -0,0 +1,81 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 4, + 4, + null + ], + "tokens": [ + [ + "(-token", + "(", + 4, + 4, + null + ], + [ + "ident-token", + "color", + 5, + 9, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 5, + 9, + { + "value": "color" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/media-not/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/media-not/0001.list-space.expect.json new file mode 100644 index 000000000..88ce51c60 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/media-not/0001.list-space.expect.json @@ -0,0 +1,79 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 4, + 4, + null + ], + "tokens": [ + [ + "(-token", + "(", + 4, + 4, + null + ], + [ + "ident-token", + "color", + 5, + 9, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 5, + 9, + { + "value": "color" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/media-not/0001.mjs b/packages/css-parser-algorithms/test/cases/media-not/0001.mjs new file mode 100644 index 000000000..5eb55f343 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/media-not/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not (color)', + 'media-not/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-comma.expect.json new file mode 100644 index 000000000..7afd42c4d --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-comma.expect.json @@ -0,0 +1,55 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-space.expect.json new file mode 100644 index 000000000..5c51b53b7 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.list-space.expect.json @@ -0,0 +1,53 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0001.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.mjs new file mode 100644 index 000000000..2686abd60 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color)', + 'mf-boolean/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-comma.expect.json new file mode 100644 index 000000000..031cdcd31 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-comma.expect.json @@ -0,0 +1,117 @@ +[ + [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 1 */", + 0, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "color", + 31, + 35, + { + "value": "color" + } + ], + [ + "comment", + "/* comment 3 */", + 36, + 50, + null + ], + [ + ")-token", + ")", + 51, + 51, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 31, + 35, + { + "value": "color" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 3 */", + 36, + 50, + null + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 4 */", + 52, + 66, + null + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-space.expect.json new file mode 100644 index 000000000..c3cc37c5c --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.list-space.expect.json @@ -0,0 +1,115 @@ +[ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 1 */", + 0, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "color", + 31, + 35, + { + "value": "color" + } + ], + [ + "comment", + "/* comment 3 */", + 36, + 50, + null + ], + [ + ")-token", + ")", + 51, + 51, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 31, + 35, + { + "value": "color" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 3 */", + 36, + 50, + null + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 4 */", + 52, + 66, + null + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0002.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.mjs new file mode 100644 index 000000000..f8266d441 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '/* comment 1 */(/* comment 2 */color/* comment 3 */)/* comment 4 */', + 'mf-boolean/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-comma.expect.json new file mode 100644 index 000000000..ea4485beb --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-comma.expect.json @@ -0,0 +1,55 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "true", + 1, + 4, + { + "value": "true" + } + ], + [ + ")-token", + ")", + 5, + 5, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "true", + 1, + 4, + { + "value": "true" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-space.expect.json new file mode 100644 index 000000000..72c7af5f3 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.list-space.expect.json @@ -0,0 +1,53 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "true", + 1, + 4, + { + "value": "true" + } + ], + [ + ")-token", + ")", + 5, + 5, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "true", + 1, + 4, + { + "value": "true" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0003.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.mjs new file mode 100644 index 000000000..c0c9f5645 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(true)', + 'mf-boolean/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-comma.expect.json new file mode 100644 index 000000000..2c9b6e948 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-comma.expect.json @@ -0,0 +1,55 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "false", + 1, + 5, + { + "value": "false" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "false", + 1, + 5, + { + "value": "false" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-space.expect.json new file mode 100644 index 000000000..8d6e7f3be --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.list-space.expect.json @@ -0,0 +1,53 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "false", + 1, + 5, + { + "value": "false" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "false", + 1, + 5, + { + "value": "false" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0004.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.mjs new file mode 100644 index 000000000..0a34a2dc8 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(false)', + 'mf-boolean/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-comma.expect.json new file mode 100644 index 000000000..b6014d3df --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-comma.expect.json @@ -0,0 +1,71 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "color(", + 1, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ], + [ + ")-token", + ")", + 8, + 8, + null + ] + ], + "value": [ + { + "type": "function", + "name": "color", + "tokens": [ + [ + "function-token", + "color(", + 1, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ] + ], + "value": [] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-space.expect.json new file mode 100644 index 000000000..536dbad65 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.list-space.expect.json @@ -0,0 +1,69 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "color(", + 1, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ], + [ + ")-token", + ")", + 8, + 8, + null + ] + ], + "value": [ + { + "type": "function", + "name": "color", + "tokens": [ + [ + "function-token", + "color(", + 1, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ] + ], + "value": [] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-boolean/0005.mjs b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.mjs new file mode 100644 index 000000000..c44c8c1f7 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-boolean/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color())', + 'mf-boolean/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-comma.expect.json new file mode 100644 index 000000000..297d582fc --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-comma.expect.json @@ -0,0 +1,120 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "min-width", + 1, + 9, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 17, + 17, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 1, + 9, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 10, + 10, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-space.expect.json new file mode 100644 index 000000000..511342310 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0001.list-space.expect.json @@ -0,0 +1,118 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "min-width", + 1, + 9, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 17, + 17, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 1, + 9, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 10, + 10, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0001.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0001.mjs new file mode 100644 index 000000000..b674721b7 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(min-width: 300px)', + 'mf-plain/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-comma.expect.json new file mode 100644 index 000000000..8669e19b6 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-comma.expect.json @@ -0,0 +1,201 @@ +[ + [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 1 */", + 0, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "min-width", + 31, + 39, + { + "value": "min-width" + } + ], + [ + "comment", + "/* comment 3 */", + 40, + 54, + null + ], + [ + "colon-token", + ":", + 55, + 55, + null + ], + [ + "comment", + "/* comment 4 */", + 56, + 70, + null + ], + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + "comment", + "/* comment 5 */", + 76, + 90, + null + ], + [ + ")-token", + ")", + 91, + 91, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 31, + 39, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 3 */", + 40, + 54, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 55, + 55, + null + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 4 */", + 56, + 70, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 5 */", + 76, + 90, + null + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 6 */", + 92, + 106, + null + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-space.expect.json new file mode 100644 index 000000000..e61e96cf2 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0002.list-space.expect.json @@ -0,0 +1,199 @@ +[ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 1 */", + 0, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "min-width", + 31, + 39, + { + "value": "min-width" + } + ], + [ + "comment", + "/* comment 3 */", + 40, + 54, + null + ], + [ + "colon-token", + ":", + 55, + 55, + null + ], + [ + "comment", + "/* comment 4 */", + 56, + 70, + null + ], + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + "comment", + "/* comment 5 */", + 76, + 90, + null + ], + [ + ")-token", + ")", + 91, + 91, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 31, + 39, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 3 */", + 40, + 54, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 55, + 55, + null + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 4 */", + 56, + 70, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 5 */", + 76, + 90, + null + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* comment 6 */", + 92, + 106, + null + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0002.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0002.mjs new file mode 100644 index 000000000..90eb3e1f9 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '/* comment 1 */(/* comment 2 */min-width/* comment 3 */:/* comment 4 */300px/* comment 5 */)/* comment 6 */', + 'mf-plain/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-comma.expect.json new file mode 100644 index 000000000..f27dd647b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-comma.expect.json @@ -0,0 +1,116 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "colon-token", + ":", + 11, + 11, + null + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 11, + 11, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-space.expect.json new file mode 100644 index 000000000..704164d67 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0003.list-space.expect.json @@ -0,0 +1,114 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "colon-token", + ":", + 11, + 11, + null + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 11, + 11, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0003.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0003.mjs new file mode 100644 index 000000000..ad7c3adf2 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(resolution: infinite)', + 'mf-plain/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-comma.expect.json new file mode 100644 index 000000000..cd0b720eb --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-comma.expect.json @@ -0,0 +1,165 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "resolution(", + 1, + 11, + { + "value": "resolution" + } + ], + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ], + [ + ")-token", + ")", + 15, + 15, + null + ], + [ + "colon-token", + ":", + 16, + 16, + null + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ], + [ + "ident-token", + "infinite", + 18, + 25, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ], + "value": [ + { + "type": "function", + "name": "resolution", + "tokens": [ + [ + "function-token", + "resolution(", + 1, + 11, + { + "value": "resolution" + } + ], + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ], + [ + ")-token", + ")", + 15, + 15, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ] + ] + } + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 16, + 16, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 17, + 17, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 18, + 25, + { + "value": "infinite" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-space.expect.json new file mode 100644 index 000000000..1410e6a8b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0004.list-space.expect.json @@ -0,0 +1,163 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "resolution(", + 1, + 11, + { + "value": "resolution" + } + ], + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ], + [ + ")-token", + ")", + 15, + 15, + null + ], + [ + "colon-token", + ":", + 16, + 16, + null + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ], + [ + "ident-token", + "infinite", + 18, + 25, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ], + "value": [ + { + "type": "function", + "name": "resolution", + "tokens": [ + [ + "function-token", + "resolution(", + 1, + 11, + { + "value": "resolution" + } + ], + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ], + [ + ")-token", + ")", + 15, + 15, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ] + ] + } + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 16, + 16, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 17, + 17, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 18, + 25, + { + "value": "infinite" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0004.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0004.mjs new file mode 100644 index 000000000..6d9a61a7b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(resolution(foo): infinite)', + 'mf-plain/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-comma.expect.json new file mode 100644 index 000000000..3afb8dc07 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-comma.expect.json @@ -0,0 +1,116 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 0, + 0, + null + ], + "tokens": [ + [ + "[-token", + "[", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "colon-token", + ":", + 11, + 11, + null + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ], + [ + "]-token", + "]", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 11, + 11, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-space.expect.json b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-space.expect.json new file mode 100644 index 000000000..a7c69ba1c --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0005.list-space.expect.json @@ -0,0 +1,114 @@ +[ + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 0, + 0, + null + ], + "tokens": [ + [ + "[-token", + "[", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "colon-token", + ":", + 11, + 11, + null + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ], + [ + "]-token", + "]", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 11, + 11, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/mf-plain/0005.mjs b/packages/css-parser-algorithms/test/cases/mf-plain/0005.mjs new file mode 100644 index 000000000..946d31898 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/mf-plain/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '[resolution: infinite]', + 'mf-plain/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-comma.expect.json new file mode 100644 index 000000000..45a0eace6 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-comma.expect.json @@ -0,0 +1,18 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-space.expect.json new file mode 100644 index 000000000..f8d27aa46 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0001.list-space.expect.json @@ -0,0 +1,16 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0001.mjs b/packages/css-parser-algorithms/test/cases/query-with-type/0001.mjs new file mode 100644 index 000000000..b8d1c3f6b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'screen', + 'query-with-type/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-comma.expect.json new file mode 100644 index 000000000..d708f4353 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-comma.expect.json @@ -0,0 +1,44 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-space.expect.json new file mode 100644 index 000000000..e27303e35 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0002.list-space.expect.json @@ -0,0 +1,42 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0002.mjs b/packages/css-parser-algorithms/test/cases/query-with-type/0002.mjs new file mode 100644 index 000000000..d9da6523a --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'only screen', + 'query-with-type/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-comma.expect.json new file mode 100644 index 000000000..4d70580d2 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-comma.expect.json @@ -0,0 +1,44 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 4, + 9, + { + "value": "screen" + } + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-space.expect.json new file mode 100644 index 000000000..445e7635b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0003.list-space.expect.json @@ -0,0 +1,42 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 4, + 9, + { + "value": "screen" + } + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/query-with-type/0003.mjs b/packages/css-parser-algorithms/test/cases/query-with-type/0003.mjs new file mode 100644 index 000000000..1684cf679 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/query-with-type/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not screen', + 'query-with-type/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0001.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0001.list-comma.expect.json new file mode 100644 index 000000000..e2cb41189 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0001.list-comma.expect.json @@ -0,0 +1,145 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "comment", + "/* a comment */", + 1, + 15, + null + ], + [ + "ident-token", + "foo", + 16, + 18, + { + "value": "foo" + } + ], + [ + "whitespace-token", + " ", + 19, + 20, + null + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 1, + 15, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "foo", + 16, + 18, + { + "value": "foo" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 19, + 20, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "something", + 23, + 31, + { + "value": "something" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 32, + 32, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "else", + 33, + 36, + { + "value": "else" + } + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0001.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0001.list-space.expect.json new file mode 100644 index 000000000..1fb3acae1 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0001.list-space.expect.json @@ -0,0 +1,143 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "comment", + "/* a comment */", + 1, + 15, + null + ], + [ + "ident-token", + "foo", + 16, + 18, + { + "value": "foo" + } + ], + [ + "whitespace-token", + " ", + 19, + 20, + null + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 1, + 15, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "foo", + 16, + 18, + { + "value": "foo" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 19, + 20, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "something", + 23, + 31, + { + "value": "something" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 32, + 32, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "else", + 33, + 36, + { + "value": "else" + } + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0001.mjs b/packages/css-parser-algorithms/test/cases/various/0001.mjs new file mode 100644 index 000000000..c5764560e --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(/* a comment */foo ) something else', + 'various/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0002.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0002.list-comma.expect.json new file mode 100644 index 000000000..b88eee50e --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0002.list-comma.expect.json @@ -0,0 +1,790 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 4, + 9, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 11, + 13, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 14, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 32, + 32, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 25, + 25, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 33, + 33, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 34, + 36, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 37, + 37, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 38, + 38, + null + ], + "tokens": [ + [ + "(-token", + "(", + 38, + 38, + null + ], + [ + "ident-token", + "prefers-color-scheme", + 39, + 58, + { + "value": "prefers-color-scheme" + } + ], + [ + "colon-token", + ":", + 59, + 59, + null + ], + [ + "comment", + "/* a comment */", + 60, + 74, + null + ], + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ], + [ + ")-token", + ")", + 79, + 79, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "prefers-color-scheme", + 39, + 58, + { + "value": "prefers-color-scheme" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 59, + 59, + null + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 60, + 74, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 80, + 80, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 81, + 83, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 84, + 84, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 85, + 85, + null + ], + "tokens": [ + [ + "(-token", + "(", + 85, + 85, + null + ], + [ + "ident-token", + "width", + 86, + 90, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 91, + 91, + null + ], + [ + "delim-token", + "<", + 92, + 92, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 93, + 93, + null + ], + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ], + [ + ")-token", + ")", + 98, + 98, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "width", + 86, + 90, + { + "value": "width" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 91, + 91, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 92, + 92, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 93, + 93, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 99, + 99, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 100, + 102, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 103, + 103, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 104, + 104, + null + ], + "tokens": [ + [ + "(-token", + "(", + 104, + 104, + null + ], + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 109, + 109, + null + ], + [ + "delim-token", + "<", + 110, + 110, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 111, + 111, + null + ], + [ + "ident-token", + "width", + 112, + 116, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 117, + 117, + null + ], + [ + "delim-token", + "<", + 118, + 118, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 119, + 119, + null + ], + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 125, + 125, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 109, + 109, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 110, + 110, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 111, + 111, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "width", + 112, + 116, + { + "value": "width" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 117, + 117, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 118, + 118, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 119, + 119, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0002.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0002.list-space.expect.json new file mode 100644 index 000000000..1df900bfc --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0002.list-space.expect.json @@ -0,0 +1,788 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 4, + 9, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 11, + 13, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 14, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 32, + 32, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 25, + 25, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 33, + 33, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 34, + 36, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 37, + 37, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 38, + 38, + null + ], + "tokens": [ + [ + "(-token", + "(", + 38, + 38, + null + ], + [ + "ident-token", + "prefers-color-scheme", + 39, + 58, + { + "value": "prefers-color-scheme" + } + ], + [ + "colon-token", + ":", + 59, + 59, + null + ], + [ + "comment", + "/* a comment */", + 60, + 74, + null + ], + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ], + [ + ")-token", + ")", + 79, + 79, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "prefers-color-scheme", + 39, + 58, + { + "value": "prefers-color-scheme" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 59, + 59, + null + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 60, + 74, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 80, + 80, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 81, + 83, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 84, + 84, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 85, + 85, + null + ], + "tokens": [ + [ + "(-token", + "(", + 85, + 85, + null + ], + [ + "ident-token", + "width", + 86, + 90, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 91, + 91, + null + ], + [ + "delim-token", + "<", + 92, + 92, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 93, + 93, + null + ], + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ], + [ + ")-token", + ")", + 98, + 98, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "width", + 86, + 90, + { + "value": "width" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 91, + 91, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 92, + 92, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 93, + 93, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 99, + 99, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 100, + 102, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 103, + 103, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 104, + 104, + null + ], + "tokens": [ + [ + "(-token", + "(", + 104, + 104, + null + ], + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 109, + 109, + null + ], + [ + "delim-token", + "<", + 110, + 110, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 111, + 111, + null + ], + [ + "ident-token", + "width", + 112, + 116, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 117, + 117, + null + ], + [ + "delim-token", + "<", + 118, + 118, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 119, + 119, + null + ], + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 125, + 125, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 109, + 109, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 110, + 110, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 111, + 111, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "width", + 112, + 116, + { + "value": "width" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 117, + 117, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 118, + 118, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 119, + 119, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0002.mjs b/packages/css-parser-algorithms/test/cases/various/0002.mjs new file mode 100644 index 000000000..964b4d9b9 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)', + 'various/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0003.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0003.list-comma.expect.json new file mode 100644 index 000000000..ad2407cda --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0003.list-comma.expect.json @@ -0,0 +1,347 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "delim-token", + "<", + 12, + 12, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 22, + 22, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 12, + 12, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 13, + 13, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 23, + 23, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 24, + 26, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 28, + 28, + null + ], + "tokens": [ + [ + "(-token", + "(", + 28, + 28, + null + ], + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ], + [ + "whitespace-token", + " ", + 37, + 37, + null + ], + [ + "delim-token", + "<", + 38, + 38, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 39, + 39, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 40, + 40, + null + ], + [ + "ident-token", + "resolution", + 41, + 50, + { + "value": "resolution" + } + ], + [ + ")-token", + ")", + 51, + 51, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 37, + 37, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 38, + 38, + { + "value": "<" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "=", + 39, + 39, + { + "value": "=" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 40, + 40, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 41, + 50, + { + "value": "resolution" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 52, + 52, + null + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0003.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0003.list-space.expect.json new file mode 100644 index 000000000..3bfcbac73 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0003.list-space.expect.json @@ -0,0 +1,345 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "delim-token", + "<", + 12, + 12, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 22, + 22, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 12, + 12, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 13, + 13, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 23, + 23, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 24, + 26, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 28, + 28, + null + ], + "tokens": [ + [ + "(-token", + "(", + 28, + 28, + null + ], + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ], + [ + "whitespace-token", + " ", + 37, + 37, + null + ], + [ + "delim-token", + "<", + 38, + 38, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 39, + 39, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 40, + 40, + null + ], + [ + "ident-token", + "resolution", + 41, + 50, + { + "value": "resolution" + } + ], + [ + ")-token", + ")", + 51, + 51, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 37, + 37, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 38, + 38, + { + "value": "<" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "=", + 39, + 39, + { + "value": "=" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 40, + 40, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 41, + 50, + { + "value": "resolution" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 52, + 52, + null + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0003.mjs b/packages/css-parser-algorithms/test/cases/various/0003.mjs new file mode 100644 index 000000000..2af154b33 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(resolution < infinite) and (infinite <= resolution) ', + 'various/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0004.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0004.list-comma.expect.json new file mode 100644 index 000000000..72fb943f6 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0004.list-comma.expect.json @@ -0,0 +1,316 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ] + ] + }, + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 18, + 18, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 20, + 20, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ] + ] + } + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0004.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0004.list-space.expect.json new file mode 100644 index 000000000..5653f5136 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0004.list-space.expect.json @@ -0,0 +1,314 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ] + ] + }, + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 18, + 18, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 20, + 20, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0004.mjs b/packages/css-parser-algorithms/test/cases/various/0004.mjs new file mode 100644 index 000000000..62f73bf0a --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(width < calc(50vw - 3rem))', + 'various/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0005.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0005.list-comma.expect.json new file mode 100644 index 000000000..8e0833bda --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0005.list-comma.expect.json @@ -0,0 +1,198 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 7, + 9, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 11, + 13, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 14, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 32, + 32, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 25, + 25, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0005.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0005.list-space.expect.json new file mode 100644 index 000000000..b41645b59 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0005.list-space.expect.json @@ -0,0 +1,196 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 7, + 9, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 11, + 13, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 14, + 14, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 15, + 15, + null + ], + "tokens": [ + [ + "(-token", + "(", + 15, + 15, + null + ], + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 32, + 32, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 25, + 25, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0005.mjs b/packages/css-parser-algorithms/test/cases/various/0005.mjs new file mode 100644 index 000000000..7c55c608d --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'screen and not (min-width: 300px)', + 'various/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0006.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0006.list-comma.expect.json new file mode 100644 index 000000000..0e5184edc --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0006.list-comma.expect.json @@ -0,0 +1,56 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0006.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0006.list-space.expect.json new file mode 100644 index 000000000..75bf497e4 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0006.list-space.expect.json @@ -0,0 +1,54 @@ +[ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0006.mjs b/packages/css-parser-algorithms/test/cases/various/0006.mjs new file mode 100644 index 000000000..2d9faf758 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0006.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'only screen ', + 'various/0006', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0007.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0007.list-comma.expect.json new file mode 100644 index 000000000..e500341e8 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0007.list-comma.expect.json @@ -0,0 +1,59 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 0, + 0, + null + ], + "tokens": [ + [ + "[-token", + "[", + 0, + 0, + null + ], + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "]-token", + "]", + 5, + 5, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0007.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0007.list-space.expect.json new file mode 100644 index 000000000..c9ead936b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0007.list-space.expect.json @@ -0,0 +1,57 @@ +[ + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 0, + 0, + null + ], + "tokens": [ + [ + "[-token", + "[", + 0, + 0, + null + ], + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "]-token", + "]", + 5, + 5, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0007.mjs b/packages/css-parser-algorithms/test/cases/various/0007.mjs new file mode 100644 index 000000000..f3e178c40 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0007.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '[10px]', + 'various/0007', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0008.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0008.list-comma.expect.json new file mode 100644 index 000000000..9fa9596f9 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0008.list-comma.expect.json @@ -0,0 +1,110 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ], + [ + ")-token", + ")", + 11, + 11, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0008.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0008.list-space.expect.json new file mode 100644 index 000000000..75447e844 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0008.list-space.expect.json @@ -0,0 +1,108 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ], + [ + ")-token", + ")", + 11, + 11, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0008.mjs b/packages/css-parser-algorithms/test/cases/various/0008.mjs new file mode 100644 index 000000000..6ea1fad9d --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0008.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(calc(10px))', + 'various/0008', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0009.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0009.list-comma.expect.json new file mode 100644 index 000000000..70fea63dd --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0009.list-comma.expect.json @@ -0,0 +1,129 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + ")-token", + ")", + 12, + 12, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0009.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0009.list-space.expect.json new file mode 100644 index 000000000..9efe5dbe6 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0009.list-space.expect.json @@ -0,0 +1,127 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + ")-token", + ")", + 12, + 12, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0009.mjs b/packages/css-parser-algorithms/test/cases/various/0009.mjs new file mode 100644 index 000000000..bdf46f868 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0009.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(calc(10px) )', + 'various/0009', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0010.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0010.list-comma.expect.json new file mode 100644 index 000000000..38a3b8c7a --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0010.list-comma.expect.json @@ -0,0 +1,129 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ], + [ + "comment", + "/* a comment */", + 11, + 25, + null + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 11, + 25, + null + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0010.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0010.list-space.expect.json new file mode 100644 index 000000000..bc616fb21 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0010.list-space.expect.json @@ -0,0 +1,127 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ], + [ + "comment", + "/* a comment */", + 11, + 25, + null + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 1, + 5, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 6, + 9, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 11, + 25, + null + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0010.mjs b/packages/css-parser-algorithms/test/cases/various/0010.mjs new file mode 100644 index 000000000..9b314124f --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0010.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(calc(10px)/* a comment */)', + 'various/0010', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0011.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0011.list-comma.expect.json new file mode 100644 index 000000000..b7d0d623d --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0011.list-comma.expect.json @@ -0,0 +1,298 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "(-token", + "(", + 1, + 1, + null + ], + [ + "function-token", + "calc(", + 2, + 6, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 11, + 11, + null + ], + [ + "comment", + "/* a comment */", + 12, + 26, + null + ], + [ + ")-token", + ")", + 27, + 27, + null + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "(-token", + "(", + 29, + 29, + null + ], + [ + "ident-token", + "other", + 30, + 34, + { + "value": "other" + } + ], + [ + ")-token", + ")", + 35, + 35, + null + ], + [ + ")-token", + ")", + 36, + 36, + null + ] + ], + "value": [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 1, + 1, + null + ], + "tokens": [ + [ + "(-token", + "(", + 1, + 1, + null + ], + [ + "function-token", + "calc(", + 2, + 6, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 11, + 11, + null + ], + [ + "comment", + "/* a comment */", + 12, + 26, + null + ], + [ + ")-token", + ")", + 27, + 27, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 2, + 6, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 11, + 11, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 12, + 26, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 28, + 28, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 29, + 29, + null + ], + "tokens": [ + [ + "(-token", + "(", + 29, + 29, + null + ], + [ + "ident-token", + "other", + 30, + 34, + { + "value": "other" + } + ], + [ + ")-token", + ")", + 35, + 35, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "other", + 30, + 34, + { + "value": "other" + } + ] + ] + } + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0011.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0011.list-space.expect.json new file mode 100644 index 000000000..ce17e46da --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0011.list-space.expect.json @@ -0,0 +1,296 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "(-token", + "(", + 1, + 1, + null + ], + [ + "function-token", + "calc(", + 2, + 6, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 11, + 11, + null + ], + [ + "comment", + "/* a comment */", + 12, + 26, + null + ], + [ + ")-token", + ")", + 27, + 27, + null + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "(-token", + "(", + 29, + 29, + null + ], + [ + "ident-token", + "other", + 30, + 34, + { + "value": "other" + } + ], + [ + ")-token", + ")", + 35, + 35, + null + ], + [ + ")-token", + ")", + 36, + 36, + null + ] + ], + "value": [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 1, + 1, + null + ], + "tokens": [ + [ + "(-token", + "(", + 1, + 1, + null + ], + [ + "function-token", + "calc(", + 2, + 6, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 11, + 11, + null + ], + [ + "comment", + "/* a comment */", + 12, + 26, + null + ], + [ + ")-token", + ")", + 27, + 27, + null + ] + ], + "value": [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 2, + 6, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 11, + 11, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 7, + 10, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 12, + 26, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 28, + 28, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 29, + 29, + null + ], + "tokens": [ + [ + "(-token", + "(", + 29, + 29, + null + ], + [ + "ident-token", + "other", + 30, + 34, + { + "value": "other" + } + ], + [ + ")-token", + ")", + 35, + 35, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "other", + 30, + 34, + { + "value": "other" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0011.mjs b/packages/css-parser-algorithms/test/cases/various/0011.mjs new file mode 100644 index 000000000..dc4fa6514 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0011.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '((calc(10px)/* a comment */) (other))', + 'various/0011', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0012.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0012.list-comma.expect.json new file mode 100644 index 000000000..b99cb9831 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0012.list-comma.expect.json @@ -0,0 +1,55 @@ +[ + [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 9, + 9, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0012.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0012.list-space.expect.json new file mode 100644 index 000000000..ed15ba06a --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0012.list-space.expect.json @@ -0,0 +1,53 @@ +[ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 9, + 9, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0012.mjs b/packages/css-parser-algorithms/test/cases/various/0012.mjs new file mode 100644 index 000000000..6f2b31339 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0012.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'calc(10px)', + 'various/0012', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0013.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0013.list-comma.expect.json new file mode 100644 index 000000000..8f8bd84d1 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0013.list-comma.expect.json @@ -0,0 +1,74 @@ +[ + [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0013.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0013.list-space.expect.json new file mode 100644 index 000000000..18a984378 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0013.list-space.expect.json @@ -0,0 +1,72 @@ +[ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0013.mjs b/packages/css-parser-algorithms/test/cases/various/0013.mjs new file mode 100644 index 000000000..94cc52dee --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0013.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'calc(10px )', + 'various/0013', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0014.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0014.list-comma.expect.json new file mode 100644 index 000000000..c7e67316b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0014.list-comma.expect.json @@ -0,0 +1,74 @@ +[ + [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comment", + "/* a comment */", + 9, + 23, + null + ], + [ + ")-token", + ")", + 24, + 24, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 9, + 23, + null + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0014.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0014.list-space.expect.json new file mode 100644 index 000000000..c59243b26 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0014.list-space.expect.json @@ -0,0 +1,72 @@ +[ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comment", + "/* a comment */", + 9, + 23, + null + ], + [ + ")-token", + ")", + 24, + 24, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 9, + 23, + null + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0014.mjs b/packages/css-parser-algorithms/test/cases/various/0014.mjs new file mode 100644 index 000000000..26a323ac3 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0014.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'calc(10px/* a comment */)', + 'various/0014', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0015.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0015.list-comma.expect.json new file mode 100644 index 000000000..d5108b8fd --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0015.list-comma.expect.json @@ -0,0 +1,347 @@ +[ + [ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comma-token", + ",", + 9, + 9, + null + ], + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "comment", + "/* a comment */", + 11, + 25, + null + ], + [ + "comma-token", + ",", + 26, + 26, + null + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ], + [ + "(-token", + "(", + 28, + 28, + null + ], + [ + "ident-token", + "other", + 29, + 33, + { + "value": "other" + } + ], + [ + "whitespace-token", + " ", + 34, + 34, + null + ], + [ + "function-token", + "calc(", + 35, + 39, + { + "value": "calc" + } + ], + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ], + [ + ")-token", + ")", + 44, + 44, + null + ], + [ + ")-token", + ")", + 45, + 45, + null + ], + [ + ")-token", + ")", + 46, + 46, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 9, + 9, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 11, + 25, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 26, + 26, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 28, + 28, + null + ], + "tokens": [ + [ + "(-token", + "(", + 28, + 28, + null + ], + [ + "ident-token", + "other", + 29, + 33, + { + "value": "other" + } + ], + [ + "whitespace-token", + " ", + 34, + 34, + null + ], + [ + "function-token", + "calc(", + 35, + 39, + { + "value": "calc" + } + ], + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ], + [ + ")-token", + ")", + 44, + 44, + null + ], + [ + ")-token", + ")", + 45, + 45, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "other", + 29, + 33, + { + "value": "other" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 34, + 34, + null + ] + ] + }, + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 35, + 39, + { + "value": "calc" + } + ], + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ], + [ + ")-token", + ")", + 44, + 44, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ] + ] + } + ] + } + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0015.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0015.list-space.expect.json new file mode 100644 index 000000000..9bf015a26 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0015.list-space.expect.json @@ -0,0 +1,345 @@ +[ + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 0, + 4, + { + "value": "calc" + } + ], + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comma-token", + ",", + 9, + 9, + null + ], + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "comment", + "/* a comment */", + 11, + 25, + null + ], + [ + "comma-token", + ",", + 26, + 26, + null + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ], + [ + "(-token", + "(", + 28, + 28, + null + ], + [ + "ident-token", + "other", + 29, + 33, + { + "value": "other" + } + ], + [ + "whitespace-token", + " ", + 34, + 34, + null + ], + [ + "function-token", + "calc(", + 35, + 39, + { + "value": "calc" + } + ], + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ], + [ + ")-token", + ")", + 44, + 44, + null + ], + [ + ")-token", + ")", + 45, + 45, + null + ], + [ + ")-token", + ")", + 46, + 46, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 5, + 8, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 9, + 9, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 11, + 25, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 26, + 26, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 28, + 28, + null + ], + "tokens": [ + [ + "(-token", + "(", + 28, + 28, + null + ], + [ + "ident-token", + "other", + 29, + 33, + { + "value": "other" + } + ], + [ + "whitespace-token", + " ", + 34, + 34, + null + ], + [ + "function-token", + "calc(", + 35, + 39, + { + "value": "calc" + } + ], + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ], + [ + ")-token", + ")", + 44, + 44, + null + ], + [ + ")-token", + ")", + 45, + 45, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "other", + 29, + 33, + { + "value": "other" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 34, + 34, + null + ] + ] + }, + { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 35, + 39, + { + "value": "calc" + } + ], + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ], + [ + ")-token", + ")", + 44, + 44, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "more", + 40, + 43, + { + "value": "more" + } + ] + ] + } + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0015.mjs b/packages/css-parser-algorithms/test/cases/various/0015.mjs new file mode 100644 index 000000000..656a7b017 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0015.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'calc(10px, /* a comment */, (other calc(more)))', + 'various/0015', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0016.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0016.list-comma.expect.json new file mode 100644 index 000000000..28f35b5cd --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0016.list-comma.expect.json @@ -0,0 +1,162 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comma-token", + ",", + 5, + 5, + null + ], + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 5, + 5, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ], + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 12, + 12, + null + ], + "tokens": [ + [ + "(-token", + "(", + 12, + 12, + null + ], + [ + "dimension-token", + "10px", + 13, + 16, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 17, + 17, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 13, + 16, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0016.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0016.list-space.expect.json new file mode 100644 index 000000000..bc31e9fd7 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0016.list-space.expect.json @@ -0,0 +1,170 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comma-token", + ",", + 5, + 5, + null + ], + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 5, + 5, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 11, + 11, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 12, + 12, + null + ], + "tokens": [ + [ + "(-token", + "(", + 12, + 12, + null + ], + [ + "dimension-token", + "10px", + 13, + 16, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 17, + 17, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 13, + 16, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0016.mjs b/packages/css-parser-algorithms/test/cases/various/0016.mjs new file mode 100644 index 000000000..238192b31 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0016.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(10px,12px),(10px)', + 'various/0016', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0017.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0017.list-comma.expect.json new file mode 100644 index 000000000..51b1cc6a1 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0017.list-comma.expect.json @@ -0,0 +1,151 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comma-token", + ",", + 5, + 5, + null + ], + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 5, + 5, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ], + [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 12, + 15, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 17, + 20, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0017.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0017.list-space.expect.json new file mode 100644 index 000000000..c5cf8279c --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0017.list-space.expect.json @@ -0,0 +1,159 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ], + [ + "comma-token", + ",", + 5, + 5, + null + ], + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 1, + 4, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 5, + 5, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "12px", + 6, + 9, + { + "value": 12, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + }, + { + "type": "token", + "tokens": [ + [ + "comma-token", + ",", + 11, + 11, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 12, + 15, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 17, + 20, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0017.mjs b/packages/css-parser-algorithms/test/cases/various/0017.mjs new file mode 100644 index 000000000..3ffaa1e3b --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0017.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(10px,12px),10px 10px', + 'various/0017', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0018.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0018.list-comma.expect.json new file mode 100644 index 000000000..d025163ca --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0018.list-comma.expect.json @@ -0,0 +1,48 @@ +[ + [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 0, + 3, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "15px", + 5, + 8, + { + "value": 15, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0018.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0018.list-space.expect.json new file mode 100644 index 000000000..fd958c8ba --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0018.list-space.expect.json @@ -0,0 +1,46 @@ +[ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "10px", + 0, + 3, + { + "value": 10, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "15px", + 5, + 8, + { + "value": 15, + "type": "integer", + "unit": "px" + } + ] + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0018.mjs b/packages/css-parser-algorithms/test/cases/various/0018.mjs new file mode 100644 index 000000000..342074128 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0018.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '10px 15px', + 'various/0018', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/cases/various/0019.list-comma.expect.json b/packages/css-parser-algorithms/test/cases/various/0019.list-comma.expect.json new file mode 100644 index 000000000..1bd898203 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0019.list-comma.expect.json @@ -0,0 +1,1704 @@ +[ + [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "whitespace-token", + " ", + 1, + 1, + null + ], + [ + "(-token", + "(", + 2, + 2, + null + ], + [ + "ident-token", + "aa", + 3, + 4, + { + "value": "aa" + } + ], + [ + ")-token", + ")", + 5, + 5, + null + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "[-token", + "[", + 7, + 7, + null + ], + [ + "ident-token", + "ab", + 8, + 9, + { + "value": "ab" + } + ], + [ + "]-token", + "]", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "{-token", + "{", + 12, + 12, + null + ], + [ + "ident-token", + "ac", + 13, + 14, + { + "value": "ac" + } + ], + [ + "}-token", + "}", + 15, + 15, + null + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "function-token", + "z(", + 17, + 18, + { + "value": "z" + } + ], + [ + "ident-token", + "ad", + 19, + 20, + { + "value": "ad" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + ")-token", + ")", + 23, + 23, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 1, + 1, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 2, + 2, + null + ], + "tokens": [ + [ + "(-token", + "(", + 2, + 2, + null + ], + [ + "ident-token", + "aa", + 3, + 4, + { + "value": "aa" + } + ], + [ + ")-token", + ")", + 5, + 5, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "aa", + 3, + 4, + { + "value": "aa" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 7, + 7, + null + ], + "tokens": [ + [ + "[-token", + "[", + 7, + 7, + null + ], + [ + "ident-token", + "ab", + 8, + 9, + { + "value": "ab" + } + ], + [ + "]-token", + "]", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ab", + 8, + 9, + { + "value": "ab" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 12, + 12, + null + ], + "tokens": [ + [ + "{-token", + "{", + 12, + 12, + null + ], + [ + "ident-token", + "ac", + 13, + 14, + { + "value": "ac" + } + ], + [ + "}-token", + "}", + 15, + 15, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ac", + 13, + 14, + { + "value": "ac" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 17, + 18, + { + "value": "z" + } + ], + [ + "ident-token", + "ad", + 19, + 20, + { + "value": "ad" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ad", + 19, + 20, + { + "value": "ad" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 25, + 25, + null + ], + "tokens": [ + [ + "[-token", + "[", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "(-token", + "(", + 27, + 27, + null + ], + [ + "ident-token", + "ba", + 28, + 29, + { + "value": "ba" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ], + [ + "whitespace-token", + " ", + 31, + 31, + null + ], + [ + "[-token", + "[", + 32, + 32, + null + ], + [ + "ident-token", + "bb", + 33, + 34, + { + "value": "bb" + } + ], + [ + "]-token", + "]", + 35, + 35, + null + ], + [ + "whitespace-token", + " ", + 36, + 36, + null + ], + [ + "{-token", + "{", + 37, + 37, + null + ], + [ + "ident-token", + "bc", + 38, + 39, + { + "value": "bc" + } + ], + [ + "}-token", + "}", + 40, + 40, + null + ], + [ + "whitespace-token", + " ", + 41, + 41, + null + ], + [ + "function-token", + "z(", + 42, + 43, + { + "value": "z" + } + ], + [ + "ident-token", + "bd", + 44, + 45, + { + "value": "bd" + } + ], + [ + ")-token", + ")", + 46, + 46, + null + ], + [ + "whitespace-token", + " ", + 47, + 47, + null + ], + [ + "]-token", + "]", + 48, + 48, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 27, + 27, + null + ], + "tokens": [ + [ + "(-token", + "(", + 27, + 27, + null + ], + [ + "ident-token", + "ba", + 28, + 29, + { + "value": "ba" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ba", + 28, + 29, + { + "value": "ba" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 31, + 31, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 32, + 32, + null + ], + "tokens": [ + [ + "[-token", + "[", + 32, + 32, + null + ], + [ + "ident-token", + "bb", + 33, + 34, + { + "value": "bb" + } + ], + [ + "]-token", + "]", + 35, + 35, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "bb", + 33, + 34, + { + "value": "bb" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 36, + 36, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 37, + 37, + null + ], + "tokens": [ + [ + "{-token", + "{", + 37, + 37, + null + ], + [ + "ident-token", + "bc", + 38, + 39, + { + "value": "bc" + } + ], + [ + "}-token", + "}", + 40, + 40, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "bc", + 38, + 39, + { + "value": "bc" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 41, + 41, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 42, + 43, + { + "value": "z" + } + ], + [ + "ident-token", + "bd", + 44, + 45, + { + "value": "bd" + } + ], + [ + ")-token", + ")", + 46, + 46, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "bd", + 44, + 45, + { + "value": "bd" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 47, + 47, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 49, + 49, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 50, + 50, + null + ], + "tokens": [ + [ + "{-token", + "{", + 50, + 50, + null + ], + [ + "whitespace-token", + " ", + 51, + 51, + null + ], + [ + "(-token", + "(", + 52, + 52, + null + ], + [ + "ident-token", + "ca", + 53, + 54, + { + "value": "ca" + } + ], + [ + ")-token", + ")", + 55, + 55, + null + ], + [ + "whitespace-token", + " ", + 56, + 56, + null + ], + [ + "[-token", + "[", + 57, + 57, + null + ], + [ + "ident-token", + "cb", + 58, + 59, + { + "value": "cb" + } + ], + [ + "]-token", + "]", + 60, + 60, + null + ], + [ + "whitespace-token", + " ", + 61, + 61, + null + ], + [ + "{-token", + "{", + 62, + 62, + null + ], + [ + "ident-token", + "cc", + 63, + 64, + { + "value": "cc" + } + ], + [ + "}-token", + "}", + 65, + 65, + null + ], + [ + "whitespace-token", + " ", + 66, + 66, + null + ], + [ + "function-token", + "z(", + 67, + 68, + { + "value": "z" + } + ], + [ + "ident-token", + "cd", + 69, + 70, + { + "value": "cd" + } + ], + [ + ")-token", + ")", + 71, + 71, + null + ], + [ + "whitespace-token", + " ", + 72, + 72, + null + ], + [ + "}-token", + "}", + 73, + 73, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 51, + 51, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 52, + 52, + null + ], + "tokens": [ + [ + "(-token", + "(", + 52, + 52, + null + ], + [ + "ident-token", + "ca", + 53, + 54, + { + "value": "ca" + } + ], + [ + ")-token", + ")", + 55, + 55, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ca", + 53, + 54, + { + "value": "ca" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 56, + 56, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 57, + 57, + null + ], + "tokens": [ + [ + "[-token", + "[", + 57, + 57, + null + ], + [ + "ident-token", + "cb", + 58, + 59, + { + "value": "cb" + } + ], + [ + "]-token", + "]", + 60, + 60, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "cb", + 58, + 59, + { + "value": "cb" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 61, + 61, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 62, + 62, + null + ], + "tokens": [ + [ + "{-token", + "{", + 62, + 62, + null + ], + [ + "ident-token", + "cc", + 63, + 64, + { + "value": "cc" + } + ], + [ + "}-token", + "}", + 65, + 65, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "cc", + 63, + 64, + { + "value": "cc" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 66, + 66, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 67, + 68, + { + "value": "z" + } + ], + [ + "ident-token", + "cd", + 69, + 70, + { + "value": "cd" + } + ], + [ + ")-token", + ")", + 71, + 71, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "cd", + 69, + 70, + { + "value": "cd" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 72, + 72, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 74, + 74, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 75, + 76, + { + "value": "z" + } + ], + [ + "whitespace-token", + " ", + 77, + 77, + null + ], + [ + "(-token", + "(", + 78, + 78, + null + ], + [ + "ident-token", + "da", + 79, + 80, + { + "value": "da" + } + ], + [ + ")-token", + ")", + 81, + 81, + null + ], + [ + "whitespace-token", + " ", + 82, + 82, + null + ], + [ + "[-token", + "[", + 83, + 83, + null + ], + [ + "ident-token", + "db", + 84, + 85, + { + "value": "db" + } + ], + [ + "]-token", + "]", + 86, + 86, + null + ], + [ + "whitespace-token", + " ", + 87, + 87, + null + ], + [ + "{-token", + "{", + 88, + 88, + null + ], + [ + "ident-token", + "dc", + 89, + 90, + { + "value": "dc" + } + ], + [ + "}-token", + "}", + 91, + 91, + null + ], + [ + "whitespace-token", + " ", + 92, + 92, + null + ], + [ + "function-token", + "z(", + 93, + 94, + { + "value": "z" + } + ], + [ + "ident-token", + "dd", + 95, + 96, + { + "value": "dd" + } + ], + [ + ")-token", + ")", + 97, + 97, + null + ], + [ + "whitespace-token", + " ", + 98, + 98, + null + ], + [ + ")-token", + ")", + 99, + 99, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 77, + 77, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 78, + 78, + null + ], + "tokens": [ + [ + "(-token", + "(", + 78, + 78, + null + ], + [ + "ident-token", + "da", + 79, + 80, + { + "value": "da" + } + ], + [ + ")-token", + ")", + 81, + 81, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "da", + 79, + 80, + { + "value": "da" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 82, + 82, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 83, + 83, + null + ], + "tokens": [ + [ + "[-token", + "[", + 83, + 83, + null + ], + [ + "ident-token", + "db", + 84, + 85, + { + "value": "db" + } + ], + [ + "]-token", + "]", + 86, + 86, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "db", + 84, + 85, + { + "value": "db" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 87, + 87, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 88, + 88, + null + ], + "tokens": [ + [ + "{-token", + "{", + 88, + 88, + null + ], + [ + "ident-token", + "dc", + 89, + 90, + { + "value": "dc" + } + ], + [ + "}-token", + "}", + 91, + 91, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "dc", + 89, + 90, + { + "value": "dc" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 92, + 92, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 93, + 94, + { + "value": "z" + } + ], + [ + "ident-token", + "dd", + 95, + 96, + { + "value": "dd" + } + ], + [ + ")-token", + ")", + 97, + 97, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "dd", + 95, + 96, + { + "value": "dd" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 98, + 98, + null + ] + ] + } + ] + } + ] +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0019.list-space.expect.json b/packages/css-parser-algorithms/test/cases/various/0019.list-space.expect.json new file mode 100644 index 000000000..72ba46a67 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0019.list-space.expect.json @@ -0,0 +1,1702 @@ +[ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "whitespace-token", + " ", + 1, + 1, + null + ], + [ + "(-token", + "(", + 2, + 2, + null + ], + [ + "ident-token", + "aa", + 3, + 4, + { + "value": "aa" + } + ], + [ + ")-token", + ")", + 5, + 5, + null + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "[-token", + "[", + 7, + 7, + null + ], + [ + "ident-token", + "ab", + 8, + 9, + { + "value": "ab" + } + ], + [ + "]-token", + "]", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "{-token", + "{", + 12, + 12, + null + ], + [ + "ident-token", + "ac", + 13, + 14, + { + "value": "ac" + } + ], + [ + "}-token", + "}", + 15, + 15, + null + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "function-token", + "z(", + 17, + 18, + { + "value": "z" + } + ], + [ + "ident-token", + "ad", + 19, + 20, + { + "value": "ad" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + ")-token", + ")", + 23, + 23, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 1, + 1, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 2, + 2, + null + ], + "tokens": [ + [ + "(-token", + "(", + 2, + 2, + null + ], + [ + "ident-token", + "aa", + 3, + 4, + { + "value": "aa" + } + ], + [ + ")-token", + ")", + 5, + 5, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "aa", + 3, + 4, + { + "value": "aa" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 7, + 7, + null + ], + "tokens": [ + [ + "[-token", + "[", + 7, + 7, + null + ], + [ + "ident-token", + "ab", + 8, + 9, + { + "value": "ab" + } + ], + [ + "]-token", + "]", + 10, + 10, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ab", + 8, + 9, + { + "value": "ab" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 12, + 12, + null + ], + "tokens": [ + [ + "{-token", + "{", + 12, + 12, + null + ], + [ + "ident-token", + "ac", + 13, + 14, + { + "value": "ac" + } + ], + [ + "}-token", + "}", + 15, + 15, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ac", + 13, + 14, + { + "value": "ac" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 17, + 18, + { + "value": "z" + } + ], + [ + "ident-token", + "ad", + 19, + 20, + { + "value": "ad" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ad", + 19, + 20, + { + "value": "ad" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 25, + 25, + null + ], + "tokens": [ + [ + "[-token", + "[", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "(-token", + "(", + 27, + 27, + null + ], + [ + "ident-token", + "ba", + 28, + 29, + { + "value": "ba" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ], + [ + "whitespace-token", + " ", + 31, + 31, + null + ], + [ + "[-token", + "[", + 32, + 32, + null + ], + [ + "ident-token", + "bb", + 33, + 34, + { + "value": "bb" + } + ], + [ + "]-token", + "]", + 35, + 35, + null + ], + [ + "whitespace-token", + " ", + 36, + 36, + null + ], + [ + "{-token", + "{", + 37, + 37, + null + ], + [ + "ident-token", + "bc", + 38, + 39, + { + "value": "bc" + } + ], + [ + "}-token", + "}", + 40, + 40, + null + ], + [ + "whitespace-token", + " ", + 41, + 41, + null + ], + [ + "function-token", + "z(", + 42, + 43, + { + "value": "z" + } + ], + [ + "ident-token", + "bd", + 44, + 45, + { + "value": "bd" + } + ], + [ + ")-token", + ")", + 46, + 46, + null + ], + [ + "whitespace-token", + " ", + 47, + 47, + null + ], + [ + "]-token", + "]", + 48, + 48, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 27, + 27, + null + ], + "tokens": [ + [ + "(-token", + "(", + 27, + 27, + null + ], + [ + "ident-token", + "ba", + 28, + 29, + { + "value": "ba" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ba", + 28, + 29, + { + "value": "ba" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 31, + 31, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 32, + 32, + null + ], + "tokens": [ + [ + "[-token", + "[", + 32, + 32, + null + ], + [ + "ident-token", + "bb", + 33, + 34, + { + "value": "bb" + } + ], + [ + "]-token", + "]", + 35, + 35, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "bb", + 33, + 34, + { + "value": "bb" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 36, + 36, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 37, + 37, + null + ], + "tokens": [ + [ + "{-token", + "{", + 37, + 37, + null + ], + [ + "ident-token", + "bc", + 38, + 39, + { + "value": "bc" + } + ], + [ + "}-token", + "}", + 40, + 40, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "bc", + 38, + 39, + { + "value": "bc" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 41, + 41, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 42, + 43, + { + "value": "z" + } + ], + [ + "ident-token", + "bd", + 44, + 45, + { + "value": "bd" + } + ], + [ + ")-token", + ")", + 46, + 46, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "bd", + 44, + 45, + { + "value": "bd" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 47, + 47, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 49, + 49, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 50, + 50, + null + ], + "tokens": [ + [ + "{-token", + "{", + 50, + 50, + null + ], + [ + "whitespace-token", + " ", + 51, + 51, + null + ], + [ + "(-token", + "(", + 52, + 52, + null + ], + [ + "ident-token", + "ca", + 53, + 54, + { + "value": "ca" + } + ], + [ + ")-token", + ")", + 55, + 55, + null + ], + [ + "whitespace-token", + " ", + 56, + 56, + null + ], + [ + "[-token", + "[", + 57, + 57, + null + ], + [ + "ident-token", + "cb", + 58, + 59, + { + "value": "cb" + } + ], + [ + "]-token", + "]", + 60, + 60, + null + ], + [ + "whitespace-token", + " ", + 61, + 61, + null + ], + [ + "{-token", + "{", + 62, + 62, + null + ], + [ + "ident-token", + "cc", + 63, + 64, + { + "value": "cc" + } + ], + [ + "}-token", + "}", + 65, + 65, + null + ], + [ + "whitespace-token", + " ", + 66, + 66, + null + ], + [ + "function-token", + "z(", + 67, + 68, + { + "value": "z" + } + ], + [ + "ident-token", + "cd", + 69, + 70, + { + "value": "cd" + } + ], + [ + ")-token", + ")", + 71, + 71, + null + ], + [ + "whitespace-token", + " ", + 72, + 72, + null + ], + [ + "}-token", + "}", + 73, + 73, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 51, + 51, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 52, + 52, + null + ], + "tokens": [ + [ + "(-token", + "(", + 52, + 52, + null + ], + [ + "ident-token", + "ca", + 53, + 54, + { + "value": "ca" + } + ], + [ + ")-token", + ")", + 55, + 55, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "ca", + 53, + 54, + { + "value": "ca" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 56, + 56, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 57, + 57, + null + ], + "tokens": [ + [ + "[-token", + "[", + 57, + 57, + null + ], + [ + "ident-token", + "cb", + 58, + 59, + { + "value": "cb" + } + ], + [ + "]-token", + "]", + 60, + 60, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "cb", + 58, + 59, + { + "value": "cb" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 61, + 61, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 62, + 62, + null + ], + "tokens": [ + [ + "{-token", + "{", + 62, + 62, + null + ], + [ + "ident-token", + "cc", + 63, + 64, + { + "value": "cc" + } + ], + [ + "}-token", + "}", + 65, + 65, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "cc", + 63, + 64, + { + "value": "cc" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 66, + 66, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 67, + 68, + { + "value": "z" + } + ], + [ + "ident-token", + "cd", + 69, + 70, + { + "value": "cd" + } + ], + [ + ")-token", + ")", + 71, + 71, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "cd", + 69, + 70, + { + "value": "cd" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 72, + 72, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 74, + 74, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 75, + 76, + { + "value": "z" + } + ], + [ + "whitespace-token", + " ", + 77, + 77, + null + ], + [ + "(-token", + "(", + 78, + 78, + null + ], + [ + "ident-token", + "da", + 79, + 80, + { + "value": "da" + } + ], + [ + ")-token", + ")", + 81, + 81, + null + ], + [ + "whitespace-token", + " ", + 82, + 82, + null + ], + [ + "[-token", + "[", + 83, + 83, + null + ], + [ + "ident-token", + "db", + 84, + 85, + { + "value": "db" + } + ], + [ + "]-token", + "]", + 86, + 86, + null + ], + [ + "whitespace-token", + " ", + 87, + 87, + null + ], + [ + "{-token", + "{", + 88, + 88, + null + ], + [ + "ident-token", + "dc", + 89, + 90, + { + "value": "dc" + } + ], + [ + "}-token", + "}", + 91, + 91, + null + ], + [ + "whitespace-token", + " ", + 92, + 92, + null + ], + [ + "function-token", + "z(", + 93, + 94, + { + "value": "z" + } + ], + [ + "ident-token", + "dd", + 95, + 96, + { + "value": "dd" + } + ], + [ + ")-token", + ")", + 97, + 97, + null + ], + [ + "whitespace-token", + " ", + 98, + 98, + null + ], + [ + ")-token", + ")", + 99, + 99, + null + ] + ], + "value": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 77, + 77, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 78, + 78, + null + ], + "tokens": [ + [ + "(-token", + "(", + 78, + 78, + null + ], + [ + "ident-token", + "da", + 79, + 80, + { + "value": "da" + } + ], + [ + ")-token", + ")", + 81, + 81, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "da", + 79, + 80, + { + "value": "da" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 82, + 82, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 83, + 83, + null + ], + "tokens": [ + [ + "[-token", + "[", + 83, + 83, + null + ], + [ + "ident-token", + "db", + 84, + 85, + { + "value": "db" + } + ], + [ + "]-token", + "]", + 86, + 86, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "db", + 84, + 85, + { + "value": "db" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 87, + 87, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "{-token", + "{", + 88, + 88, + null + ], + "tokens": [ + [ + "{-token", + "{", + 88, + 88, + null + ], + [ + "ident-token", + "dc", + 89, + 90, + { + "value": "dc" + } + ], + [ + "}-token", + "}", + 91, + 91, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "dc", + 89, + 90, + { + "value": "dc" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 92, + 92, + null + ] + ] + }, + { + "type": "function", + "name": "z", + "tokens": [ + [ + "function-token", + "z(", + 93, + 94, + { + "value": "z" + } + ], + [ + "ident-token", + "dd", + 95, + 96, + { + "value": "dd" + } + ], + [ + ")-token", + ")", + 97, + 97, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "dd", + 95, + 96, + { + "value": "dd" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 98, + 98, + null + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/css-parser-algorithms/test/cases/various/0019.mjs b/packages/css-parser-algorithms/test/cases/various/0019.mjs new file mode 100644 index 000000000..32c9385e6 --- /dev/null +++ b/packages/css-parser-algorithms/test/cases/various/0019.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '( (aa) [ab] {ac} z(ad) ) [ (ba) [bb] {bc} z(bd) ] { (ca) [cb] {cc} z(cd) } z( (da) [db] {dc} z(dd) )', + 'various/0019', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/css-parser-algorithms/test/test.mjs b/packages/css-parser-algorithms/test/test.mjs new file mode 100644 index 000000000..fa184c135 --- /dev/null +++ b/packages/css-parser-algorithms/test/test.mjs @@ -0,0 +1,37 @@ +import './cases/media-not/0001.mjs'; + +import './cases/mf-boolean/0001.mjs'; +import './cases/mf-boolean/0002.mjs'; +import './cases/mf-boolean/0003.mjs'; +import './cases/mf-boolean/0004.mjs'; +import './cases/mf-boolean/0005.mjs'; + +import './cases/mf-plain/0001.mjs'; +import './cases/mf-plain/0002.mjs'; +import './cases/mf-plain/0003.mjs'; +import './cases/mf-plain/0004.mjs'; +import './cases/mf-plain/0005.mjs'; + +import './cases/query-with-type/0001.mjs'; +import './cases/query-with-type/0002.mjs'; +import './cases/query-with-type/0003.mjs'; + +import './cases/various/0001.mjs'; +import './cases/various/0002.mjs'; +import './cases/various/0003.mjs'; +import './cases/various/0004.mjs'; +import './cases/various/0005.mjs'; +import './cases/various/0006.mjs'; +import './cases/various/0007.mjs'; +import './cases/various/0008.mjs'; +import './cases/various/0009.mjs'; +import './cases/various/0010.mjs'; +import './cases/various/0011.mjs'; +import './cases/various/0012.mjs'; +import './cases/various/0013.mjs'; +import './cases/various/0014.mjs'; +import './cases/various/0015.mjs'; +import './cases/various/0016.mjs'; +import './cases/various/0017.mjs'; +import './cases/various/0018.mjs'; +import './cases/various/0019.mjs'; diff --git a/packages/css-parser-algorithms/test/util/run-test.mjs b/packages/css-parser-algorithms/test/util/run-test.mjs new file mode 100644 index 000000000..5d58211c9 --- /dev/null +++ b/packages/css-parser-algorithms/test/util/run-test.mjs @@ -0,0 +1,65 @@ +import fs from 'fs'; +import path from 'path'; +import { parseCommaSeparatedListOfComponentValues, parseListOfComponentValues } from '@csstools/css-parser-algorithms'; +import { tokenizer } from '@csstools/css-tokenizer'; + +export function runTest(source, testPath, assertEqual) { + const onParseError = (err) => { + console.warn(err); + throw new Error(`Unable to parse "${source}"`); + }; + const t = tokenizer({ css: source }, { + commentsAreTokens: true, + onParseError: onParseError, + }); + + const tokens = []; + + { + while (!t.endOfFile()) { + tokens.push(t.nextToken()); + } + + tokens.push(t.nextToken()); // EOF-token + } + + { + // Space separated list of component values + const resultAST = parseListOfComponentValues(tokens, { + onParseError: onParseError, + }); + const resultAST_JSON = JSON.stringify(resultAST, null, '\t'); + + if (process.env['REWRITE_EXPECTS'] === 'true') { + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-space.expect.json`), resultAST_JSON); + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-space.result.json`), resultAST_JSON); + } else { + const expectData = JSON.parse(fs.readFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-space.expect.json`)).toString()); + + assertEqual( + JSON.parse(resultAST_JSON), + expectData, + ); + } + } + + { + // Comma separated list of component values + const resultAST = parseCommaSeparatedListOfComponentValues(tokens, { + onParseError: onParseError, + }); + const resultAST_JSON = JSON.stringify(resultAST, null, '\t'); + + if (process.env['REWRITE_EXPECTS'] === 'true') { + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-comma.expect.json`), resultAST_JSON); + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-comma.result.json`), resultAST_JSON); + } else { + const expectData = JSON.parse(fs.readFileSync(path.join(process.cwd(), `./test/cases/${testPath}.list-comma.expect.json`)).toString()); + + assertEqual( + JSON.parse(resultAST_JSON), + expectData, + ); + } + } +} diff --git a/packages/css-parser-algorithms/tsconfig.json b/packages/css-parser-algorithms/tsconfig.json new file mode 100644 index 000000000..e0d06239c --- /dev/null +++ b/packages/css-parser-algorithms/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "." + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} diff --git a/packages/css-tokenizer/.gitignore b/packages/css-tokenizer/.gitignore new file mode 100644 index 000000000..7172b04f1 --- /dev/null +++ b/packages/css-tokenizer/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +dist/* diff --git a/packages/css-tokenizer/.nvmrc b/packages/css-tokenizer/.nvmrc new file mode 100644 index 000000000..f0b10f153 --- /dev/null +++ b/packages/css-tokenizer/.nvmrc @@ -0,0 +1 @@ +v16.13.1 diff --git a/packages/css-tokenizer/CHANGELOG.md b/packages/css-tokenizer/CHANGELOG.md new file mode 100644 index 000000000..b0ff6b082 --- /dev/null +++ b/packages/css-tokenizer/CHANGELOG.md @@ -0,0 +1,3 @@ +### 1.0.0 + +- Initial version diff --git a/packages/css-tokenizer/LICENSE.md b/packages/css-tokenizer/LICENSE.md new file mode 100644 index 000000000..af5411fa2 --- /dev/null +++ b/packages/css-tokenizer/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/css-tokenizer/README.md b/packages/css-tokenizer/README.md new file mode 100644 index 000000000..3fc1cef61 --- /dev/null +++ b/packages/css-tokenizer/README.md @@ -0,0 +1,128 @@ +# CSS Tokenizer + +[npm version][npm-url] +[Build Status][cli-url] +[Discord][discord] + +Implemented from : https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/ + +## Usage + +Add [CSS Tokenizer] to your project: + +```bash +npm install postcss @csstools/css-tokenizer --save-dev +``` + +```js +import { tokenizer, TokenType } from '@csstools/css-tokenizer'; + +const myCSS = `@media only screen and (min-width: 768rem) { + .foo { + content: 'Some content!' !important; + } +} +`; + +const t = tokenizer({ + css: myCSS, +}); + +while (true) { + const token = t.nextToken(); + if (token[0] === TokenType.EOF) { + break; + } + + console.log(token); +} +``` + +### Options + +```ts +{ + commentsAreTokens?: false, + onParseError?: (error: ParserError) => void +} +``` + +#### `commentsAreTokens` + +Following the CSS specification comments are never returned by the tokenizer. +For many tools however it is desirable to be able to convert tokens back to a string. + +```js +import { tokenizer, TokenType } from '@csstools/css-tokenizer'; + +const t = tokenizer({ + css: `/* a comment */`, +}, { commentsAreTokens: true }); + +while (true) { + const token = t.nextToken(); + if (token[0] === TokenType.EOF) { + break; + } + + console.log(token); +} +``` + +logs : `['comment', '/* a comment */', , , undefined]` + + +#### `onParseError` + +The tokenizer is forgiving and won't stop when a parse error is encountered. +Parse errors also aren't tokens. + +To receive parsing error information you can set a callback. + +```js +import { tokenizer, TokenType } from '@csstools/css-tokenizer'; + +const t = tokenizer({ + css: '\\', +}, { onParseError: (err) => console.warn(err) }); + +while (true) { + const token = t.nextToken(); + if (token[0] === TokenType.EOF) { + break; + } +} +``` + +logs : + +```js +{ + message: 'Unexpected EOF while consuming an escaped code point.', + start: 0, + end: 0, + state: ['4.3.7. Consume an escaped code point', 'Unexpected EOF'], +} +``` + +Parser errors will try to inform you about the point in the tokenizer logic the error happened. +This tells you the kind of error. + +`start` and `end` are the location in your CSS source code. + +## Goals and non-goals + +Things this package aims to be: +- specification compliant CSS tokenizer +- a reliable low level package to be used in CSS parsers + +What it is not: +- opinionated +- fast +- small + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/css-tokenizer + +[CSS Tokenizer]: https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer diff --git a/packages/css-tokenizer/package.json b/packages/css-tokenizer/package.json new file mode 100644 index 000000000..ec22c6324 --- /dev/null +++ b/packages/css-tokenizer/package.json @@ -0,0 +1,65 @@ +{ + "name": "@csstools/css-tokenizer", + "description": "Tokenize CSS", + "version": "1.0.0", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke", + "email": "romainmenke@gmail.com" + } + ], + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "scripts": { + "build": "rollup -c ../../rollup/default.js", + "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", + "lint": "npm run lint:eslint && npm run lint:package-json", + "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", + "lint:package-json": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run clean && npm run build && npm run test", + "stryker": "stryker run --logLevel error", + "test": "npm run test:exports && node ./test/test.mjs", + "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer#readme", + "repository": { + "type": "git", + "url": "https://github.com/csstools/postcss-plugins.git", + "directory": "packages/css-tokenizer" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "css", + "tokenizer" + ], + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/css-tokenizer/src/checks/four-code-points-would-start-cdo.ts b/packages/css-tokenizer/src/checks/four-code-points-would-start-cdo.ts new file mode 100644 index 000000000..722a0e6ec --- /dev/null +++ b/packages/css-tokenizer/src/checks/four-code-points-would-start-cdo.ts @@ -0,0 +1,11 @@ +import { EXCLAMATION_MARK, HYPHEN_MINUS, LESS_THAN_SIGN } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-token +export function checkIfFourCodePointsWouldStartCDO(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekFourCodePoints(); + const [first, second, third, fourth] = peeked; + + return first === LESS_THAN_SIGN && second === EXCLAMATION_MARK && third === HYPHEN_MINUS && fourth === HYPHEN_MINUS; +} diff --git a/packages/css-tokenizer/src/checks/matches-url-ident.ts b/packages/css-tokenizer/src/checks/matches-url-ident.ts new file mode 100644 index 000000000..b8f7bd829 --- /dev/null +++ b/packages/css-tokenizer/src/checks/matches-url-ident.ts @@ -0,0 +1,28 @@ +import { Context } from '../interfaces/context'; + +const u = 'u'.charCodeAt(0); +const U = 'U'.charCodeAt(0); +const r = 'r'.charCodeAt(0); +const R = 'R'.charCodeAt(0); +const l = 'l'.charCodeAt(0); +const L = 'L'.charCodeAt(0); + +export function checkIfCodePointsMatchURLIdent(ctx: Context, codePoints: Array): boolean { + if (codePoints.length !== 3) { + return false; + } + + if (codePoints[0] !== u && codePoints[0] !== U) { + return false; + } + + if (codePoints[1] !== r && codePoints[1] !== R) { + return false; + } + + if (codePoints[2] !== l && codePoints[2] !== L) { + return false; + } + + return true; +} diff --git a/packages/css-tokenizer/src/checks/next-is-eof.ts b/packages/css-tokenizer/src/checks/next-is-eof.ts new file mode 100644 index 000000000..18ee497a7 --- /dev/null +++ b/packages/css-tokenizer/src/checks/next-is-eof.ts @@ -0,0 +1,11 @@ +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +export function checkIfNextIsEOF(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + return true; + } + + return false; +} diff --git a/packages/css-tokenizer/src/checks/three-code-points-would-start-cdc.ts b/packages/css-tokenizer/src/checks/three-code-points-would-start-cdc.ts new file mode 100644 index 000000000..38cdf70dc --- /dev/null +++ b/packages/css-tokenizer/src/checks/three-code-points-would-start-cdc.ts @@ -0,0 +1,11 @@ +import { GREATER_THAN_SIGN, HYPHEN_MINUS } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-token +export function checkIfThreeCodePointsWouldStartCDC(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekThreeCodePoints(); + const [first, second, third] = peeked; + + return first === HYPHEN_MINUS && second === HYPHEN_MINUS && third === GREATER_THAN_SIGN; +} diff --git a/packages/css-tokenizer/src/checks/three-code-points-would-start-ident-sequence.ts b/packages/css-tokenizer/src/checks/three-code-points-would-start-ident-sequence.ts new file mode 100644 index 000000000..28ffc8fc6 --- /dev/null +++ b/packages/css-tokenizer/src/checks/three-code-points-would-start-ident-sequence.ts @@ -0,0 +1,48 @@ +import { HYPHEN_MINUS, LINE_FEED, REVERSE_SOLIDUS } from '../code-points/code-points'; +import { isIdentStartCodePoint } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { checkIfTwoCodePointsAreAValidEscape } from './two-code-points-are-valid-escape'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#would-start-an-identifier +export function checkIfThreeCodePointsWouldStartAnIdentSequence(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekThreeCodePoints(); + const [first, second, third] = peeked; + + // // U+002D HYPHEN-MINUS + if (first === HYPHEN_MINUS) { + // If the second code point is a U+002D HYPHEN-MINUS return true + if (second === HYPHEN_MINUS) { + return true; + } + + // If the second code point is an ident-start code point return true + if (isIdentStartCodePoint(second)) { + return true; + } + + // If the second and third code points are a valid escape return true + if (second === REVERSE_SOLIDUS && third !== LINE_FEED) { + return true; + } + + return false; + } + + // ident-start code point + // Return true. + if (isIdentStartCodePoint(first)) { + return true; + } + + // U+005C REVERSE SOLIDUS (\) + if (first === REVERSE_SOLIDUS) { + // If the first and second code points are a valid escape, return true. + // Otherwise, return false. + return checkIfTwoCodePointsAreAValidEscape(ctx, reader); + } + + // anything else + // Return false. + return false; +} diff --git a/packages/css-tokenizer/src/checks/three-code-points-would-start-number.ts b/packages/css-tokenizer/src/checks/three-code-points-would-start-number.ts new file mode 100644 index 000000000..e4b964e9c --- /dev/null +++ b/packages/css-tokenizer/src/checks/three-code-points-would-start-number.ts @@ -0,0 +1,39 @@ +import { FULL_STOP, HYPHEN_MINUS, PLUS_SIGN } from '../code-points/code-points'; +import { isDigitCodePoint } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#starts-with-a-number +export function checkIfThreeCodePointsWouldStartANumber(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekThreeCodePoints(); + const [first, second, third] = peeked; + + if (first === PLUS_SIGN || first === HYPHEN_MINUS) { // U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-) + // If the second code point is a digit, return true. + if (isDigitCodePoint(second)) { + return true; + } + + // Otherwise, if the second code point is a U+002E FULL STOP (.) + if (second === FULL_STOP) { + // and the third code point is a digit, return true. + return isDigitCodePoint(third); + } + + // Otherwise, return false. + return false; + + } else if (first === FULL_STOP) { // U+002E FULL STOP (.) + // If the second code point is a digit, return true. + // Otherwise, return false. + return isDigitCodePoint(second); + + } else if (isDigitCodePoint(first)) { // digit + // Return true. + return true; + } + + // anything else + // Return false. + return false; +} diff --git a/packages/css-tokenizer/src/checks/two-code-points-are-valid-escape.ts b/packages/css-tokenizer/src/checks/two-code-points-are-valid-escape.ts new file mode 100644 index 000000000..4e30730fb --- /dev/null +++ b/packages/css-tokenizer/src/checks/two-code-points-are-valid-escape.ts @@ -0,0 +1,20 @@ +import { LINE_FEED, REVERSE_SOLIDUS } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#starts-with-a-valid-escape +export function checkIfTwoCodePointsAreAValidEscape(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekTwoCodePoints(); + // If the first code point is not U+005C REVERSE SOLIDUS (\), return false. + if (peeked[0] !== REVERSE_SOLIDUS) { // "\" + return false; + } + + // Otherwise, if the second code point is a newline, return false. + if (peeked[1] === LINE_FEED) { + return false; + } + + // Otherwise, return true. + return true; +} diff --git a/packages/css-tokenizer/src/checks/two-code-points-start-comment.ts b/packages/css-tokenizer/src/checks/two-code-points-start-comment.ts new file mode 100644 index 000000000..f24fabf56 --- /dev/null +++ b/packages/css-tokenizer/src/checks/two-code-points-start-comment.ts @@ -0,0 +1,18 @@ +import { ASTERISK, SOLIDUS } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-comments +export function checkIfTwoCodePointsStartAComment(ctx: Context, reader: CodePointReader): boolean { + const peeked = reader.peekTwoCodePoints(); + if (peeked[0] !== SOLIDUS) { + return false; + } + + if (peeked[1] !== ASTERISK) { + return false; + } + + // Otherwise, return true. + return true; +} diff --git a/packages/css-tokenizer/src/code-points/code-points.ts b/packages/css-tokenizer/src/code-points/code-points.ts new file mode 100644 index 000000000..fca7ea361 --- /dev/null +++ b/packages/css-tokenizer/src/code-points/code-points.ts @@ -0,0 +1,78 @@ +/** ' */ +export const APOSTROPHE = '\u{27}'.charCodeAt(0); +/** * */ +export const ASTERISK = '\u{2a}'.charCodeAt(0); +/** \b */ +export const BACKSPACE = '\u{8}'.charCodeAt(0); +/** \r */ +export const CARRIAGE_RETURN = '\u{d}'.charCodeAt(0); +/** \t */ +export const CHARACTER_TABULATION = '\u{9}'.charCodeAt(0); +/** : */ +export const COLON = '\u{3a}'.charCodeAt(0); +/** , */ +export const COMMA = '\u{2c}'.charCodeAt(0); +/** @ */ +export const COMMERCIAL_AT = '\u{40}'.charCodeAt(0); +/** \x7F */ +export const DELETE = '\u{7f}'.charCodeAt(0); +/** ! */ +export const EXCLAMATION_MARK = '\u{21}'.charCodeAt(0); +/** \f */ +export const FORM_FEED = '\u{c}'.charCodeAt(0); +/** . */ +export const FULL_STOP = '\u{2e}'.charCodeAt(0); +/** > */ +export const GREATER_THAN_SIGN = '\u{3e}'.charCodeAt(0); +/** - */ +export const HYPHEN_MINUS = '\u{2d}'.charCodeAt(0); +/** \x1F */ +export const INFORMATION_SEPARATOR_ONE = '\u{1f}'.charCodeAt(0); +/** E */ +export const LATIN_CAPITAL_LETTER_E = '\u{45}'.charCodeAt(0); +/** e */ +export const LATIN_SMALL_LETTER_E = '\u{65}'.charCodeAt(0); +/** { */ +export const LEFT_CURLY_BRACKET = '\u{7b}'.charCodeAt(0); +/** ( */ +export const LEFT_PARENTHESIS = '\u{28}'.charCodeAt(0); +/** [ */ +export const LEFT_SQUARE_BRACKET = '\u{5b}'.charCodeAt(0); +/** < */ +export const LESS_THAN_SIGN = '\u{3c}'.charCodeAt(0); +/** \n */ +export const LINE_FEED = '\u{a}'.charCodeAt(0); +/** \v */ +export const LINE_TABULATION = '\u{b}'.charCodeAt(0); +/** _ */ +export const LOW_LINE = '\u{5f}'.charCodeAt(0); +/** \x10FFFF */ +export const MAXIMUM_ALLOWED_CODEPOINT = '\u{10FFFF}'.charCodeAt(0); +/** \x00 */ +export const NULL = '\u{0}'.charCodeAt(0); +/** # */ +export const NUMBER_SIGN = '\u{23}'.charCodeAt(0); +/** % */ +export const PERCENTAGE_SIGN = '\u{25}'.charCodeAt(0); +/** + */ +export const PLUS_SIGN = '\u{2b}'.charCodeAt(0); +/** " */ +export const QUOTATION_MARK = '\u{22}'.charCodeAt(0); +/** ๏ฟฝ */ +export const REPLACEMENT_CHARACTER = '\u{0FFFD}'.charCodeAt(0); +/** \ */ +export const REVERSE_SOLIDUS = '\u{5c}'.charCodeAt(0); +/** } */ +export const RIGHT_CURLY_BRACKET = '\u{7d}'.charCodeAt(0); +/** ) */ +export const RIGHT_PARENTHESIS = '\u{29}'.charCodeAt(0); +/** ] */ +export const RIGHT_SQUARE_BRACKET = '\u{5d}'.charCodeAt(0); +/** ; */ +export const SEMICOLON = '\u{3b}'.charCodeAt(0); +/** \u0E */ +export const SHIFT_OUT = '\u{e}'.charCodeAt(0); +/** / */ +export const SOLIDUS = '\u{2f}'.charCodeAt(0); +/** \u20 */ +export const SPACE = '\u{20}'.charCodeAt(0); diff --git a/packages/css-tokenizer/src/code-points/ranges.ts b/packages/css-tokenizer/src/code-points/ranges.ts new file mode 100644 index 000000000..9aa4628bd --- /dev/null +++ b/packages/css-tokenizer/src/code-points/ranges.ts @@ -0,0 +1,170 @@ +import { BACKSPACE, DELETE, INFORMATION_SEPARATOR_ONE, LINE_TABULATION, LOW_LINE, HYPHEN_MINUS, NULL, SHIFT_OUT, LINE_FEED, CARRIAGE_RETURN, FORM_FEED, CHARACTER_TABULATION, SPACE } from './code-points'; +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokenizer-definitions + +const digitsLow = '\u{30}'.charCodeAt(0); +const digitsHigh = '\u{39}'.charCodeAt(0); + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#digit +export function isDigitCodePoint(search: number): boolean { + if (digitsLow <= search && search <= digitsHigh) { + return true; + } + + return false; +} + +const letterUppercaseLow = '\u{41}'.charCodeAt(0); +const letterUppercaseHigh = '\u{5a}'.charCodeAt(0); + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#uppercase-letter +export function isUppercaseLetterCodePoint(search: number): boolean { + if (letterUppercaseLow <= search && search <= letterUppercaseHigh) { + return true; + } + + return false; +} + +const letterLowercaseLow = '\u{61}'.charCodeAt(0); +const letterLowercaseHigh = '\u{7a}'.charCodeAt(0); + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#lowercase-letter +export function isLowercaseLetterCodePoint(search: number): boolean { + if (letterLowercaseLow <= search && search <= letterLowercaseHigh) { + return true; + } + + return false; +} + +const afUppercaseHigh = '\u{46}'.charCodeAt(0); +const afLowercaseHigh = '\u{66}'.charCodeAt(0); + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#hex-digit +export function isHexDigitCodePoint(search: number): boolean { + if (isDigitCodePoint(search)) { + return true; + } + + if (letterUppercaseLow <= search && search <= afUppercaseHigh) { + return true; + } + + if (letterLowercaseLow <= search && search <= afLowercaseHigh) { + return true; + } + + return false; +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#letter +export function isLetterCodePoint(search: number): boolean { + if (isUppercaseLetterCodePoint(search) || isLowercaseLetterCodePoint(search)) { + return true; + } + + return false; +} + +const nonASCIILow = '\u{80}'.charCodeAt(0); + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#non-ascii-code-point +export function isNonASCIICodePoint(search: number): boolean { + return search >= nonASCIILow; +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#ident-start-code-point +export function isIdentStartCodePoint(search: number): boolean { + if (isLetterCodePoint(search)) { + return true; + } + + if (isNonASCIICodePoint(search)) { + return true; + } + + return search === LOW_LINE; +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#ident-code-point +export function isIdentCodePoint(search: number): boolean { + if (isIdentStartCodePoint(search)) { + return true; + } + + if (isDigitCodePoint(search)) { + return true; + } + + return search === HYPHEN_MINUS; +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#non-printable-code-point +export function isNonPrintableCodePoint(search: number): boolean { + if (search === LINE_TABULATION) { + return true; + } + + if (search === DELETE) { + return true; + } + + if (NULL <= search && search <= BACKSPACE) { + return true; + } + + if (SHIFT_OUT <= search && search <= INFORMATION_SEPARATOR_ONE) { + return true; + } + + return false; +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#whitespace +export function isNewLine(search: number): boolean { + switch (search) { + case LINE_FEED: + case CARRIAGE_RETURN: + case FORM_FEED: + // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#input-preprocessing + // We can not follow the preprocessing rules because our output is text and must be minimally different from the input. + // Applying the preprocessing rules would make it impossible to match the input. + // A side effect of this is that our definition of whitespace is broader. + return true; + default: + return false; + } +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#whitespace +export function isWhitespace(search: number): boolean { + switch (search) { + case LINE_FEED: + case CARRIAGE_RETURN: + case FORM_FEED: + // https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#input-preprocessing + // We can not follow the preprocessing rules because our output is text and must be minimally different from the input. + // Applying the preprocessing rules would make it impossible to match the input. + // A side effect of this is that our definition of whitespace is broader. + return true; + case CHARACTER_TABULATION: + return true; + case SPACE: + return true; + + default: + return false; + } +} + +const surrogateLow = '\u{d800}'.charCodeAt(0); +const surrogateHigh = '\u{dfff}'.charCodeAt(0); + +// https://infra.spec.whatwg.org/#surrogate +export function isSurrogate(search: number): boolean { + if (surrogateLow <= search && search <= surrogateHigh) { + return true; + } + + return false; +} diff --git a/packages/css-tokenizer/src/consume/bad-url.ts b/packages/css-tokenizer/src/consume/bad-url.ts new file mode 100644 index 000000000..40a44f9ab --- /dev/null +++ b/packages/css-tokenizer/src/consume/bad-url.ts @@ -0,0 +1,30 @@ +import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape'; +import { RIGHT_PARENTHESIS } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { consumeEscapedCodePoint } from './escaped-code-point'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-remnants-of-bad-url +export function consumeBadURL(ctx: Context, reader: CodePointReader) { + // eslint-disable-next-line no-constant-condition + while (true) { + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + return; + } + + if (peeked === RIGHT_PARENTHESIS) { + reader.readCodePoint(); + return; + } + + if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) { + reader.readCodePoint(); + consumeEscapedCodePoint(ctx, reader); + continue; + } + + reader.readCodePoint(); + continue; + } +} diff --git a/packages/css-tokenizer/src/consume/comment.ts b/packages/css-tokenizer/src/consume/comment.ts new file mode 100644 index 000000000..6fc6d75c0 --- /dev/null +++ b/packages/css-tokenizer/src/consume/comment.ts @@ -0,0 +1,50 @@ +import { ASTERISK, SOLIDUS } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { TokenComment, TokenType } from '../interfaces/token'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-comment +export function consumeComment(ctx: Context, reader: CodePointReader): TokenComment { + reader.readCodePoint(); + reader.readCodePoint(); + + // eslint-disable-next-line no-constant-condition + while (true) { + const codePoint = reader.readCodePoint(); + if (codePoint === false) { + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected EOF while consuming a comment.', + start: representation[0], + end: representation[1], + state: [ + '4.3.2. Consume comments', + 'Unexpected EOF', + ], + }); + + break; + } + + if (codePoint !== ASTERISK) { + continue; + } + + const close = reader.peekOneCodePoint(); + if (close === false) { + continue; + } + + if (close === SOLIDUS) { + reader.readCodePoint(); + break; + } + } + + return [ + TokenType.Comment, + reader.representationString(), + ...reader.representation(), + undefined, + ]; +} diff --git a/packages/css-tokenizer/src/consume/escaped-code-point.ts b/packages/css-tokenizer/src/consume/escaped-code-point.ts new file mode 100644 index 000000000..19caeaaab --- /dev/null +++ b/packages/css-tokenizer/src/consume/escaped-code-point.ts @@ -0,0 +1,53 @@ +import { MAXIMUM_ALLOWED_CODEPOINT, REPLACEMENT_CHARACTER } from '../code-points/code-points'; +import { isHexDigitCodePoint, isSurrogate, isWhitespace } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-escaped-code-point +export function consumeEscapedCodePoint(ctx: Context, reader: CodePointReader): number { + const codePoint = reader.readCodePoint(); + if (codePoint === false) { + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected EOF while consuming an escaped code point.', + start: representation[0], + end: representation[1], + state: [ + '4.3.7. Consume an escaped code point', + 'Unexpected EOF', + ], + }); + + return REPLACEMENT_CHARACTER; + } + + if (isHexDigitCodePoint(codePoint)) { + const hexSequence: Array = [codePoint]; + + let peeked = reader.peekOneCodePoint(); + while (peeked !== false && isHexDigitCodePoint(peeked) && hexSequence.length < 6) { + reader.readCodePoint(); + hexSequence.push(peeked); + peeked = reader.peekOneCodePoint(); + } + + if (peeked !== false && isWhitespace(peeked)) { + reader.readCodePoint(); + } + + const codePointLiteral = parseInt(hexSequence.map((x) => String.fromCharCode(x)).join(''), 16); + if (codePointLiteral === 0) { + return REPLACEMENT_CHARACTER; + } + if (isSurrogate(codePointLiteral)) { + return REPLACEMENT_CHARACTER; + } + if (codePointLiteral > MAXIMUM_ALLOWED_CODEPOINT) { + return REPLACEMENT_CHARACTER; + } + + return codePointLiteral; + } + + return codePoint; +} diff --git a/packages/css-tokenizer/src/consume/hash-token.ts b/packages/css-tokenizer/src/consume/hash-token.ts new file mode 100644 index 000000000..e9000903a --- /dev/null +++ b/packages/css-tokenizer/src/consume/hash-token.ts @@ -0,0 +1,45 @@ +import { checkIfThreeCodePointsWouldStartAnIdentSequence } from '../checks/three-code-points-would-start-ident-sequence'; +import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape'; +import { isIdentCodePoint } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { HashType, TokenDelim, TokenHash, TokenType } from '../interfaces/token'; +import { consumeIdentSequence } from './ident-sequence'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-token +export function consumeHashToken(ctx: Context, reader: CodePointReader): TokenDelim|TokenHash { + reader.readCodePoint(); + + const peeked = reader.peekOneCodePoint(); + if ( + peeked !== false && + isIdentCodePoint(peeked) || + checkIfTwoCodePointsAreAValidEscape(ctx, reader) + ) { + let hashType = HashType.Unrestricted; + + if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) { + hashType = HashType.ID; + } + + const identSequence = consumeIdentSequence(ctx, reader); + return [ + TokenType.Hash, + reader.representationString(), + ...reader.representation(), + { + value: identSequence.map((x) => String.fromCharCode(x)).join(''), + type: hashType, + }, + ]; + } + + return [ + TokenType.Delim, + reader.representationString(), + ...reader.representation(), + { + value: '#', + }, + ]; +} diff --git a/packages/css-tokenizer/src/consume/ident-like-token.ts b/packages/css-tokenizer/src/consume/ident-like-token.ts new file mode 100644 index 000000000..f828602c2 --- /dev/null +++ b/packages/css-tokenizer/src/consume/ident-like-token.ts @@ -0,0 +1,77 @@ +import { checkIfCodePointsMatchURLIdent } from '../checks/matches-url-ident'; +import { APOSTROPHE, LEFT_PARENTHESIS, QUOTATION_MARK } from '../code-points/code-points'; +import { isWhitespace } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { TokenBadURL, TokenFunction, TokenIdent, TokenType, TokenURL } from '../interfaces/token'; +import { consumeIdentSequence } from './ident-sequence'; +import { consumeUrlToken } from './url-token'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-ident-like-token +export function consumeIdentLikeToken(ctx: Context, reader: CodePointReader): TokenIdent|TokenFunction|TokenURL|TokenBadURL { + const codePoints = consumeIdentSequence(ctx, reader); + if (checkIfCodePointsMatchURLIdent(ctx, codePoints)) { + const peeked = reader.peekOneCodePoint(); + if (peeked === LEFT_PARENTHESIS) { + reader.readCodePoint(); + + let read = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const peeked2 = reader.peekTwoCodePoints(); + if (isWhitespace(peeked2[0]) && isWhitespace(peeked2[1])) { + read += 2; + reader.readCodePoint(); + reader.readCodePoint(); + continue; + } + + const firstNonWhitespace = isWhitespace(peeked2[0]) ? peeked2[1] : peeked2[0]; + if (firstNonWhitespace === QUOTATION_MARK || firstNonWhitespace === APOSTROPHE) { + for (let i = 0; i < read; i++) { + reader.unreadCodePoint(); + } + + return [ + TokenType.Function, + reader.representationString(), + ...reader.representation(), + { + value: codePoints.map((x) => String.fromCharCode(x)).join(''), + }, + ]; + } + + break; + } + + for (let i = 0; i < read; i++) { + reader.unreadCodePoint(); + } + + return consumeUrlToken(ctx, reader); + } + } + + const peeked = reader.peekOneCodePoint(); + if (peeked === LEFT_PARENTHESIS) { + reader.readCodePoint(); + return [ + TokenType.Function, + reader.representationString(), + ...reader.representation(), + { + value: codePoints.map((x) => String.fromCharCode(x)).join(''), + }, + ]; + } + + return [ + TokenType.Ident, + reader.representationString(), + ...reader.representation(), + { + value: codePoints.map((x) => String.fromCharCode(x)).join(''), + }, + ]; +} diff --git a/packages/css-tokenizer/src/consume/ident-sequence.ts b/packages/css-tokenizer/src/consume/ident-sequence.ts new file mode 100644 index 000000000..a62243771 --- /dev/null +++ b/packages/css-tokenizer/src/consume/ident-sequence.ts @@ -0,0 +1,32 @@ +import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape'; +import { isIdentCodePoint } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { consumeEscapedCodePoint } from './escaped-code-point'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-name +export function consumeIdentSequence(ctx: Context, reader: CodePointReader): Array { + const result = []; + + // eslint-disable-next-line no-constant-condition + while (true) { + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + return result; + } + + if (isIdentCodePoint(peeked)) { + reader.readCodePoint(); + result.push(peeked); + continue; + } + + if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) { + reader.readCodePoint(); + result.push(consumeEscapedCodePoint(ctx, reader)); + continue; + } + + return result; + } +} diff --git a/packages/css-tokenizer/src/consume/number.ts b/packages/css-tokenizer/src/consume/number.ts new file mode 100644 index 000000000..5b399cb31 --- /dev/null +++ b/packages/css-tokenizer/src/consume/number.ts @@ -0,0 +1,217 @@ +import { FULL_STOP, HYPHEN_MINUS, LATIN_CAPITAL_LETTER_E, LATIN_SMALL_LETTER_E, PLUS_SIGN } from '../code-points/code-points'; +import { isDigitCodePoint } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { NumberType } from '../interfaces/token'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-number +export function consumeNumber(ctx: Context, reader: CodePointReader): [number, NumberType] { + // 1. Initially set type to "integer". + // Let repr be the empty string. + let type = NumberType.Integer; + const repr: Array = []; + + { + // 2. If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), consume it and append it to repr. + const peeked = reader.peekOneCodePoint(); + if (peeked === PLUS_SIGN || peeked === HYPHEN_MINUS) { + reader.readCodePoint(); + repr.push(peeked); + } + + // 3. While the next input code point is a digit, consume it and append it to repr. + repr.push(...consumeDigits(reader)); + } + + { + // 4. If the next 2 input code points are U+002E FULL STOP (.) followed by a digit, then: + const peeked = reader.peekTwoCodePoints(); + if (peeked[0] === FULL_STOP && isDigitCodePoint(peeked[1])) { + // 4.1. Consume them. + reader.readCodePoint(); + reader.readCodePoint(); + + // 4.2. Append them to repr. + repr.push(...peeked); + + // 4.3. Set type to "number". + type = NumberType.Number; + + // 4.4. While the next input code point is a digit, consume it and append it to repr. + repr.push(...consumeDigits(reader)); + } + } + + { + // 5. If the next 2 or 3 input code points are U+0045 LATIN CAPITAL LETTER E (E) or U+0065 LATIN SMALL LETTER E (e), + // optionally followed by U+002D HYPHEN-MINUS (-) or U+002B PLUS SIGN (+), + // followed by a digit, then: + const peeked = reader.peekThreeCodePoints(); + if ( + (peeked[0] === LATIN_SMALL_LETTER_E || peeked[0] === LATIN_CAPITAL_LETTER_E) && + isDigitCodePoint(peeked[1]) + ) { + // 5.1. Consume them. + reader.readCodePoint(); + reader.readCodePoint(); + + // 5.2. Append them to repr. + repr.push(...peeked); + + // 5.3. Set type to "number". + type = NumberType.Number; + + // 5.4. While the next input code point is a digit, consume it and append it to repr. + repr.push(...consumeDigits(reader)); + } + + if ( + (peeked[0] === LATIN_SMALL_LETTER_E || peeked[0] === LATIN_CAPITAL_LETTER_E) && + ( + (peeked[1] === HYPHEN_MINUS || peeked[1] === PLUS_SIGN) && + isDigitCodePoint(peeked[2]) + ) + ) { + // 5.1. Consume them. + reader.readCodePoint(); + reader.readCodePoint(); + reader.readCodePoint(); + + // 5.2. Append them to repr. + repr.push(...peeked); + + // 5.3. Set type to "number". + type = NumberType.Number; + + // 5.4. While the next input code point is a digit, consume it and append it to repr. + repr.push(...consumeDigits(reader)); + } + } + + // 6. Convert repr to a number, and set the value to the returned value. + const value = convertCodePointsToNumber(repr); + + // 7. Return value and type. + return [value, type]; +} + +function consumeDigits(reader: CodePointReader): Array { + const value: Array = []; + + // eslint-disable-next-line no-constant-condition + while (true) { + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + return value; + } + + if (isDigitCodePoint(peeked)) { + value.push(peeked); + reader.readCodePoint(); + } else { + return value; + } + } +} + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#convert-string-to-number +function convertCodePointsToNumber(codePoints: Array): number { + let s = 1; + const iCodePoints: Array = []; + let i = 0; + + let d = 0; + const fCodePoints: Array = []; + let f = 0; + + let t = 1; + + const eCodePoints: Array = []; + let e = 0; + + let cursor = 0; + + // 1. A sign: a single U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), or the empty string. + // Let s be the number -1 if the sign is U+002D HYPHEN-MINUS (-); + // otherwise, let s be the number 1. + if (codePoints[cursor] === HYPHEN_MINUS) { + cursor++; + s = -1; + } else if (codePoints[cursor] === PLUS_SIGN) { + cursor++; + } + + // 2. An integer part: zero or more digits. + // If there is at least one digit, + // let i be the number formed by interpreting the digits as a base-10 integer; + // otherwise, let i be the number 0. + while (cursor < codePoints.length && isDigitCodePoint(codePoints[cursor])) { + iCodePoints.push(codePoints[cursor]); + cursor++; + } + + i = digitCodePointsToInteger(iCodePoints); + + // 3. A decimal point: a single U+002E FULL STOP (.), or the empty string. + if (codePoints[cursor] === FULL_STOP) { + cursor++; + } + + // 4. A fractional part: zero or more digits. + // If there is at least one digit, + // let f be the number formed by interpreting the digits as a base-10 integer and d be the number of digits; + // otherwise, let f and d be the number 0. + while (cursor < codePoints.length && isDigitCodePoint(codePoints[cursor])) { + fCodePoints.push(codePoints[cursor]); + cursor++; + } + + d = fCodePoints.length; + f = (digitCodePointsToInteger(fCodePoints) / Math.pow(10, d)); + + // 5. An exponent indicator: a single U+0045 LATIN CAPITAL LETTER E (E) or U+0065 LATIN SMALL LETTER E (e), or the empty string. + if (codePoints[cursor] === LATIN_SMALL_LETTER_E || codePoints[cursor] === LATIN_CAPITAL_LETTER_E) { + cursor++; + } + + // 6. An exponent sign: a single U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), or the empty string. + // Let t be the number -1 if the sign is U+002D HYPHEN-MINUS (-); + // otherwise, let t be the number 1. + if (codePoints[cursor] === HYPHEN_MINUS) { + cursor++; + t = -1; + } else if (codePoints[cursor] === PLUS_SIGN) { + cursor++; + } + + // 7. An exponent: zero or more digits. + // If there is at least one digit, + // let e be the number formed by interpreting the digits as a base-10 integer; + // otherwise, let e be the number 0. + while (cursor < codePoints.length && isDigitCodePoint(codePoints[cursor])) { + eCodePoints.push(codePoints[cursor]); + cursor++; + } + + e = digitCodePointsToInteger(eCodePoints); + + // Return the number sยท(i + fยท10-d)ยท10te. + return s * (i + f) * Math.pow(10, t * e); +} + +function digitCodePointsToInteger(codePoints: Array): number { + if (codePoints.length === 0) { + return 0; + } + + const stringValue = codePoints.map((x) => { + return String.fromCharCode(x); + }).join(''); + + const integerValue = Number.parseInt(stringValue, 10); + if (Number.isNaN(integerValue)) { + throw new Error(`Unexpected "NaN" result when parsing a number from digit code points: "${stringValue}"`); + } + + return integerValue; +} diff --git a/packages/css-tokenizer/src/consume/numeric-token.ts b/packages/css-tokenizer/src/consume/numeric-token.ts new file mode 100644 index 000000000..3059005c5 --- /dev/null +++ b/packages/css-tokenizer/src/consume/numeric-token.ts @@ -0,0 +1,53 @@ +import { checkIfThreeCodePointsWouldStartAnIdentSequence } from '../checks/three-code-points-would-start-ident-sequence'; +import { PERCENTAGE_SIGN } from '../code-points/code-points'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { TokenDimension, TokenNumber, TokenPercentage, TokenType } from '../interfaces/token'; +import { consumeIdentSequence } from './ident-sequence'; +import { consumeNumber } from './number'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-numeric-token +export function consumeNumericToken(ctx: Context, reader: CodePointReader): TokenPercentage|TokenNumber|TokenDimension { + const numberValue = consumeNumber(ctx, reader); + + if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) { + const unit = consumeIdentSequence(ctx, reader); + + return [ + TokenType.Dimension, + reader.representationString(), + ...reader.representation(), + { + value: numberValue[0], + type: numberValue[1], + unit: unit.map((x) => String.fromCharCode(x)).join(''), + }, + ]; + } + + { + const peeked = reader.peekOneCodePoint(); + if (peeked === PERCENTAGE_SIGN) { + reader.readCodePoint(); + + return [ + TokenType.Percentage, + reader.representationString(), + ...reader.representation(), + { + value: numberValue[0], + }, + ]; + } + } + + return [ + TokenType.Number, + reader.representationString(), + ...reader.representation(), + { + value: numberValue[0], + type: numberValue[1], + }, + ]; +} diff --git a/packages/css-tokenizer/src/consume/string-token.ts b/packages/css-tokenizer/src/consume/string-token.ts new file mode 100644 index 000000000..5bc5ff921 --- /dev/null +++ b/packages/css-tokenizer/src/consume/string-token.ts @@ -0,0 +1,74 @@ +import { REVERSE_SOLIDUS } from '../code-points/code-points'; +import { isNewLine } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { TokenBadString, TokenString, TokenType } from '../interfaces/token'; +import { consumeEscapedCodePoint } from './escaped-code-point'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-string-token +export function consumeStringToken(ctx: Context, reader: CodePointReader): TokenBadString|TokenString { + let result = ''; + + const first = reader.readCodePoint(); + if (first === false) { + throw new Error('Unexpected EOF'); + } + + // eslint-disable-next-line no-constant-condition + while (true) { + const next = reader.readCodePoint(); + if (next === false) { + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected EOF while consuming a string token.', + start: representation[0], + end: representation[1], + state: [ + '4.3.5. Consume a string token', + 'Unexpected EOF', + ], + }); + + return [TokenType.String, reader.representationString(), ...representation, { value: result }]; + } + + if (isNewLine(next)) { + { + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected newline while consuming a string token.', + start: representation[0], + end: representation[1], + state: [ + '4.3.5. Consume a string token', + 'Unexpected newline', + ], + }); + } + + reader.unreadCodePoint(); + return [TokenType.BadString, reader.representationString(), ...reader.representation(), undefined]; + } + + if (next === first) { + return [TokenType.String, reader.representationString(), ...reader.representation(), { value: result }]; + } + + if (next === REVERSE_SOLIDUS) { + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + continue; + } + + if (isNewLine(peeked)) { + reader.readCodePoint(); + continue; + } + + result += String.fromCharCode(consumeEscapedCodePoint(ctx, reader)); + continue; + } + + result += String.fromCharCode(next); + } +} diff --git a/packages/css-tokenizer/src/consume/url-token.ts b/packages/css-tokenizer/src/consume/url-token.ts new file mode 100644 index 000000000..bac0bd77a --- /dev/null +++ b/packages/css-tokenizer/src/consume/url-token.ts @@ -0,0 +1,153 @@ +import { checkIfTwoCodePointsAreAValidEscape } from '../checks/two-code-points-are-valid-escape'; +import { APOSTROPHE, LEFT_PARENTHESIS, QUOTATION_MARK, REVERSE_SOLIDUS, RIGHT_PARENTHESIS } from '../code-points/code-points'; +import { isNonPrintableCodePoint, isWhitespace } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { TokenBadURL, TokenType, TokenURL } from '../interfaces/token'; +import { consumeBadURL } from './bad-url'; +import { consumeEscapedCodePoint } from './escaped-code-point'; +import { consumeWhiteSpace } from './whitespace-token'; + +// https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#consume-url-token +export function consumeUrlToken(ctx: Context, reader: CodePointReader): TokenURL|TokenBadURL { + consumeWhiteSpace(ctx, reader); + let string = ''; + + // eslint-disable-next-line no-constant-condition + while (true) { + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected EOF while consuming a url token.', + start: representation[0], + end: representation[1], + state: [ + '4.3.6. Consume a url token', + 'Unexpected EOF', + ], + }); + + return [ + TokenType.URL, + reader.representationString(), + ...representation, + { + value: string, + }, + ]; + } + + if (peeked === RIGHT_PARENTHESIS) { + reader.readCodePoint(); + return [ + TokenType.URL, + reader.representationString(), + ...reader.representation(), + { + value: string, + }, + ]; + } + + if (isWhitespace(peeked)) { + consumeWhiteSpace(ctx, reader); + const peeked2 = reader.peekOneCodePoint(); + if (peeked2 === false) { + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected EOF while consuming a url token.', + start: representation[0], + end: representation[1], + state: [ + '4.3.6. Consume a url token', + 'Consume as much whitespace as possible', + 'Unexpected EOF', + ], + }); + + return [ + TokenType.URL, + reader.representationString(), + ...representation, + { + value: string, + }, + ]; + } + + if (peeked2 === RIGHT_PARENTHESIS) { + reader.readCodePoint(); + return [ + TokenType.URL, + reader.representationString(), + ...reader.representation(), + { + value: string, + }, + ]; + } + + consumeBadURL(ctx, reader); + return [ + TokenType.BadURL, + reader.representationString(), + ...reader.representation(), + undefined, + ]; + } + + if (peeked === QUOTATION_MARK || peeked === APOSTROPHE || peeked === LEFT_PARENTHESIS || isNonPrintableCodePoint(peeked)) { + consumeBadURL(ctx, reader); + + const representation = reader.representation(); + ctx.onParseError({ + message: 'Unexpected character while consuming a url token.', + start: representation[0], + end: representation[1], + state: [ + '4.3.6. Consume a url token', + 'Unexpected U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE (\'), U+0028 LEFT PARENTHESIS (() or non-printable code point', + ], + }); + + return [ + TokenType.BadURL, + reader.representationString(), + ...representation, + undefined, + ]; + } + + if (peeked === REVERSE_SOLIDUS) { + if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) { + string += String.fromCharCode(consumeEscapedCodePoint(ctx, reader)); + continue; + } + + consumeBadURL(ctx, reader); + + const representation = reader.representation(); + ctx.onParseError({ + message: 'Invalid escape sequence while consuming a url token.', + start: representation[0], + end: representation[1], + state: [ + '4.3.6. Consume a url token', + 'U+005C REVERSE SOLIDUS (\\)', + 'The input stream does not start with a valid escape sequence', + ], + }); + + return [ + TokenType.BadURL, + reader.representationString(), + ...representation, + undefined, + ]; + } + + reader.readCodePoint(); + string += String.fromCharCode(peeked); + } +} diff --git a/packages/css-tokenizer/src/consume/whitespace-token.ts b/packages/css-tokenizer/src/consume/whitespace-token.ts new file mode 100644 index 000000000..b23c3f285 --- /dev/null +++ b/packages/css-tokenizer/src/consume/whitespace-token.ts @@ -0,0 +1,44 @@ +import { isWhitespace } from '../code-points/ranges'; +import { CodePointReader } from '../interfaces/code-point-reader'; +import { Context } from '../interfaces/context'; +import { TokenType, TokenWhitespace } from '../interfaces/token'; + +export function consumeWhiteSpace(ctx: Context, reader: CodePointReader, max = -1): TokenWhitespace { + let current = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (max !== -1 && current === max) { + return [ + TokenType.Whitespace, + reader.representationString(), + ...reader.representation(), + undefined, + ]; + } + + current++; + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + return [ + TokenType.Whitespace, + reader.representationString(), + ...reader.representation(), + undefined, + ]; + } + + if (!isWhitespace(peeked)) { + break; + } + + reader.readCodePoint(); + } + + return [ + TokenType.Whitespace, + reader.representationString(), + ...reader.representation(), + undefined, + ]; +} diff --git a/packages/css-tokenizer/src/index.ts b/packages/css-tokenizer/src/index.ts new file mode 100644 index 000000000..015d99029 --- /dev/null +++ b/packages/css-tokenizer/src/index.ts @@ -0,0 +1,35 @@ +export type { CSSToken } from './interfaces/token'; +export { Reader } from './reader'; +export { TokenType, NumberType, mirrorVariantType, isToken } from './interfaces/token'; +export { stringify } from './stringify'; +export { tokenizer } from './tokenizer'; +export { cloneTokens } from './util/clone-tokens'; + +export type { + TokenAtKeyword, + TokenBadString, + TokenBadURL, + TokenCDC, + TokenCDO, + TokenColon, + TokenComma, + TokenComment, + TokenDelim, + TokenDimension, + TokenEOF, + TokenFunction, + TokenHash, + TokenIdent, + TokenNumber, + TokenPercentage, + TokenSemicolon, + TokenString, + TokenURL, + TokenWhitespace, + TokenOpenParen, + TokenCloseParen, + TokenOpenSquare, + TokenCloseSquare, + TokenOpenCurly, + TokenCloseCurly, +} from './interfaces/token'; diff --git a/packages/css-tokenizer/src/interfaces/code-point-reader.ts b/packages/css-tokenizer/src/interfaces/code-point-reader.ts new file mode 100644 index 000000000..6f1c8b216 --- /dev/null +++ b/packages/css-tokenizer/src/interfaces/code-point-reader.ts @@ -0,0 +1,23 @@ +// Note 1 +// The CSS specification refers to units of text as code points. +// We follow this naming. +// This must not be confused with `codePointAt`|`charCodeAt` which are JS API's. +// A char code in JS is equivalent to a code point from the CSS Specification. + +export type CodePointReader = { + cursorPositionOfLastReadCodePoint(): number; + + peekOneCodePoint(): number | false + peekTwoCodePoints(): [number, number] | [number] | [] + peekThreeCodePoints(): [number, number, number] | [number, number] | [number] | [] + peekFourCodePoints(): [number, number, number, number] | [number, number, number] | [number, number] | [number] | [] + + readCodePoint(): number | false + unreadCodePoint(): boolean + + representation(): [number, number] + representationString(): string + resetRepresentation() + + slice(start: number, end: number): string +} diff --git a/packages/css-tokenizer/src/interfaces/context.ts b/packages/css-tokenizer/src/interfaces/context.ts new file mode 100644 index 000000000..27eb77de6 --- /dev/null +++ b/packages/css-tokenizer/src/interfaces/context.ts @@ -0,0 +1,5 @@ +import { ParserError } from './error'; + +export type Context = { + onParseError: (error: ParserError) => void +} diff --git a/packages/css-tokenizer/src/interfaces/error.ts b/packages/css-tokenizer/src/interfaces/error.ts new file mode 100644 index 000000000..e8c677745 --- /dev/null +++ b/packages/css-tokenizer/src/interfaces/error.ts @@ -0,0 +1,6 @@ +export type ParserError = { + message: string, + start: number, + end: number, + state: Array +} diff --git a/packages/css-tokenizer/src/interfaces/token.ts b/packages/css-tokenizer/src/interfaces/token.ts new file mode 100644 index 000000000..81a916036 --- /dev/null +++ b/packages/css-tokenizer/src/interfaces/token.ts @@ -0,0 +1,185 @@ +export enum TokenType { + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#comment-diagram */ + Comment = 'comment', + + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-at-keyword-token */ + AtKeyword = 'at-keyword-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-bad-string-token */ + BadString = 'bad-string-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-bad-url-token */ + BadURL = 'bad-url-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-cdc-token */ + CDC = 'CDC-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-cdo-token */ + CDO = 'CDO-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-colon-token */ + Colon = 'colon-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-comma-token */ + Comma = 'comma-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-delim-token */ + Delim = 'delim-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-dimension-token */ + Dimension = 'dimension-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-eof-token */ + EOF = 'EOF-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-function-token */ + Function = 'function-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-hash-token */ + Hash = 'hash-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-ident-token */ + Ident = 'ident-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-percentage-token */ + Number = 'number-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-percentage-token */ + Percentage = 'percentage-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-semicolon-token */ + Semicolon = 'semicolon-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-string-token */ + String = 'string-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-url-token */ + URL = 'url-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#typedef-whitespace-token */ + Whitespace = 'whitespace-token', + + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-open-paren */ + OpenParen = '(-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-close-paren */ + CloseParen = ')-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-open-square */ + OpenSquare = '[-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-close-square */ + CloseSquare = ']-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-open-curly */ + OpenCurly = '{-token', + /** https://www.w3.org/TR/2021/CRD-css-syntax-3-20211224/#tokendef-close-curly */ + CloseCurly = '}-token', +} + +export enum NumberType { + Integer = 'integer', + Number = 'number', +} + +export enum HashType { + Unrestricted = 'unrestricted', + ID = 'id', +} + +export type TokenAtKeyword = Token; +export type TokenBadString = Token; +export type TokenBadURL = Token; +export type TokenCDC = Token; +export type TokenCDO = Token; +export type TokenColon = Token; +export type TokenComma = Token; +export type TokenComment = Token; +export type TokenDelim = Token; +export type TokenDimension = Token; +export type TokenEOF = Token; +export type TokenFunction = Token; +export type TokenHash = Token; +export type TokenIdent = Token; +export type TokenNumber = Token; +export type TokenPercentage = Token; +export type TokenSemicolon = Token; +export type TokenString = Token; +export type TokenURL = Token; +export type TokenWhitespace = Token; + +export type TokenOpenParen = Token; +export type TokenCloseParen = Token; +export type TokenOpenSquare = Token; +export type TokenCloseSquare = Token; +export type TokenOpenCurly = Token; +export type TokenCloseCurly = Token; + +export type CSSToken = TokenAtKeyword | + TokenBadString | + TokenBadURL | + TokenCDC | + TokenCDO | + TokenColon | + TokenComma | + TokenComment | + TokenDelim | + TokenDimension | + TokenEOF | + TokenFunction | + TokenHash | + TokenIdent | + TokenNumber | + TokenPercentage | + TokenSemicolon | + TokenString | + TokenURL | + TokenWhitespace | + TokenOpenParen | + TokenCloseParen | + TokenOpenSquare | + TokenCloseSquare | + TokenOpenCurly | + TokenCloseCurly; + +export type Token = [ + /** The type of token */ + T, + /** The token representation */ + string, + /** Start position of representation */ + number, + /** End position of representation */ + number, + /** Extra data */ + U, +] + +export function mirrorVariantType(type: TokenType): TokenType|null { + switch (type) { + case TokenType.OpenParen: + return TokenType.CloseParen; + case TokenType.CloseParen: + return TokenType.OpenParen; + + case TokenType.OpenCurly: + return TokenType.CloseCurly; + case TokenType.CloseCurly: + return TokenType.OpenCurly; + + case TokenType.OpenSquare: + return TokenType.CloseSquare; + case TokenType.CloseSquare: + return TokenType.OpenSquare; + + default: + return null; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isToken(x: any): x is CSSToken { + if (!Array.isArray(x)) { + return false; + } + + if (x.length < 4) { + return false; + } + + if (!(x[0] in TokenType)) { + return false; + } + + if (typeof x[1] !== 'string') { + return false; + } + + if (typeof x[2] !== 'number') { + return false; + } + + if (typeof x[3] !== 'number') { + return false; + } + + return true; +} diff --git a/packages/css-tokenizer/src/reader.ts b/packages/css-tokenizer/src/reader.ts new file mode 100644 index 000000000..287b5627d --- /dev/null +++ b/packages/css-tokenizer/src/reader.ts @@ -0,0 +1,139 @@ +import { CodePointReader } from './interfaces/code-point-reader'; + +export class Reader implements CodePointReader { + #cursor: number; + #stringSource = ''; + #codePointSource: Array = []; + #length = 0; + + #representationStart = 0; + #representationEnd = -1; + + constructor(source: string) { + this.#cursor = 0; + this.#stringSource = source; + this.#length = source.length; + + for (let i = 0; i < this.#length; i++) { + this.#codePointSource.push(this.#stringSource.charCodeAt(i)); + } + } + + cursorPositionOfLastReadCodePoint(): number { + return this.#cursor - 1; + } + + peekOneCodePoint(): number | false { + const first = this.#codePointSource[this.#cursor]; + if (typeof first === 'undefined') { + return false; + } + + return first; + } + + peekTwoCodePoints(): [number, number] | [number] | [] { + const first = this.#codePointSource[this.#cursor]; + if (typeof first === 'undefined') { + return []; + } + + const second = this.#codePointSource[this.#cursor + 1]; + if (typeof second === 'undefined') { + return [first]; + } + + return [first, second]; + } + + peekThreeCodePoints(): [number, number, number] | [number, number] | [number] | [] { + const first = this.#codePointSource[this.#cursor]; + if (typeof first === 'undefined') { + return []; + } + + const second = this.#codePointSource[this.#cursor + 1]; + if (typeof second === 'undefined') { + return [first]; + } + + const third = this.#codePointSource[this.#cursor + 2]; + if (typeof third === 'undefined') { + return [first, second]; + } + + return [first, second, third]; + } + + peekFourCodePoints(): [number, number, number, number] | [number, number, number] | [number, number] | [number] | [] { + const first = this.#codePointSource[this.#cursor]; + if (typeof first === 'undefined') { + return []; + } + + const second = this.#codePointSource[this.#cursor + 1]; + if (typeof second === 'undefined') { + return [first]; + } + + const third = this.#codePointSource[this.#cursor + 2]; + if (typeof third === 'undefined') { + return [first, second]; + } + + const fourth = this.#codePointSource[this.#cursor + 2]; + if (typeof fourth === 'undefined') { + return [first, second, third]; + } + + return [first, second, third, fourth]; + } + + readCodePoint(): number | false { + const codePoint = this.#codePointSource[this.#cursor]; + if (typeof codePoint === 'undefined') { + return false; + } + + this.#representationEnd = this.#cursor; + this.#cursor += 1; + + return codePoint; + } + + unreadCodePoint(): boolean { + if (this.#cursor === 0) { + return false; + } + + this.#representationEnd = this.#cursor - 1; + this.#cursor -= 1; + + return true; + } + + representation(): [number, number] { + return [ + this.#representationStart, + this.#representationEnd, + ]; + } + + representationString(): string { + const representation = this.representation(); + if (representation[1] === -1) { + return ''; + } + + return this.slice(representation[0], representation[1] + 1); + } + + resetRepresentation() { + this.#representationStart = this.#cursor; + this.#representationEnd = -1; + } + + slice(start: number, end: number): string { + return this.#stringSource.slice(start, end); + } +} diff --git a/packages/css-tokenizer/src/stringify.ts b/packages/css-tokenizer/src/stringify.ts new file mode 100644 index 000000000..90d190871 --- /dev/null +++ b/packages/css-tokenizer/src/stringify.ts @@ -0,0 +1,10 @@ +import type { CSSToken } from './interfaces/token'; + +export function stringify(...tokens: Array): string { + let buffer = ''; + for (let i = 0; i < tokens.length; i++) { + buffer = buffer + tokens[i][1]; + } + + return buffer; +} diff --git a/packages/css-tokenizer/src/tokenizer.ts b/packages/css-tokenizer/src/tokenizer.ts new file mode 100644 index 000000000..b6db134d2 --- /dev/null +++ b/packages/css-tokenizer/src/tokenizer.ts @@ -0,0 +1,206 @@ +import { checkIfFourCodePointsWouldStartCDO } from './checks/four-code-points-would-start-cdo'; +import { checkIfThreeCodePointsWouldStartAnIdentSequence } from './checks/three-code-points-would-start-ident-sequence'; +import { checkIfThreeCodePointsWouldStartANumber } from './checks/three-code-points-would-start-number'; +import { checkIfTwoCodePointsStartAComment } from './checks/two-code-points-start-comment'; +import { checkIfThreeCodePointsWouldStartCDC } from './checks/three-code-points-would-start-cdc'; +import { APOSTROPHE, COLON, COMMA, COMMERCIAL_AT, FULL_STOP, HYPHEN_MINUS, LEFT_CURLY_BRACKET, LEFT_PARENTHESIS, LEFT_SQUARE_BRACKET, LESS_THAN_SIGN, NUMBER_SIGN, PLUS_SIGN, QUOTATION_MARK, REVERSE_SOLIDUS, RIGHT_CURLY_BRACKET, RIGHT_PARENTHESIS, RIGHT_SQUARE_BRACKET, SEMICOLON } from './code-points/code-points'; +import { isDigitCodePoint, isIdentStartCodePoint, isWhitespace } from './code-points/ranges'; +import { consumeComment } from './consume/comment'; +import { consumeHashToken } from './consume/hash-token'; +import { consumeIdentSequence } from './consume/ident-sequence'; +import { consumeNumericToken } from './consume/numeric-token'; +import { consumeWhiteSpace } from './consume/whitespace-token'; +import { CSSToken, TokenType } from './interfaces/token'; +import { Reader } from './reader'; +import { consumeStringToken } from './consume/string-token'; +import { consumeIdentLikeToken } from './consume/ident-like-token'; +import { checkIfTwoCodePointsAreAValidEscape } from './checks/two-code-points-are-valid-escape'; +import { ParserError } from './interfaces/error'; + +interface Stringer { + valueOf(): string +} + +export function tokenizer(input: { css: Stringer }, options?: { commentsAreTokens?: boolean, onParseError?: (error: ParserError) => void }) { + const css = input.css.valueOf(); + + const reader = new Reader(css); + + const ctx = { + onParseError: options?.onParseError ?? (() => { /* noop */ }), + }; + + function endOfFile() { + return reader.peekOneCodePoint() === false; + } + + function nextToken(): CSSToken | undefined { + reader.resetRepresentation(); + + if (checkIfTwoCodePointsStartAComment(ctx, reader)) { + if (options?.commentsAreTokens) { + return consumeComment(ctx, reader); + } else { + consumeComment(ctx, reader); + } + } + + reader.resetRepresentation(); + + const peeked = reader.peekOneCodePoint(); + if (peeked === false) { + return [TokenType.EOF, '', -1, -1, undefined]; + } + + // Simple, one character tokens: + switch (peeked) { + case COMMA: + reader.readCodePoint(); + return [TokenType.Comma, reader.representationString(), ...reader.representation(), undefined]; + case COLON: + reader.readCodePoint(); + return [TokenType.Colon, reader.representationString(), ...reader.representation(), undefined]; + case SEMICOLON: + reader.readCodePoint(); + return [TokenType.Semicolon, reader.representationString(), ...reader.representation(), undefined]; + case LEFT_PARENTHESIS: + reader.readCodePoint(); + return [TokenType.OpenParen, reader.representationString(), ...reader.representation(), undefined]; + case RIGHT_PARENTHESIS: + reader.readCodePoint(); + return [TokenType.CloseParen, reader.representationString(), ...reader.representation(), undefined]; + case LEFT_SQUARE_BRACKET: + reader.readCodePoint(); + return [TokenType.OpenSquare, reader.representationString(), ...reader.representation(), undefined]; + case RIGHT_SQUARE_BRACKET: + reader.readCodePoint(); + return [TokenType.CloseSquare, reader.representationString(), ...reader.representation(), undefined]; + case LEFT_CURLY_BRACKET: + reader.readCodePoint(); + return [TokenType.OpenCurly, reader.representationString(), ...reader.representation(), undefined]; + case RIGHT_CURLY_BRACKET: + reader.readCodePoint(); + return [TokenType.CloseCurly, reader.representationString(), ...reader.representation(), undefined]; + } + + switch (peeked) { + case APOSTROPHE: + case QUOTATION_MARK: + return consumeStringToken(ctx, reader); + case NUMBER_SIGN: + return consumeHashToken(ctx, reader); + + case PLUS_SIGN: + case FULL_STOP: { + if (checkIfThreeCodePointsWouldStartANumber(ctx, reader)) { + return consumeNumericToken(ctx, reader); + } + + reader.readCodePoint(); + return [TokenType.Delim, reader.representationString(), ...reader.representation(), { + value: String.fromCharCode(peeked), + }]; + } + + case HYPHEN_MINUS: { + if (checkIfThreeCodePointsWouldStartANumber(ctx, reader)) { + return consumeNumericToken(ctx, reader); + } + + if (checkIfThreeCodePointsWouldStartCDC(ctx, reader)) { + reader.readCodePoint(); + reader.readCodePoint(); + reader.readCodePoint(); + + return [TokenType.CDC, reader.representationString(), ...reader.representation(), undefined]; + } + + if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) { + return consumeIdentLikeToken(ctx, reader); + } + + reader.readCodePoint(); + return [TokenType.Delim, reader.representationString(), ...reader.representation(), { + value: '-', + }]; + } + + case LESS_THAN_SIGN: { + if (checkIfFourCodePointsWouldStartCDO(ctx, reader)) { + reader.readCodePoint(); + reader.readCodePoint(); + reader.readCodePoint(); + reader.readCodePoint(); + + return [TokenType.CDO, reader.representationString(), ...reader.representation(), undefined]; + } + + reader.readCodePoint(); + return [TokenType.Delim, reader.representationString(), ...reader.representation(), { + value: '<', + }]; + } + + case COMMERCIAL_AT: { + reader.readCodePoint(); + if (checkIfThreeCodePointsWouldStartAnIdentSequence(ctx, reader)) { + const identSequence = consumeIdentSequence(ctx, reader); + + return [TokenType.AtKeyword, reader.representationString(), ...reader.representation(), { + value: identSequence.map((x) => String.fromCharCode(x)).join(''), + }]; + } + + return [TokenType.Delim, reader.representationString(), ...reader.representation(), { + value: '@', + }]; + } + + case REVERSE_SOLIDUS: { + if (checkIfTwoCodePointsAreAValidEscape(ctx, reader)) { + return consumeIdentLikeToken(ctx, reader); + } + + reader.readCodePoint(); + + const representation = reader.representation(); + ctx.onParseError({ + message: 'Invalid escape sequence after "\\"', + start: representation[0], + end: representation[1], + state: [ + '4.3.1. Consume a token', + 'U+005C REVERSE SOLIDUS (\\)', + 'The input stream does not start with a valid escape sequence', + ], + }); + + return [TokenType.Delim, reader.representationString(), ...representation, { + value: '\\', + }]; + } + } + + if (isWhitespace(peeked)) { + return consumeWhiteSpace(ctx, reader); + } + + if (isDigitCodePoint(peeked)) { + return consumeNumericToken(ctx, reader); + } + + if (isIdentStartCodePoint(peeked)) { + return consumeIdentLikeToken(ctx, reader); + } + + reader.readCodePoint(); + return [TokenType.Delim, reader.representationString(), ...reader.representation(), { + value: String.fromCharCode(peeked), + }]; + } + + return { + nextToken: nextToken, + endOfFile: endOfFile, + }; +} diff --git a/packages/css-tokenizer/src/util/clone-tokens.ts b/packages/css-tokenizer/src/util/clone-tokens.ts new file mode 100644 index 000000000..e70353307 --- /dev/null +++ b/packages/css-tokenizer/src/util/clone-tokens.ts @@ -0,0 +1,9 @@ +import { CSSToken } from '../interfaces/token'; + +export function cloneTokens(tokens: Array): Array { + if ((typeof globalThis !== 'undefined') && 'structuredClone' in globalThis) { + return structuredClone(tokens); + } + + return JSON.parse(JSON.stringify(tokens)); +} diff --git a/packages/css-tokenizer/stryker.conf.json b/packages/css-tokenizer/stryker.conf.json new file mode 100644 index 000000000..015ebbb73 --- /dev/null +++ b/packages/css-tokenizer/stryker.conf.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "mutate": [ + "src/**/*.ts" + ], + "buildCommand": "npm run build", + "testRunner": "command", + "coverageAnalysis": "perTest", + "tempDirName": "../../.stryker-tmp", + "commandRunner": { + "command": "npm run test" + }, + "thresholds": { + "high": 100, + "low": 100, + "break": 100 + }, + "inPlace": true +} diff --git a/packages/css-tokenizer/test/_import.mjs b/packages/css-tokenizer/test/_import.mjs new file mode 100644 index 000000000..c1c61bf2b --- /dev/null +++ b/packages/css-tokenizer/test/_import.mjs @@ -0,0 +1,5 @@ +import { tokenizer } from '@csstools/css-tokenizer'; + +tokenizer({ + css: '.some { css: ""; }', +}); diff --git a/packages/css-tokenizer/test/_require.cjs b/packages/css-tokenizer/test/_require.cjs new file mode 100644 index 000000000..abc74a96e --- /dev/null +++ b/packages/css-tokenizer/test/_require.cjs @@ -0,0 +1,5 @@ +const { tokenizer } = require('@csstools/css-tokenizer'); + +tokenizer({ + css: '.some { css: ""; }', +}); diff --git a/packages/css-tokenizer/test/code-points/code-points.mjs b/packages/css-tokenizer/test/code-points/code-points.mjs new file mode 100644 index 000000000..49a0ce318 --- /dev/null +++ b/packages/css-tokenizer/test/code-points/code-points.mjs @@ -0,0 +1,401 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; + +// Single characters +{ + const testCases = [ + [ + 'a', + [['ident-token', 'a', 0, 0, { value: 'a' }]], + ], + [ + '\\', + [['ident-token', '\\', 0, 0, { value: '๏ฟฝ' }]], + ], + [ + '\\\\', + [['ident-token', '\\\\', 0, 1, { value: '\\' }]], + ], + [ + 'ยง', + [['ident-token', 'ยง', 0, 0, { value: 'ยง' }]], + ], + [ + 'ยฑ', + [['ident-token', 'ยฑ', 0, 0, { value: 'ยฑ' }]], + ], + [ + '!', + [['delim-token', '!', 0, 0, { value: '!' }]], + ], + [ + '@', + [['delim-token', '@', 0, 0, { value: '@' }]], + ], + [ + '#', + [['delim-token', '#', 0, 0, { value: '#' }]], + ], + [ + '$', + [['delim-token', '$', 0, 0, { value: '$' }]], + ], + [ + '%', + [['delim-token', '%', 0, 0, { value: '%' }]], + ], + [ + '^', + [['delim-token', '^', 0, 0, { value: '^' }]], + ], + [ + '&', + [['delim-token', '&', 0, 0, { value: '&' }]], + ], + [ + '*', + [['delim-token', '*', 0, 0, { value: '*' }]], + ], + [ + '(', + [['(-token', '(', 0, 0, undefined]], + ], + [ + ')', + [[')-token', ')', 0, 0, undefined]], + ], + [ + '-', + [['delim-token', '-', 0, 0, { value: '-' }]], + ], + [ + '_', + [['ident-token', '_', 0, 0, { value: '_' }]], + ], + [ + '+', + [['delim-token', '+', 0, 0, { value: '+' }]], + ], + [ + '=', + [['delim-token', '=', 0, 0, { value: '=' }]], + ], + [ + '[', + [['[-token', '[', 0, 0, undefined]], + ], + [ + ']', + [[']-token', ']', 0, 0, undefined]], + ], + [ + '{', + [['{-token', '{', 0, 0, undefined]], + ], + [ + '}', + [['}-token', '}', 0, 0, undefined]], + ], + [ + ';', + [['semicolon-token', ';', 0, 0, undefined]], + ], + [ + ':', + [['colon-token', ':', 0, 0, undefined]], + ], + [ + '`', + [['delim-token', '`', 0, 0, { value: '`' }]], + ], + [ + '~', + [['delim-token', '~', 0, 0, { value: '~' }]], + ], + [ + '\'', + [['string-token', '\'', 0, 0, { value: '' }]], + ], + [ + '"', + [['string-token', '"', 0, 0, { value: '' }]], + ], + [ + '|', + [['delim-token', '|', 0, 0, { value: '|' }]], + ], + [ + ',', + [['comma-token', ',', 0, 0, undefined]], + ], + [ + '<', + [['delim-token', '<', 0, 0, { value: '<' }]], + ], + [ + '>', + [['delim-token', '>', 0, 0, { value: '>' }]], + ], + [ + '.', + [['delim-token', '.', 0, 0, { value: '.' }]], + ], + [ + '?', + [['delim-token', '?', 0, 0, { value: '?' }]], + ], + [ + '/', + [['delim-token', '/', 0, 0, { value: '/' }]], + ], + ]; + + testCases.forEach((testCase) => { + const t = tokenizer({ + css: testCase[0], + }); + + assert.deepEqual( + collectTokens(t).slice(0,-1), + testCase[1], + ); + }); +} + + +// Followed by a letter +{ + const testCases = [ + [ + 'aa', + [['ident-token', 'aa', 0, 1, { value: 'aa' }]], + ], + [ + '\\a', + [['ident-token', '\\a', 0, 1, { value: '\n' }]], + ], + [ + '\\\\a', + [['ident-token', '\\\\a', 0, 2, { value: '\\a' }]], + ], + [ + 'ยงa', + [['ident-token', 'ยงa', 0, 1, { value: 'ยงa' }]], + ], + [ + 'ยฑa', + [['ident-token', 'ยฑa', 0, 1, { value: 'ยฑa' }]], + ], + [ + '!a', + [ + ['delim-token', '!', 0, 0, { value: '!' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '@a', + [['at-keyword-token', '@a', 0, 1, { value: 'a' }]], + ], + [ + '#a', + [['hash-token', '#a', 0, 1, { value: 'a', type: 'id' }]], + ], + [ + '$a', + [ + ['delim-token', '$', 0, 0, { value: '$' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '%a', + [ + ['delim-token', '%', 0, 0, { value: '%' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '^a', + [ + ['delim-token', '^', 0, 0, { value: '^' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '&a', + [ + ['delim-token', '&', 0, 0, { value: '&' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '*a', + [ + ['delim-token', '*', 0, 0, { value: '*' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '(a', + [ + ['(-token', '(', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + ')a', + [ + [')-token', ')', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '-a', + [['ident-token', '-a', 0, 1, { value: '-a' }]], + ], + [ + '_a', + [['ident-token', '_a', 0, 1, { value: '_a' }]], + ], + [ + '+a', + [ + ['delim-token', '+', 0, 0, { value: '+' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '=a', + [ + ['delim-token', '=', 0, 0, { value: '=' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '[a', + [ + ['[-token', '[', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + ']a', + [ + [']-token', ']', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '{a', + [ + ['{-token', '{', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '}a', + [ + ['}-token', '}', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + ';a', + [ + ['semicolon-token', ';', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + ':a', + [ + ['colon-token', ':', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '`a', + [ + ['delim-token', '`', 0, 0, { value: '`' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '~a', + [ + ['delim-token', '~', 0, 0, { value: '~' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '\'a', + [['string-token', '\'a', 0, 1, { value: 'a' }]], + ], + [ + '"a', + [['string-token', '"a', 0, 1, { value: 'a' }]], + ], + [ + '|a', + [ + ['delim-token', '|', 0, 0, { value: '|' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + ',a', + [ + ['comma-token', ',', 0, 0, undefined], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '<', 0, 0, { value: '<' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '>a', + [ + ['delim-token', '>', 0, 0, { value: '>' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '.a', + [ + ['delim-token', '.', 0, 0, { value: '.' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '?a', + [ + ['delim-token', '?', 0, 0, { value: '?' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + [ + '/a', + [ + ['delim-token', '/', 0, 0, { value: '/' }], + ['ident-token', 'a', 1, 1, { value: 'a' }], + ], + ], + ]; + + testCases.forEach((testCase) => { + const t = tokenizer({ + css: testCase[0], + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + testCase[1], + ); + }); +} diff --git a/packages/css-tokenizer/test/code-points/ranges.mjs b/packages/css-tokenizer/test/code-points/ranges.mjs new file mode 100644 index 000000000..f7ff5b573 --- /dev/null +++ b/packages/css-tokenizer/test/code-points/ranges.mjs @@ -0,0 +1,60 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; + +// Single characters +{ + const testCases = [ + [ + '\\00ae', + [['ident-token', '\\00ae', 0, 4, { value: 'ยฎ' }]], + ], + [ + '\\00aE', + [['ident-token', '\\00aE', 0, 4, { value: 'ยฎ' }]], + ], + [ + '\\00af', + [['ident-token', '\\00af', 0, 4, { value: 'ยฏ' }]], + ], + [ + '\\00aF', + [['ident-token', '\\00aF', 0, 4, { value: 'ยฏ' }]], + ], + [ + '\\00ag', + [['ident-token', '\\00ag', 0, 4, { value: '\ng' }]], + ], + [ + '\\00aG', + [['ident-token', '\\00aG', 0, 4, { value: '\nG' }]], + ], + [ + '\\7f', + [['ident-token', '\\7f', 0, 2, { value: '\x7F' }]], + ], + [ + '\\80', + [['ident-token', '\\80', 0, 2, { value: '\x80' }]], + ], + [ + '\\81', + [['ident-token', '\\81', 0, 2, { value: '\x81' }]], + ], + [ + '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + [['ident-token', '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', 0, 10, { value: '๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ' }]], + ], + ]; + + testCases.forEach((testCase) => { + const t = tokenizer({ + css: testCase[0], + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + testCase[1], + ); + }); +} diff --git a/packages/css-tokenizer/test/complex/at-media-params.mjs b/packages/css-tokenizer/test/complex/at-media-params.mjs new file mode 100644 index 000000000..c40a438c0 --- /dev/null +++ b/packages/css-tokenizer/test/complex/at-media-params.mjs @@ -0,0 +1,253 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; + +{ + const testCases = [ + [ + 'screen', + [['ident-token', 'screen', 0, 5, { value: 'screen' }]], + ], + [ + 'true', + [['ident-token', 'true', 0, 3, { value: 'true' }]], + ], + [ + 'false', + [['ident-token', 'false', 0, 4, { value: 'false' }]], + ], + [ + 'only screen', + [ + ['ident-token', 'only', 0, 3, { value: 'only' }], + ['whitespace-token', ' ', 4, 4, undefined], + ['ident-token', 'screen', 5, 10, { value: 'screen' }], + ], + ], + [ + 'only screen and (min-width: 10px)', + [ + ['ident-token', 'only', 0, 3, { value: 'only' }], + ['whitespace-token', ' ', 4, 4, undefined], + ['ident-token', 'screen', 5, 10, { value: 'screen' }], + ['whitespace-token', ' ', 11, 11, undefined], + ['ident-token', 'and', 12, 14, { value: 'and' }], + ['whitespace-token', ' ', 15, 15, undefined], + ['(-token', '(', 16, 16, undefined], + ['ident-token', 'min-width', 17, 25, { value: 'min-width' }], + ['colon-token', ':', 26, 26, undefined], + ['whitespace-token', ' ', 27, 27, undefined], + [ + 'dimension-token', + '10px', + 28, + 31, + { value: 10, type: 'integer', unit: 'px' }, + ], + [')-token', ')', 32, 32, undefined], + ], + ], + [ + '(10rem <= width <= 40rem)', + [ + ['(-token', '(', 0, 0, undefined], + [ + 'dimension-token', + '10rem', + 1, + 5, + { value: 10, type: 'integer', unit: 'rem' }, + ], + ['whitespace-token', ' ', 6, 6, undefined], + ['delim-token', '<', 7, 7, { value: '<' }], + ['delim-token', '=', 8, 8, { value: '=' }], + ['whitespace-token', ' ', 9, 9, undefined], + ['ident-token', 'width', 10, 14, { value: 'width' }], + ['whitespace-token', ' ', 15, 15, undefined], + ['delim-token', '<', 16, 16, { value: '<' }], + ['delim-token', '=', 17, 17, { value: '=' }], + ['whitespace-token', ' ', 18, 18, undefined], + [ + 'dimension-token', + '40rem', + 19, + 23, + { value: 40, type: 'integer', unit: 'rem' }, + ], + [')-token', ')', 24, 24, undefined], + ], + ], + [ + '(40rem > width > 10rem)', + [ + ['(-token', '(', 0, 0, undefined], + [ + 'dimension-token', + '40rem', + 1, + 5, + { value: 40, type: 'integer', unit: 'rem' }, + ], + ['whitespace-token', ' ', 6, 6, undefined], + ['delim-token', '>', 7, 7, { value: '>' }], + ['whitespace-token', ' ', 8, 8, undefined], + ['ident-token', 'width', 9, 13, { value: 'width' }], + ['whitespace-token', ' ', 14, 14, undefined], + ['delim-token', '>', 15, 15, { value: '>' }], + ['whitespace-token', ' ', 16, 16, undefined], + [ + 'dimension-token', + '10rem', + 17, + 21, + { value: 10, type: 'integer', unit: 'rem' }, + ], + [')-token', ')', 22, 22, undefined], + ], + ], + [ + '(--custom-mq)', + [ + ['(-token', '(', 0, 0, undefined], + ['ident-token', '--custom-mq', 1, 11, { value: '--custom-mq' }], + [')-token', ')', 12, 12, undefined], + ], + ], + [ + 'screen/* a comment */(--custom-mq)', + [ + ['ident-token', 'screen', 0, 5, { value: 'screen' }], + ['(-token', '(', 21, 21, undefined], + ['ident-token', '--custom-mq', 22, 32, { value: '--custom-mq' }], + [')-token', ')', 33, 33, undefined], + ], + ], + [ + '((min-width: 300px) and (prefers-color-scheme: dark))', + [ + ['(-token', '(', 0, 0, undefined], + ['(-token', '(', 1, 1, undefined], + ['ident-token', 'min-width', 2, 10, { value: 'min-width' }], + ['colon-token', ':', 11, 11, undefined], + ['whitespace-token', ' ', 12, 12, undefined], + [ + 'dimension-token', + '300px', + 13, + 17, + { value: 300, type: 'integer', unit: 'px' }, + ], + [')-token', ')', 18, 18, undefined], + ['whitespace-token', ' ', 19, 19, undefined], + ['ident-token', 'and', 20, 22, { value: 'and' }], + ['whitespace-token', ' ', 23, 23, undefined], + ['(-token', '(', 24, 24, undefined], + [ + 'ident-token', + 'prefers-color-scheme', + 25, + 44, + { value: 'prefers-color-scheme' }, + ], + ['colon-token', ':', 45, 45, undefined], + ['whitespace-token', ' ', 46, 46, undefined], + ['ident-token', 'dark', 47, 50, { value: 'dark' }], + [')-token', ')', 51, 51, undefined], + [')-token', ')', 52, 52, undefined], + ], + ], + [ + '((min-width:300px)and (prefers-color-scheme:dark))', + [ + ['(-token', '(', 0, 0, undefined], + ['(-token', '(', 1, 1, undefined], + ['ident-token', 'min-width', 2, 10, { value: 'min-width' }], + ['colon-token', ':', 11, 11, undefined], + [ + 'dimension-token', + '300px', + 12, + 16, + { value: 300, type: 'integer', unit: 'px' }, + ], + [')-token', ')', 17, 17, undefined], + ['ident-token', 'and', 18, 20, { value: 'and' }], + ['whitespace-token', ' ', 21, 21, undefined], + ['(-token', '(', 22, 22, undefined], + [ + 'ident-token', + 'prefers-color-scheme', + 23, + 42, + { value: 'prefers-color-scheme' }, + ], + ['colon-token', ':', 43, 43, undefined], + ['ident-token', 'dark', 44, 47, { value: 'dark' }], + [')-token', ')', 48, 48, undefined], + [')-token', ')', 49, 49, undefined], + ], + ], + [ + ' ( ( min-width : 300px ) and ( prefers-color-scheme : dark ) ) ', + [ + ['whitespace-token', ' ', 0, 0, undefined], + ['(-token', '(', 1, 1, undefined], + ['whitespace-token', ' ', 2, 2, undefined], + ['(-token', '(', 3, 3, undefined], + ['whitespace-token', ' ', 4, 4, undefined], + ['ident-token', 'min-width', 5, 13, { value: 'min-width' }], + ['whitespace-token', ' ', 14, 14, undefined], + ['colon-token', ':', 15, 15, undefined], + ['whitespace-token', ' ', 16, 16, undefined], + [ + 'dimension-token', + '300px', + 17, + 21, + { value: 300, type: 'integer', unit: 'px' }, + ], + ['whitespace-token', ' ', 22, 22, undefined], + [')-token', ')', 23, 23, undefined], + ['whitespace-token', ' ', 24, 24, undefined], + ['ident-token', 'and', 25, 27, { value: 'and' }], + ['whitespace-token', ' ', 28, 28, undefined], + ['(-token', '(', 29, 29, undefined], + ['whitespace-token', ' ', 30, 30, undefined], + [ + 'ident-token', + 'prefers-color-scheme', + 31, + 50, + { value: 'prefers-color-scheme' }, + ], + ['whitespace-token', ' ', 51, 51, undefined], + ['colon-token', ':', 52, 52, undefined], + ['whitespace-token', ' ', 53, 53, undefined], + ['ident-token', 'dark', 54, 57, { value: 'dark' }], + ['whitespace-token', ' ', 58, 58, undefined], + [')-token', ')', 59, 59, undefined], + ['whitespace-token', ' ', 60, 60, undefined], + [')-token', ')', 61, 61, undefined], + ['whitespace-token', ' ', 62, 63, undefined], + ], + ], + ]; + + testCases.forEach((testCase) => { + const t = tokenizer({ + css: testCase[0], + }); + + const tokens = collectTokens(t); + + assert.deepEqual( + tokens.slice(0, -1), + testCase[1], + ); + + assert.deepEqual( + tokens[tokens.length -1][0], + 'EOF-token', + ); + }); +} diff --git a/packages/css-tokenizer/test/complex/parse-error.mjs b/packages/css-tokenizer/test/complex/parse-error.mjs new file mode 100644 index 000000000..99517f52f --- /dev/null +++ b/packages/css-tokenizer/test/complex/parse-error.mjs @@ -0,0 +1,36 @@ +import { tokenizer, TokenType } from '@csstools/css-tokenizer'; +import assert from 'assert'; + +{ + const parseErrors = []; + const t = tokenizer( + { + css: '\\', + }, + { + onParseError: (err) => { + parseErrors.push(err); + }, + }, + ); + + // eslint-disable-next-line no-constant-condition + while (true) { + const token = t.nextToken(); + if (token[0] === TokenType.EOF) { + break; + } + } + + assert.deepEqual( + parseErrors, + [ + { + message: 'Unexpected EOF while consuming an escaped code point.', + start: 0, + end: 0, + state: ['4.3.7. Consume an escaped code point', 'Unexpected EOF'], + }, + ], + ); +} diff --git a/packages/css-tokenizer/test/css/multi-line.css b/packages/css-tokenizer/test/css/multi-line.css new file mode 100644 index 000000000..379fa1193 --- /dev/null +++ b/packages/css-tokenizer/test/css/multi-line.css @@ -0,0 +1,8 @@ +.foo { + src: url("\ +https: //\ +example.com\ +/some-path/\ +?query=param\ +&more-query=params"); +} diff --git a/packages/css-tokenizer/test/test-reader.mjs b/packages/css-tokenizer/test/test-reader.mjs new file mode 100644 index 000000000..69c59e706 --- /dev/null +++ b/packages/css-tokenizer/test/test-reader.mjs @@ -0,0 +1,186 @@ +import assert from 'assert'; +import { Reader } from '@csstools/css-tokenizer'; + +{ + const r = new Reader('abc๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆd'); + + { + const peeked = r.peekOneCodePoint(); + assert.deepEqual( + peeked, + 97, + ); + + assert.deepEqual( + String.fromCharCode(peeked), + 'a', + ); + + assert.deepEqual( + r.representation(), + [ + 0, + -1, + ], + ); + } + + { + const peeked = r.peekTwoCodePoints(); + assert.deepEqual( + peeked, + [97, 98], + ); + + assert.deepEqual( + String.fromCharCode(peeked[0]), + 'a', + ); + + assert.deepEqual( + String.fromCharCode(peeked[1]), + 'b', + ); + + assert.deepEqual( + r.representation(), + [ + 0, + -1, + ], + ); + } + + { + const peeked = r.peekThreeCodePoints(); + assert.deepEqual( + peeked, + [97, 98, 99], + ); + + assert.deepEqual( + String.fromCharCode(peeked[0]), + 'a', + ); + + assert.deepEqual( + String.fromCharCode(peeked[1]), + 'b', + ); + + assert.deepEqual( + String.fromCharCode(peeked[2]), + 'c', + ); + + assert.deepEqual( + r.representation(), + [ + 0, + -1, + ], + ); + } + + { + const read = r.readCodePoint(); + assert.deepEqual( + read, + 97, + ); + + assert.deepEqual( + String.fromCharCode(read), + 'a', + ); + + assert.deepEqual( + r.representation(), + [ + 0, + 0, + ], + ); + + assert.deepEqual( + r.representationString(), + 'a', + ); + } + + r.resetRepresentation(); + + { + const read1 = r.readCodePoint(); + assert.deepEqual( + read1, + 98, + ); + + const read2 = r.readCodePoint(); + assert.deepEqual( + read2, + 99, + ); + + const read3 = r.readCodePoint(); + assert.deepEqual( + read3, + 55357, + ); + + assert.deepEqual( + String.fromCharCode(read3), + '\uD83D', + ); + + const read4 = r.readCodePoint(); + assert.deepEqual( + read4, + 56424, + ); + + assert.deepEqual( + String.fromCharCode(read4), + '\uDC68', + ); + + assert.deepEqual( + r.representation(), + [ + 1, + 4, + ], + ); + + // Read to the end + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + r.readCodePoint(); + + assert.deepEqual( + r.representation(), + [ + 1, + 14, + ], + ); + + assert.deepEqual( + r.slice(r.representation()[0], r.representation()[1] + 1), + 'bc๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆd', + ); + + assert.deepEqual( + r.representationString(), + 'bc๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆd', + ); + } +} diff --git a/packages/css-tokenizer/test/test.mjs b/packages/css-tokenizer/test/test.mjs new file mode 100644 index 000000000..71d21591a --- /dev/null +++ b/packages/css-tokenizer/test/test.mjs @@ -0,0 +1,13 @@ +// Reader +import './test-reader.mjs'; +// Code points +import './code-points/code-points.mjs'; +import './code-points/ranges.mjs'; +// Tokens +import './token/basic.mjs'; +import './token/comment.mjs'; +import './token/numeric.mjs'; +import './token/url.mjs'; +// Complex +import './complex/at-media-params.mjs'; +import './complex/parse-error.mjs'; diff --git a/packages/css-tokenizer/test/token/basic.mjs b/packages/css-tokenizer/test/token/basic.mjs new file mode 100644 index 000000000..46033d7d5 --- /dev/null +++ b/packages/css-tokenizer/test/token/basic.mjs @@ -0,0 +1,253 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; + +{ + const t = tokenizer({ + css: '@charset "utf-8";', + }); + + assert.deepEqual( + collectTokens(t), + [ + ['at-keyword-token', '@charset', 0, 7, { value: 'charset' }], + ['whitespace-token', ' ', 8, 8, undefined], + ['string-token', '"utf-8"', 9, 15, { value: 'utf-8' }], + ['semicolon-token', ';', 16, 16, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'foo { width: calc(-infinity) }', + }); + + assert.deepEqual( + collectTokens(t), + [ + ['ident-token', 'foo', 0, 2, { value: 'foo' }], + ['whitespace-token', ' ', 3, 3, undefined], + ['{-token', '{', 4, 4, undefined], + ['whitespace-token', ' ', 5, 5, undefined], + ['ident-token', 'width', 6, 10, { value: 'width' }], + ['colon-token', ':', 11, 11, undefined], + ['whitespace-token', ' ', 12, 12, undefined], + ['function-token', 'calc(', 13, 17, { value: 'calc' }], + ['ident-token', '-infinity', 18, 26, { value: '-infinity' }], + [')-token', ')', 27, 27, undefined], + ['whitespace-token', ' ', 28, 28, undefined], + ['}-token', '}', 29, 29, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '@import url(https://example.com/stylesheet.css) layer( base.tokens ) supports( display: grid ) not screen and ((400px <= width < 1024px) and (prefers-color-scheme: dark));', + }); + + assert.deepEqual( + collectTokens(t), + [ + ['at-keyword-token', '@import', 0, 6, { value: 'import' }], + ['whitespace-token', ' ', 7, 7, undefined], + [ + 'url-token', + 'url(https://example.com/stylesheet.css)', + 8, + 46, + { value: 'https://example.com/stylesheet.css' }, + ], + ['whitespace-token', ' ', 47, 47, undefined], + ['function-token', 'layer(', 48, 53, { value: 'layer' }], + ['whitespace-token', ' ', 54, 54, undefined], + ['ident-token', 'base', 55, 58, { value: 'base' }], + ['delim-token', '.', 59, 59, { value: '.' }], + ['ident-token', 'tokens', 60, 65, { value: 'tokens' }], + ['whitespace-token', ' ', 66, 66, undefined], + [')-token', ')', 67, 67, undefined], + ['whitespace-token', ' ', 68, 68, undefined], + ['function-token', 'supports(', 69, 77, { value: 'supports' }], + ['whitespace-token', ' ', 78, 78, undefined], + ['ident-token', 'display', 79, 85, { value: 'display' }], + ['colon-token', ':', 86, 86, undefined], + ['whitespace-token', ' ', 87, 87, undefined], + ['ident-token', 'grid', 88, 91, { value: 'grid' }], + ['whitespace-token', ' ', 92, 92, undefined], + [')-token', ')', 93, 93, undefined], + ['whitespace-token', ' ', 94, 94, undefined], + ['ident-token', 'not', 95, 97, { value: 'not' }], + ['whitespace-token', ' ', 98, 98, undefined], + ['ident-token', 'screen', 99, 104, { value: 'screen' }], + ['whitespace-token', ' ', 105, 105, undefined], + ['ident-token', 'and', 106, 108, { value: 'and' }], + ['whitespace-token', ' ', 109, 109, undefined], + ['(-token', '(', 110, 110, undefined], + ['(-token', '(', 111, 111, undefined], + [ + 'dimension-token', + '400px', + 112, + 116, + { value: 400, type: 'integer', unit: 'px' }, + ], + ['whitespace-token', ' ', 117, 117, undefined], + ['delim-token', '<', 118, 118, { value: '<' }], + ['delim-token', '=', 119, 119, { value: '=' }], + ['whitespace-token', ' ', 120, 120, undefined], + ['ident-token', 'width', 121, 125, { value: 'width' }], + ['whitespace-token', ' ', 126, 126, undefined], + ['delim-token', '<', 127, 127, { value: '<' }], + ['whitespace-token', ' ', 128, 128, undefined], + [ + 'dimension-token', + '1024px', + 129, + 134, + { value: 1024, type: 'integer', unit: 'px' }, + ], + [')-token', ')', 135, 135, undefined], + ['whitespace-token', ' ', 136, 136, undefined], + ['ident-token', 'and', 137, 139, { value: 'and' }], + ['whitespace-token', ' ', 140, 140, undefined], + ['(-token', '(', 141, 141, undefined], + [ + 'ident-token', + 'prefers-color-scheme', + 142, + 161, + { value: 'prefers-color-scheme' }, + ], + ['colon-token', ':', 162, 162, undefined], + ['whitespace-token', ' ', 163, 163, undefined], + ['ident-token', 'dark', 164, 167, { value: 'dark' }], + [')-token', ')', 168, 168, undefined], + [')-token', ')', 169, 169, undefined], + ['semicolon-token', ';', 170, 170, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: `@media only screen and (min-width: 768rem) { + .foo { + content: 'Some content!' !important; + } +} +`, + }); + + assert.deepEqual( + collectTokens(t), + [ + ['at-keyword-token', '@media', 0, 5, { value: 'media' }], + ['whitespace-token', ' ', 6, 6, undefined], + ['ident-token', 'only', 7, 10, { value: 'only' }], + ['whitespace-token', ' ', 11, 11, undefined], + ['ident-token', 'screen', 12, 17, { value: 'screen' }], + ['whitespace-token', ' ', 18, 18, undefined], + ['ident-token', 'and', 19, 21, { value: 'and' }], + ['whitespace-token', ' ', 22, 22, undefined], + ['(-token', '(', 23, 23, undefined], + ['ident-token', 'min-width', 24, 32, { value: 'min-width' }], + ['colon-token', ':', 33, 33, undefined], + ['whitespace-token', ' ', 34, 34, undefined], + [ + 'dimension-token', + '768rem', + 35, + 40, + { value: 768, type: 'integer', unit: 'rem' }, + ], + [')-token', ')', 41, 41, undefined], + ['whitespace-token', ' ', 42, 42, undefined], + ['{-token', '{', 43, 43, undefined], + ['whitespace-token', '\n\t', 44, 45, undefined], + ['delim-token', '.', 46, 46, { value: '.' }], + ['ident-token', 'foo', 47, 49, { value: 'foo' }], + ['whitespace-token', ' ', 50, 50, undefined], + ['{-token', '{', 51, 51, undefined], + ['whitespace-token', '\n\t\t', 52, 54, undefined], + ['ident-token', 'content', 55, 61, { value: 'content' }], + ['colon-token', ':', 62, 62, undefined], + ['whitespace-token', ' ', 63, 63, undefined], + [ + 'string-token', + // eslint-disable-next-line quotes + "'Some content!'", + 64, + 78, + { value: 'Some content!' }, + ], + ['whitespace-token', ' ', 79, 79, undefined], + ['delim-token', '!', 80, 80, { value: '!' }], + ['ident-token', 'important', 81, 89, { value: 'important' }], + ['semicolon-token', ';', 90, 90, undefined], + ['whitespace-token', '\n\t', 91, 92, undefined], + ['}-token', '}', 93, 93, undefined], + ['whitespace-token', '\n', 94, 94, undefined], + ['}-token', '}', 95, 95, undefined], + ['whitespace-token', '\n', 96, 96, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: `@media screen and ((min-width: 200px) and (foo: "\\A9\\ +bar") and (fancy(baz))) {}`, + }); + + assert.deepEqual( + collectTokens(t), + [ + ['at-keyword-token', '@media', 0, 5, { value: 'media' }], + ['whitespace-token', ' ', 6, 6, undefined], + ['ident-token', 'screen', 7, 12, { value: 'screen' }], + ['whitespace-token', ' ', 13, 13, undefined], + ['ident-token', 'and', 14, 16, { value: 'and' }], + ['whitespace-token', ' ', 17, 17, undefined], + ['(-token', '(', 18, 18, undefined], + ['(-token', '(', 19, 19, undefined], + ['ident-token', 'min-width', 20, 28, { value: 'min-width' }], + ['colon-token', ':', 29, 29, undefined], + ['whitespace-token', ' ', 30, 30, undefined], + [ + 'dimension-token', + '200px', + 31, + 35, + { value: 200, type: 'integer', unit: 'px' }, + ], + [')-token', ')', 36, 36, undefined], + ['whitespace-token', ' ', 37, 37, undefined], + ['ident-token', 'and', 38, 40, { value: 'and' }], + ['whitespace-token', ' ', 41, 41, undefined], + ['(-token', '(', 42, 42, undefined], + ['ident-token', 'foo', 43, 45, { value: 'foo' }], + ['colon-token', ':', 46, 46, undefined], + ['whitespace-token', ' ', 47, 47, undefined], + ['string-token', '"\\A9\\\nbar"', 48, 57, { value: 'ยฉbar' }], + [')-token', ')', 58, 58, undefined], + ['whitespace-token', ' ', 59, 59, undefined], + ['ident-token', 'and', 60, 62, { value: 'and' }], + ['whitespace-token', ' ', 63, 63, undefined], + ['(-token', '(', 64, 64, undefined], + ['function-token', 'fancy(', 65, 70, { value: 'fancy' }], + ['ident-token', 'baz', 71, 73, { value: 'baz' }], + [')-token', ')', 74, 74, undefined], + [')-token', ')', 75, 75, undefined], + [')-token', ')', 76, 76, undefined], + ['whitespace-token', ' ', 77, 77, undefined], + ['{-token', '{', 78, 78, undefined], + ['}-token', '}', 79, 79, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} diff --git a/packages/css-tokenizer/test/token/comment.mjs b/packages/css-tokenizer/test/token/comment.mjs new file mode 100644 index 000000000..230f3965b --- /dev/null +++ b/packages/css-tokenizer/test/token/comment.mjs @@ -0,0 +1,88 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; + +{ + const t = tokenizer({ + css: 'a /* a comment */ b', + }, { commentsAreTokens : true }); + + assert.deepEqual( + collectTokens(t), + [ + ['ident-token', 'a', 0, 0, { value: 'a' }], + ['whitespace-token', ' ', 1, 1, undefined], + ['comment', '/* a comment */', 2, 16, undefined], + ['whitespace-token', ' ', 17, 17, undefined], + ['ident-token', 'b', 18, 18, { value: 'b' }], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'a/* a comment */b', + }, { commentsAreTokens: true }); + + assert.deepEqual( + collectTokens(t), + [ + ['ident-token', 'a', 0, 0, { value: 'a' }], + ['comment', '/* a comment */', 1, 15, undefined], + ['ident-token', 'b', 16, 16, { value: 'b' }], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'a /* a comment', + }, { commentsAreTokens: true }); + + assert.deepEqual( + collectTokens(t), + [ + ['ident-token', 'a', 0, 0, { value: 'a' }], + ['whitespace-token', ' ', 1, 1, undefined], + ['comment', '/* a comment', 2, 13, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: `a /* a comment +*/`, + }, { commentsAreTokens: true }); + + assert.deepEqual( + collectTokens(t), + [ + ['ident-token', 'a', 0, 0, { value: 'a' }], + ['whitespace-token', ' ', 1, 1, undefined], + ['comment', '/* a comment\n*/', 2, 16, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'a /* a comment \\*/ b', + }, { commentsAreTokens: true }); + + assert.deepEqual( + collectTokens(t), + [ + ['ident-token', 'a', 0, 0, { value: 'a' }], + ['whitespace-token', ' ', 1, 1, undefined], + ['comment', '/* a comment \\*/', 2, 17, undefined], + ['whitespace-token', ' ', 18, 18, undefined], + ['ident-token', 'b', 19, 19, { value: 'b' }], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} diff --git a/packages/css-tokenizer/test/token/numeric.mjs b/packages/css-tokenizer/test/token/numeric.mjs new file mode 100644 index 000000000..0ee6c1f0f --- /dev/null +++ b/packages/css-tokenizer/test/token/numeric.mjs @@ -0,0 +1,427 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; + +{ + const t = tokenizer({ + css: '10px ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'dimension-token', + '10px', + 0, + 3, + { value: 10, type: 'integer', unit: 'px' }, + ], + ['whitespace-token', ' ', 4, 4, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '4.01 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '4.01', 0, 3, { value: 4.01, type: 'number' }], + ['whitespace-token', ' ', 4, 4, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-456.8 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'number-token', + '-456.8', + 0, + 5, + { value: -456.8, type: 'number' }, + ], + ['whitespace-token', ' ', 6, 6, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '0.0 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '0.0', 0, 2, { value: 0, type: 'number' }], + ['whitespace-token', ' ', 3, 3, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '+0.0 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '+0.0', 0, 3, { value: 0, type: 'number' }], + ['whitespace-token', ' ', 4, 4, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-0.0 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '-0.0', 0, 3, { value: -0, type: 'number' }], + ['whitespace-token', ' ', 4, 4, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '.60 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '.60', 0, 2, { value: 0.6, type: 'number' }], + ['whitespace-token', ' ', 3, 3, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '10e3 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '10e3', 0, 3, { value: 10000, type: 'number' }], + ['whitespace-token', ' ', 4, 4, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-3.4e-2 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'number-token', + '-3.4e-2', + 0, + 6, + { value: -0.034, type: 'number' }, + ], + ['whitespace-token', ' ', 7, 7, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-3.4e2 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '-3.4e2', 0, 5, { value: -340, type: 'number' }], + ['whitespace-token', ' ', 6, 6, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-3.4e+2 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'number-token', + '-3.4e+2', + 0, + 6, + { value: -340, type: 'number' }, + ], + ['whitespace-token', ' ', 7, 7, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-3.4e ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'dimension-token', + '-3.4e', + 0, + 4, + { value: -3.4, type: 'number', unit: 'e' }, + ], + ['whitespace-token', ' ', 5, 5, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '-3.4ef ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'dimension-token', + '-3.4ef', + 0, + 5, + { value: -3.4, type: 'number', unit: 'ef' }, + ], + ['whitespace-token', ' ', 6, 6, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '1e2 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'number-token', + '1e2', + 0, + 2, + { value: 100, type: 'number' }, + ], + ['whitespace-token', ' ', 3, 3, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '12rem ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'dimension-token', + '12rem', + 0, + 4, + { value: 12, type: 'integer', unit: 'rem' }, + ], + ['whitespace-token', ' ', 5, 5, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '12.2rem ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'dimension-token', + '12.2rem', + 0, + 6, + { value: 12.2, type: 'number', unit: 'rem' }, + ], + ['whitespace-token', ' ', 7, 7, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '13% ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['percentage-token', '13%', 0, 2, { value: 13 }], + ['whitespace-token', ' ', 3, 3, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '0.13% ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['percentage-token', '0.13%', 0, 4, { value: 0.13 }], + ['whitespace-token', ' ', 5, 5, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '14--foo ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ + 'dimension-token', + '14--foo', + 0, + 6, + { value: 14, type: 'integer', unit: '--foo' }, + ], + ['whitespace-token', ' ', 7, 7, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '12. ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '12', 0, 1, { value: 12, type: 'integer' }], + ['delim-token', '.', 2, 2, { value: '.' }], + ['whitespace-token', ' ', 3, 3, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '+-12.2 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['delim-token', '+', 0, 0, { value: '+' }], + ['number-token', '-12.2', 1, 5, { value: -12.2, type: 'number' }], + ['whitespace-token', ' ', 6, 6, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: '12.1.1 ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['number-token', '12.1', 0, 3, { value: 12.1, type: 'number' }], + ['number-token', '.1', 4, 5, { value: 0.1, type: 'number' }], + ['whitespace-token', ' ', 6, 6, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: ':nth-last-child(n+3) ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + [ 'colon-token', ':', 0, 0, undefined ], + [ + 'function-token', + 'nth-last-child(', + 1, + 15, + { value: 'nth-last-child' }, + ], + [ 'ident-token', 'n', 16, 16, { value: 'n' } ], + [ 'number-token', '+3', 17, 18, { value: 3, type: 'integer' } ], + [')-token', ')', 19, 19, undefined], + ['whitespace-token', ' ', 20, 20, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: ':nth-last-child( n + 3 ) ', + }); + + assert.deepEqual( + collectTokens(t).slice(0, -1), + [ + ['colon-token', ':', 0, 0, undefined], + [ + 'function-token', + 'nth-last-child(', + 1, + 15, + { value: 'nth-last-child' }, + ], + ['whitespace-token', ' ', 16, 16, undefined], + ['ident-token', 'n', 17, 17, { value: 'n' }], + ['whitespace-token', ' ', 18, 18, undefined], + ['delim-token', '+', 19, 19, { value: '+' }], + ['whitespace-token', ' ', 20, 20, undefined], + ['number-token', '3', 21, 21, { value: 3, type: 'integer' }], + ['whitespace-token', ' ', 22, 22, undefined], + [')-token', ')', 23, 23, undefined], + ['whitespace-token', ' ', 24, 24, undefined], + ], + ); +} diff --git a/packages/css-tokenizer/test/token/url.mjs b/packages/css-tokenizer/test/token/url.mjs new file mode 100644 index 000000000..eeada65c4 --- /dev/null +++ b/packages/css-tokenizer/test/token/url.mjs @@ -0,0 +1,261 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import assert from 'assert'; +import { collectTokens } from '../util/collect-tokens.mjs'; +import fs from 'fs'; + +{ + const t = tokenizer({ + css: ` +url( a )\ +Url( b )\ +uRl( c )\ +urL( d )\ +URl( e )\ +uRL( f )\ +UrL( g )\ +URL( h )\ +`, + }); + + assert.deepEqual( + collectTokens(t), + [ + ['whitespace-token', '\n', 0, 0, undefined], + ['url-token', 'url( a )', 1, 8, { value: 'a' }], + ['url-token', 'Url( b )', 9, 16, { value: 'b' }], + ['url-token', 'uRl( c )', 17, 24, { value: 'c' }], + ['url-token', 'urL( d )', 25, 32, { value: 'd' }], + ['url-token', 'URl( e )', 33, 40, { value: 'e' }], + ['url-token', 'uRL( f )', 41, 48, { value: 'f' }], + ['url-token', 'UrL( g )', 49, 56, { value: 'g' }], + ['url-token', 'URL( h )', 57, 64, { value: 'h' }], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: ` +url("\\ +https://\\ +example.com\\ +/some-path/\\ +?query=param\\ +&more-query=params")`, + }); + + assert.deepEqual( + collectTokens(t), + [ + ['whitespace-token', '\n', 0, 0, undefined], + ['function-token', 'url(', 1, 4, { value: 'url' }], + [ + 'string-token', + '"\\\n' + + 'https://\\\n' + + 'example.com\\\n' + + '/some-path/\\\n' + + '?query=param\\\n' + + '&more-query=params"', + 5, + 76, + { + value: 'https://example.com/some-path/?query=param&more-query=params', + }, + ], + [')-token', ')', 77, 77, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: fs.readFileSync('./test/css/multi-line.css').toString(), + }); + + assert.deepEqual( + collectTokens(t), + [ + ['delim-token', '.', 0, 0, { value: '.' }], + ['ident-token', 'foo', 1, 3, { value: 'foo' }], + ['whitespace-token', ' ', 4, 4, undefined], + ['{-token', '{', 5, 5, undefined], + ['whitespace-token', '\n\t', 6, 7, undefined], + ['ident-token', 'src', 8, 10, { value: 'src' }], + ['colon-token', ':', 11, 11, undefined], + ['whitespace-token', ' ', 12, 12, undefined], + ['function-token', 'url(', 13, 16, { value: 'url' }], + [ + 'string-token', + '"\\\n' + + 'https: //\\\n' + + 'example.com\\\n' + + '/some-path/\\\n' + + '?query=param\\\n' + + '&more-query=params"', + 17, + 89, + { + value: 'https: //example.com/some-path/?query=param&more-query=params', + }, + ], + [')-token', ')', 90, 90, undefined], + ['semicolon-token', ';', 91, 91, undefined], + ['whitespace-token', '\n', 92, 92, undefined], + ['}-token', '}', 93, 93, undefined], + ['whitespace-token', '\n', 94, 94, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'url(https://example.com a', + }); + + assert.deepEqual( + collectTokens(t), + [ + ['bad-url-token', 'url(https://example.com a', 0, 24, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'url("https://example.com', + }); + + assert.deepEqual( + collectTokens(t), + [ + ['function-token', 'url(', 0, 3, { value: 'url' }], + [ + 'string-token', + '"https://example.com', + 4, + 23, + { value: 'https://example.com' }, + ], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'url("https://example.com ', + }); + + assert.deepEqual( + collectTokens(t), + [ + ['function-token', 'url(', 0, 3, { value: 'url' }], + [ + 'string-token', + '"https://example.com ', + 4, + 24, + { value: 'https://example.com ' }, + ], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'url(https://example.com', + }); + + assert.deepEqual( + collectTokens(t), + [ + [ + 'url-token', + 'url(https://example.com', + 0, + 22, + { value: 'https://example.com' }, + ], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: 'url(https://example.com ', + }); + + assert.deepEqual( + collectTokens(t), + [ + [ + 'url-token', + 'url(https://example.com ', + 0, + 23, + { value: 'https://example.com' }, + ], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} + +{ + const t = tokenizer({ + css: ` +url( 'single-quoted' )\ +url( "double-quoted" )\ +url( 'mix-quoted" ' )\ +`, + }); + + assert.deepEqual( + collectTokens(t), + [ + ['whitespace-token', '\n', 0, 0, undefined], + ['function-token', 'url(', 1, 4, { value: 'url' }], + ['whitespace-token', ' ', 5, 5, undefined], + [ + 'string-token', + // eslint-disable-next-line quotes + "'single-quoted'", + 6, + 20, + { value: 'single-quoted' }, + ], + ['whitespace-token', ' ', 21, 21, undefined], + [')-token', ')', 22, 22, undefined], + ['function-token', 'url(', 23, 26, { value: 'url' }], + ['whitespace-token', ' ', 27, 27, undefined], + [ + 'string-token', + '"double-quoted"', + 28, + 42, + { value: 'double-quoted' }, + ], + ['whitespace-token', ' ', 43, 43, undefined], + [')-token', ')', 44, 44, undefined], + ['function-token', 'url(', 45, 48, { value: 'url' }], + ['whitespace-token', ' ', 49, 49, undefined], + [ + 'string-token', + // eslint-disable-next-line quotes + `'mix-quoted" '`, + 50, + 63, + { value: 'mix-quoted" ' }, + ], + ['whitespace-token', ' ', 64, 64, undefined], + [')-token', ')', 65, 65, undefined], + ['EOF-token', '', -1, -1, undefined], + ], + ); +} diff --git a/packages/css-tokenizer/test/util/collect-tokens.mjs b/packages/css-tokenizer/test/util/collect-tokens.mjs new file mode 100644 index 000000000..5ab1c998b --- /dev/null +++ b/packages/css-tokenizer/test/util/collect-tokens.mjs @@ -0,0 +1,11 @@ +export function collectTokens(t) { + const bag = []; + + while (!t.endOfFile()) { + bag.push(t.nextToken()); + } + + bag.push(t.nextToken()); // EOF-token + + return bag; +} diff --git a/packages/css-tokenizer/tsconfig.json b/packages/css-tokenizer/tsconfig.json new file mode 100644 index 000000000..e0d06239c --- /dev/null +++ b/packages/css-tokenizer/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "." + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} diff --git a/packages/media-query-list-parser/.gitignore b/packages/media-query-list-parser/.gitignore new file mode 100644 index 000000000..f548255b0 --- /dev/null +++ b/packages/media-query-list-parser/.gitignore @@ -0,0 +1,7 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.json +dist/* diff --git a/packages/media-query-list-parser/.nvmrc b/packages/media-query-list-parser/.nvmrc new file mode 100644 index 000000000..f0b10f153 --- /dev/null +++ b/packages/media-query-list-parser/.nvmrc @@ -0,0 +1 @@ +v16.13.1 diff --git a/packages/media-query-list-parser/CHANGELOG.md b/packages/media-query-list-parser/CHANGELOG.md new file mode 100644 index 000000000..b0ff6b082 --- /dev/null +++ b/packages/media-query-list-parser/CHANGELOG.md @@ -0,0 +1,3 @@ +### 1.0.0 + +- Initial version diff --git a/packages/media-query-list-parser/LICENSE.md b/packages/media-query-list-parser/LICENSE.md new file mode 100644 index 000000000..af5411fa2 --- /dev/null +++ b/packages/media-query-list-parser/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2022 Romain Menke, Antonio Laguna + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/media-query-list-parser/README.md b/packages/media-query-list-parser/README.md new file mode 100644 index 000000000..4f17a86b8 --- /dev/null +++ b/packages/media-query-list-parser/README.md @@ -0,0 +1,60 @@ +# Media Query List Parser + +[npm version][npm-url] +[Build Status][cli-url] +[Discord][discord] + +Implemented from : https://www.w3.org/TR/mediaqueries-5/ + +## Usage + +Add [Media Query List Parser] to your project: + +```bash +npm install postcss @csstools/media-query-list-parser --save-dev +``` + +```ts +import { parse } from '@csstools/media-query-list-parser'; + +export function parseCustomMedia() { + const mediaQueryList = parse('screen and (min-width: 300px), (50px < height < 30vw)'); + + mediaQueryList.forEach((mediaQuery) => { + mediaQuery.walk((entry, index) => { + // Index of the current Node in `parent`. + console.log(index); + // Type of `parent`. + console.log(entry.parent.type); + + // Type of `node` + { + // Sometimes nodes can be arrays. + if (Array.isArray(entry.node)) { + entry.node.forEach((item) => { + console.log(item.type); + }); + } + + if ('type' in entry.node) { + console.log(entry.node.type); + } + } + + // stringified version of the current node. + console.log(entry.node.toString()); + + // Return `false` to stop the walker. + return false; + }); + }); +} +``` + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/media-query-list-parser + +[Media Query List Parser]: https://github.com/csstools/postcss-plugins/tree/main/packages/media-query-list-parser + + diff --git a/packages/media-query-list-parser/package.json b/packages/media-query-list-parser/package.json new file mode 100644 index 000000000..fa77ed6e8 --- /dev/null +++ b/packages/media-query-list-parser/package.json @@ -0,0 +1,71 @@ +{ + "name": "@csstools/media-query-list-parser", + "description": "Parse CSS media query lists.", + "version": "1.0.0", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke", + "email": "romainmenke@gmail.com" + } + ], + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^1.0.0", + "@csstools/css-tokenizer": "^1.0.0" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.js", + "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", + "lint": "npm run lint:eslint && npm run lint:package-json", + "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", + "lint:package-json": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run clean && npm run build && npm run test", + "stryker": "stryker run --logLevel error", + "test": "node ./test/test.mjs", + "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", + "test:rewrite-expects": "REWRITE_EXPECTS=true node ./test/test.mjs" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/media-query-list-parser#readme", + "repository": { + "type": "git", + "url": "https://github.com/csstools/postcss-plugins.git", + "directory": "packages/media-query-list-parser" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "css", + "media query", + "parser" + ], + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/media-query-list-parser/src/index.ts b/packages/media-query-list-parser/src/index.ts new file mode 100644 index 000000000..57680d295 --- /dev/null +++ b/packages/media-query-list-parser/src/index.ts @@ -0,0 +1,46 @@ +export { parse, parseFromTokens } from './parser/parse'; +export { NodeType } from './util/node-type'; +export { + isGeneralEnclosed, + isMediaAnd, + isMediaCondition, + isMediaConditionList, + isMediaConditionListWithAnd, + isMediaConditionListWithOr, + isMediaFeature, + isMediaFeatureBoolean, + isMediaFeatureName, + isMediaFeaturePlain, + isMediaFeatureRange, + isMediaFeatureRangeNameValue, + isMediaFeatureRangeValueName, + isMediaFeatureRangeValueNameValue, + isMediaFeatureValue, + isMediaInParens, + isMediaNot, + isMediaOr, + isMediaQuery, + isMediaQueryInvalid, + isMediaQueryWithType, + isMediaQueryWithoutType, +} from './util/type-predicates'; + +export { GeneralEnclosed } from './nodes/general-enclosed'; +export { MediaAnd } from './nodes/media-and'; +export { MediaCondition } from './nodes/media-condition'; +export { MediaConditionList, MediaConditionListWithAnd, MediaConditionListWithOr } from './nodes/media-condition-list'; +export { MediaFeature, newMediaFeatureBoolean, newMediaFeaturePlain } from './nodes/media-feature'; +export { MediaFeatureBoolean } from './nodes/media-feature-boolean'; +export { MediaFeatureName } from './nodes/media-feature-name'; +export { MediaFeaturePlain } from './nodes/media-feature-plain'; +export { MediaFeatureRange, MediaFeatureRangeNameValue, MediaFeatureRangeValueName, MediaFeatureRangeValueNameValue } from './nodes/media-feature-range'; +export { MediaFeatureValue, matchesRatio, matchesRatioExactly } from './nodes/media-feature-value'; +export { MediaInParens } from './nodes/media-in-parens'; +export { MediaNot } from './nodes/media-not'; +export { MediaOr } from './nodes/media-or'; +export { MediaQuery, MediaQueryWithType, MediaQueryWithoutType, MediaQueryInvalid } from './nodes/media-query'; + +export { MediaFeatureComparison, MediaFeatureEQ, MediaFeatureGT, MediaFeatureLT, invertComparison, matchesComparison, comparisonFromTokens } from './nodes/media-feature-comparison'; +export { MediaType, typeFromToken } from './nodes/media-type'; +export { MediaQueryModifier, modifierFromToken } from './nodes/media-query-modifier'; +export { cloneMediaQuery } from './util/clone-media-query'; diff --git a/packages/media-query-list-parser/src/nodes/general-enclosed.ts b/packages/media-query-list-parser/src/nodes/general-enclosed.ts new file mode 100644 index 000000000..73d443cba --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/general-enclosed.ts @@ -0,0 +1,71 @@ +import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms'; +import { CSSToken } from '@csstools/css-tokenizer'; +import { NodeType } from '../util/node-type'; + +export class GeneralEnclosed { + type = NodeType.GeneralEnclosed; + + value: ComponentValue; + + constructor(value: ComponentValue) { + this.value = value; + } + + tokens(): Array { + return this.value.tokens(); + } + + toString(): string { + return this.value.toString(); + } + + indexOf(item: ComponentValue): number | string { + if (item === this.value) { + return 'value'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'value') { + return this.value; + } + } + + walk(cb: (entry: { node: GeneralEnclosedWalkerEntry, parent: GeneralEnclosedWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.value, parent: this }, 'value') === false) { + return false; + } + + if ('walk' in this.value) { + return this.value.walk(cb); + } + } + + toJSON() { + return { + type: this.type, + tokens: this.tokens(), + }; + } + + isGeneralEnclosed(): this is GeneralEnclosed { + return GeneralEnclosed.isGeneralEnclosed(this); + } + + static isGeneralEnclosed(x: unknown): x is GeneralEnclosed { + if (!x) { + return false; + } + + if (!(x instanceof GeneralEnclosed)) { + return false; + } + + return x.type === NodeType.GeneralEnclosed; + } +} + +export type GeneralEnclosedWalkerEntry = ComponentValue; +export type GeneralEnclosedWalkerParent = ContainerNode | GeneralEnclosed; diff --git a/packages/media-query-list-parser/src/nodes/media-and.ts b/packages/media-query-list-parser/src/nodes/media-and.ts new file mode 100644 index 000000000..376d79f8b --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-and.ts @@ -0,0 +1,75 @@ +import { CSSToken, stringify } from '@csstools/css-tokenizer'; +import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens'; +import { NodeType } from '../util/node-type'; + +export class MediaAnd { + type = NodeType.MediaAnd; + + modifier: Array; + media: MediaInParens; + + constructor(modifier: Array, media: MediaInParens) { + this.modifier = modifier; + this.media = media; + } + + tokens(): Array { + return [ + ...this.modifier, + ...this.media.tokens(), + ]; + } + + toString(): string { + return stringify(...this.modifier) + this.media.toString(); + } + + indexOf(item: MediaInParens): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaAndWalkerEntry, parent: MediaAndWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + return this.media.walk(cb); + } + + toJSON() { + return { + type: this.type, + modifier: this.modifier, + media: this.media.toJSON(), + }; + } + + isMediaAnd(): this is MediaAnd { + return MediaAnd.isMediaAnd(this); + } + + static isMediaAnd(x: unknown): x is MediaAnd { + if (!x) { + return false; + } + + if (!(x instanceof MediaAnd)) { + return false; + } + + return x.type === NodeType.MediaAnd; + } +} + +export type MediaAndWalkerEntry = MediaInParensWalkerEntry | MediaInParens; +export type MediaAndWalkerParent = MediaInParensWalkerParent | MediaAnd; diff --git a/packages/media-query-list-parser/src/nodes/media-condition-list.ts b/packages/media-query-list-parser/src/nodes/media-condition-list.ts new file mode 100644 index 000000000..9ba0e4499 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-condition-list.ts @@ -0,0 +1,245 @@ +import { CSSToken, stringify } from '@csstools/css-tokenizer'; +import { MediaAnd, MediaAndWalkerEntry, MediaAndWalkerParent } from './media-and'; +import { MediaInParens } from './media-in-parens'; +import { MediaOr, MediaOrWalkerEntry, MediaOrWalkerParent } from './media-or'; +import { NodeType } from '../util/node-type'; + +export type MediaConditionList = MediaConditionListWithAnd | MediaConditionListWithOr; + +export class MediaConditionListWithAnd { + type = NodeType.MediaConditionListWithAnd; + + leading: MediaInParens; + list: Array; + before: Array; + after: Array; + + constructor(leading: MediaInParens, list: Array, before: Array = [], after: Array = []) { + this.leading = leading; + this.list = list; + this.before = before; + this.after = after; + } + + tokens(): Array { + return [ + ...this.before, + ...this.leading.tokens(), + ...this.list.flatMap((item) => item.tokens()), + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + this.leading.toString() + this.list.map((item) => item.toString()).join('') + stringify(...this.after); + } + + indexOf(item: MediaInParens | MediaAnd): number | string { + if (item === this.leading) { + return 'leading'; + } + + if (item.type === 'media-and') { + return this.list.indexOf(item as MediaAnd); + } + + return -1; + } + + at(index: number | string) { + if (index === 'leading') { + return this.leading; + } + + if (typeof index === 'number') { + if (index < 0) { + index = this.list.length + index; + } + return this.list[index]; + } + } + + walk(cb: (entry: { node: MediaConditionListWithAndWalkerEntry, parent: MediaConditionListWithAndWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.leading, parent: this }, 'leading') === false) { + return false; + } + + if ('walk' in this.leading) { + if (this.leading.walk(cb) === false) { + return false; + } + } + + let aborted = false; + + this.list.forEach((child, index) => { + if (aborted) { + return; + } + + if (cb({ node: child, parent: this }, index) === false) { + aborted = true; + return; + } + + if ('walk' in child) { + if (child.walk(cb) === false) { + aborted = true; + return; + } + } + }); + + if (aborted) { + return false; + } + } + + toJSON() { + return { + type: this.type, + leading: this.leading.toJSON(), + list: this.list.map((x) => x.toJSON()), + before: this.before, + after: this.after, + }; + } + + isMediaConditionListWithAnd(): this is MediaConditionListWithAnd { + return MediaConditionListWithAnd.isMediaConditionListWithAnd(this); + } + + static isMediaConditionListWithAnd(x: unknown): x is MediaConditionListWithAnd { + if (!x) { + return false; + } + + if (!(x instanceof MediaConditionListWithAnd)) { + return false; + } + + return x.type === NodeType.MediaConditionListWithAnd; + } +} + +export type MediaConditionListWithAndWalkerEntry = MediaAndWalkerEntry | MediaAnd; +export type MediaConditionListWithAndWalkerParent = MediaAndWalkerParent | MediaConditionListWithAnd; + +export class MediaConditionListWithOr { + type = NodeType.MediaConditionListWithOr; + + leading: MediaInParens; + list: Array; + before: Array; + after: Array; + + constructor(leading: MediaInParens, list: Array, before: Array = [], after: Array = []) { + this.leading = leading; + this.list = list; + this.before = before; + this.after = after; + } + + tokens(): Array { + return [ + ...this.before, + ...this.leading.tokens(), + ...this.list.flatMap((item) => item.tokens()), + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + this.leading.toString() + this.list.map((item) => item.toString()).join('') + stringify(...this.after); + } + + indexOf(item: MediaInParens | MediaOr): number | string { + if (item === this.leading) { + return 'leading'; + } + + if (item.type === 'media-or') { + return this.list.indexOf(item as MediaOr); + } + + return -1; + } + + at(index: number | string) { + if (index === 'leading') { + return this.leading; + } + + if (typeof index === 'number') { + if (index < 0) { + index = this.list.length + index; + } + return this.list[index]; + } + } + + walk(cb: (entry: { node: MediaConditionListWithOrWalkerEntry, parent: MediaConditionListWithOrWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.leading, parent: this }, 'leading') === false) { + return false; + } + + if ('walk' in this.leading) { + if (this.leading.walk(cb) === false) { + return false; + } + } + + let aborted = false; + + this.list.forEach((child, index) => { + if (aborted) { + return; + } + + if (cb({ node: child, parent: this }, index) === false) { + aborted = true; + return; + } + + if ('walk' in child) { + if (child.walk(cb) === false) { + aborted = true; + return; + } + } + }); + + if (aborted) { + return false; + } + } + + toJSON() { + return { + type: this.type, + leading: this.leading.toJSON(), + list: this.list.map((x) => x.toJSON()), + before: this.before, + after: this.after, + }; + } + + isMediaConditionListWithOr(): this is MediaConditionListWithOr { + return MediaConditionListWithOr.isMediaConditionListWithOr(this); + } + + static isMediaConditionListWithOr(x: unknown): x is MediaConditionListWithOr { + if (!x) { + return false; + } + + if (!(x instanceof MediaConditionListWithOr)) { + return false; + } + + return x.type === NodeType.MediaConditionListWithOr; + } +} + +export type MediaConditionListWithOrWalkerEntry = MediaOrWalkerEntry | MediaOr; +export type MediaConditionListWithOrWalkerParent = MediaOrWalkerParent | MediaConditionListWithOr; diff --git a/packages/media-query-list-parser/src/nodes/media-condition.ts b/packages/media-query-list-parser/src/nodes/media-condition.ts new file mode 100644 index 000000000..766c39eaf --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-condition.ts @@ -0,0 +1,71 @@ +import { CSSToken } from '@csstools/css-tokenizer'; +import { MediaConditionListWithAnd, MediaConditionListWithAndWalkerEntry, MediaConditionListWithAndWalkerParent, MediaConditionListWithOr, MediaConditionListWithOrWalkerEntry, MediaConditionListWithOrWalkerParent } from './media-condition-list'; +import { MediaInParens } from './media-in-parens'; +import { MediaNot, MediaNotWalkerEntry, MediaNotWalkerParent } from './media-not'; +import { NodeType } from '../util/node-type'; + +export class MediaCondition { + type = NodeType.MediaCondition; + + media: MediaNot | MediaInParens | MediaConditionListWithAnd | MediaConditionListWithOr; + + constructor(media: MediaNot | MediaInParens |MediaConditionListWithAnd | MediaConditionListWithOr) { + this.media = media; + } + + tokens(): Array { + return this.media.tokens(); + } + + toString(): string { + return this.media.toString(); + } + + indexOf(item: MediaNot | MediaInParens | MediaConditionListWithAnd | MediaConditionListWithOr): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaConditionWalkerEntry, parent: MediaConditionWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + return this.media.walk(cb); + } + + toJSON() { + return { + type: this.type, + media: this.media.toJSON(), + }; + } + + isMediaCondition(): this is MediaCondition { + return MediaCondition.isMediaCondition(this); + } + + static isMediaCondition(x: unknown): x is MediaCondition { + if (!x) { + return false; + } + + if (!(x instanceof MediaCondition)) { + return false; + } + + return x.type === NodeType.MediaCondition; + } +} + +export type MediaConditionWalkerEntry = MediaNotWalkerEntry | MediaConditionListWithAndWalkerEntry | MediaConditionListWithOrWalkerEntry | MediaNot | MediaConditionListWithAnd | MediaConditionListWithOr; +export type MediaConditionWalkerParent = MediaNotWalkerParent | MediaConditionListWithAndWalkerParent | MediaConditionListWithOrWalkerParent | MediaCondition; diff --git a/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts b/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts new file mode 100644 index 000000000..f53344500 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature-boolean.ts @@ -0,0 +1,82 @@ +import { ComponentValue, TokenNode } from '@csstools/css-parser-algorithms'; +import { parseMediaFeatureName } from './media-feature-name'; +import { NodeType } from '../util/node-type'; +import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer'; + +export class MediaFeatureBoolean { + type = NodeType.MediaFeatureBoolean; + + name: ComponentValue; + before: Array; + after: Array; + + constructor(name: ComponentValue, before: Array = [], after: Array = []) { + this.name = name; + this.before = before; + this.after = after; + } + + getName() { + const token = (((this.name as TokenNode).value as CSSToken) as TokenIdent); + return token[4].value; + } + + tokens(): Array { + return [ + ...this.before, + ...this.name.tokens(), + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + this.name.toString() + stringify(...this.after); + } + + indexOf(item: ComponentValue): number | string { + if (item === this.name) { + return 'name'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'name') { + return this.name; + } + } + + toJSON() { + return { + type: this.type, + name: this.getName(), + tokens: this.tokens(), + }; + } + + isMediaFeatureBoolean(): this is MediaFeatureBoolean { + return MediaFeatureBoolean.isMediaFeatureBoolean(this); + } + + static isMediaFeatureBoolean(x: unknown): x is MediaFeatureBoolean { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeatureBoolean)) { + return false; + } + + return x.type === NodeType.MediaFeatureBoolean; + } +} + +export function parseMediaFeatureBoolean(componentValues: Array) { + const mediaFeatureName = parseMediaFeatureName(componentValues); + if (mediaFeatureName === false) { + return mediaFeatureName; + } + + return new MediaFeatureBoolean(mediaFeatureName.name, mediaFeatureName.before, mediaFeatureName.after); +} diff --git a/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts new file mode 100644 index 000000000..60520ca74 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature-comparison.ts @@ -0,0 +1,114 @@ +import { ComponentValue, ComponentValueType } from '@csstools/css-parser-algorithms'; +import { CSSToken, TokenDelim, TokenType } from '@csstools/css-tokenizer'; + +export enum MediaFeatureLT { + LT = '<', + LT_OR_EQ = '<=', +} + +export enum MediaFeatureGT { + GT = '>', + GT_OR_EQ = '>=', +} + +export enum MediaFeatureEQ { + EQ = '=', +} + +export type MediaFeatureComparison = MediaFeatureLT | MediaFeatureGT | MediaFeatureEQ + +export function matchesComparison(componentValues: Array): false | [number, number] { + let firstTokenIndex = -1; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Token) { + const token = componentValue.value as CSSToken; + if (token[0] === TokenType.Delim) { + if (token[4].value === MediaFeatureEQ.EQ) { + if (firstTokenIndex !== -1) { + return [firstTokenIndex, i]; + } + + firstTokenIndex = i; + continue; + } + if (token[4].value === MediaFeatureLT.LT) { + firstTokenIndex = i; + continue; + } + if (token[4].value === MediaFeatureGT.GT) { + firstTokenIndex = i; + continue; + } + } + } + + break; + } + + if (firstTokenIndex !== -1) { + return [firstTokenIndex, firstTokenIndex]; + } + + return false; +} + +export function comparisonFromTokens(tokens: [TokenDelim, TokenDelim] | [TokenDelim]): MediaFeatureComparison | false { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + /* @ts-ignore */ + if (tokens.length === 0 || tokens.length > 2) { + return false; + } + + if (tokens[0][0] !== TokenType.Delim) { + return false; + } + + if (tokens.length === 1) { + switch (tokens[0][4].value) { + case MediaFeatureEQ.EQ: + return MediaFeatureEQ.EQ; + case MediaFeatureLT.LT: + return MediaFeatureLT.LT; + case MediaFeatureGT.GT: + return MediaFeatureGT.GT; + default: + return false; + } + } + + if (tokens[1][0] !== TokenType.Delim) { + return false; + } + + if (tokens[1][4].value !== MediaFeatureEQ.EQ) { + return false; + } + + switch (tokens[0][4].value) { + case MediaFeatureLT.LT: + return MediaFeatureLT.LT_OR_EQ; + case MediaFeatureGT.GT: + return MediaFeatureGT.GT_OR_EQ; + default: + return false; + } +} + +export function invertComparison(operator: MediaFeatureComparison): MediaFeatureComparison | false { + switch (operator) { + case MediaFeatureEQ.EQ: + return MediaFeatureEQ.EQ; + case MediaFeatureLT.LT: + return MediaFeatureGT.GT; + case MediaFeatureLT.LT_OR_EQ: + return MediaFeatureGT.GT_OR_EQ; + case MediaFeatureGT.GT: + return MediaFeatureLT.LT; + case MediaFeatureGT.GT_OR_EQ: + return MediaFeatureLT.LT_OR_EQ; + default: + return false; + } +} diff --git a/packages/media-query-list-parser/src/nodes/media-feature-name.ts b/packages/media-query-list-parser/src/nodes/media-feature-name.ts new file mode 100644 index 000000000..85bd0e592 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature-name.ts @@ -0,0 +1,113 @@ +import { ComponentValue, ComponentValueType, TokenNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify, TokenIdent } from '@csstools/css-tokenizer'; +import { isIdent } from '../util/component-value-is'; +import { NodeType } from '../util/node-type'; + +export class MediaFeatureName { + type = NodeType.MediaFeatureName; + + name: ComponentValue; + before: Array; + after: Array; + + constructor(name: ComponentValue, before: Array = [], after: Array = []) { + this.name = name; + this.before = before; + this.after = after; + } + + getName() { + const token = (((this.name as TokenNode).value as CSSToken) as TokenIdent); + return token[4].value; + } + + tokens(): Array { + return [ + ...this.before, + ...this.name.tokens(), + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + this.name.toString() + stringify(...this.after); + } + + indexOf(item: ComponentValue): number | string { + if (item === this.name) { + return 'name'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'name') { + return this.name; + } + } + + toJSON() { + return { + type: this.type, + name: this.getName(), + tokens: this.tokens(), + }; + } + + isMediaFeatureName(): this is MediaFeatureName { + return MediaFeatureName.isMediaFeatureName(this); + } + + static isMediaFeatureName(x: unknown): x is MediaFeatureName { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeatureName)) { + return false; + } + + return x.type === NodeType.MediaFeatureName; + } +} + +export function parseMediaFeatureName(componentValues: Array) { + let singleIdentTokenIndex = -1; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (isIdent(componentValue)) { + if (singleIdentTokenIndex !== -1) { + return false; + } + + singleIdentTokenIndex = i; + continue; + } + + return false; + } + + if (singleIdentTokenIndex === -1) { + return false; + } + + return new MediaFeatureName( + componentValues[singleIdentTokenIndex], + componentValues.slice(0, singleIdentTokenIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(singleIdentTokenIndex + 1).flatMap((x) => { + return x.tokens(); + }), + ); +} diff --git a/packages/media-query-list-parser/src/nodes/media-feature-plain.ts b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts new file mode 100644 index 000000000..4d1a32d5f --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature-plain.ts @@ -0,0 +1,124 @@ +import { ComponentValue, ComponentValueType } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify, TokenColon, TokenType } from '@csstools/css-tokenizer'; +import { parseMediaFeatureName, MediaFeatureName } from './media-feature-name'; +import { parseMediaFeatureValue, MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent } from './media-feature-value'; +import { NodeType } from '../util/node-type'; + +export class MediaFeaturePlain { + type = NodeType.MediaFeaturePlain; + + name: MediaFeatureName; + colon: TokenColon; + value: MediaFeatureValue; + + constructor(name: MediaFeatureName, colon: TokenColon, value: MediaFeatureValue) { + this.name = name; + this.colon = colon; + this.value = value; + } + + tokens(): Array { + return [ + ...this.name.tokens(), + this.colon, + ...this.value.tokens(), + ]; + } + + toString(): string { + return this.name.toString() + stringify(this.colon) + this.value.toString(); + } + + indexOf(item: MediaFeatureName | MediaFeatureValue): number | string { + if (item === this.name) { + return 'name'; + } + + if (item === this.value) { + return 'value'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'name') { + return this.name; + } + + if (index === 'value') { + return this.value; + } + } + + walk(cb: (entry: { node: MediaFeaturePlainWalkerEntry, parent: MediaFeaturePlainWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.value, parent: this }, 'value') === false) { + return false; + } + + return this.value.walk(cb); + } + + toJSON() { + return { + type: this.type, + name: this.name.toJSON(), + value: this.value.toJSON(), + tokens: this.tokens(), + }; + } + + isMediaFeaturePlain(): this is MediaFeaturePlain { + return MediaFeaturePlain.isMediaFeaturePlain(this); + } + + static isMediaFeaturePlain(x: unknown): x is MediaFeaturePlain { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeaturePlain)) { + return false; + } + + return x.type === NodeType.MediaFeaturePlain; + } +} + +export type MediaFeaturePlainWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue; +export type MediaFeaturePlainWalkerParent = MediaFeatureValueWalkerParent | MediaFeaturePlain; + +export function parseMediaFeaturePlain(componentValues: Array) { + let a: Array = []; + let b: Array = []; + let colon: TokenColon | null = null; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Token) { + const token = componentValue.value as CSSToken; + if (token[0] === TokenType.Colon) { + a = componentValues.slice(0, i); + b = componentValues.slice(i + 1); + colon = token; + break; + } + } + } + + if (!a.length || !b.length) { + return false; + } + + const name = parseMediaFeatureName(a); + if (name === false) { + return false; + } + + const value = parseMediaFeatureValue(b); + if (value === false) { + return false; + } + + return new MediaFeaturePlain(name, colon , value); +} diff --git a/packages/media-query-list-parser/src/nodes/media-feature-range.ts b/packages/media-query-list-parser/src/nodes/media-feature-range.ts new file mode 100644 index 000000000..7f9f5aff4 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature-range.ts @@ -0,0 +1,502 @@ +import { ComponentValue, ComponentValueType, TokenNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify, TokenDelim, TokenType } from '@csstools/css-tokenizer'; +import { comparisonFromTokens, matchesComparison, MediaFeatureEQ, MediaFeatureGT, MediaFeatureLT } from './media-feature-comparison'; +import { MediaFeatureName, parseMediaFeatureName } from './media-feature-name'; +import { MediaFeatureValue, MediaFeatureValueWalkerEntry, MediaFeatureValueWalkerParent, parseMediaFeatureValue } from './media-feature-value'; +import { NodeType } from '../util/node-type'; + +export type MediaFeatureRange = MediaFeatureRangeNameValue | + MediaFeatureRangeValueName | + MediaFeatureRangeValueNameValue; + +export class MediaFeatureRangeNameValue { + type = NodeType.MediaFeatureRangeNameValue; + + name: MediaFeatureName; + operator: [TokenDelim, TokenDelim] | [TokenDelim]; + value: MediaFeatureValue; + + constructor(name: MediaFeatureName, operator: [TokenDelim, TokenDelim] | [TokenDelim], value: MediaFeatureValue) { + this.name = name; + this.operator = operator; + this.value = value; + } + + operatorKind() { + return comparisonFromTokens(this.operator); + } + + tokens(): Array { + return [ + ...this.name.tokens(), + ...this.operator, + ...this.value.tokens(), + ]; + } + + toString(): string { + return this.name.toString() + stringify(...this.operator) + this.value.toString(); + } + + indexOf(item: MediaFeatureName | MediaFeatureValue): number | string { + if (item === this.name) { + return 'name'; + } + + if (item === this.value) { + return 'value'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'name') { + return this.name; + } + + if (index === 'value') { + return this.value; + } + } + + walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.value, parent: this }, 'value') === false) { + return false; + } + + if ('walk' in this.value) { + return this.value.walk(cb); + } + } + + toJSON() { + return { + type: this.type, + name: this.name.toJSON(), + value: this.value.toJSON(), + tokens: this.tokens(), + }; + } + + isMediaFeatureRangeNameValue(): this is MediaFeatureRangeNameValue { + return MediaFeatureRangeNameValue.isMediaFeatureRangeNameValue(this); + } + + static isMediaFeatureRangeNameValue(x: unknown): x is MediaFeatureRangeNameValue { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeatureRangeNameValue)) { + return false; + } + + return x.type === NodeType.MediaFeatureRangeNameValue; + } +} + +export class MediaFeatureRangeValueName { + type = NodeType.MediaFeatureRangeValueName; + + name: MediaFeatureName; + operator: [TokenDelim, TokenDelim] | [TokenDelim]; + value: MediaFeatureValue; + + constructor(name: MediaFeatureName, operator: [TokenDelim, TokenDelim] | [TokenDelim], value: MediaFeatureValue) { + this.name = name; + this.operator = operator; + this.value = value; + } + + operatorKind() { + return comparisonFromTokens(this.operator); + } + + tokens(): Array { + return [ + ...this.value.tokens(), + ...this.operator, + ...this.name.tokens(), + ]; + } + + toString(): string { + return this.value.toString() + stringify(...this.operator) + this.name.toString(); + } + + indexOf(item: MediaFeatureName | MediaFeatureValue): number | string { + if (item === this.name) { + return 'name'; + } + + if (item === this.value) { + return 'value'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'name') { + return this.name; + } + + if (index === 'value') { + return this.value; + } + } + + walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.value, parent: this }, 'value') === false) { + return false; + } + + if ('walk' in this.value) { + return this.value.walk(cb); + } + } + + toJSON() { + return { + type: this.type, + name: this.name.toJSON(), + value: this.value.toJSON(), + tokens: this.tokens(), + }; + } + + isMediaFeatureRangeValueName(): this is MediaFeatureRangeValueName { + return MediaFeatureRangeValueName.isMediaFeatureRangeValueName(this); + } + + static isMediaFeatureRangeValueName(x: unknown): x is MediaFeatureRangeValueName { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeatureRangeValueName)) { + return false; + } + + return x.type === NodeType.MediaFeatureRangeValueName; + } +} + +export class MediaFeatureRangeValueNameValue { + type = NodeType.MediaFeatureRangeValueNameValue; + + name: MediaFeatureName; + valueOne: MediaFeatureValue; + valueOneOperator: [TokenDelim, TokenDelim] | [TokenDelim]; + valueTwo: MediaFeatureValue; + valueTwoOperator: [TokenDelim, TokenDelim] | [TokenDelim]; + + constructor(name: MediaFeatureName, valueOne: MediaFeatureValue, valueOneOperator: [TokenDelim, TokenDelim] | [TokenDelim], valueTwo: MediaFeatureValue, valueTwoOperator: [TokenDelim, TokenDelim] | [TokenDelim]) { + this.name = name; + this.valueOne = valueOne; + this.valueOneOperator = valueOneOperator; + this.valueTwo = valueTwo; + this.valueTwoOperator = valueTwoOperator; + } + + valueOneOperatorKind() { + return comparisonFromTokens(this.valueOneOperator); + } + + valueTwoOperatorKind() { + return comparisonFromTokens(this.valueTwoOperator); + } + + tokens(): Array { + return [ + ...this.valueOne.tokens(), + ...this.valueOneOperator, + ...this.name.tokens(), + ...this.valueTwoOperator, + ...this.valueTwo.tokens(), + ]; + } + + toString(): string { + return this.valueOne.toString() + stringify(...this.valueOneOperator) + this.name.toString() + stringify(...this.valueTwoOperator) + this.valueTwo.toString(); + } + + indexOf(item: MediaFeatureName | MediaFeatureValue): number | string { + if (item === this.name) { + return 'name'; + } + + if (item === this.valueOne) { + return 'valueOne'; + } + + if (item === this.valueTwo) { + return 'valueTwo'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'name') { + return this.name; + } + + if (index === 'valueOne') { + return this.valueOne; + } + + if (index === 'valueTwo') { + return this.valueTwo; + } + } + + walk(cb: (entry: { node: MediaFeatureRangeWalkerEntry, parent: MediaFeatureRangeWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.valueOne, parent: this }, 'valueOne') === false) { + return false; + } + + if ('walk' in this.valueOne) { + if (this.valueOne.walk(cb) === false) { + return false; + } + } + + if (cb({ node: this.valueTwo, parent: this }, 'valueTwo') === false) { + return false; + } + + if ('walk' in this.valueTwo) { + if (this.valueTwo.walk(cb) === false) { + return false; + } + } + } + + toJSON() { + return { + type: this.type, + name: this.name.toJSON(), + valueOne: this.valueOne.toJSON(), + valueTwo: this.valueTwo.toJSON(), + tokens: this.tokens(), + }; + } + + isMediaFeatureRangeValueNameValue(): this is MediaFeatureRangeValueNameValue { + return MediaFeatureRangeValueNameValue.isMediaFeatureRangeValueNameValue(this); + } + + static isMediaFeatureRangeValueNameValue(x: unknown): x is MediaFeatureRangeValueNameValue { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeatureRangeValueNameValue)) { + return false; + } + + return x.type === NodeType.MediaFeatureRangeValueNameValue; + } +} + +export type MediaFeatureRangeWalkerEntry = MediaFeatureValueWalkerEntry | MediaFeatureValue; +export type MediaFeatureRangeWalkerParent = MediaFeatureValueWalkerParent | MediaFeatureRange; + +export function parseMediaFeatureRange(componentValues: Array) { + let comparisonOne: false | [number, number] = false; + let comparisonTwo: false | [number, number] = false; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Token) { + const token = componentValue.value as CSSToken; + if (token[0] === TokenType.Delim) { + const comparison = matchesComparison(componentValues.slice(i)); + if (comparison !== false) { + if (comparisonOne === false) { + comparisonOne = [ + comparison[0] + i, + comparison[1] + i, + ]; + i += comparison[1]; + } else { + comparisonTwo = [ + comparison[0] + i, + comparison[1] + i, + ]; + break; + } + } + } + } + } + + if (comparisonOne === false) { + return false; + } + + const comparisonTokensOne: [TokenDelim, TokenDelim] | [TokenDelim] = [ + (componentValues[comparisonOne[0]] as TokenNode).value as TokenDelim, + ]; + if (comparisonOne[0] !== comparisonOne[1]) { + comparisonTokensOne.push( + (componentValues[comparisonOne[1]] as TokenNode).value as TokenDelim, + ); + } + + if (comparisonTwo === false) { + const a = componentValues.slice(0, comparisonOne[0]); + const b = componentValues.slice(comparisonOne[1] + 1); + + const nameA = parseMediaFeatureName(a); + const nameB = parseMediaFeatureName(b); + + if (!nameA && !nameB) { + return false; + } + + if ( + (nameA && !nameB) || + nameA && mediaDescriptors.has(nameA.getName().toLowerCase()) + ) { + const value = parseMediaFeatureValue(b); + if (!value) { + return false; + } + + return new MediaFeatureRangeNameValue(nameA, comparisonTokensOne, value); + } + + if ( + (!nameA && nameB) || + nameB && mediaDescriptors.has(nameB.getName().toLowerCase()) + ) { + const value = parseMediaFeatureValue(a); + if (!value) { + return false; + } + + return new MediaFeatureRangeValueName(nameB, comparisonTokensOne, value); + } + + return false; + } + + const comparisonTokensTwo: [TokenDelim, TokenDelim] | [TokenDelim] = [ + (componentValues[comparisonTwo[0]] as TokenNode).value as TokenDelim, + ]; + if (comparisonTwo[0] !== comparisonTwo[1]) { + comparisonTokensTwo.push( + (componentValues[comparisonTwo[1]] as TokenNode).value as TokenDelim, + ); + } + + const a = componentValues.slice(0, comparisonOne[0]); + const b = componentValues.slice(comparisonOne[1] + 1, comparisonTwo[0]); + const c = componentValues.slice(comparisonTwo[1] + 1); + + const valueA = parseMediaFeatureValue(a); + const nameB = parseMediaFeatureName(b); + const valueC = parseMediaFeatureValue(c); + + if (!valueA || !nameB || !valueC) { + return false; + } + + // https://www.w3.org/TR/mediaqueries-5/#mq-range-context + // Only certain comparison operators are allowed and only in certain combinations. + { + const comparisonOneCheck = comparisonFromTokens(comparisonTokensOne); + if ( + comparisonOneCheck === false || + comparisonOneCheck === MediaFeatureEQ.EQ + ) { + return false; + } + const comparisonTwoCheck = comparisonFromTokens(comparisonTokensTwo); + if ( + comparisonTwoCheck === false || + comparisonTwoCheck === MediaFeatureEQ.EQ + ) { + return false; + } + + if ( + ( + comparisonOneCheck === MediaFeatureLT.LT || + comparisonOneCheck === MediaFeatureLT.LT_OR_EQ + ) + && + ( + comparisonTwoCheck === MediaFeatureGT.GT || + comparisonTwoCheck === MediaFeatureGT.GT_OR_EQ + ) + ) { + return false; + } + + if ( + ( + comparisonOneCheck === MediaFeatureGT.GT || + comparisonOneCheck === MediaFeatureGT.GT_OR_EQ + ) + && + ( + comparisonTwoCheck === MediaFeatureLT.LT || + comparisonTwoCheck === MediaFeatureLT.LT_OR_EQ + ) + ) { + return false; + } + } + + return new MediaFeatureRangeValueNameValue( + nameB, + valueA, + comparisonTokensOne, + valueC, + comparisonTokensTwo, + ); +} + +export const mediaDescriptors = new Set([ + 'any-hover', + 'any-pointer', + 'aspect-ratio', + 'color', + 'color-gamut', + 'color-index', + 'device-aspect-ratio', + 'device-height', + 'device-width', + 'display-mode', + 'dynamic-range', + 'environment-blending', + 'forced-colors', + 'grid', + 'height', + 'horizontal-viewport-segments', + 'hover', + 'inverted-colors', + 'monochrome', + 'nav-controls', + 'orientation', + 'overflow-block', + 'overflow-inline', + 'pointer', + 'prefers-color-scheme', + 'prefers-contrast', + 'prefers-reduced-data', + 'prefers-reduced-motion', + 'prefers-reduced-transparency', + 'resolution', + 'scan', + 'scripting', + 'update', + 'vertical-viewport-segments', + 'video-color-gamut', + 'video-dynamic-range', + 'width', +]); diff --git a/packages/media-query-list-parser/src/nodes/media-feature-value.ts b/packages/media-query-list-parser/src/nodes/media-feature-value.ts new file mode 100644 index 000000000..642a7487d --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature-value.ts @@ -0,0 +1,244 @@ +import { ComponentValue, ComponentValueType, ContainerNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer'; +import { isDimension, isIdent, isNumber } from '../util/component-value-is'; +import { NodeType } from '../util/node-type'; + +export class MediaFeatureValue { + type = NodeType.MediaFeatureValue; + + value: ComponentValue | Array; + before: Array; + after: Array; + + constructor(value: ComponentValue | Array, before: Array = [], after: Array = []) { + if (Array.isArray(value) && value.length === 1) { + this.value = value[0]; + } else { + this.value = value; + } + + this.before = before; + this.after = after; + } + + tokens(): Array { + if (Array.isArray(this.value)) { + return [ + ...this.before, + ...this.value.flatMap((x) => x.tokens()), + ...this.after, + ]; + } + + return [ + ...this.before, + ...this.value.tokens(), + ...this.after, + ]; + } + + toString(): string { + if (Array.isArray(this.value)) { + return stringify(...this.before) + this.value.map((x) => x.toString()).join('') + stringify(...this.after); + } + + return stringify(...this.before) + this.value.toString() + stringify(...this.after); + } + + indexOf(item: ComponentValue): number | string { + if (item === this.value) { + return 'value'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'value') { + return this.value; + } + } + + walk(cb: (entry: { node: MediaFeatureValueWalkerEntry, parent: MediaFeatureValueWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.value, parent: this }, 'value') === false) { + return false; + } + + if ('walk' in this.value) { + return this.value.walk(cb); + } + } + + toJSON() { + if (Array.isArray(this.value)) { + return { + type: this.type, + value: this.value.map((x) => x.toJSON()), + tokens: this.tokens(), + }; + } + + return { + type: this.type, + value: this.value.toJSON(), + tokens: this.tokens(), + }; + } + + isMediaFeatureValue(): this is MediaFeatureValue { + return MediaFeatureValue.isMediaFeatureValue(this); + } + + static isMediaFeatureValue(x: unknown): x is MediaFeatureValue { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeatureValue)) { + return false; + } + + return x.type === NodeType.MediaFeatureValue; + } +} + +export type MediaFeatureValueWalkerEntry = ComponentValue | Array; +export type MediaFeatureValueWalkerParent = ContainerNode | MediaFeatureValue; + +export function parseMediaFeatureValue(componentValues: Array) { + let candidateIndexStart = -1; + let candidateIndexEnd = -1; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (candidateIndexStart !== -1) { + return false; + } + + if (isNumber(componentValue)) { + const maybeRatio = matchesRatioExactly(componentValues.slice(i)); + if (maybeRatio !== -1) { + candidateIndexStart = maybeRatio[0] + i; + candidateIndexEnd = maybeRatio[1] + i; + i += maybeRatio[1] - maybeRatio[0]; + continue; + } + + candidateIndexStart = i; + candidateIndexEnd = i; + continue; + } + + if (isDimension(componentValue)) { + candidateIndexStart = i; + candidateIndexEnd = i; + continue; + } + + if (isIdent(componentValue)) { + candidateIndexStart = i; + candidateIndexEnd = i; + continue; + } + + return false; + } + + if (candidateIndexStart === -1) { + return false; + } + + return new MediaFeatureValue( + componentValues.slice(candidateIndexStart, candidateIndexEnd + 1), + componentValues.slice(0, candidateIndexStart).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(candidateIndexEnd + 1).flatMap((x) => { + return x.tokens(); + }), + ); +} + +export function matchesRatioExactly(componentValues: Array) { + let firstNumber = -1; + let secondNumber = -1; + + const result = matchesRatio(componentValues); + if (result === -1) { + return -1; + } + + firstNumber = result[0]; + secondNumber = result[1]; + + for (let i = secondNumber+1; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === 'whitespace') { + continue; + } + + if (componentValue.type === 'comment') { + continue; + } + + return -1; + } + + return [firstNumber, secondNumber]; +} + +export function matchesRatio(componentValues: Array) { + let firstNumber = -1; + let delim = -1; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === 'whitespace') { + continue; + } + + if (componentValue.type === 'comment') { + continue; + } + + if (componentValue.type === 'token') { + const token = componentValue.value as CSSToken; + if (token[0] === TokenType.Delim && token[4].value === '/') { + if (firstNumber === -1) { + return -1; + } + + if (delim !== -1) { + return -1; + } + + delim = i; + continue; + } + } + + if (isNumber(componentValue)) { + if (delim !== -1) { + return [firstNumber, i]; + } else if (firstNumber !== -1) { + return -1; + } else { + firstNumber = i; + continue; + } + } + + return -1; + } + + return -1; +} + diff --git a/packages/media-query-list-parser/src/nodes/media-feature.ts b/packages/media-query-list-parser/src/nodes/media-feature.ts new file mode 100644 index 000000000..d9a9a4137 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-feature.ts @@ -0,0 +1,143 @@ +import { SimpleBlockNode, TokenNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer'; +import { MediaFeatureBoolean, parseMediaFeatureBoolean } from './media-feature-boolean'; +import { MediaFeatureName } from './media-feature-name'; +import { MediaFeaturePlain, MediaFeaturePlainWalkerEntry, MediaFeaturePlainWalkerParent, parseMediaFeaturePlain } from './media-feature-plain'; +import { MediaFeatureRange, MediaFeatureRangeWalkerEntry, MediaFeatureRangeWalkerParent, parseMediaFeatureRange } from './media-feature-range'; +import { MediaFeatureValue } from './media-feature-value'; +import { NodeType } from '../util/node-type'; + +export class MediaFeature { + type = NodeType.MediaFeature; + + feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange; + before: Array; + after: Array; + + constructor(feature: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange, before: Array = [], after: Array = []) { + this.feature = feature; + this.before = before; + this.after = after; + } + + tokens(): Array { + return [ + ...this.before, + ...this.feature.tokens(), + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + this.feature.toString() + stringify(...this.after); + } + + indexOf(item: MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange): number | string { + if (item === this.feature) { + return 'feature'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'feature') { + return this.feature; + } + } + + walk(cb: (entry: { node: MediaFeatureWalkerEntry, parent: MediaFeatureWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.feature, parent: this }, 'feature') === false) { + return false; + } + + if ('walk' in this.feature) { + return this.feature.walk(cb); + } + } + + toJSON() { + return { + type: this.type, + feature: this.feature.toJSON(), + before: this.before, + after: this.after, + }; + } + + isMediaFeature(): this is MediaFeature { + return MediaFeature.isMediaFeature(this); + } + + static isMediaFeature(x: unknown): x is MediaFeature { + if (!x) { + return false; + } + + if (!(x instanceof MediaFeature)) { + return false; + } + + return x.type === NodeType.MediaFeature; + } +} + +export type MediaFeatureWalkerEntry = MediaFeaturePlainWalkerEntry | MediaFeatureRangeWalkerEntry | MediaFeaturePlain | MediaFeatureBoolean | MediaFeatureRange; +export type MediaFeatureWalkerParent = MediaFeaturePlainWalkerParent | MediaFeatureRangeWalkerParent | MediaFeature; + +export function parseMediaFeature(simpleBlock: SimpleBlockNode, before: Array = [], after: Array = []) { + if (simpleBlock.startToken[0] !== TokenType.OpenParen) { + return false; + } + + const boolean = parseMediaFeatureBoolean(simpleBlock.value); + if (boolean !== false) { + return new MediaFeature(boolean, before, after); + } + + const plain = parseMediaFeaturePlain(simpleBlock.value); + if (plain !== false) { + return new MediaFeature(plain, before, after); + } + + const range = parseMediaFeatureRange(simpleBlock.value); + if (range !== false) { + return new MediaFeature(range, before, after); + } + + return false; +} + +export function newMediaFeatureBoolean(name: string) { + return new MediaFeature( + new MediaFeatureBoolean( + new TokenNode([TokenType.Ident, name, -1, -1, { value: name }]), + ), + [ + [TokenType.OpenParen, '(', -1, -1, undefined], + ], + [ + [TokenType.CloseParen, ')', -1, -1, undefined], + ], + ); +} + +export function newMediaFeaturePlain(name: string, ...value: Array) { + return new MediaFeature( + new MediaFeaturePlain( + new MediaFeatureName( + new TokenNode([TokenType.Ident, name, -1, -1, { value: name }]), + ), + [TokenType.Colon, ':', -1, -1, undefined], + new MediaFeatureValue( + value.map((x) => new TokenNode(x)), + ), + ), + [ + [TokenType.OpenParen, '(', -1, -1, undefined], + ], + [ + [TokenType.CloseParen, ')', -1, -1, undefined], + ], + ); +} diff --git a/packages/media-query-list-parser/src/nodes/media-in-parens.ts b/packages/media-query-list-parser/src/nodes/media-in-parens.ts new file mode 100644 index 000000000..02aff59a3 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-in-parens.ts @@ -0,0 +1,91 @@ +import { ComponentValue, ContainerNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify } from '@csstools/css-tokenizer'; +import { GeneralEnclosed } from './general-enclosed'; +import { MediaAnd } from './media-and'; +import { MediaCondition } from './media-condition'; +import { MediaConditionList } from './media-condition-list'; +import { MediaFeature } from './media-feature'; +import { MediaFeatureBoolean } from './media-feature-boolean'; +import { MediaFeatureName } from './media-feature-name'; +import { MediaFeaturePlain } from './media-feature-plain'; +import { MediaFeatureRange } from './media-feature-range'; +import { MediaFeatureValue } from './media-feature-value'; +import { NodeType } from '../util/node-type'; + +export class MediaInParens { + type = NodeType.MediaInParens; + + media: MediaCondition | MediaFeature | GeneralEnclosed; + before: Array; + after: Array; + + constructor(media: MediaCondition | MediaFeature | GeneralEnclosed, before: Array = [], after: Array = []) { + this.media = media; + this.before = before; + this.after = after; + } + + tokens(): Array { + return [ + ...this.before, + ...this.media.tokens(), + ...this.after, + ]; + } + + toString(): string { + return stringify(...this.before) + this.media.toString() + stringify(...this.after); + } + + indexOf(item: MediaCondition | MediaFeature | GeneralEnclosed): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaInParensWalkerEntry, parent: MediaInParensWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + if ('walk' in this.media) { + return this.media.walk(cb); + } + } + + toJSON() { + return { + type: this.type, + media: this.media.toJSON(), + before: this.before, + after: this.after, + }; + } + + isMediaInParens(): this is MediaInParens { + return MediaInParens.isMediaInParens(this); + } + + static isMediaInParens(x: unknown): x is MediaInParens { + if (!x) { + return false; + } + + if (!(x instanceof MediaInParens)) { + return false; + } + + return x.type === NodeType.MediaInParens; + } +} + +export type MediaInParensWalkerEntry = ComponentValue | Array | GeneralEnclosed | MediaAnd | MediaConditionList | MediaCondition | MediaFeatureBoolean | MediaFeatureName | MediaFeaturePlain | MediaFeatureRange | MediaFeatureValue | MediaFeature | GeneralEnclosed | MediaInParens; +export type MediaInParensWalkerParent = ContainerNode | GeneralEnclosed | MediaAnd | MediaConditionList | MediaCondition | MediaFeatureBoolean | MediaFeatureName | MediaFeaturePlain | MediaFeatureRange | MediaFeatureValue | MediaFeature | GeneralEnclosed | MediaInParens; diff --git a/packages/media-query-list-parser/src/nodes/media-not.ts b/packages/media-query-list-parser/src/nodes/media-not.ts new file mode 100644 index 000000000..1a81910e6 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-not.ts @@ -0,0 +1,75 @@ +import { CSSToken, stringify } from '@csstools/css-tokenizer'; +import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens'; +import { NodeType } from '../util/node-type'; + +export class MediaNot { + type = NodeType.MediaNot; + + modifier: Array; + media: MediaInParens; + + constructor(modifier: Array, media: MediaInParens) { + this.modifier = modifier; + this.media = media; + } + + tokens(): Array { + return [ + ...this.modifier, + ...this.media.tokens(), + ]; + } + + toString(): string { + return stringify(...this.modifier) + this.media.toString(); + } + + indexOf(item: MediaInParens): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaNotWalkerEntry, parent: MediaNotWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + return this.media.walk(cb); + } + + toJSON() { + return { + type: this.type, + modifier: this.modifier, + media: this.media.toJSON(), + }; + } + + isMediaNot(): this is MediaNot { + return MediaNot.isMediaNot(this); + } + + static isMediaNot(x: unknown): x is MediaNot { + if (!x) { + return false; + } + + if (!(x instanceof MediaNot)) { + return false; + } + + return x.type === NodeType.MediaNot; + } +} + +export type MediaNotWalkerEntry = MediaInParensWalkerEntry | MediaInParens; +export type MediaNotWalkerParent = MediaInParensWalkerParent | MediaNot; diff --git a/packages/media-query-list-parser/src/nodes/media-or.ts b/packages/media-query-list-parser/src/nodes/media-or.ts new file mode 100644 index 000000000..9d45e629f --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-or.ts @@ -0,0 +1,75 @@ +import { CSSToken, stringify } from '@csstools/css-tokenizer'; +import { MediaInParens, MediaInParensWalkerEntry, MediaInParensWalkerParent } from './media-in-parens'; +import { NodeType } from '../util/node-type'; + +export class MediaOr { + type = NodeType.MediaOr; + + modifier: Array; + media: MediaInParens; + + constructor(modifier: Array, media: MediaInParens) { + this.modifier = modifier; + this.media = media; + } + + tokens(): Array { + return [ + ...this.modifier, + ...this.media.tokens(), + ]; + } + + toString(): string { + return stringify(...this.modifier) + this.media.toString(); + } + + indexOf(item: MediaInParens): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaOrWalkerEntry, parent: MediaOrWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + return this.media.walk(cb); + } + + toJSON() { + return { + type: this.type, + modifier: this.modifier, + media: this.media.toJSON(), + }; + } + + isMediaOr(): this is MediaOr { + return MediaOr.isMediaOr(this); + } + + static isMediaOr(x: unknown): x is MediaOr { + if (!x) { + return false; + } + + if (!(x instanceof MediaOr)) { + return false; + } + + return x.type === NodeType.MediaOr; + } +} + +export type MediaOrWalkerEntry = MediaInParensWalkerEntry | MediaInParens; +export type MediaOrWalkerParent = MediaInParensWalkerParent | MediaOr; diff --git a/packages/media-query-list-parser/src/nodes/media-query-modifier.ts b/packages/media-query-list-parser/src/nodes/media-query-modifier.ts new file mode 100644 index 000000000..22d94d092 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-query-modifier.ts @@ -0,0 +1,22 @@ +import { TokenIdent, TokenType } from '@csstools/css-tokenizer'; + +export enum MediaQueryModifier { + Not = 'not', + Only = 'only' +} + +export function modifierFromToken(token: TokenIdent): MediaQueryModifier | false { + if (token[0] !== TokenType.Ident) { + return false; + } + + const matchingValue = token[4].value.toLowerCase(); + switch (matchingValue) { + case MediaQueryModifier.Not: + return MediaQueryModifier.Not; + case MediaQueryModifier.Only: + return MediaQueryModifier.Only; + default: + return false; + } +} diff --git a/packages/media-query-list-parser/src/nodes/media-query.ts b/packages/media-query-list-parser/src/nodes/media-query.ts new file mode 100644 index 000000000..e6a1a6531 --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-query.ts @@ -0,0 +1,350 @@ +import { ComponentValue } from '@csstools/css-parser-algorithms'; +import { CSSToken, stringify, TokenType } from '@csstools/css-tokenizer'; +import { NodeType } from '../util/node-type'; +import { MediaCondition, MediaConditionWalkerEntry, MediaConditionWalkerParent } from './media-condition'; +import { MediaInParens } from './media-in-parens'; +import { MediaNot } from './media-not'; + +export type MediaQuery = MediaQueryWithType | MediaQueryWithoutType | MediaQueryInvalid; + +export class MediaQueryWithType { + type = NodeType.MediaQueryWithType; + + modifier: Array; + mediaType: Array; + and: Array; + media: MediaCondition | null = null; + + constructor(modifier: Array, mediaType: Array, and?: Array, media?: MediaCondition | null) { + this.modifier = modifier; + this.mediaType = mediaType; + + if (and && media) { + this.and = and; + this.media = media; + } + } + + getModifier() { + if (!this.modifier.length) { + return ''; + } + + for (let i = 0; i < this.modifier.length; i++) { + const token = this.modifier[i]; + if (token[0] === TokenType.Ident) { + return token[4].value; + } + } + + return ''; + } + + negateQuery(): MediaQuery { + const copy = new MediaQueryWithType([...this.modifier], [...this.mediaType], this.and, this.media); + if (copy.modifier.length === 0) { + copy.modifier = [ + [TokenType.Ident, 'not', -1, -1, { value: 'not' }], + [TokenType.Whitespace, ' ', -1, -1, undefined], + ]; + + return copy; + } + + for (let i = 0; i < copy.modifier.length; i++) { + const token = copy.modifier[i]; + if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'not') { + copy.modifier.splice(i, 1); + break; + } + + if (token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'only') { + copy.modifier[i][1] = 'not'; + copy.modifier[i][4].value = 'not'; + break; + } + } + + return copy; + } + + getMediaType() { + if (!this.mediaType.length) { + return ''; + } + + for (let i = 0; i < this.mediaType.length; i++) { + const token = this.mediaType[i]; + if (token[0] === TokenType.Ident) { + return token[4].value; + } + } + + return ''; + } + + tokens() { + if (this.and && this.media) { + return [ + ...this.modifier, + ...this.mediaType, + ...this.and, + ...this.media.tokens(), + ]; + } + + return [ + ...this.modifier, + ...this.mediaType, + ]; + } + + toString() { + if (this.and && this.media) { + return stringify(...this.modifier) + stringify(...this.mediaType) + stringify(...this.and) + this.media.toString(); + } + + return stringify(...this.modifier) + stringify(...this.mediaType); + } + + indexOf(item: MediaCondition): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaQueryWithTypeWalkerEntry, parent: MediaQueryWithTypeWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + if (!this.media) { + return; + } + + return this.media.walk(cb); + } + + toJSON() { + return { + type: this.type, + string: this.toString(), + modifier: this.modifier, + mediaType: this.mediaType, + and: this.and, + media: this.media, + }; + } + + isMediaQueryWithType(): this is MediaQueryWithType { + return MediaQueryWithType.isMediaQueryWithType(this); + } + + static isMediaQueryWithType(x: unknown): x is MediaQueryWithType { + if (!x) { + return false; + } + + if (!(x instanceof MediaQueryWithType)) { + return false; + } + + return x.type === NodeType.MediaQueryWithType; + } +} + +export type MediaQueryWithTypeWalkerEntry = MediaConditionWalkerEntry | MediaCondition; +export type MediaQueryWithTypeWalkerParent = MediaConditionWalkerParent | MediaQueryWithType; + +export class MediaQueryWithoutType { + type = NodeType.MediaQueryWithoutType; + + media: MediaCondition; + + constructor(media: MediaCondition) { + this.media = media; + } + + negateQuery(): MediaQuery { + let mediaCondition = this.media; + if (mediaCondition.media.type === NodeType.MediaNot) { + return new MediaQueryWithoutType( + new MediaCondition( + (mediaCondition.media as MediaNot).media, + ), + ); + } + + if (mediaCondition.media.type === NodeType.MediaConditionListWithOr) { + mediaCondition = new MediaCondition( + new MediaInParens( + mediaCondition, + [ + [TokenType.Whitespace, ' ', 0, 0, undefined], + [TokenType.OpenParen, '(', 0, 0, undefined], + ], + [ + [TokenType.CloseParen, ')', 0, 0, undefined], + ], + ), + ); + } + + const query = new MediaQueryWithType( + [ + [TokenType.Ident, 'not', 0, 0, { value: 'not' }], + [TokenType.Whitespace, ' ', 0, 0, undefined], + ], + [ + [TokenType.Ident, 'all', 0, 0, { value: 'all' }], + [TokenType.Whitespace, ' ', 0, 0, undefined], + ], + [ + [TokenType.Ident, 'and', 0, 0, { value: 'and' }], + ], + mediaCondition, + ); + + return query; + } + + tokens(): Array { + return this.media.tokens(); + } + + toString(): string { + return this.media.toString(); + } + + indexOf(item: MediaCondition): number | string { + if (item === this.media) { + return 'media'; + } + + return -1; + } + + at(index: number | string) { + if (index === 'media') { + return this.media; + } + } + + walk(cb: (entry: { node: MediaQueryWithoutTypeWalkerEntry, parent: MediaQueryWithoutTypeWalkerParent }, index: number | string) => boolean | void) { + if (cb({ node: this.media, parent: this }, 'media') === false) { + return false; + } + + return this.media.walk(cb); + } + + toJSON() { + return { + type: this.type, + string: this.toString(), + media: this.media, + }; + } + + isMediaQueryWithoutType(): this is MediaQueryWithoutType { + return MediaQueryWithoutType.isMediaQueryWithoutType(this); + } + + static isMediaQueryWithoutType(x: unknown): x is MediaQueryWithoutType { + if (!x) { + return false; + } + + if (!(x instanceof MediaQueryWithoutType)) { + return false; + } + + return x.type === NodeType.MediaQueryWithoutType; + } +} + +export type MediaQueryWithoutTypeWalkerEntry = MediaConditionWalkerEntry | MediaCondition; +export type MediaQueryWithoutTypeWalkerParent = MediaConditionWalkerParent | MediaQueryWithoutType; + +export class MediaQueryInvalid { + type = NodeType.MediaQueryInvalid; + + media: Array; + + constructor(media: Array) { + this.media = media; + } + + negateQuery(): MediaQuery { + return new MediaQueryInvalid(this.media); + } + + tokens(): Array { + return this.media.flatMap((x) => x.tokens()); + } + + toString(): string { + return this.media.map((x) => x.toString()).join(''); + } + + walk(cb: (entry: { node: MediaQueryInvalidWalkerEntry, parent: MediaQueryInvalidWalkerParent }, index: number | string) => boolean | void) { + let aborted = false; + + this.media.forEach((child, index) => { + if (aborted) { + return; + } + + if (cb({ node: child, parent: this }, index) === false) { + aborted = true; + return; + } + + if ('walk' in child) { + if (child.walk(cb) === false) { + aborted = true; + return; + } + } + }); + + if (aborted) { + return false; + } + } + + toJSON() { + return { + type: this.type, + string: this.toString(), + media: this.media, + }; + } + + isMediaQueryInvalid(): this is MediaQueryInvalid { + return MediaQueryInvalid.isMediaQueryInvalid(this); + } + + static isMediaQueryInvalid(x: unknown): x is MediaQueryInvalid { + if (!x) { + return false; + } + + if (!(x instanceof MediaQueryInvalid)) { + return false; + } + + return x.type === NodeType.MediaQueryInvalid; + } +} + +export type MediaQueryInvalidWalkerEntry = ComponentValue; +export type MediaQueryInvalidWalkerParent = ComponentValue | MediaQueryInvalid; diff --git a/packages/media-query-list-parser/src/nodes/media-type.ts b/packages/media-query-list-parser/src/nodes/media-type.ts new file mode 100644 index 000000000..d30336bea --- /dev/null +++ b/packages/media-query-list-parser/src/nodes/media-type.ts @@ -0,0 +1,58 @@ +import { TokenIdent, TokenType } from '@csstools/css-tokenizer'; + +export enum MediaType { + /** Always matches */ + All = 'all', + Print = 'print', + Screen = 'screen', + /** Never matches */ + Tty = 'tty', + /** Never matches */ + Tv = 'tv', + /** Never matches */ + Projection = 'projection', + /** Never matches */ + Handheld = 'handheld', + /** Never matches */ + Braille = 'braille', + /** Never matches */ + Embossed = 'embossed', + /** Never matches */ + Aural = 'aural', + /** Never matches */ + Speech = 'speech', +} + +export function typeFromToken(token: TokenIdent): MediaType | false { + if (token[0] !== TokenType.Ident) { + return false; + } + + const matchingValue = token[4].value.toLowerCase(); + switch (matchingValue) { + case MediaType.All: + return MediaType.All; + case MediaType.Print: + return MediaType.Print; + case MediaType.Screen: + return MediaType.Screen; + case MediaType.Tty: + return MediaType.Tty; + case MediaType.Tv: + return MediaType.Tv; + case MediaType.Projection: + return MediaType.Projection; + case MediaType.Handheld: + return MediaType.Handheld; + case MediaType.Braille: + return MediaType.Braille; + case MediaType.Embossed: + return MediaType.Embossed; + case MediaType.Aural: + return MediaType.Aural; + case MediaType.Speech: + return MediaType.Speech; + default: + return false; + } +} diff --git a/packages/media-query-list-parser/src/parser/parse-media-query.ts b/packages/media-query-list-parser/src/parser/parse-media-query.ts new file mode 100644 index 000000000..9e95b638e --- /dev/null +++ b/packages/media-query-list-parser/src/parser/parse-media-query.ts @@ -0,0 +1,510 @@ +import { ComponentValue, ComponentValueType, isCommentNode, isTokenNode, isWhitespaceNode, SimpleBlockNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, TokenIdent, TokenType } from '@csstools/css-tokenizer'; +import { GeneralEnclosed } from '../nodes/general-enclosed'; +import { MediaAnd } from '../nodes/media-and'; +import { MediaCondition } from '../nodes/media-condition'; +import { MediaConditionListWithAnd, MediaConditionListWithOr } from '../nodes/media-condition-list'; +import { parseMediaFeature } from '../nodes/media-feature'; +import { MediaInParens } from '../nodes/media-in-parens'; +import { MediaNot } from '../nodes/media-not'; +import { MediaOr } from '../nodes/media-or'; +import { MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query'; +import { modifierFromToken } from '../nodes/media-query-modifier'; +import { isIdent } from '../util/component-value-is'; + +export function parseMediaQuery(componentValues: Array) { + { + const condition = parseMediaCondition(componentValues); + if (condition !== false) { + return new MediaQueryWithoutType(condition); + } + } + + { + let modifierIndex = -1; + let typeIndex = -1; + let andIndex = -1; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i] as ComponentValue; + if (isWhitespaceNode(componentValue)) { + continue; + } + + if (isCommentNode(componentValue)) { + continue; + } + + if (isTokenNode(componentValue)) { + const token = componentValue.value; + if (modifierIndex === -1 && token[0] === TokenType.Ident && modifierFromToken(token)) { + modifierIndex = i; + continue; + } + + if (typeIndex === -1 && token[0] === TokenType.Ident && !modifierFromToken(token)) { + typeIndex = i; + continue; + } + + if (andIndex === -1 && token[0] === TokenType.Ident && token[4].value.toLowerCase() === 'and') { + andIndex = i; + const condition = parseMediaConditionWithoutOr(componentValues.slice(i+1)); + if (condition === false) { + return false; + } + + break; + } + + return false; + } + + return false; + } + + let modifierTokens: Array = []; + let typeTokens: Array = []; + + if (modifierIndex !== -1) { + modifierTokens = componentValues.slice(0, modifierIndex + 1).flatMap((x) => { + return x.tokens(); + }); + + if (typeIndex !== -1) { + typeTokens = componentValues.slice(modifierIndex + 1, typeIndex + 1).flatMap((x) => { + return x.tokens(); + }); + } + } else if (typeIndex !== -1) { + typeTokens = componentValues.slice(0, typeIndex + 1).flatMap((x) => { + return x.tokens(); + }); + } + + const remainder = componentValues.slice(Math.max(modifierIndex, typeIndex, andIndex) + 1); + const condition = parseMediaConditionWithoutOr(remainder); + if (condition === false) { + return new MediaQueryWithType( + modifierTokens, + [ + ...typeTokens, + ...componentValues.slice(typeIndex + 1).flatMap((x) => { + return x.tokens(); + }), + ], + ); + } + + return new MediaQueryWithType( + modifierTokens, + typeTokens, + componentValues.slice(typeIndex + 1, andIndex + 1).flatMap((x) => { + return x.tokens(); + }), + condition, + ); + } +} + +export function parseMediaConditionListWithOr(componentValues: Array) { + let leading: MediaInParens | false = false; + const list: Array = []; + let firstIndex = -1; + let lastIndex = -1; + + for (let i = 0; i < componentValues.length; i++) { + if (leading) { + const part = parseMediaOr(componentValues.slice(i)); + if (part !== false) { + i += part.advance; + list.push(part.node); + lastIndex = i; + continue; + } + } + + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (leading) { + return false; + } + + if (leading === false && componentValue.type === ComponentValueType.SimpleBlock) { + leading = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode); + if (leading === false) { + return false; + } + + firstIndex = i; + continue; + } + + return false; + } + + if (leading && list.length) { + return new MediaConditionListWithOr( + leading, + list, + componentValues.slice(0, firstIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(lastIndex + 1).flatMap((x) => { + return x.tokens(); + }), + ); + } + + return false; +} + +export function parseMediaConditionListWithAnd(componentValues: Array) { + let leading: MediaInParens | false = false; + const list: Array = []; + let firstIndex = -1; + let lastIndex = -1; + + for (let i = 0; i < componentValues.length; i++) { + if (leading) { + const part = parseMediaAnd(componentValues.slice(i)); + if (part !== false) { + i += part.advance; + list.push(part.node); + lastIndex = i; + continue; + } + } + + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (leading) { + return false; + } + + if (leading === false && componentValue.type === ComponentValueType.SimpleBlock) { + leading = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode); + if (leading === false) { + return false; + } + + firstIndex = i; + continue; + } + + return false; + } + + if (leading && list.length) { + return new MediaConditionListWithAnd( + leading, + list, + componentValues.slice(0, firstIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(lastIndex + 1).flatMap((x) => { + return x.tokens(); + }), + ); + } + + return false; +} + +export function parseMediaCondition(componentValues: Array) { + const mediaNot = parseMediaNot(componentValues); + if (mediaNot !== false) { + return new MediaCondition(mediaNot); + } + + const mediaListAnd = parseMediaConditionListWithAnd(componentValues); + if (mediaListAnd !== false) { + return new MediaCondition(mediaListAnd); + } + + const mediaListOr = parseMediaConditionListWithOr(componentValues); + if (mediaListOr !== false) { + return new MediaCondition(mediaListOr); + } + + const mediaInParens = parseMediaInParens(componentValues); + if (mediaInParens !== false) { + return new MediaCondition(mediaInParens); + } + + return false; +} + +export function parseMediaConditionWithoutOr(componentValues: Array) { + const mediaNot = parseMediaNot(componentValues); + if (mediaNot !== false) { + return new MediaCondition(mediaNot); + } + + const mediaListAnd = parseMediaConditionListWithAnd(componentValues); + if (mediaListAnd !== false) { + return new MediaCondition(mediaListAnd); + } + + const mediaInParens = parseMediaInParens(componentValues); + if (mediaInParens !== false) { + return new MediaCondition(mediaInParens); + } + + return false; +} + +export function parseMediaInParens(componentValues: Array) { + let singleSimpleBlockIndex = -1; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (componentValue.type === ComponentValueType.SimpleBlock) { + if (singleSimpleBlockIndex !== -1) { + return false; + } + + singleSimpleBlockIndex = i; + continue; + } + + return false; + } + + if (singleSimpleBlockIndex === -1) { + return false; + } + + const simpleBlock = componentValues[singleSimpleBlockIndex] as SimpleBlockNode; + if (simpleBlock.startToken[0] !== TokenType.OpenParen) { + return false; + } + + const before = [ + ...componentValues.slice(0, singleSimpleBlockIndex).flatMap((x) => { + return x.tokens(); + }), + simpleBlock.startToken, + ]; + + const after = [ + simpleBlock.endToken, + ...componentValues.slice(singleSimpleBlockIndex + 1).flatMap((x) => { + return x.tokens(); + }), + ]; + + const feature = parseMediaFeature(simpleBlock, before, after); + if (feature !== false) { + return new MediaInParens(feature); + } + + const condition = parseMediaCondition(simpleBlock.value); + if (condition !== false) { + return new MediaInParens(condition, before, after); + } + + return new MediaInParens( + new GeneralEnclosed(simpleBlock), + componentValues.slice(0, singleSimpleBlockIndex).flatMap((x) => { + return x.tokens(); + }), + componentValues.slice(singleSimpleBlockIndex + 1).flatMap((x) => { + return x.tokens(); + }), + ); +} + +export function parseMediaInParensFromSimpleBlock(simpleBlock: SimpleBlockNode) { + if (simpleBlock.startToken[0] !== TokenType.OpenParen) { + return false; + } + + const feature = parseMediaFeature(simpleBlock, [simpleBlock.startToken], [simpleBlock.endToken]); + if (feature !== false) { + return new MediaInParens(feature); + } + + const condition = parseMediaCondition(simpleBlock.value); + if (condition !== false) { + return new MediaInParens(condition, [simpleBlock.startToken], [simpleBlock.endToken]); + } + + return new MediaInParens(new GeneralEnclosed(simpleBlock)); +} + +export function parseMediaNot(componentValues: Array) { + let sawNot = false; + let node: MediaNot | null = null; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (isIdent(componentValue)) { + const token = (componentValue.value as TokenIdent); + if (token[4].value.toLowerCase() === 'not') { + if (sawNot) { + return false; + } + + sawNot = true; + continue; + } + + return false; + } + + if (sawNot && componentValue.type === ComponentValueType.SimpleBlock) { + const media = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode); + if (media === false) { + return false; + } + + node = new MediaNot( + componentValues.slice(0, i).flatMap((x) => { + return x.tokens(); + }), + media, + ); + + continue; + } + + return false; + } + + if (node) { + return node; + } + + return false; +} + +export function parseMediaOr(componentValues: Array) { + let sawOr = false; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (isIdent(componentValue)) { + const token = (componentValue.value as TokenIdent); + if (token[4].value.toLowerCase() === 'or') { + if (sawOr) { + return false; + } + + sawOr = true; + continue; + } + + return false; + } + + if (sawOr && componentValue.type === ComponentValueType.SimpleBlock) { + const media = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode); + if (media === false) { + return false; + } + + return { + advance: i, + node: new MediaOr( + componentValues.slice(0, i).flatMap((x) => { + return x.tokens(); + }), + media, + ), + }; + } + + return false; + } + + return false; +} + +export function parseMediaAnd(componentValues: Array) { + let sawAnd = false; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (componentValue.type === ComponentValueType.Whitespace) { + continue; + } + + if (componentValue.type === ComponentValueType.Comment) { + continue; + } + + if (isIdent(componentValue)) { + const token = (componentValue.value as TokenIdent); + if (token[4].value.toLowerCase() === 'and') { + if (sawAnd) { + return false; + } + + sawAnd = true; + continue; + } + + return false; + } + + if (sawAnd && componentValue.type === ComponentValueType.SimpleBlock) { + const media = parseMediaInParensFromSimpleBlock(componentValue as SimpleBlockNode); + if (media === false) { + return false; + } + + return { + advance: i, + node: new MediaAnd( + componentValues.slice(0, i).flatMap((x) => { + return x.tokens(); + }), + media, + ), + }; + } + + return false; + } + + return false; +} diff --git a/packages/media-query-list-parser/src/parser/parse.ts b/packages/media-query-list-parser/src/parser/parse.ts new file mode 100644 index 000000000..950ed7153 --- /dev/null +++ b/packages/media-query-list-parser/src/parser/parse.ts @@ -0,0 +1,44 @@ +import { parseCommaSeparatedListOfComponentValues } from '@csstools/css-parser-algorithms'; +import { ParserError } from '@csstools/css-parser-algorithms/dist/interfaces/error'; +import { CSSToken, tokenizer } from '@csstools/css-tokenizer'; +import { MediaQuery, MediaQueryInvalid } from '../nodes/media-query'; +import { parseMediaQuery } from './parse-media-query'; + +export type Options = { + preserveInvalidMediaQueries?: boolean, + onParseError?: (error: ParserError) => void +} + +export function parseFromTokens(tokens: Array, options?: Options) { + const componentValuesLists = parseCommaSeparatedListOfComponentValues(tokens, { + onParseError: options?.onParseError, + }); + + return componentValuesLists.map((componentValuesList, index) => { + const mediaQuery = parseMediaQuery(componentValuesList); + if (mediaQuery == false && options?.preserveInvalidMediaQueries === true) { + return new MediaQueryInvalid(componentValuesLists[index]); + } + + return mediaQuery; + }).filter((x) => !!x) as Array; +} + +export function parse(source: string, options?: Options) { + const t = tokenizer({ css: source }, { + commentsAreTokens: true, + onParseError: options?.onParseError, + }); + + const tokens = []; + + { + while (!t.endOfFile()) { + tokens.push(t.nextToken()); + } + + tokens.push(t.nextToken()); // EOF-token + } + + return parseFromTokens(tokens, options); +} diff --git a/packages/media-query-list-parser/src/util/clone-media-query.ts b/packages/media-query-list-parser/src/util/clone-media-query.ts new file mode 100644 index 000000000..ec8b69bde --- /dev/null +++ b/packages/media-query-list-parser/src/util/clone-media-query.ts @@ -0,0 +1,27 @@ +import { cloneTokens, stringify } from '@csstools/css-tokenizer'; +import { MediaQueryInvalid, MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query'; +import { parseFromTokens } from '../parser/parse'; +import { isMediaQueryInvalid, isMediaQueryWithoutType, isMediaQueryWithType } from './type-predicates'; + +export function cloneMediaQuery(x: T): T { + const tokens = cloneTokens(x.tokens()); + const parsed = parseFromTokens(tokens, { preserveInvalidMediaQueries: true }); + const firstQuery = parsed[0]; + if (!firstQuery) { + throw new Error(`Failed to clone media query for : "${stringify(...tokens)}"`); + } + + if (isMediaQueryInvalid(x) && isMediaQueryInvalid(firstQuery)) { + return firstQuery as T; + } + + if (isMediaQueryWithType(x) && isMediaQueryWithType(firstQuery)) { + return firstQuery as T; + } + + if (isMediaQueryWithoutType(x) && isMediaQueryWithoutType(firstQuery)) { + return firstQuery as T; + } + + throw new Error(`Failed to clone media query for : "${stringify(...tokens)}"`); +} diff --git a/packages/media-query-list-parser/src/util/component-value-is.ts b/packages/media-query-list-parser/src/util/component-value-is.ts new file mode 100644 index 000000000..1088cc72b --- /dev/null +++ b/packages/media-query-list-parser/src/util/component-value-is.ts @@ -0,0 +1,29 @@ +import { ComponentValue, ComponentValueType, FunctionNode } from '@csstools/css-parser-algorithms'; +import { CSSToken, TokenFunction, TokenType } from '@csstools/css-tokenizer'; + +export function isNumber(componentValue: ComponentValue) { + if ( + (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Number) || + (componentValue.type === ComponentValueType.Function && ((componentValue as FunctionNode).name as TokenFunction)[4].value === 'calc') + ) { + return true; + } + + return false; +} + +export function isDimension(componentValue: ComponentValue) { + if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Dimension) { + return true; + } + + return false; +} + +export function isIdent(componentValue: ComponentValue) { + if (componentValue.type === ComponentValueType.Token && (componentValue.value as CSSToken)[0] === TokenType.Ident) { + return true; + } + + return false; +} diff --git a/packages/media-query-list-parser/src/util/node-type.ts b/packages/media-query-list-parser/src/util/node-type.ts new file mode 100644 index 000000000..dd8fd4374 --- /dev/null +++ b/packages/media-query-list-parser/src/util/node-type.ts @@ -0,0 +1,21 @@ +export enum NodeType { + GeneralEnclosed = 'general-enclosed', + MediaAnd = 'media-and', + MediaCondition = 'media-condition', + MediaConditionListWithAnd = 'media-condition-list-and', + MediaConditionListWithOr = 'media-condition-list-or', + MediaFeature = 'media-feature', + MediaFeatureBoolean = 'mf-boolean', + MediaFeatureName = 'mf-name', + MediaFeaturePlain = 'mf-plain', + MediaFeatureRangeNameValue = 'mf-range-name-value', + MediaFeatureRangeValueName = 'mf-range-value-name', + MediaFeatureRangeValueNameValue = 'mf-range-value-name-value', + MediaFeatureValue = 'mf-value', + MediaInParens = 'media-in-parens', + MediaNot = 'media-not', + MediaOr = 'media-or', + MediaQueryWithType = 'media-query-with-type', + MediaQueryWithoutType = 'media-query-without-type', + MediaQueryInvalid = 'media-query-invalid', +} diff --git a/packages/media-query-list-parser/src/util/type-predicates.ts b/packages/media-query-list-parser/src/util/type-predicates.ts new file mode 100644 index 000000000..659f048b4 --- /dev/null +++ b/packages/media-query-list-parser/src/util/type-predicates.ts @@ -0,0 +1,102 @@ +import { GeneralEnclosed } from '../nodes/general-enclosed'; +import { MediaAnd } from '../nodes/media-and'; +import { MediaCondition } from '../nodes/media-condition'; +import { MediaConditionList, MediaConditionListWithAnd, MediaConditionListWithOr } from '../nodes/media-condition-list'; +import { MediaFeature } from '../nodes/media-feature'; +import { MediaFeatureBoolean } from '../nodes/media-feature-boolean'; +import { MediaFeatureName } from '../nodes/media-feature-name'; +import { MediaFeaturePlain } from '../nodes/media-feature-plain'; +import { MediaFeatureRange, MediaFeatureRangeNameValue, MediaFeatureRangeValueName, MediaFeatureRangeValueNameValue } from '../nodes/media-feature-range'; +import { MediaFeatureValue } from '../nodes/media-feature-value'; +import { MediaInParens } from '../nodes/media-in-parens'; +import { MediaNot } from '../nodes/media-not'; +import { MediaOr } from '../nodes/media-or'; +import { MediaQuery, MediaQueryInvalid, MediaQueryWithoutType, MediaQueryWithType } from '../nodes/media-query'; + +export function isGeneralEnclosed(x: unknown): x is GeneralEnclosed { + return GeneralEnclosed.isGeneralEnclosed(x); +} + +export function isMediaAnd(x: unknown): x is MediaAnd { + return MediaAnd.isMediaAnd(x); +} + +export function isMediaConditionList(x: unknown): x is MediaConditionList { + return isMediaConditionListWithAnd(x) || isMediaConditionListWithOr(x); +} + +export function isMediaConditionListWithAnd(x: unknown): x is MediaConditionListWithAnd { + return MediaConditionListWithAnd.isMediaConditionListWithAnd(x); +} + +export function isMediaConditionListWithOr(x: unknown): x is MediaConditionListWithOr { + return MediaConditionListWithOr.isMediaConditionListWithOr(x); +} + +export function isMediaCondition(x: unknown): x is MediaCondition { + return MediaCondition.isMediaCondition(x); +} + +export function isMediaFeatureBoolean(x: unknown): x is MediaFeatureBoolean { + return MediaFeatureBoolean.isMediaFeatureBoolean(x); +} + +export function isMediaFeatureName(x: unknown): x is MediaFeatureName { + return MediaFeatureName.isMediaFeatureName(x); +} + +export function isMediaFeatureValue(x: unknown): x is MediaFeatureValue { + return MediaFeatureValue.isMediaFeatureValue(x); +} + +export function isMediaFeaturePlain(x: unknown): x is MediaFeaturePlain { + return MediaFeaturePlain.isMediaFeaturePlain(x); +} + +export function isMediaFeatureRange(x: unknown): x is MediaFeatureRange { + return isMediaFeatureRangeNameValue(x) || isMediaFeatureRangeValueName(x) || isMediaFeatureRangeValueNameValue(x); +} + +export function isMediaFeatureRangeNameValue(x: unknown): x is MediaFeatureRangeNameValue { + return MediaFeatureRangeNameValue.isMediaFeatureRangeNameValue(x); +} + +export function isMediaFeatureRangeValueName(x: unknown): x is MediaFeatureRangeValueName { + return MediaFeatureRangeValueName.isMediaFeatureRangeValueName(x); +} + +export function isMediaFeatureRangeValueNameValue(x: unknown): x is MediaFeatureRangeValueNameValue { + return MediaFeatureRangeValueNameValue.isMediaFeatureRangeValueNameValue(x); +} + +export function isMediaFeature(x: unknown): x is MediaFeature { + return MediaFeature.isMediaFeature(x); +} + +export function isMediaInParens(x: unknown): x is MediaInParens { + return MediaInParens.isMediaInParens(x); +} + +export function isMediaNot(x: unknown): x is MediaNot { + return MediaNot.isMediaNot(x); +} + +export function isMediaOr(x: unknown): x is MediaOr { + return MediaOr.isMediaOr(x); +} + +export function isMediaQuery(x: unknown): x is MediaQuery { + return isMediaQueryWithType(x) || isMediaQueryWithoutType(x) || isMediaQueryInvalid(x); +} + +export function isMediaQueryWithType(x: unknown): x is MediaQueryWithType { + return MediaQueryWithType.isMediaQueryWithType(x); +} + +export function isMediaQueryWithoutType(x: unknown): x is MediaQueryWithoutType { + return MediaQueryWithoutType.isMediaQueryWithoutType(x); +} + +export function isMediaQueryInvalid(x: unknown): x is MediaQueryInvalid { + return MediaQueryInvalid.isMediaQueryInvalid(x); +} diff --git a/packages/media-query-list-parser/stryker.conf.json b/packages/media-query-list-parser/stryker.conf.json new file mode 100644 index 000000000..015ebbb73 --- /dev/null +++ b/packages/media-query-list-parser/stryker.conf.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "mutate": [ + "src/**/*.ts" + ], + "buildCommand": "npm run build", + "testRunner": "command", + "coverageAnalysis": "perTest", + "tempDirName": "../../.stryker-tmp", + "commandRunner": { + "command": "npm run test" + }, + "thresholds": { + "high": 100, + "low": 100, + "break": 100 + }, + "inPlace": true +} diff --git a/packages/media-query-list-parser/test/_import.mjs b/packages/media-query-list-parser/test/_import.mjs new file mode 100644 index 000000000..3215dfbbb --- /dev/null +++ b/packages/media-query-list-parser/test/_import.mjs @@ -0,0 +1,3 @@ +import { parse } from '@csstools/media-query-list-parser'; + +parse('(min-width: 300px)'); diff --git a/packages/media-query-list-parser/test/_require.cjs b/packages/media-query-list-parser/test/_require.cjs new file mode 100644 index 000000000..09406cf78 --- /dev/null +++ b/packages/media-query-list-parser/test/_require.cjs @@ -0,0 +1,3 @@ +const { parse } = require('@csstools/media-query-list-parser'); + +parse('(min-width: 300px)'); diff --git a/packages/media-query-list-parser/test/api/options.mjs b/packages/media-query-list-parser/test/api/options.mjs new file mode 100644 index 000000000..9268779f4 --- /dev/null +++ b/packages/media-query-list-parser/test/api/options.mjs @@ -0,0 +1,71 @@ +import assert from 'assert'; +import { parse } from '@csstools/media-query-list-parser'; + +{ + const resultAST = parse('[a, b], all', { + preserveInvalidMediaQueries: true, + }); + + assert.equal( + resultAST.length, + 2, + ); + + assert.equal( + resultAST[0].type, + 'media-query-invalid', + ); +} + +{ + const resultAST = parse('[a, b], all', { + preserveInvalidMediaQueries: false, + }); + + assert.equal( + resultAST.length, + 1, + ); +} + +{ + const resultAST = parse('[a, b], all'); + + assert.equal( + resultAST.length, + 1, + ); +} + +{ + const resultAST = parse('(foo'); + + assert.equal( + resultAST.length, + 0, + ); +} + +{ + let error; + const resultAST = parse('(foo', { + onParseError: (err) => { + error = err; + }, + }); + + assert.equal( + resultAST.length, + 0, + ); + + assert.deepEqual( + error, + { + message: 'Unexpected EOF while consuming a simple block.', + start: 0, + end: -1, + state: ['5.4.8. Consume a simple block', 'Unexpected EOF'], + }, + ); +} diff --git a/packages/media-query-list-parser/test/cases/media-not/0001.expect.json b/packages/media-query-list-parser/test/cases/media-not/0001.expect.json new file mode 100644 index 000000000..5a9906847 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/media-not/0001.expect.json @@ -0,0 +1,71 @@ +[ + { + "type": "media-query-without-type", + "string": "not (color)", + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 5, + 9, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 4, + 4, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 10, + 10, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/media-not/0001.mjs b/packages/media-query-list-parser/test/cases/media-not/0001.mjs new file mode 100644 index 000000000..5eb55f343 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/media-not/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not (color)', + 'media-not/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0001.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0001.expect.json new file mode 100644 index 000000000..af1643cf2 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0001.expect.json @@ -0,0 +1,50 @@ +[ + { + "type": "media-query-without-type", + "string": "(color)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 6, + 6, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0001.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0001.mjs new file mode 100644 index 000000000..2686abd60 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color)', + 'mf-boolean/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0002.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0002.expect.json new file mode 100644 index 000000000..91c136e7d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0002.expect.json @@ -0,0 +1,78 @@ +[ + { + "type": "media-query-without-type", + "string": "/* comment 1 */(/* comment 2 */color/* comment 3 */)/* comment 4 */", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "color", + 31, + 35, + { + "value": "color" + } + ], + [ + "comment", + "/* comment 3 */", + 36, + 50, + null + ] + ] + }, + "before": [ + [ + "comment", + "/* comment 1 */", + 0, + 14, + null + ], + [ + "(-token", + "(", + 15, + 15, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 51, + 51, + null + ], + [ + "comment", + "/* comment 4 */", + 52, + 66, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0002.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0002.mjs new file mode 100644 index 000000000..f8266d441 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '/* comment 1 */(/* comment 2 */color/* comment 3 */)/* comment 4 */', + 'mf-boolean/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0003.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0003.expect.json new file mode 100644 index 000000000..af07020b7 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0003.expect.json @@ -0,0 +1,50 @@ +[ + { + "type": "media-query-without-type", + "string": "(true)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "true", + "tokens": [ + [ + "ident-token", + "true", + 1, + 4, + { + "value": "true" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 5, + 5, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0003.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0003.mjs new file mode 100644 index 000000000..c0c9f5645 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(true)', + 'mf-boolean/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0004.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0004.expect.json new file mode 100644 index 000000000..ec092ecf0 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0004.expect.json @@ -0,0 +1,50 @@ +[ + { + "type": "media-query-without-type", + "string": "(false)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "false", + "tokens": [ + [ + "ident-token", + "false", + 1, + 5, + { + "value": "false" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 6, + 6, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0004.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0004.mjs new file mode 100644 index 000000000..0a34a2dc8 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(false)', + 'mf-boolean/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0005.expect.json b/packages/media-query-list-parser/test/cases/mf-boolean/0005.expect.json new file mode 100644 index 000000000..924b11622 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0005.expect.json @@ -0,0 +1,49 @@ +[ + { + "type": "media-query-without-type", + "string": "(color())", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "color(", + 1, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ], + [ + ")-token", + ")", + 8, + 8, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-boolean/0005.mjs b/packages/media-query-list-parser/test/cases/mf-boolean/0005.mjs new file mode 100644 index 000000000..fd8afbd15 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-boolean/0005.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color())', + 'mf-boolean/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0001.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0001.expect.json new file mode 100644 index 000000000..7a0c3891e --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0001.expect.json @@ -0,0 +1,128 @@ +[ + { + "type": "media-query-without-type", + "string": "(min-width: 300px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "min-width", + "tokens": [ + [ + "ident-token", + "min-width", + 1, + 9, + { + "value": "min-width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "min-width", + 1, + 9, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 10, + 10, + null + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "dimension-token", + "300px", + 12, + 16, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 17, + 17, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0001.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0001.mjs new file mode 100644 index 000000000..b674721b7 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(min-width: 300px)', + 'mf-plain/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0002.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0002.expect.json new file mode 100644 index 000000000..dbcb81161 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0002.expect.json @@ -0,0 +1,184 @@ +[ + { + "type": "media-query-without-type", + "string": "/* comment 1 */(/* comment 2 */min-width/* comment 3 */:/* comment 4 */300px/* comment 5 */)/* comment 6 */", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "min-width", + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "min-width", + 31, + 39, + { + "value": "min-width" + } + ], + [ + "comment", + "/* comment 3 */", + 40, + 54, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "comment", + "/* comment 4 */", + 56, + 70, + null + ], + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + "comment", + "/* comment 5 */", + 76, + 90, + null + ] + ] + }, + "tokens": [ + [ + "comment", + "/* comment 2 */", + 16, + 30, + null + ], + [ + "ident-token", + "min-width", + 31, + 39, + { + "value": "min-width" + } + ], + [ + "comment", + "/* comment 3 */", + 40, + 54, + null + ], + [ + "colon-token", + ":", + 55, + 55, + null + ], + [ + "comment", + "/* comment 4 */", + 56, + 70, + null + ], + [ + "dimension-token", + "300px", + 71, + 75, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + "comment", + "/* comment 5 */", + 76, + 90, + null + ] + ] + }, + "before": [ + [ + "comment", + "/* comment 1 */", + 0, + 14, + null + ], + [ + "(-token", + "(", + 15, + 15, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 91, + 91, + null + ], + [ + "comment", + "/* comment 6 */", + 92, + 106, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0002.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0002.mjs new file mode 100644 index 000000000..90eb3e1f9 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '/* comment 1 */(/* comment 2 */min-width/* comment 3 */:/* comment 4 */300px/* comment 5 */)/* comment 6 */', + 'mf-plain/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0003.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0003.expect.json new file mode 100644 index 000000000..1eb0b3ce3 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0003.expect.json @@ -0,0 +1,122 @@ +[ + { + "type": "media-query-without-type", + "string": "(resolution: infinite)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "resolution", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "colon-token", + ":", + 11, + 11, + null + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 21, + 21, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0003.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0003.mjs new file mode 100644 index 000000000..ad7c3adf2 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(resolution: infinite)', + 'mf-plain/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0004.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0004.expect.json new file mode 100644 index 000000000..5c69041e4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0004.expect.json @@ -0,0 +1,81 @@ +[ + { + "type": "media-query-without-type", + "string": "(resolution(foo): infinite)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "function-token", + "resolution(", + 1, + 11, + { + "value": "resolution" + } + ], + [ + "ident-token", + "foo", + 12, + 14, + { + "value": "foo" + } + ], + [ + ")-token", + ")", + 15, + 15, + null + ], + [ + "colon-token", + ":", + 16, + 16, + null + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ], + [ + "ident-token", + "infinite", + 18, + 25, + { + "value": "infinite" + } + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0004.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0004.mjs new file mode 100644 index 000000000..7c310ffb4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0004.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(resolution(foo): infinite)', + 'mf-plain/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0005.expect.json b/packages/media-query-list-parser/test/cases/mf-plain/0005.expect.json new file mode 100644 index 000000000..81dc5863d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0005.expect.json @@ -0,0 +1,120 @@ +[ + { + "type": "media-query-invalid", + "string": "[resolution: infinite]", + "media": [ + { + "type": "simple-block", + "startToken": [ + "[-token", + "[", + 0, + 0, + null + ], + "tokens": [ + [ + "[-token", + "[", + 0, + 0, + null + ], + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "colon-token", + ":", + 11, + 11, + null + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ], + [ + "]-token", + "]", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 11, + 11, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 13, + 20, + { + "value": "infinite" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-plain/0005.mjs b/packages/media-query-list-parser/test/cases/mf-plain/0005.mjs new file mode 100644 index 000000000..946d31898 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-plain/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '[resolution: infinite]', + 'mf-plain/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0001.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0001.expect.json new file mode 100644 index 000000000..11eadf3af --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0001.expect.json @@ -0,0 +1,452 @@ +[ + { + "type": "media-query-without-type", + "string": "(20px < width <= calc(20px * 5))", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "width", + 8, + 12, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 13, + 13, + null + ] + ] + }, + "valueOne": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "20px", + 1, + 4, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "20px", + 1, + 4, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 5, + 5, + null + ] + ] + }, + "valueTwo": { + "type": "mf-value", + "value": { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 17, + 21, + { + "value": "calc" + } + ], + [ + "dimension-token", + "20px", + 22, + 25, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "delim-token", + "*", + 27, + 27, + { + "value": "*" + } + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "number-token", + "5", + 29, + 29, + { + "value": 5, + "type": "integer" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "20px", + 22, + 25, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "*", + 27, + 27, + { + "value": "*" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 28, + 28, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "number-token", + "5", + 29, + 29, + { + "value": 5, + "type": "integer" + } + ] + ] + } + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "function-token", + "calc(", + 17, + 21, + { + "value": "calc" + } + ], + [ + "dimension-token", + "20px", + 22, + 25, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "delim-token", + "*", + 27, + 27, + { + "value": "*" + } + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "number-token", + "5", + 29, + 29, + { + "value": 5, + "type": "integer" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "20px", + 1, + 4, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 5, + 5, + null + ], + [ + "delim-token", + "<", + 6, + 6, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "width", + 8, + 12, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "delim-token", + "<", + 14, + 14, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 15, + 15, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "function-token", + "calc(", + 17, + 21, + { + "value": "calc" + } + ], + [ + "dimension-token", + "20px", + 22, + 25, + { + "value": 20, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "delim-token", + "*", + 27, + 27, + { + "value": "*" + } + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "number-token", + "5", + 29, + 29, + { + "value": 5, + "type": "integer" + } + ], + [ + ")-token", + ")", + 30, + 30, + null + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 31, + 31, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0001.mjs b/packages/media-query-list-parser/test/cases/mf-range/0001.mjs new file mode 100644 index 000000000..37aba33f4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(20px < width <= calc(20px * 5))', + 'mf-range/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0002.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0002.expect.json new file mode 100644 index 000000000..00ce71e31 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0002.expect.json @@ -0,0 +1,186 @@ +[ + { + "type": "media-query-without-type", + "string": "/* comment 1 */(/* comment 2 */30px/* comment 3 */ { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0003.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0003.expect.json new file mode 100644 index 000000000..b509b256c --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0003.expect.json @@ -0,0 +1,488 @@ +[ + { + "type": "media-query-without-type", + "string": "( 1 / 5 < aspect-ratio < 3 / 2 )", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name-value", + "name": { + "type": "mf-name", + "name": "aspect-ratio", + "tokens": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "aspect-ratio", + 10, + 21, + { + "value": "aspect-ratio" + } + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + }, + "valueOne": { + "type": "mf-value", + "value": [ + { + "type": "token", + "tokens": [ + [ + "number-token", + "1", + 2, + 2, + { + "value": 1, + "type": "integer" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "/", + 4, + 4, + { + "value": "/" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 5, + 5, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "number-token", + "5", + 6, + 6, + { + "value": 5, + "type": "integer" + } + ] + ] + } + ], + "tokens": [ + [ + "whitespace-token", + " ", + 1, + 1, + null + ], + [ + "number-token", + "1", + 2, + 2, + { + "value": 1, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 3, + 3, + null + ], + [ + "delim-token", + "/", + 4, + 4, + { + "value": "/" + } + ], + [ + "whitespace-token", + " ", + 5, + 5, + null + ], + [ + "number-token", + "5", + 6, + 6, + { + "value": 5, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ] + ] + }, + "valueTwo": { + "type": "mf-value", + "value": [ + { + "type": "token", + "tokens": [ + [ + "number-token", + "3", + 25, + 25, + { + "value": 3, + "type": "integer" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "/", + 27, + 27, + { + "value": "/" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 28, + 28, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "number-token", + "2", + 29, + 29, + { + "value": 2, + "type": "integer" + } + ] + ] + } + ], + "tokens": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ], + [ + "number-token", + "3", + 25, + 25, + { + "value": 3, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "delim-token", + "/", + 27, + 27, + { + "value": "/" + } + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "number-token", + "2", + 29, + 29, + { + "value": 2, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 30, + 30, + null + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 1, + 1, + null + ], + [ + "number-token", + "1", + 2, + 2, + { + "value": 1, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 3, + 3, + null + ], + [ + "delim-token", + "/", + 4, + 4, + { + "value": "/" + } + ], + [ + "whitespace-token", + " ", + 5, + 5, + null + ], + [ + "number-token", + "5", + 6, + 6, + { + "value": 5, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + "<", + 8, + 8, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "aspect-ratio", + 10, + 21, + { + "value": "aspect-ratio" + } + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + "delim-token", + "<", + 23, + 23, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 24, + 24, + null + ], + [ + "number-token", + "3", + 25, + 25, + { + "value": 3, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "delim-token", + "/", + 27, + 27, + { + "value": "/" + } + ], + [ + "whitespace-token", + " ", + 28, + 28, + null + ], + [ + "number-token", + "2", + 29, + 29, + { + "value": 2, + "type": "integer" + } + ], + [ + "whitespace-token", + " ", + 30, + 30, + null + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 31, + 31, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0003.mjs b/packages/media-query-list-parser/test/cases/mf-range/0003.mjs new file mode 100644 index 000000000..5626fc00a --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '( 1 / 5 < aspect-ratio < 3 / 2 )', + 'mf-range/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0004.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0004.expect.json new file mode 100644 index 000000000..8990ac58b --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0004.expect.json @@ -0,0 +1,300 @@ +[ + { + "type": "media-query-without-type", + "string": "(1/5<3/2)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name-value", + "name": { + "type": "mf-name", + "name": "aspect-ratio", + "tokens": [ + [ + "ident-token", + "aspect-ratio", + 5, + 16, + { + "value": "aspect-ratio" + } + ] + ] + }, + "valueOne": { + "type": "mf-value", + "value": [ + { + "type": "token", + "tokens": [ + [ + "number-token", + "1", + 1, + 1, + { + "value": 1, + "type": "integer" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "/", + 2, + 2, + { + "value": "/" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "number-token", + "5", + 3, + 3, + { + "value": 5, + "type": "integer" + } + ] + ] + } + ], + "tokens": [ + [ + "number-token", + "1", + 1, + 1, + { + "value": 1, + "type": "integer" + } + ], + [ + "delim-token", + "/", + 2, + 2, + { + "value": "/" + } + ], + [ + "number-token", + "5", + 3, + 3, + { + "value": 5, + "type": "integer" + } + ] + ] + }, + "valueTwo": { + "type": "mf-value", + "value": [ + { + "type": "token", + "tokens": [ + [ + "number-token", + "3", + 18, + 18, + { + "value": 3, + "type": "integer" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "/", + 19, + 19, + { + "value": "/" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "number-token", + "2", + 20, + 20, + { + "value": 2, + "type": "integer" + } + ] + ] + } + ], + "tokens": [ + [ + "number-token", + "3", + 18, + 18, + { + "value": 3, + "type": "integer" + } + ], + [ + "delim-token", + "/", + 19, + 19, + { + "value": "/" + } + ], + [ + "number-token", + "2", + 20, + 20, + { + "value": 2, + "type": "integer" + } + ] + ] + }, + "tokens": [ + [ + "number-token", + "1", + 1, + 1, + { + "value": 1, + "type": "integer" + } + ], + [ + "delim-token", + "/", + 2, + 2, + { + "value": "/" + } + ], + [ + "number-token", + "5", + 3, + 3, + { + "value": 5, + "type": "integer" + } + ], + [ + "delim-token", + "<", + 4, + 4, + { + "value": "<" + } + ], + [ + "ident-token", + "aspect-ratio", + 5, + 16, + { + "value": "aspect-ratio" + } + ], + [ + "delim-token", + "<", + 17, + 17, + { + "value": "<" + } + ], + [ + "number-token", + "3", + 18, + 18, + { + "value": 3, + "type": "integer" + } + ], + [ + "delim-token", + "/", + 19, + 19, + { + "value": "/" + } + ], + [ + "number-token", + "2", + 20, + 20, + { + "value": 2, + "type": "integer" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 21, + 21, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0004.mjs b/packages/media-query-list-parser/test/cases/mf-range/0004.mjs new file mode 100644 index 000000000..3863dfccc --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(1/5<3/2)', + 'mf-range/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0005.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0005.expect.json new file mode 100644 index 000000000..58e7ee127 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0005.expect.json @@ -0,0 +1,144 @@ +[ + { + "type": "media-query-without-type", + "string": "(width = 50px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50px", + 9, + 12, + { + "value": 50, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "dimension-token", + "50px", + 9, + 12, + { + "value": 50, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "delim-token", + "=", + 7, + 7, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "dimension-token", + "50px", + 9, + 12, + { + "value": 50, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 13, + 13, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0005.mjs b/packages/media-query-list-parser/test/cases/mf-range/0005.mjs new file mode 100644 index 000000000..fbaf31925 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(width = 50px)', + 'mf-range/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0006.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0006.expect.json new file mode 100644 index 000000000..dc8669cc7 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0006.expect.json @@ -0,0 +1,144 @@ +[ + { + "type": "media-query-without-type", + "string": "(50px = width)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "width", + 8, + 12, + { + "value": "width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50px", + 1, + 4, + { + "value": 50, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "50px", + 1, + 4, + { + "value": 50, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 5, + 5, + null + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "50px", + 1, + 4, + { + "value": 50, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 5, + 5, + null + ], + [ + "delim-token", + "=", + 6, + 6, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "width", + 8, + 12, + { + "value": "width" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 13, + 13, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0006.mjs b/packages/media-query-list-parser/test/cases/mf-range/0006.mjs new file mode 100644 index 000000000..cc1c77c20 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0006.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(50px = width)', + 'mf-range/0006', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0007.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0007.expect.json new file mode 100644 index 000000000..2f4ab2f30 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0007.expect.json @@ -0,0 +1,224 @@ +[ + { + "type": "media-query-without-type", + "string": "(1000px > height > 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name-value", + "name": { + "type": "mf-name", + "name": "height", + "tokens": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "height", + 10, + 15, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ] + }, + "valueOne": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ] + ] + }, + "valueTwo": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "100px", + 19, + 23, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "dimension-token", + "100px", + 19, + 23, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "height", + 10, + 15, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "delim-token", + ">", + 17, + 17, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "dimension-token", + "100px", + 19, + 23, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 24, + 24, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0007.mjs b/packages/media-query-list-parser/test/cases/mf-range/0007.mjs new file mode 100644 index 000000000..0db8883c7 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0007.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(1000px > height > 100px)', + 'mf-range/0007', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0008.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0008.expect.json new file mode 100644 index 000000000..7609cc506 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0008.expect.json @@ -0,0 +1,110 @@ +[ + { + "type": "media-query-without-type", + "string": "(1000px > height < 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "height", + 10, + 15, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "delim-token", + "<", + 17, + 17, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "dimension-token", + "100px", + 19, + 23, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 24, + 24, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0008.mjs b/packages/media-query-list-parser/test/cases/mf-range/0008.mjs new file mode 100644 index 000000000..7b9fc7609 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0008.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(1000px > height < 100px)', + 'mf-range/0008', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0009.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0009.expect.json new file mode 100644 index 000000000..67cfa7111 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0009.expect.json @@ -0,0 +1,119 @@ +[ + { + "type": "media-query-without-type", + "string": "(1000px > height <= 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "height", + 10, + 15, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "delim-token", + "<", + 17, + 17, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 18, + 18, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 19, + 19, + null + ], + [ + "dimension-token", + "100px", + 20, + 24, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0009.mjs b/packages/media-query-list-parser/test/cases/mf-range/0009.mjs new file mode 100644 index 000000000..95098d1ca --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0009.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(1000px > height <= 100px)', + 'mf-range/0009', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0010.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0010.expect.json new file mode 100644 index 000000000..ca2e475cb --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0010.expect.json @@ -0,0 +1,110 @@ +[ + { + "type": "media-query-without-type", + "string": "(1000px > height = 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "height", + 10, + 15, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "delim-token", + "=", + 17, + 17, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "dimension-token", + "100px", + 19, + 23, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 24, + 24, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0010.mjs b/packages/media-query-list-parser/test/cases/mf-range/0010.mjs new file mode 100644 index 000000000..6ff9a2ba0 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0010.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(1000px > height = 100px)', + 'mf-range/0010', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0011.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0011.expect.json new file mode 100644 index 000000000..d78ab5dcd --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0011.expect.json @@ -0,0 +1,126 @@ +[ + { + "type": "media-query-without-type", + "string": "(1000px > height > = 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "dimension-token", + "1000px", + 1, + 6, + { + "value": 1000, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "height", + 10, + 15, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ], + [ + "delim-token", + ">", + 17, + 17, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "=", + 19, + 19, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "100px", + 21, + 25, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 26, + 26, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0011.mjs b/packages/media-query-list-parser/test/cases/mf-range/0011.mjs new file mode 100644 index 000000000..5d1a49b55 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0011.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(1000px > height > = 100px)', + 'mf-range/0011', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0012.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0012.expect.json new file mode 100644 index 000000000..945713f41 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0012.expect.json @@ -0,0 +1,92 @@ +[ + { + "type": "media-query-without-type", + "string": "(height > = 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "height", + 1, + 6, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "delim-token", + "=", + 10, + 10, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "dimension-token", + "100px", + 12, + 16, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 17, + 17, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0012.mjs b/packages/media-query-list-parser/test/cases/mf-range/0012.mjs new file mode 100644 index 000000000..01cd32f50 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0012.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(height > = 100px)', + 'mf-range/0012', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0013.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0013.expect.json new file mode 100644 index 000000000..2cf2497d3 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0013.expect.json @@ -0,0 +1,153 @@ +[ + { + "type": "media-query-without-type", + "string": "(height >= 100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "height", + "tokens": [ + [ + "ident-token", + "height", + 1, + 6, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "100px", + 11, + 15, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "dimension-token", + "100px", + 11, + 15, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "height", + 1, + 6, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "delim-token", + ">", + 8, + 8, + { + "value": ">" + } + ], + [ + "delim-token", + "=", + 9, + 9, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "dimension-token", + "100px", + 11, + 15, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 16, + 16, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0013.mjs b/packages/media-query-list-parser/test/cases/mf-range/0013.mjs new file mode 100644 index 000000000..ea88732f7 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0013.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(height >= 100px)', + 'mf-range/0013', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/mf-range/0014.expect.json b/packages/media-query-list-parser/test/cases/mf-range/0014.expect.json new file mode 100644 index 000000000..4f0b3df0f --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0014.expect.json @@ -0,0 +1,153 @@ +[ + { + "type": "media-query-without-type", + "string": "(100px <= width)", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "width", + 10, + 14, + { + "value": "width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "100px", + 1, + 5, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "100px", + 1, + 5, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "100px", + 1, + 5, + { + "value": 100, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 8, + 8, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 9, + 9, + null + ], + [ + "ident-token", + "width", + 10, + 14, + { + "value": "width" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 15, + 15, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/mf-range/0014.mjs b/packages/media-query-list-parser/test/cases/mf-range/0014.mjs new file mode 100644 index 000000000..07057e069 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/mf-range/0014.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(100px <= width)', + 'mf-range/0014', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0001.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0001.expect.json new file mode 100644 index 000000000..ca5c59492 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0001.expect.json @@ -0,0 +1,19 @@ +[ + { + "type": "media-query-with-type", + "string": "screen", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0001.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0001.mjs new file mode 100644 index 000000000..b8d1c3f6b --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'screen', + 'query-with-type/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0002.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0002.expect.json new file mode 100644 index 000000000..f8f8ec84d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0002.expect.json @@ -0,0 +1,36 @@ +[ + { + "type": "media-query-with-type", + "string": "only screen", + "modifier": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ], + "mediaType": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ], + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0002.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0002.mjs new file mode 100644 index 000000000..d9da6523a --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'only screen', + 'query-with-type/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0003.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0003.expect.json new file mode 100644 index 000000000..428d65884 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0003.expect.json @@ -0,0 +1,36 @@ +[ + { + "type": "media-query-with-type", + "string": "not screen", + "modifier": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ], + "mediaType": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ], + [ + "ident-token", + "screen", + 4, + 9, + { + "value": "screen" + } + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0003.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0003.mjs new file mode 100644 index 000000000..1684cf679 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not screen', + 'query-with-type/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0004.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0004.expect.json new file mode 100644 index 000000000..c1793f870 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0004.expect.json @@ -0,0 +1,182 @@ +[ + { + "type": "media-query-with-type", + "string": "only screen and (min-width: 300px)", + "modifier": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ], + "mediaType": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ], + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ] + ], + "and": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "ident-token", + "and", + 12, + 14, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "min-width", + "tokens": [ + [ + "ident-token", + "min-width", + 17, + 25, + { + "value": "min-width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 28, + 32, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 27, + 27, + null + ], + [ + "dimension-token", + "300px", + 28, + 32, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "min-width", + 17, + 25, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 26, + 26, + null + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ], + [ + "dimension-token", + "300px", + 28, + 32, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "whitespace-token", + " ", + 15, + 15, + null + ], + [ + "(-token", + "(", + 16, + 16, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 33, + 33, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0004.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0004.mjs new file mode 100644 index 000000000..e056d5497 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'only screen and (min-width: 300px)', + 'query-with-type/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0005.expect.json b/packages/media-query-list-parser/test/cases/query-with-type/0005.expect.json new file mode 100644 index 000000000..2beac0b28 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0005.expect.json @@ -0,0 +1,203 @@ +[ + { + "type": "media-query-with-type", + "string": "/* a comment 0 */only/* a comment 1 */screen/* a comment 2 */ /* a comment 3 */and/* a comment 4 */(min-width:/* a comment 5 */300px)", + "modifier": [ + [ + "comment", + "/* a comment 0 */", + 0, + 16, + null + ], + [ + "ident-token", + "only", + 17, + 20, + { + "value": "only" + } + ] + ], + "mediaType": [ + [ + "comment", + "/* a comment 1 */", + 21, + 37, + null + ], + [ + "ident-token", + "screen", + 38, + 43, + { + "value": "screen" + } + ] + ], + "and": [ + [ + "comment", + "/* a comment 2 */", + 44, + 60, + null + ], + [ + "whitespace-token", + " ", + 61, + 61, + null + ], + [ + "comment", + "/* a comment 3 */", + 62, + 78, + null + ], + [ + "ident-token", + "and", + 79, + 81, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "min-width", + "tokens": [ + [ + "ident-token", + "min-width", + 100, + 108, + { + "value": "min-width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 127, + 131, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "comment", + "/* a comment 5 */", + 110, + 126, + null + ], + [ + "dimension-token", + "300px", + 127, + 131, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "min-width", + 100, + 108, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 109, + 109, + null + ], + [ + "comment", + "/* a comment 5 */", + 110, + 126, + null + ], + [ + "dimension-token", + "300px", + 127, + 131, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "comment", + "/* a comment 4 */", + 82, + 98, + null + ], + [ + "(-token", + "(", + 99, + 99, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 132, + 132, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/query-with-type/0005.mjs b/packages/media-query-list-parser/test/cases/query-with-type/0005.mjs new file mode 100644 index 000000000..7dffdd9d5 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/query-with-type/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '/* a comment 0 */only/* a comment 1 */screen/* a comment 2 */ /* a comment 3 */and/* a comment 4 */(min-width:/* a comment 5 */300px)', + 'query-with-type/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0001.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0001.expect.json new file mode 100644 index 000000000..ca5c59492 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0001.expect.json @@ -0,0 +1,19 @@ +[ + { + "type": "media-query-with-type", + "string": "screen", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0001.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0001.mjs new file mode 100644 index 000000000..9b78044a6 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'screen', + 'specification-examples/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0002.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0002.expect.json new file mode 100644 index 000000000..a4c943d65 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0002.expect.json @@ -0,0 +1,19 @@ +[ + { + "type": "media-query-with-type", + "string": "print", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "print", + 0, + 4, + { + "value": "print" + } + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0002.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0002.mjs new file mode 100644 index 000000000..fc91ac129 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'print', + 'specification-examples/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0003.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0003.expect.json new file mode 100644 index 000000000..632d6888f --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0003.expect.json @@ -0,0 +1,179 @@ +[ + { + "type": "media-query-with-type", + "string": "screen and (color)", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ], + "and": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "ident-token", + "and", + 7, + 9, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 12, + 16, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "(-token", + "(", + 11, + 11, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 17, + 17, + null + ] + ] + }, + "before": [], + "after": [] + } + } + }, + { + "type": "media-query-with-type", + "string": " projection and (color)", + "modifier": [], + "mediaType": [ + [ + "whitespace-token", + " ", + 19, + 19, + null + ], + [ + "ident-token", + "projection", + 20, + 29, + { + "value": "projection" + } + ] + ], + "and": [ + [ + "whitespace-token", + " ", + 30, + 30, + null + ], + [ + "ident-token", + "and", + 31, + 33, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 36, + 40, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "whitespace-token", + " ", + 34, + 34, + null + ], + [ + "(-token", + "(", + 35, + 35, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 41, + 41, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0003.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0003.mjs new file mode 100644 index 000000000..70f1591ed --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'screen and (color), projection and (color)', + 'specification-examples/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0004.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0004.expect.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0004.expect.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0004.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0004.mjs new file mode 100644 index 000000000..531984b8b --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '', + 'specification-examples/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0005.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0005.expect.json new file mode 100644 index 000000000..4a93fb3d2 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0005.expect.json @@ -0,0 +1,19 @@ +[ + { + "type": "media-query-with-type", + "string": "all", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "all", + 0, + 2, + { + "value": "all" + } + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0005.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0005.mjs new file mode 100644 index 000000000..f9a7370f3 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'all', + 'specification-examples/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0006.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0006.expect.json new file mode 100644 index 000000000..0a1810226 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0006.expect.json @@ -0,0 +1,231 @@ +[ + { + "type": "media-query-with-type", + "string": "speech and (device-aspect-ratio: 16/9)", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "speech", + 0, + 5, + { + "value": "speech" + } + ] + ], + "and": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "ident-token", + "and", + 7, + 9, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "device-aspect-ratio", + "tokens": [ + [ + "ident-token", + "device-aspect-ratio", + 12, + 30, + { + "value": "device-aspect-ratio" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": [ + { + "type": "token", + "tokens": [ + [ + "number-token", + "16", + 33, + 34, + { + "value": 16, + "type": "integer" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "/", + 35, + 35, + { + "value": "/" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "number-token", + "9", + 36, + 36, + { + "value": 9, + "type": "integer" + } + ] + ] + } + ], + "tokens": [ + [ + "whitespace-token", + " ", + 32, + 32, + null + ], + [ + "number-token", + "16", + 33, + 34, + { + "value": 16, + "type": "integer" + } + ], + [ + "delim-token", + "/", + 35, + 35, + { + "value": "/" + } + ], + [ + "number-token", + "9", + 36, + 36, + { + "value": 9, + "type": "integer" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "device-aspect-ratio", + 12, + 30, + { + "value": "device-aspect-ratio" + } + ], + [ + "colon-token", + ":", + 31, + 31, + null + ], + [ + "whitespace-token", + " ", + 32, + 32, + null + ], + [ + "number-token", + "16", + 33, + 34, + { + "value": 16, + "type": "integer" + } + ], + [ + "delim-token", + "/", + 35, + 35, + { + "value": "/" + } + ], + [ + "number-token", + "9", + 36, + 36, + { + "value": 9, + "type": "integer" + } + ] + ] + }, + "before": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "(-token", + "(", + 11, + 11, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 37, + 37, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0006.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0006.mjs new file mode 100644 index 000000000..2a9da88e0 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0006.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'speech and (device-aspect-ratio: 16/9)', + 'specification-examples/0006', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0007.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0007.expect.json new file mode 100644 index 000000000..d3955b3fd --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0007.expect.json @@ -0,0 +1,174 @@ +[ + { + "type": "media-query-without-type", + "string": "not (width <= -100px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "ident-token", + "width", + 5, + 9, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 10, + 10, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "-100px", + 14, + 19, + { + "value": -100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "dimension-token", + "-100px", + 14, + 19, + { + "value": -100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "width", + 5, + 9, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "delim-token", + "<", + 11, + 11, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 12, + 12, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "dimension-token", + "-100px", + 14, + 19, + { + "value": -100, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 4, + 4, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 20, + 20, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0007.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0007.mjs new file mode 100644 index 000000000..910e53f13 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0007.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not (width <= -100px)', + 'specification-examples/0007', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0008.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0008.expect.json new file mode 100644 index 000000000..3ba2afa4c --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0008.expect.json @@ -0,0 +1,314 @@ +[ + { + "type": "media-query-without-type", + "string": "(width < 600px) and (height < 600px)", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "600px", + 9, + 13, + { + "value": 600, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "dimension-token", + "600px", + 9, + 13, + { + "value": 600, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "dimension-token", + "600px", + 9, + 13, + { + "value": 600, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 14, + 14, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 15, + 15, + null + ], + [ + "ident-token", + "and", + 16, + 18, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 19, + 19, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "height", + "tokens": [ + [ + "ident-token", + "height", + 21, + 26, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "600px", + 30, + 34, + { + "value": 600, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 29, + 29, + null + ], + [ + "dimension-token", + "600px", + 30, + 34, + { + "value": 600, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "height", + 21, + 26, + { + "value": "height" + } + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ], + [ + "delim-token", + "<", + 28, + 28, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 29, + 29, + null + ], + [ + "dimension-token", + "600px", + 30, + 34, + { + "value": 600, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 20, + 20, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 35, + 35, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0008.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0008.mjs new file mode 100644 index 000000000..a29e3a438 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0008.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(width < 600px) and (height < 600px)', + 'specification-examples/0008', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0009.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0009.expect.json new file mode 100644 index 000000000..d55ed0c15 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0009.expect.json @@ -0,0 +1,270 @@ +[ + { + "type": "media-query-without-type", + "string": "(update: slow) or (hover: none)", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-or", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "update", + "tokens": [ + [ + "ident-token", + "update", + 1, + 6, + { + "value": "update" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "ident-token", + "slow", + 9, + 12, + { + "value": "slow" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "ident-token", + "slow", + 9, + 12, + { + "value": "slow" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "update", + 1, + 6, + { + "value": "update" + } + ], + [ + "colon-token", + ":", + 7, + 7, + null + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "ident-token", + "slow", + 9, + 12, + { + "value": "slow" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 13, + 13, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-or", + "modifier": [ + [ + "whitespace-token", + " ", + 14, + 14, + null + ], + [ + "ident-token", + "or", + 15, + 16, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "hover", + "tokens": [ + [ + "ident-token", + "hover", + 19, + 23, + { + "value": "hover" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "ident-token", + "none", + 26, + 29, + { + "value": "none" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 25, + 25, + null + ], + [ + "ident-token", + "none", + 26, + 29, + { + "value": "none" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "hover", + 19, + 23, + { + "value": "hover" + } + ], + [ + "colon-token", + ":", + 24, + 24, + null + ], + [ + "whitespace-token", + " ", + 25, + 25, + null + ], + [ + "ident-token", + "none", + 26, + 29, + { + "value": "none" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 18, + 18, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 30, + 30, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0009.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0009.mjs new file mode 100644 index 000000000..35c04d1af --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0009.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(update: slow) or (hover: none)', + 'specification-examples/0009', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0010.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0010.expect.json new file mode 100644 index 000000000..86d043cf0 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0010.expect.json @@ -0,0 +1,171 @@ +[ + { + "type": "media-query-without-type", + "string": "(not (color)) or (hover)", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-or", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "ident-token", + "not", + 1, + 3, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 6, + 10, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 5, + 5, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 11, + 11, + null + ] + ] + }, + "before": [], + "after": [] + } + } + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 12, + 12, + null + ] + ] + }, + "list": [ + { + "type": "media-or", + "modifier": [ + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "ident-token", + "or", + 14, + 15, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "hover", + "tokens": [ + [ + "ident-token", + "hover", + 18, + 22, + { + "value": "hover" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 17, + 17, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 23, + 23, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0010.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0010.mjs new file mode 100644 index 000000000..490eb23b4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0010.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(not (color)) or (hover)', + 'specification-examples/0010', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0011.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0011.expect.json new file mode 100644 index 000000000..388ba1017 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0011.expect.json @@ -0,0 +1,171 @@ +[ + { + "type": "media-query-without-type", + "string": "not ((color) or (hover))", + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 3, + 3, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-or", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 6, + 10, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 5, + 5, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 11, + 11, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-or", + "modifier": [ + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "ident-token", + "or", + 13, + 14, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 15, + 15, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "hover", + "tokens": [ + [ + "ident-token", + "hover", + 17, + 21, + { + "value": "hover" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 16, + 16, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 22, + 22, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + }, + "before": [ + [ + "(-token", + "(", + 4, + 4, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 23, + 23, + null + ] + ] + } + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0011.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0011.mjs new file mode 100644 index 000000000..4e6c07660 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0011.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not ((color) or (hover))', + 'specification-examples/0011', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0012.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0012.expect.json new file mode 100644 index 000000000..0ff982293 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0012.expect.json @@ -0,0 +1,216 @@ +[ + { + "type": "media-query-without-type", + "string": "(not (color)) and (not (hover))", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "ident-token", + "not", + 1, + 3, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 6, + 10, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 5, + 5, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 11, + 11, + null + ] + ] + }, + "before": [], + "after": [] + } + } + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 12, + 12, + null + ] + ] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "ident-token", + "and", + 14, + 16, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 17, + 17, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "ident-token", + "not", + 19, + 21, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "hover", + "tokens": [ + [ + "ident-token", + "hover", + 24, + 28, + { + "value": "hover" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 23, + 23, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 29, + 29, + null + ] + ] + }, + "before": [], + "after": [] + } + } + }, + "before": [ + [ + "(-token", + "(", + 18, + 18, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 30, + 30, + null + ] + ] + } + } + ], + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0012.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0012.mjs new file mode 100644 index 000000000..c5e04a01d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0012.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(not (color)) and (not (hover))', + 'specification-examples/0012', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0013.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0013.expect.json new file mode 100644 index 000000000..24a092e18 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0013.expect.json @@ -0,0 +1,226 @@ +[ + { + "type": "media-query-without-type", + "string": "(color) and ((pointer) or (hover))", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 6, + 6, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ], + [ + "ident-token", + "and", + 8, + 10, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-or", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "pointer", + "tokens": [ + [ + "ident-token", + "pointer", + 14, + 20, + { + "value": "pointer" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 13, + 13, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 21, + 21, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-or", + "modifier": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + "ident-token", + "or", + 23, + 24, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 25, + 25, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "hover", + "tokens": [ + [ + "ident-token", + "hover", + 27, + 31, + { + "value": "hover" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 26, + 26, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 32, + 32, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + }, + "before": [ + [ + "(-token", + "(", + 12, + 12, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 33, + 33, + null + ] + ] + } + } + ], + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0013.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0013.mjs new file mode 100644 index 000000000..29fb57b1c --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0013.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color) and ((pointer) or (hover))', + 'specification-examples/0013', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0014.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0014.expect.json new file mode 100644 index 000000000..26292910c --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0014.expect.json @@ -0,0 +1,226 @@ +[ + { + "type": "media-query-without-type", + "string": "((color) and (pointer)) or (hover)", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-or", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 2, + 6, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 1, + 1, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 7, + 7, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "ident-token", + "and", + 9, + 11, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "pointer", + "tokens": [ + [ + "ident-token", + "pointer", + 14, + 20, + { + "value": "pointer" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 13, + 13, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 21, + 21, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 22, + 22, + null + ] + ] + }, + "list": [ + { + "type": "media-or", + "modifier": [ + [ + "whitespace-token", + " ", + 23, + 23, + null + ], + [ + "ident-token", + "or", + 24, + 25, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "hover", + "tokens": [ + [ + "ident-token", + "hover", + 28, + 32, + { + "value": "hover" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 27, + 27, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 33, + 33, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0014.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0014.mjs new file mode 100644 index 000000000..1426a6960 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0014.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '((color) and (pointer)) or (hover)', + 'specification-examples/0014', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0015.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0015.expect.json new file mode 100644 index 000000000..c20caa59e --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0015.expect.json @@ -0,0 +1,237 @@ +[ + { + "type": "media-query-invalid", + "string": "(color) and (pointer) or (hover)", + "media": [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 8, + 10, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 12, + 12, + null + ], + "tokens": [ + [ + "(-token", + "(", + 12, + 12, + null + ], + [ + "ident-token", + "pointer", + 13, + 19, + { + "value": "pointer" + } + ], + [ + ")-token", + ")", + 20, + 20, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "pointer", + 13, + 19, + { + "value": "pointer" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 21, + 21, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "or", + 22, + 23, + { + "value": "or" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 25, + 25, + null + ], + "tokens": [ + [ + "(-token", + "(", + 25, + 25, + null + ], + [ + "ident-token", + "hover", + 26, + 30, + { + "value": "hover" + } + ], + [ + ")-token", + ")", + 31, + 31, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "hover", + 26, + 30, + { + "value": "hover" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0015.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0015.mjs new file mode 100644 index 000000000..befcbc3a4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0015.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color) and (pointer) or (hover)', + 'specification-examples/0015', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0016.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0016.expect.json new file mode 100644 index 000000000..0a0968a3d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0016.expect.json @@ -0,0 +1,237 @@ +[ + { + "type": "media-query-invalid", + "string": "(color) not (pointer) or (hover)", + "media": [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 6, + 6, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 1, + 5, + { + "value": "color" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 7, + 7, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 8, + 10, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 12, + 12, + null + ], + "tokens": [ + [ + "(-token", + "(", + 12, + 12, + null + ], + [ + "ident-token", + "pointer", + 13, + 19, + { + "value": "pointer" + } + ], + [ + ")-token", + ")", + 20, + 20, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "pointer", + 13, + 19, + { + "value": "pointer" + } + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 21, + 21, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "or", + 22, + 23, + { + "value": "or" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 25, + 25, + null + ], + "tokens": [ + [ + "(-token", + "(", + 25, + 25, + null + ], + [ + "ident-token", + "hover", + 26, + 30, + { + "value": "hover" + } + ], + [ + ")-token", + ")", + 31, + 31, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "hover", + 26, + 30, + { + "value": "hover" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0016.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0016.mjs new file mode 100644 index 000000000..a45f0c406 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0016.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(color) not (pointer) or (hover)', + 'specification-examples/0016', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0017.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0017.expect.json new file mode 100644 index 000000000..85ad37800 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0017.expect.json @@ -0,0 +1,148 @@ +[ + { + "type": "media-query-without-type", + "string": "((color) and (pointer) or (hover))", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "(-token", + "(", + 1, + 1, + null + ], + [ + "ident-token", + "color", + 2, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "ident-token", + "and", + 9, + 11, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "(-token", + "(", + 13, + 13, + null + ], + [ + "ident-token", + "pointer", + 14, + 20, + { + "value": "pointer" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + "ident-token", + "or", + 23, + 24, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 25, + 25, + null + ], + [ + "(-token", + "(", + 26, + 26, + null + ], + [ + "ident-token", + "hover", + 27, + 31, + { + "value": "hover" + } + ], + [ + ")-token", + ")", + 32, + 32, + null + ], + [ + ")-token", + ")", + 33, + 33, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0017.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0017.mjs new file mode 100644 index 000000000..37ded5cb8 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0017.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '((color) and (pointer) or (hover))', + 'specification-examples/0017', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0018.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0018.expect.json new file mode 100644 index 000000000..fd2eda502 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0018.expect.json @@ -0,0 +1,148 @@ +[ + { + "type": "media-query-without-type", + "string": "((color) not (pointer) or (hover))", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "general-enclosed", + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "(-token", + "(", + 1, + 1, + null + ], + [ + "ident-token", + "color", + 2, + 6, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 7, + 7, + null + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "ident-token", + "not", + 9, + 11, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ], + [ + "(-token", + "(", + 13, + 13, + null + ], + [ + "ident-token", + "pointer", + 14, + 20, + { + "value": "pointer" + } + ], + [ + ")-token", + ")", + 21, + 21, + null + ], + [ + "whitespace-token", + " ", + 22, + 22, + null + ], + [ + "ident-token", + "or", + 23, + 24, + { + "value": "or" + } + ], + [ + "whitespace-token", + " ", + 25, + 25, + null + ], + [ + "(-token", + "(", + 26, + 26, + null + ], + [ + "ident-token", + "hover", + 27, + 31, + { + "value": "hover" + } + ], + [ + ")-token", + ")", + 32, + 32, + null + ], + [ + ")-token", + ")", + 33, + 33, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0018.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0018.mjs new file mode 100644 index 000000000..46e80a9a7 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0018.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '((color) not (pointer) or (hover))', + 'specification-examples/0018', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, + 1, +); diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0019.expect.json b/packages/media-query-list-parser/test/cases/specification-examples/0019.expect.json new file mode 100644 index 000000000..67bb1b659 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0019.expect.json @@ -0,0 +1,150 @@ +[ + { + "type": "media-query-without-type", + "string": "((color) and (width))", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "color", + "tokens": [ + [ + "ident-token", + "color", + 2, + 6, + { + "value": "color" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 1, + 1, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 7, + 7, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "ident-token", + "and", + 9, + 11, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 12, + 12, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-boolean", + "name": "width", + "tokens": [ + [ + "ident-token", + "width", + 14, + 18, + { + "value": "width" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 13, + 13, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 19, + 19, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [] + } + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 20, + 20, + null + ] + ] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/specification-examples/0019.mjs b/packages/media-query-list-parser/test/cases/specification-examples/0019.mjs new file mode 100644 index 000000000..80b9aa5e4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/specification-examples/0019.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '((color) and (width))', + 'specification-examples/0019', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0001.expect.json b/packages/media-query-list-parser/test/cases/various/0001.expect.json new file mode 100644 index 000000000..36d0caf8d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0001.expect.json @@ -0,0 +1,149 @@ +[ + { + "type": "media-query-invalid", + "string": "(/* a comment */foo ) something else", + "media": [ + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 0, + 0, + null + ], + "tokens": [ + [ + "(-token", + "(", + 0, + 0, + null + ], + [ + "comment", + "/* a comment */", + 1, + 15, + null + ], + [ + "ident-token", + "foo", + 16, + 18, + { + "value": "foo" + } + ], + [ + "whitespace-token", + " ", + 19, + 20, + null + ], + [ + ")-token", + ")", + 21, + 21, + null + ] + ], + "value": [ + { + "type": "comment", + "tokens": [ + [ + "comment", + "/* a comment */", + 1, + 15, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "foo", + 16, + 18, + { + "value": "foo" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 19, + 20, + null + ] + ] + } + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 22, + 22, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "something", + 23, + 31, + { + "value": "something" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 32, + 32, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "else", + 33, + 36, + { + "value": "else" + } + ] + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0001.mjs b/packages/media-query-list-parser/test/cases/various/0001.mjs new file mode 100644 index 000000000..c5764560e --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(/* a comment */foo ) something else', + 'various/0001', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0002.expect.json b/packages/media-query-list-parser/test/cases/various/0002.expect.json new file mode 100644 index 000000000..7d8dd3926 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0002.expect.json @@ -0,0 +1,737 @@ +[ + { + "type": "media-query-with-type", + "string": "not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)", + "modifier": [ + [ + "ident-token", + "not", + 0, + 2, + { + "value": "not" + } + ] + ], + "mediaType": [ + [ + "whitespace-token", + " ", + 3, + 3, + null + ], + [ + "ident-token", + "screen", + 4, + 9, + { + "value": "screen" + } + ] + ], + "and": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "ident-token", + "and", + 11, + 13, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "min-width", + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 15, + 15, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 32, + 32, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 33, + 33, + null + ], + [ + "ident-token", + "and", + 34, + 36, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 37, + 37, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "prefers-color-scheme", + "tokens": [ + [ + "ident-token", + "prefers-color-scheme", + 39, + 58, + { + "value": "prefers-color-scheme" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ] + ] + }, + "tokens": [ + [ + "comment", + "/* a comment */", + 60, + 74, + null + ], + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "prefers-color-scheme", + 39, + 58, + { + "value": "prefers-color-scheme" + } + ], + [ + "colon-token", + ":", + 59, + 59, + null + ], + [ + "comment", + "/* a comment */", + 60, + 74, + null + ], + [ + "ident-token", + "dark", + 75, + 78, + { + "value": "dark" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 38, + 38, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 79, + 79, + null + ] + ] + }, + "before": [], + "after": [] + } + }, + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 80, + 80, + null + ], + [ + "ident-token", + "and", + 81, + 83, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 84, + 84, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "ident-token", + "width", + 86, + 90, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 91, + 91, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 93, + 93, + null + ], + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "width", + 86, + 90, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 91, + 91, + null + ], + [ + "delim-token", + "<", + 92, + 92, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 93, + 93, + null + ], + [ + "dimension-token", + "40vw", + 94, + 97, + { + "value": 40, + "type": "integer", + "unit": "vw" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 85, + 85, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 98, + 98, + null + ] + ] + }, + "before": [], + "after": [] + } + }, + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 99, + 99, + null + ], + [ + "ident-token", + "and", + 100, + 102, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 103, + 103, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "whitespace-token", + " ", + 111, + 111, + null + ], + [ + "ident-token", + "width", + 112, + 116, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 117, + 117, + null + ] + ] + }, + "valueOne": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 109, + 109, + null + ] + ] + }, + "valueTwo": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 119, + 119, + null + ], + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ] + ] + }, + "tokens": [ + [ + "dimension-token", + "30px", + 105, + 108, + { + "value": 30, + "type": "integer", + "unit": "px" + } + ], + [ + "whitespace-token", + " ", + 109, + 109, + null + ], + [ + "delim-token", + "<", + 110, + 110, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 111, + 111, + null + ], + [ + "ident-token", + "width", + 112, + 116, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 117, + 117, + null + ], + [ + "delim-token", + "<", + 118, + 118, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 119, + 119, + null + ], + [ + "dimension-token", + "50rem", + 120, + 124, + { + "value": 50, + "type": "integer", + "unit": "rem" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 104, + 104, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 125, + 125, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [ + [ + "whitespace-token", + " ", + 14, + 14, + null + ] + ], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0002.mjs b/packages/media-query-list-parser/test/cases/various/0002.mjs new file mode 100644 index 000000000..964b4d9b9 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0002.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'not screen and (min-width: 300px) and (prefers-color-scheme:/* a comment */dark) and (width < 40vw) and (30px < width < 50rem)', + 'various/0002', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0003.expect.json b/packages/media-query-list-parser/test/cases/various/0003.expect.json new file mode 100644 index 000000000..16e294933 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0003.expect.json @@ -0,0 +1,319 @@ +[ + { + "type": "media-query-without-type", + "string": "(resolution < infinite) and (infinite <= resolution) ", + "media": { + "type": "media-condition", + "media": { + "type": "media-condition-list-and", + "leading": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "resolution", + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "resolution", + 1, + 10, + { + "value": "resolution" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ], + [ + "delim-token", + "<", + 12, + 12, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 13, + 13, + null + ], + [ + "ident-token", + "infinite", + 14, + 21, + { + "value": "infinite" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 22, + 22, + null + ] + ] + }, + "before": [], + "after": [] + }, + "list": [ + { + "type": "media-and", + "modifier": [ + [ + "whitespace-token", + " ", + 23, + 23, + null + ], + [ + "ident-token", + "and", + 24, + 26, + { + "value": "and" + } + ], + [ + "whitespace-token", + " ", + 27, + 27, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-value-name", + "name": { + "type": "mf-name", + "name": "resolution", + "tokens": [ + [ + "whitespace-token", + " ", + 40, + 40, + null + ], + [ + "ident-token", + "resolution", + 41, + 50, + { + "value": "resolution" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ], + [ + "whitespace-token", + " ", + 37, + 37, + null + ] + ] + }, + "tokens": [ + [ + "ident-token", + "infinite", + 29, + 36, + { + "value": "infinite" + } + ], + [ + "whitespace-token", + " ", + 37, + 37, + null + ], + [ + "delim-token", + "<", + 38, + 38, + { + "value": "<" + } + ], + [ + "delim-token", + "=", + 39, + 39, + { + "value": "=" + } + ], + [ + "whitespace-token", + " ", + 40, + 40, + null + ], + [ + "ident-token", + "resolution", + 41, + 50, + { + "value": "resolution" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 28, + 28, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 51, + 51, + null + ] + ] + }, + "before": [], + "after": [] + } + } + ], + "before": [], + "after": [ + [ + "whitespace-token", + " ", + 52, + 52, + null + ] + ] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0003.mjs b/packages/media-query-list-parser/test/cases/various/0003.mjs new file mode 100644 index 000000000..2af154b33 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0003.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(resolution < infinite) and (infinite <= resolution) ', + 'various/0003', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0004.expect.json b/packages/media-query-list-parser/test/cases/various/0004.expect.json new file mode 100644 index 000000000..5288c94e4 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0004.expect.json @@ -0,0 +1,367 @@ +[ + { + "type": "media-query-without-type", + "string": "(width < calc(50vw - 3rem))", + "media": { + "type": "media-condition", + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-range-name-value", + "name": { + "type": "mf-name", + "name": "width", + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "function", + "name": "calc", + "tokens": [ + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 18, + 18, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 20, + 20, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ] + ] + } + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ] + ] + }, + "tokens": [ + [ + "ident-token", + "width", + 1, + 5, + { + "value": "width" + } + ], + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "delim-token", + "<", + 7, + 7, + { + "value": "<" + } + ], + [ + "whitespace-token", + " ", + 8, + 8, + null + ], + [ + "function-token", + "calc(", + 9, + 13, + { + "value": "calc" + } + ], + [ + "dimension-token", + "50vw", + 14, + 17, + { + "value": 50, + "type": "integer", + "unit": "vw" + } + ], + [ + "whitespace-token", + " ", + 18, + 18, + null + ], + [ + "delim-token", + "-", + 19, + 19, + { + "value": "-" + } + ], + [ + "whitespace-token", + " ", + 20, + 20, + null + ], + [ + "dimension-token", + "3rem", + 21, + 24, + { + "value": 3, + "type": "integer", + "unit": "rem" + } + ], + [ + ")-token", + ")", + 25, + 25, + null + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 0, + 0, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 26, + 26, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0004.mjs b/packages/media-query-list-parser/test/cases/various/0004.mjs new file mode 100644 index 000000000..62f73bf0a --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0004.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + '(width < calc(50vw - 3rem))', + 'various/0004', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0005.expect.json b/packages/media-query-list-parser/test/cases/various/0005.expect.json new file mode 100644 index 000000000..a954c0d33 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0005.expect.json @@ -0,0 +1,186 @@ +[ + { + "type": "media-query-with-type", + "string": "screen and not (min-width: 300px)", + "modifier": [], + "mediaType": [ + [ + "ident-token", + "screen", + 0, + 5, + { + "value": "screen" + } + ] + ], + "and": [ + [ + "whitespace-token", + " ", + 6, + 6, + null + ], + [ + "ident-token", + "and", + 7, + 9, + { + "value": "and" + } + ] + ], + "media": { + "type": "media-condition", + "media": { + "type": "media-not", + "modifier": [ + [ + "whitespace-token", + " ", + 10, + 10, + null + ], + [ + "ident-token", + "not", + 11, + 13, + { + "value": "not" + } + ], + [ + "whitespace-token", + " ", + 14, + 14, + null + ] + ], + "media": { + "type": "media-in-parens", + "media": { + "type": "media-feature", + "feature": { + "type": "mf-plain", + "name": { + "type": "mf-name", + "name": "min-width", + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ] + ] + }, + "value": { + "type": "mf-value", + "value": { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "tokens": [ + [ + "ident-token", + "min-width", + 16, + 24, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 25, + 25, + null + ], + [ + "whitespace-token", + " ", + 26, + 26, + null + ], + [ + "dimension-token", + "300px", + 27, + 31, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + }, + "before": [ + [ + "(-token", + "(", + 15, + 15, + null + ] + ], + "after": [ + [ + ")-token", + ")", + 32, + 32, + null + ] + ] + }, + "before": [], + "after": [] + } + } + } + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0005.mjs b/packages/media-query-list-parser/test/cases/various/0005.mjs new file mode 100644 index 000000000..7c55c608d --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0005.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'screen and not (min-width: 300px)', + 'various/0005', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0006.expect.json b/packages/media-query-list-parser/test/cases/various/0006.expect.json new file mode 100644 index 000000000..4e7482d4b --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0006.expect.json @@ -0,0 +1,43 @@ +[ + { + "type": "media-query-with-type", + "string": "only screen ", + "modifier": [ + [ + "ident-token", + "only", + 0, + 3, + { + "value": "only" + } + ] + ], + "mediaType": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ], + [ + "ident-token", + "screen", + 5, + 10, + { + "value": "screen" + } + ], + [ + "whitespace-token", + " ", + 11, + 11, + null + ] + ], + "media": null + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0006.mjs b/packages/media-query-list-parser/test/cases/various/0006.mjs new file mode 100644 index 000000000..2d9faf758 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0006.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +runTest( + 'only screen ', + 'various/0006', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/cases/various/0007.expect.json b/packages/media-query-list-parser/test/cases/various/0007.expect.json new file mode 100644 index 000000000..18df98287 --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0007.expect.json @@ -0,0 +1,740 @@ +[ + { + "type": "media-query-invalid", + "string": " and and", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 0, + 0, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 1, + 3, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 4, + 4, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 5, + 7, + { + "value": "and" + } + ] + ] + } + ] + }, + { + "type": "media-query-invalid", + "string": " screen and and (color)", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 9, + 9, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 10, + 15, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 16, + 16, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 17, + 19, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 20, + 20, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 21, + 23, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 24, + 24, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 25, + 25, + null + ], + "tokens": [ + [ + "(-token", + "(", + 25, + 25, + null + ], + [ + "ident-token", + "color", + 26, + 30, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 31, + 31, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 26, + 30, + { + "value": "color" + } + ] + ] + } + ] + } + ] + }, + { + "type": "media-query-invalid", + "string": " only screen print", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 33, + 33, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 34, + 37, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 38, + 38, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 39, + 44, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 45, + 45, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "print", + 46, + 50, + { + "value": "print" + } + ] + ] + } + ] + }, + { + "type": "media-query-invalid", + "string": " only only", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 52, + 52, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 53, + 56, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 57, + 57, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 58, + 61, + { + "value": "only" + } + ] + ] + } + ] + }, + { + "type": "media-query-invalid", + "string": " not print or", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 63, + 63, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "not", + 64, + 66, + { + "value": "not" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 67, + 67, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "print", + 68, + 72, + { + "value": "print" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 73, + 73, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "or", + 74, + 75, + { + "value": "or" + } + ] + ] + } + ] + }, + { + "type": "media-query-invalid", + "string": " and (color)", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 77, + 77, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "and", + 78, + 80, + { + "value": "and" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 81, + 81, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 82, + 82, + null + ], + "tokens": [ + [ + "(-token", + "(", + 82, + 82, + null + ], + [ + "ident-token", + "color", + 83, + 87, + { + "value": "color" + } + ], + [ + ")-token", + ")", + 88, + 88, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "color", + 83, + 87, + { + "value": "color" + } + ] + ] + } + ] + } + ] + }, + { + "type": "media-query-invalid", + "string": " only screen foo (min-width: 300px)", + "media": [ + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 90, + 90, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "only", + 91, + 94, + { + "value": "only" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 95, + 95, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "screen", + 96, + 101, + { + "value": "screen" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 102, + 102, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "ident-token", + "foo", + 103, + 105, + { + "value": "foo" + } + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 106, + 106, + null + ] + ] + }, + { + "type": "simple-block", + "startToken": [ + "(-token", + "(", + 107, + 107, + null + ], + "tokens": [ + [ + "(-token", + "(", + 107, + 107, + null + ], + [ + "ident-token", + "min-width", + 108, + 116, + { + "value": "min-width" + } + ], + [ + "colon-token", + ":", + 117, + 117, + null + ], + [ + "whitespace-token", + " ", + 118, + 118, + null + ], + [ + "dimension-token", + "300px", + 119, + 123, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ], + [ + ")-token", + ")", + 124, + 124, + null + ] + ], + "value": [ + { + "type": "token", + "tokens": [ + [ + "ident-token", + "min-width", + 108, + 116, + { + "value": "min-width" + } + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "colon-token", + ":", + 117, + 117, + null + ] + ] + }, + { + "type": "whitespace", + "tokens": [ + [ + "whitespace-token", + " ", + 118, + 118, + null + ] + ] + }, + { + "type": "token", + "tokens": [ + [ + "dimension-token", + "300px", + 119, + 123, + { + "value": 300, + "type": "integer", + "unit": "px" + } + ] + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/packages/media-query-list-parser/test/cases/various/0007.mjs b/packages/media-query-list-parser/test/cases/various/0007.mjs new file mode 100644 index 000000000..1b5de51ff --- /dev/null +++ b/packages/media-query-list-parser/test/cases/various/0007.mjs @@ -0,0 +1,14 @@ +import assert from 'assert'; +import { runTest } from '../../util/run-test.mjs'; + +// MUST all be invalid: +runTest( + ' and and, screen and and (color), only screen print, only only, not print or, and (color), only screen foo (min-width: 300px)', + 'various/0007', + (actual, expected) => { + assert.deepStrictEqual( + actual, + expected, + ); + }, +); diff --git a/packages/media-query-list-parser/test/serialize/0001.mjs b/packages/media-query-list-parser/test/serialize/0001.mjs new file mode 100644 index 000000000..d1f41f3fe --- /dev/null +++ b/packages/media-query-list-parser/test/serialize/0001.mjs @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { newMediaFeatureBoolean, newMediaFeaturePlain } from '@csstools/media-query-list-parser'; +import { TokenType } from '@csstools/css-tokenizer'; + +assert.strictEqual( + newMediaFeaturePlain('min-width', [TokenType.Dimension, '300px', 0, 0, { value: 300, unit: 'px' }]).toString(), + '(min-width:300px)', +); + +assert.strictEqual( + newMediaFeatureBoolean('color').toString(), + '(color)', +); diff --git a/packages/media-query-list-parser/test/test.mjs b/packages/media-query-list-parser/test/test.mjs new file mode 100644 index 000000000..58b7e395e --- /dev/null +++ b/packages/media-query-list-parser/test/test.mjs @@ -0,0 +1,66 @@ +import './api/options.mjs'; + +import './cases/media-not/0001.mjs'; + +import './cases/mf-boolean/0001.mjs'; +import './cases/mf-boolean/0002.mjs'; +import './cases/mf-boolean/0003.mjs'; +import './cases/mf-boolean/0004.mjs'; +import './cases/mf-boolean/0005.mjs'; + +import './cases/mf-plain/0001.mjs'; +import './cases/mf-plain/0002.mjs'; +import './cases/mf-plain/0003.mjs'; +import './cases/mf-plain/0004.mjs'; +import './cases/mf-plain/0005.mjs'; + +import './cases/mf-range/0001.mjs'; +import './cases/mf-range/0002.mjs'; +import './cases/mf-range/0003.mjs'; +import './cases/mf-range/0004.mjs'; +import './cases/mf-range/0005.mjs'; +import './cases/mf-range/0006.mjs'; +import './cases/mf-range/0007.mjs'; +import './cases/mf-range/0008.mjs'; +import './cases/mf-range/0009.mjs'; +import './cases/mf-range/0010.mjs'; +import './cases/mf-range/0011.mjs'; +import './cases/mf-range/0012.mjs'; +import './cases/mf-range/0013.mjs'; +import './cases/mf-range/0014.mjs'; + +import './cases/query-with-type/0001.mjs'; +import './cases/query-with-type/0002.mjs'; +import './cases/query-with-type/0003.mjs'; +import './cases/query-with-type/0004.mjs'; +import './cases/query-with-type/0005.mjs'; + +import './cases/specification-examples/0001.mjs'; +import './cases/specification-examples/0002.mjs'; +import './cases/specification-examples/0003.mjs'; +import './cases/specification-examples/0004.mjs'; +import './cases/specification-examples/0005.mjs'; +import './cases/specification-examples/0006.mjs'; +import './cases/specification-examples/0007.mjs'; +import './cases/specification-examples/0008.mjs'; +import './cases/specification-examples/0009.mjs'; +import './cases/specification-examples/0010.mjs'; +import './cases/specification-examples/0011.mjs'; +import './cases/specification-examples/0012.mjs'; +import './cases/specification-examples/0013.mjs'; +import './cases/specification-examples/0014.mjs'; +import './cases/specification-examples/0015.mjs'; +import './cases/specification-examples/0016.mjs'; +import './cases/specification-examples/0017.mjs'; +import './cases/specification-examples/0018.mjs'; +import './cases/specification-examples/0019.mjs'; + +import './cases/various/0001.mjs'; +import './cases/various/0002.mjs'; +import './cases/various/0003.mjs'; +import './cases/various/0004.mjs'; +import './cases/various/0005.mjs'; +import './cases/various/0006.mjs'; +import './cases/various/0007.mjs'; + +import './serialize/0001.mjs'; diff --git a/packages/media-query-list-parser/test/util/run-test.mjs b/packages/media-query-list-parser/test/util/run-test.mjs new file mode 100644 index 000000000..f96dc172e --- /dev/null +++ b/packages/media-query-list-parser/test/util/run-test.mjs @@ -0,0 +1,42 @@ +import fs from 'fs'; +import path from 'path'; +import { isGeneralEnclosed, parse } from '@csstools/media-query-list-parser'; + +export function runTest(source, testPath, assertEqual, expectGeneralEnclosed = 0) { + const resultAST = parse(source, { + preserveInvalidMediaQueries: true, + }); + + const resultAST_JSON = JSON.stringify(resultAST, null, '\t'); + + if (process.env['REWRITE_EXPECTS'] === 'true') { + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.expect.json`), resultAST_JSON); + fs.writeFileSync(path.join(process.cwd(), `./test/cases/${testPath}.result.json`), resultAST_JSON); + } else { + const expectData = JSON.parse(fs.readFileSync(path.join(process.cwd(), `./test/cases/${testPath}.expect.json`)).toString()); + + assertEqual( + resultAST.map((x) => x.toString()).join(','), + expectData.map((x) => x.string).join(','), + ); + + assertEqual( + JSON.parse(resultAST_JSON), + expectData, + ); + + let generalEnclosedCounter = 0; + resultAST.map((x) => { + x.walk((entry) => { + if (isGeneralEnclosed(entry.node)) { + generalEnclosedCounter++; + } + }); + }); + + assertEqual( + generalEnclosedCounter, + expectGeneralEnclosed, + ); + } +} diff --git a/packages/media-query-list-parser/tsconfig.json b/packages/media-query-list-parser/tsconfig.json new file mode 100644 index 000000000..e0d06239c --- /dev/null +++ b/packages/media-query-list-parser/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "." + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} diff --git a/plugins/postcss-custom-media/.tape.cjs b/plugins/postcss-custom-media/.tape.cjs deleted file mode 100644 index 60fff7ba9..000000000 --- a/plugins/postcss-custom-media/.tape.cjs +++ /dev/null @@ -1,255 +0,0 @@ -const postcssTape = require('../../packages/postcss-tape/dist/index.cjs'); -const plugin = require('postcss-custom-media'); -const fs = require('fs'); - -postcssTape(plugin)({ - 'basic': { - message: 'supports basic usage' - }, - 'basic:preserve': { - message: 'supports { preserve: true } usage', - options: { - preserve: true - } - }, - 'examples/example': { - message: 'minimal example', - }, - 'examples/example:preserve': { - message: 'minimal example', - options: { - preserve: true - } - }, - 'complex': { - message: 'supports complex usage' - }, - 'import': { - message: 'supports { importFrom: { customMedia: { ... } } } usage', - options: { - importFrom: { - customMedia: { - '--mq-a': '(max-width: 30em), (max-height: 30em)', - '--not-mq-a': 'not all and (--mq-a)' - } - } - } - }, - 'import:import-fn': { - message: 'supports { importFrom() } usage', - options: { - importFrom() { - return { - customMedia: { - '--mq-a': '(max-width: 30em), (max-height: 30em)', - '--not-mq-a': 'not all and (--mq-a)' - } - }; - } - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:import-fn-promise': { - message: 'supports { async importFrom() } usage', - options: { - importFrom() { - return new Promise(resolve => { - resolve({ - customMedia: { - '--mq-a': '(max-width: 30em), (max-height: 30em)', - '--not-mq-a': 'not all and (--mq-a)' - } - }) - }); - } - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:json': { - message: 'supports { importFrom: "test/import-media.json" } usage', - options: { - importFrom: 'test/import-media.json' - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:js': { - message: 'supports { importFrom: "test/import-media.js" } usage', - options: { - importFrom: 'test/import-media.js' - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:css': { - message: 'supports { importFrom: "test/import-media.css" } usage', - options: { - importFrom: 'test/import-media.css' - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:css-from': { - message: 'supports { importFrom: { from: "test/import-media.css" } } usage', - options: { - importFrom: { from: 'test/import-media.css' } - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:css-from-type': { - message: 'supports { importFrom: [ { from: "test/import-media.css", type: "css" } ] } usage', - options: { - importFrom: [{ from: 'test/import-media.css', type: 'css' }] - }, - expect: 'import.expect.css', - result: 'import.result.css' - }, - 'import:empty': { - message: 'supports { importFrom: {} } usage', - options: { - importFrom: {} - } - }, - 'basic:export': { - message: 'supports { exportTo: { customMedia: { ... } } } usage', - options: { - exportTo: (global.__exportMediaObject = global.__exportMediaObject || { - customMedia: null - }) - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - after() { - if (__exportMediaObject.customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') { - throw new Error('The exportTo function failed'); - } - } - }, - 'basic:export-fn': { - message: 'supports { exportTo() } usage', - options: { - exportTo(customMedia) { - if (customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') { - throw new Error('The exportTo function failed'); - } - } - }, - expect: 'basic.expect.css', - result: 'basic.result.css' - }, - 'basic:export-fn-promise': { - message: 'supports { async exportTo() } usage', - options: { - exportTo(customMedia) { - return new Promise((resolve, reject) => { - if (customMedia['--mq-a'] !== '(max-width: 30em), (max-height: 30em)') { - reject('The exportTo function failed'); - } else { - resolve(); - } - }); - } - }, - expect: 'basic.expect.css', - result: 'basic.result.css' - }, - 'basic:export-json': { - message: 'supports { exportTo: "test/export-media.json" } usage', - options: { - exportTo: 'test/export-media.json' - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - before() { - global.__exportMediaString = fs.readFileSync('test/export-media.json', 'utf8'); - }, - after() { - if (global.__exportMediaString !== fs.readFileSync('test/export-media.json', 'utf8')) { - throw new Error('The original file did not match the freshly exported copy'); - } - } - }, - 'basic:export-js': { - message: 'supports { exportTo: "test/export-media.js" } usage', - options: { - exportTo: 'test/export-media.js' - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - before() { - global.__exportMediaString = fs.readFileSync('test/export-media.js', 'utf8'); - }, - after() { - if (global.__exportMediaString !== fs.readFileSync('test/export-media.js', 'utf8')) { - throw new Error('The original file did not match the freshly exported copy'); - } - } - }, - 'basic:export-mjs': { - message: 'supports { exportTo: "test/export-media.mjs" } usage', - options: { - exportTo: 'test/export-media.mjs' - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - before() { - global.__exportMediaString = fs.readFileSync('test/export-media.mjs', 'utf8'); - }, - after() { - if (global.__exportMediaString !== fs.readFileSync('test/export-media.mjs', 'utf8')) { - throw new Error('The original file did not match the freshly exported copy'); - } - } - }, - 'basic:export-css': { - message: 'supports { exportTo: "test/export-media.css" } usage', - options: { - exportTo: 'test/export-media.css' - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - before() { - global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8'); - }, - after() { - if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) { - throw new Error('The original file did not match the freshly exported copy'); - } - } - }, - 'basic:export-css-to': { - message: 'supports { exportTo: { to: "test/export-media.css" } } usage', - options: { - exportTo: { to: 'test/export-media.css' } - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - before() { - global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8'); - }, - after() { - if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) { - throw new Error('The original file did not match the freshly exported copy'); - } - } - }, - 'basic:export-css-to-type': { - message: 'supports { exportTo: { to: "test/export-media.css", type: "css" } } usage', - options: { - exportTo: { to: 'test/export-media.css', type: 'css' } - }, - expect: 'basic.expect.css', - result: 'basic.result.css', - before() { - global.__exportMediaString = fs.readFileSync('test/export-media.css', 'utf8'); - }, - after() { - if (global.__exportMediaString !== fs.readFileSync('test/export-media.css', 'utf8')) { - throw new Error('The original file did not match the freshly exported copy'); - } - } - } -}); diff --git a/plugins/postcss-custom-media/.tape.mjs b/plugins/postcss-custom-media/.tape.mjs new file mode 100644 index 000000000..7dac6d271 --- /dev/null +++ b/plugins/postcss-custom-media/.tape.mjs @@ -0,0 +1,86 @@ +import postcssTape from '../../packages/postcss-tape/dist/index.cjs'; +import plugin from 'postcss-custom-media'; + +postcssTape(plugin)({ + 'basic-after-v9': { + message: 'supports basic usage' + }, + 'basic-after-v9:preserve': { + message: 'supports { preserve: true } usage', + options: { + preserve: true + } + }, + 'basic': { + message: 'supports basic usage (old)', + warnings: 1, + }, + 'examples/example': { + message: 'minimal example', + }, + 'examples/example:preserve': { + message: 'minimal example', + options: { + preserve: true + } + }, + 'nesting': { + message: 'works when nested' + }, + 'not-processable': { + message: 'only handles processable @custom-media rules' + }, + 'parser-checks': { + message: 'supports more obscure CSS' + }, + 'eof-1': { + message: 'handles EOF correctly (1)', + warnings: 1, + }, + 'eof-2': { + message: 'handles EOF correctly (2)', + warnings: 1, + }, + 'eof-3': { + message: 'handles EOF correctly (3)', + warnings: 1, + }, + 'eof-4': { + message: 'handles EOF correctly (4)', + warnings: 1, + }, + 'complex': { + message: 'supports complex usage' + }, + 'cyclic': { + message: 'handles cyclic references', + warnings: 3, + }, + 'override': { + message: 'handles reference overrides' + }, + 'modifiers': { + message: 'supports media query modifiers' + }, + 'list': { + message: 'supports media query lists' + }, + 'true-false': { + message: 'supports true|false keywords' + }, + 'comma-1': { + message: 'can correctly split media query lists' + }, + 'comma-2': { + message: 'can correctly split media query lists' + }, + 'and': { + message: 'supports media queries with "and"' + }, + 'not': { + message: 'supports media queries with "not"' + }, + 'or': { + message: 'supports media queries with "or"' + } +}); diff --git a/plugins/postcss-custom-media/CHANGELOG.md b/plugins/postcss-custom-media/CHANGELOG.md index 7ded46ba5..4e8143f47 100644 --- a/plugins/postcss-custom-media/CHANGELOG.md +++ b/plugins/postcss-custom-media/CHANGELOG.md @@ -3,6 +3,10 @@ ### Unreleased (major) - Updated: Support for Node v14+ (major). +- Removed: `importFrom` feature (breaking). +- Removed: `exportTo` feature (breaking). +- Fixed: implement logical evaluation of custom media queries. +- Added: Support for `true` and `false` keywords in `@custom-media`. ### 8.0.2 (June 4, 2022) diff --git a/plugins/postcss-custom-media/README.md b/plugins/postcss-custom-media/README.md index 73437387f..e847e2fba 100644 --- a/plugins/postcss-custom-media/README.md +++ b/plugins/postcss-custom-media/README.md @@ -74,90 +74,6 @@ postcssCustomMedia({ preserve: true }) } ``` - -### importFrom - -The `importFrom` option specifies sources where custom media can be imported -from, which might be CSS, JS, and JSON files, functions, and directly passed -objects. - -```js -postcssCustomMedia({ - importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em); -}); -``` - -```pcss -@media (max-width: 30em) { - /* styles for small viewport */ -} - -@media (--small-viewport) { - /* styles for small viewport */ -} -``` - -Multiple sources can be passed into this option, and they will be parsed in the -order they are received. JavaScript files, JSON files, functions, and objects -will need to namespace custom media using the `customMedia` or -`custom-media` key. - -```js -postcssCustomMedia({ - importFrom: [ - 'path/to/file.css', - 'and/then/this.js', - 'and/then/that.json', - { - customMedia: { '--small-viewport': '(max-width: 30em)' } - }, - () => { - const customMedia = { '--small-viewport': '(max-width: 30em)' }; - - return { customMedia }; - } - ] -}); -``` - -### exportTo - -The `exportTo` option specifies destinations where custom media can be exported -to, which might be CSS, JS, and JSON files, functions, and directly passed -objects. - -```js -postcssCustomMedia({ - exportTo: 'path/to/file.css' // @custom-media --small-viewport (max-width: 30em); -}); -``` - -Multiple destinations can be passed into this option, and they will be parsed -in the order they are received. JavaScript files, JSON files, and objects will -need to namespace custom media using the `customMedia` or -`custom-media` key. - -```js -const cachedObject = { customMedia: {} }; - -postcssCustomMedia({ - exportTo: [ - 'path/to/file.css', // @custom-media --small-viewport (max-width: 30em); - 'and/then/this.js', // module.exports = { customMedia: { '--small-viewport': '(max-width: 30em)' } } - 'and/then/this.mjs', // export const customMedia = { '--small-viewport': '(max-width: 30em)' } } - 'and/then/that.json', // { "custom-media": { "--small-viewport": "(max-width: 30em)" } } - cachedObject, - customMedia => { - customMedia // { '--small-viewport': '(max-width: 30em)' } - } - ] -}); -``` - -See example exports written to [CSS](test/export-media.css), -[JS](test/export-media.js), [MJS](test/export-media.mjs), and -[JSON](test/export-media.json). - [cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test [css-url]: https://cssdb.org/#custom-media-queries [discord]: https://discord.gg/bUadyRwkJS diff --git a/plugins/postcss-custom-media/docs/README.md b/plugins/postcss-custom-media/docs/README.md index 9961d39fd..6d3d8dc76 100644 --- a/plugins/postcss-custom-media/docs/README.md +++ b/plugins/postcss-custom-media/docs/README.md @@ -49,89 +49,5 @@ is preserved. By default, it is not preserved. ``` - -### importFrom - -The `importFrom` option specifies sources where custom media can be imported -from, which might be CSS, JS, and JSON files, functions, and directly passed -objects. - -```js -({ - importFrom: 'path/to/file.css' // => @custom-selector --small-viewport (max-width: 30em); -}); -``` - -```pcss -@media (max-width: 30em) { - /* styles for small viewport */ -} - -@media (--small-viewport) { - /* styles for small viewport */ -} -``` - -Multiple sources can be passed into this option, and they will be parsed in the -order they are received. JavaScript files, JSON files, functions, and objects -will need to namespace custom media using the `customMedia` or -`custom-media` key. - -```js -({ - importFrom: [ - 'path/to/file.css', - 'and/then/this.js', - 'and/then/that.json', - { - customMedia: { '--small-viewport': '(max-width: 30em)' } - }, - () => { - const customMedia = { '--small-viewport': '(max-width: 30em)' }; - - return { customMedia }; - } - ] -}); -``` - -### exportTo - -The `exportTo` option specifies destinations where custom media can be exported -to, which might be CSS, JS, and JSON files, functions, and directly passed -objects. - -```js -({ - exportTo: 'path/to/file.css' // @custom-media --small-viewport (max-width: 30em); -}); -``` - -Multiple destinations can be passed into this option, and they will be parsed -in the order they are received. JavaScript files, JSON files, and objects will -need to namespace custom media using the `customMedia` or -`custom-media` key. - -```js -const cachedObject = { customMedia: {} }; - -({ - exportTo: [ - 'path/to/file.css', // @custom-media --small-viewport (max-width: 30em); - 'and/then/this.js', // module.exports = { customMedia: { '--small-viewport': '(max-width: 30em)' } } - 'and/then/this.mjs', // export const customMedia = { '--small-viewport': '(max-width: 30em)' } } - 'and/then/that.json', // { "custom-media": { "--small-viewport": "(max-width: 30em)" } } - cachedObject, - customMedia => { - customMedia // { '--small-viewport': '(max-width: 30em)' } - } - ] -}); -``` - -See example exports written to [CSS](test/export-media.css), -[JS](test/export-media.js), [MJS](test/export-media.mjs), and -[JSON](test/export-media.json). - [Custom Media Specification]: diff --git a/plugins/postcss-custom-media/package.json b/plugins/postcss-custom-media/package.json index 1f760d0dd..39dc809f5 100644 --- a/plugins/postcss-custom-media/package.json +++ b/plugins/postcss-custom-media/package.json @@ -45,7 +45,8 @@ "dist" ], "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^1.0.0", + "@csstools/media-query-list-parser": "^1.0.0" }, "peerDependencies": { "postcss": "^8.4" @@ -58,9 +59,9 @@ "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", "lint:package-json": "node ../../.github/bin/format-package-json.mjs", "prepublishOnly": "npm run clean && npm run build && npm run test", - "test": "node .tape.cjs && npm run test:exports", + "test": "node .tape.mjs && npm run test:exports", "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", - "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.cjs" + "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs" }, "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-custom-media#readme", "repository": { diff --git a/plugins/postcss-custom-media/src/custom-media-from-root.js b/plugins/postcss-custom-media/src/custom-media-from-root.js deleted file mode 100644 index d2ddbef6a..000000000 --- a/plugins/postcss-custom-media/src/custom-media-from-root.js +++ /dev/null @@ -1,62 +0,0 @@ -import mediaASTFromString from './media-ast-from-string'; -import valueParser from 'postcss-value-parser'; - -// return custom selectors from the css root, conditionally removing them -export default (root, opts) => { - // initialize custom selectors - const customMedias = {}; - - // for each custom selector atrule that is a child of the css root - root.nodes.slice().forEach(node => { - if (node.type !== 'atrule') { - return; - } - - if (node.name.toLowerCase() !== 'custom-media') { - return; - } - - let paramsAst = null; - try { - paramsAst = valueParser(node.params); - } catch (_) { - return; - } - - if (!paramsAst || !paramsAst.nodes || !paramsAst.nodes.length) { - return; - } - - let nameNodeIndex = -1; - for (let i = 0; i < paramsAst.nodes.length; i++) { - const node = paramsAst.nodes[i]; - if (node.type === 'space' || node.type === 'comment') { - continue; - } - - if (node.type === 'word' && node.value.startsWith('--')) { - nameNodeIndex = i; - break; - } - - return; /* invalid starting node */ - } - - if (nameNodeIndex < 0) { - return; - } - - const name = paramsAst.nodes[nameNodeIndex].value.trim(); - const selectors = valueParser.stringify(paramsAst.nodes.slice(nameNodeIndex + 1)).trim(); - - // write the parsed selectors to the custom selector - customMedias[name] = mediaASTFromString(selectors); - - // conditionally remove the custom selector atrule - if (!Object(opts).preserve) { - node.remove(); - } - }); - - return customMedias; -}; diff --git a/plugins/postcss-custom-media/src/custom-media-from-root.ts b/plugins/postcss-custom-media/src/custom-media-from-root.ts new file mode 100644 index 000000000..964e69bc0 --- /dev/null +++ b/plugins/postcss-custom-media/src/custom-media-from-root.ts @@ -0,0 +1,62 @@ +import { MediaQuery } from '@csstools/media-query-list-parser'; +import type { ChildNode, Container, Document, Root as PostCSSRoot } from 'postcss'; +import { isProcessableCustomMediaRule } from './is-processable-custom-media-rule'; +import { removeCyclicReferences } from './toposort'; +import { parseCustomMedia } from './transform-at-media/custom-media'; + +// return custom media from the css root, conditionally removing them +export default function getCustomMedia(root: PostCSSRoot, result, opts: { preserve?: boolean }): Map, falsy: Array }> { + // initialize custom media + const customMedia: Map, falsy: Array }> = new Map(); + const customMediaGraph: Array<[string, string]> = []; + + root.walkAtRules((atRule) => { + if (!isProcessableCustomMediaRule(atRule)) { + return; + } + + const parsed = parseCustomMedia(atRule.params); + if (!parsed) { + return; + } + + if (parsed.truthy.length === 0) { + return; + } + + customMedia.set(parsed.name, { + truthy: parsed.truthy, + falsy: parsed.falsy, + }); + + customMediaGraph.push(...parsed.dependsOn); + + if (!opts.preserve) { + const parent = atRule.parent; + atRule.remove(); + removeEmptyAncestorBlocks(parent); + } + }); + + const cyclicReferences = removeCyclicReferences(customMedia, customMediaGraph); + for (const cyclicReference of cyclicReferences.values()) { + root.warn(result, `@custom-media rules have cyclic dependencies for "${cyclicReference}"`); + } + + return customMedia; +} + +function removeEmptyAncestorBlocks(block: Container) { + let currentNode: Document | Container = block; + + while (currentNode) { + if (currentNode.nodes && currentNode.nodes.length > 0) { + return; + } + + const parent = currentNode.parent; + currentNode.remove(); + currentNode = parent; + } +} + diff --git a/plugins/postcss-custom-media/src/custom-media-name.js b/plugins/postcss-custom-media/src/custom-media-name.js deleted file mode 100644 index f9a7aaa5f..000000000 --- a/plugins/postcss-custom-media/src/custom-media-name.js +++ /dev/null @@ -1,47 +0,0 @@ -import valueParser from 'postcss-value-parser'; - -export function getCustomMediaNameReference(source) { - if (!source) { - return; - } - - let paramsAst = null; - try { - paramsAst = valueParser(source); - } catch (_) { - return; - } - - if (!paramsAst || !paramsAst.nodes || !paramsAst.nodes.length) { - return; - } - - if (paramsAst.nodes.length !== 1) { - return; - } - - while (paramsAst.nodes[0].type === 'function' && paramsAst.nodes[0].value === '') { - paramsAst = paramsAst.nodes[0]; - } - - let nameNodeIndex = -1; - for (let i = 0; i < paramsAst.nodes.length; i++) { - const node = paramsAst.nodes[i]; - if (node.type === 'space' || node.type === 'comment') { - continue; - } - - if (node.type === 'word' && node.value.startsWith('--')) { - nameNodeIndex = i; - break; - } - - return; /* invalid starting node */ - } - - if (nameNodeIndex < 0) { - return; - } - - return paramsAst.nodes[nameNodeIndex].value.trim(); -} diff --git a/plugins/postcss-custom-media/src/get-custom-media-from-imports.js b/plugins/postcss-custom-media/src/get-custom-media-from-imports.js deleted file mode 100644 index 944795ac5..000000000 --- a/plugins/postcss-custom-media/src/get-custom-media-from-imports.js +++ /dev/null @@ -1,110 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { parse } from 'postcss'; -import getMediaAstFromMediaString from './media-ast-from-string'; -import getCustomMedia from './custom-media-from-root'; - -/* Get Custom Media from CSS File -/* ========================================================================== */ - -async function getCustomMediaFromCSSFile(from) { - const css = await readFile(from); - const root = parse(css, { from }); - - return getCustomMedia(root, { preserve: true }); -} - -/* Get Custom Media from Object -/* ========================================================================== */ - -function getCustomMediaFromObject(object) { - const customMedia = Object.assign( - {}, - Object(object).customMedia, - Object(object)['custom-media'], - ); - - for (const key in customMedia) { - customMedia[key] = getMediaAstFromMediaString(customMedia[key]); - } - - return customMedia; -} - -/* Get Custom Media from JSON file -/* ========================================================================== */ - -async function getCustomMediaFromJSONFile(from) { - const object = await readJSON(from); - - return getCustomMediaFromObject(object); -} - -/* Get Custom Media from JS file -/* ========================================================================== */ - -async function getCustomMediaFromJSFile(from) { - const object = await import(from); - - return getCustomMediaFromObject(object); -} - -/* Get Custom Media from Sources -/* ========================================================================== */ - -export default function getCustomMediaFromSources(sources) { - return sources.map(source => { - if (source instanceof Promise) { - return source; - } else if (source instanceof Function) { - return source(); - } - - // read the source as an object - const opts = source === Object(source) ? source : { from: String(source) }; - - // skip objects with custom media - if (Object(opts).customMedia || Object(opts)['custom-media']) { - return opts; - } - - // source pathname - const from = path.resolve(String(opts.from || '')); - - // type of file being read from - const type = (opts.type || path.extname(from).slice(1)).toLowerCase(); - - return { type, from }; - }).reduce(async (customMedia, source) => { - const { type, from } = await source; - - if (type === 'css' || type === 'pcss') { - return Object.assign(await customMedia, await getCustomMediaFromCSSFile(from)); - } - - if (type === 'js') { - return Object.assign(await customMedia, await getCustomMediaFromJSFile(from)); - } - - if (type === 'json') { - return Object.assign(await customMedia, await getCustomMediaFromJSONFile(from)); - } - - return Object.assign(await customMedia, getCustomMediaFromObject(await source)); - }, {}); -} - -/* Helper utilities -/* ========================================================================== */ - -const readFile = from => new Promise((resolve, reject) => { - fs.readFile(from, 'utf8', (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); -}); - -const readJSON = async from => JSON.parse(await readFile(from)); diff --git a/plugins/postcss-custom-media/src/index.js b/plugins/postcss-custom-media/src/index.js deleted file mode 100644 index 17d08a2ed..000000000 --- a/plugins/postcss-custom-media/src/index.js +++ /dev/null @@ -1,46 +0,0 @@ -import getCustomMediaFromRoot from './custom-media-from-root'; -import getCustomMediaFromImports from './get-custom-media-from-imports'; -import transformAtrules from './transform-atrules'; -import writeCustomMediaToExports from './write-custom-media-to-exports'; - -const creator = opts => { - // whether to preserve custom media and at-rules using them - const preserve = 'preserve' in Object(opts) ? Boolean(opts.preserve) : false; - - // sources to import custom media from - const importFrom = [].concat(Object(opts).importFrom || []); - - - // destinations to export custom media to - const exportTo = [].concat(Object(opts).exportTo || []); - - // promise any custom media are imported - const customMediaImportsPromise = getCustomMediaFromImports(importFrom); - - const customMediaHelperKey = Symbol('customMediaHelper'); - - return { - postcssPlugin: 'postcss-custom-media', - Once: async (root, helpers) => { - - // combine rules from root and from imports - helpers[customMediaHelperKey] = Object.assign( - await customMediaImportsPromise, - getCustomMediaFromRoot(root, { preserve }), - ); - - await writeCustomMediaToExports(helpers[customMediaHelperKey], exportTo); - }, - AtRule: (atrule, helpers) => { - if (atrule.name !== 'media') { - return; - } - - transformAtrules(atrule, helpers[customMediaHelperKey], { preserve }); - }, - }; -}; - -creator.postcss = true; - -export default creator; diff --git a/plugins/postcss-custom-media/src/index.ts b/plugins/postcss-custom-media/src/index.ts new file mode 100644 index 000000000..9ab51864f --- /dev/null +++ b/plugins/postcss-custom-media/src/index.ts @@ -0,0 +1,111 @@ +import { MediaQuery } from '@csstools/media-query-list-parser'; +import type { PluginCreator } from 'postcss'; +import getCustomMedia from './custom-media-from-root'; +import { transformAtMediaListTokens } from './transform-at-media/transform-at-media'; + +export interface PluginOptions { + /** Determines whether Custom Media and media queries using custom media should be preserved in their original form. */ + preserve?: boolean +} + +const creator: PluginCreator = (opts?: PluginOptions) => { + // whether to preserve custom media and rules using them + const preserve = Boolean(Object(opts).preserve); + + if ('importFrom' in Object(opts)) { + throw new Error('[postcss-custom-media] "importFrom" is no longer supported'); + } + + if ('exportTo' in Object(opts)) { + throw new Error('[postcss-custom-media] "exportTo" is no longer supported'); + } + + return { + postcssPlugin: 'postcss-custom-media', + prepare() { + let customMedia: Map, falsy: Array }> = new Map(); + + return { + Once: (root, { result }) => { + customMedia = getCustomMedia(root, result, { preserve: preserve }); + }, + AtRule: (atRule, { result }) => { + if (!atRule.params) { + return; + } + + if (!atRule.params.includes('--')) { + return; + } + + let transformedParams: Array<{ replaceWith: string, encapsulateWith?: string }> = []; + + try { + transformedParams = transformAtMediaListTokens(atRule.params, customMedia); + } catch (err) { + atRule.warn(result, `Failed to parse @custom-media params with error message: "${err.message}"`); + return; + } + + if (!transformedParams || transformedParams.length === 0) { + return; + } + + if (transformedParams.length === 1) { + if (atRule.params.trim() === transformedParams[0].replaceWith.trim()) { + return; + } + + atRule.cloneBefore({ params: transformedParams[0].replaceWith.trim() }); + + if (!preserve) { + atRule.remove(); + return; + } + + return; + } + + const needsEncapsulation = !!(transformedParams.find((x) => { + return !!x.encapsulateWith; + })); + + if (!needsEncapsulation) { + atRule.cloneBefore({ params: transformedParams.map((x) => x.replaceWith).join(',').trim() }); + + if (!preserve) { + atRule.remove(); + } + + return; + } + + transformedParams.forEach((transformed) => { + if (!transformed.encapsulateWith) { + atRule.cloneBefore({ params: transformed.replaceWith.trim() }); + return; + } + + const clone = atRule.clone({ params: transformed.replaceWith }); + const encapsulate = atRule.clone({ params: transformed.encapsulateWith.trim(), nodes: [] }); + + clone.parent = null; + encapsulate.parent = null; + + encapsulate.append(clone); + atRule.before(encapsulate); + }); + + if (!preserve) { + atRule.remove(); + return; + } + }, + }; + }, + }; +}; + +creator.postcss = true; + +export default creator; diff --git a/plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts b/plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts new file mode 100644 index 000000000..ba3947ccd --- /dev/null +++ b/plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts @@ -0,0 +1,28 @@ +import type { AtRule, ChildNode, Container, Document } from 'postcss'; + +const allowedParentAtRules = new Set(['scope', 'container']); + +export function isProcessableCustomMediaRule(atRule: AtRule): boolean { + if (atRule.name.toLowerCase() !== 'custom-media') { + return false; + } + + if (!atRule.params || !atRule.params.includes('--')) { + return false; + } + + if (atRule.nodes && atRule.nodes.length > 0) { + return false; + } + + let parent: Container | Document = atRule.parent; + while (parent) { + if (parent.type === 'atrule' && !allowedParentAtRules.has((parent as AtRule).name.toLowerCase())) { + return false; + } + + parent = parent.parent; + } + + return true; +} diff --git a/plugins/postcss-custom-media/src/media-ast-from-string.js b/plugins/postcss-custom-media/src/media-ast-from-string.js deleted file mode 100644 index 3821a319c..000000000 --- a/plugins/postcss-custom-media/src/media-ast-from-string.js +++ /dev/null @@ -1,134 +0,0 @@ -function parse(string, splitByAnd) { - const array = []; - let buffer = ''; - let split = false; - let func = 0; - let i = -1; - - while (++i < string.length) { - const char = string[i]; - - if (char === '(') { - func += 1; - } else if (char === ')') { - if (func > 0) { - func -= 1; - } - } else if (func === 0) { - if (splitByAnd && andRegExp.test(buffer + char)) { - split = true; - } else if (!splitByAnd && char === ',') { - split = true; - } - } - - if (split) { - array.push(splitByAnd ? new MediaExpression(buffer + char) : new MediaQuery(buffer)); - - buffer = ''; - split = false; - } else { - buffer += char; - } - } - - if (buffer !== '') { - array.push(splitByAnd ? new MediaExpression(buffer) : new MediaQuery(buffer)); - } - - return array; -} - -class MediaQueryList { - constructor(string) { - this.nodes = parse(string); - } - - invert() { - this.nodes.forEach(node => { - node.invert(); - }); - - return this; - } - - clone() { - return new MediaQueryList(String(this)); - } - - toString() { - return this.nodes.join(','); - } -} - -class MediaQuery { - constructor(string) { - const [, before, media, after ] = string.match(spaceWrapRegExp); - const [, modifier = '', afterModifier = ' ', type = '', beforeAnd = '', and = '', beforeExpression = '', expression1 = '', expression2 = ''] = media.match(mediaRegExp) || []; - const raws = { before, after, afterModifier, originalModifier: modifier || '', beforeAnd, and, beforeExpression }; - const nodes = parse(expression1 || expression2, true); - - Object.assign(this, { - modifier, - type, - raws, - nodes, - }); - } - - clone(overrides) { - const instance = new MediaQuery(String(this)); - - Object.assign(instance, overrides); - - return instance; - } - - invert() { - this.modifier = this.modifier ? '' : this.raws.originalModifier; - - return this; - } - - toString() { - const { raws } = this; - - return `${raws.before}${this.modifier}${this.modifier ? `${raws.afterModifier}` : ''}${this.type}${raws.beforeAnd}${raws.and}${raws.beforeExpression}${this.nodes.join('')}${this.raws.after}`; - } -} - -class MediaExpression { - constructor(string) { - const [, value, after = '', and = '', afterAnd = '' ] = string.match(andRegExp) || [null, string]; - const raws = { after, and, afterAnd }; - - Object.assign(this, { value, raws }); - } - - clone(overrides) { - const instance = new MediaExpression(String(this)); - - Object.assign(instance, overrides); - - return instance; - } - - toString() { - const { raws } = this; - - return `${this.value}${raws.after}${raws.and}${raws.afterAnd}`; - } -} - -const modifierRE = '(not|only)'; -const typeRE = '(all|print|screen|speech)'; -const noExpressionRE = '([\\W\\w]*)'; -const expressionRE = '([\\W\\w]+)'; -const noSpaceRE = '(\\s*)'; -const spaceRE = '(\\s+)'; -const andRE = '(?:(\\s+)(and))'; -const andRegExp = new RegExp(`^${expressionRE}(?:${andRE}${spaceRE})$`, 'i'); -const spaceWrapRegExp = new RegExp(`^${noSpaceRE}${noExpressionRE}${noSpaceRE}$`); -const mediaRegExp = new RegExp(`^(?:${modifierRE}${spaceRE})?(?:${typeRE}(?:${andRE}${spaceRE}${expressionRE})?|${expressionRE})$`, 'i'); - -export default string => new MediaQueryList(string); diff --git a/plugins/postcss-custom-media/src/toposort.ts b/plugins/postcss-custom-media/src/toposort.ts new file mode 100644 index 000000000..f3166f2f3 --- /dev/null +++ b/plugins/postcss-custom-media/src/toposort.ts @@ -0,0 +1,134 @@ +// Toposort - Topological sorting for node.js +// Copyright (c) 2012 by Marcel Klehr +// MIT LICENSE +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// We (ab)use `toposort` to find cyclic references. +// At the moment this is not optimized and uses a brute force approach. +// +// while there are cyclic ref errors +// - remove node from graph +// - re-do toposort +export function removeCyclicReferences(nodes: Map, edges: Array>): Set { + const cyclicReferences: Set = new Set(); + + // eslint-disable-next-line no-constant-condition + let _edges = edges; + while (nodes.size > 0) { + try { + toposort(Array.from(nodes.keys()), _edges); + break; + } catch (e) { + /* see the hack below */ + if (e['_graphNode']) { + + nodes.delete(e['_graphNode']); + cyclicReferences.add(e['_graphNode']); + _edges = _edges.filter((x) => { + return x.indexOf(e['_graphNode']) === -1; + }); + } else { + throw e; + } + } + } + + return cyclicReferences; +} + +function toposort(nodes: Array, edges: Array>): Array { + let cursor = nodes.length; + const sorted: Array = new Array(cursor); + const visited: Record = {}; + let i = cursor; + // Better data structures make algorithm much faster. + const outgoingEdges = makeOutgoingEdges(edges); + const nodesHash = makeNodesHash(nodes); + + // check for unknown nodes + edges.forEach(function (edge) { + if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) { + throw new Error('Unknown token. Make sure to provide all tokens used in aliases.'); + } + }); + + while (i--) { + if (!visited[i]) { + visit(nodes[i], i, new Set()); + } + } + + return sorted; + + function visit(node: string, j: number, predecessors: Set) { + if (predecessors.has(node)) { + const err = new Error('Cyclic dependency' + JSON.stringify(node)); + err['_graphNode'] = node; /* a hack to communicate which node is causing the cyclic error */ + + throw err; + } + + if (!nodesHash.has(node)) { + throw new Error('Found unknown token. Make sure to provided all involved tokens. Unknown token: ' + JSON.stringify(node)); + } + + if (visited[j]) { + return; + } + visited[j] = true; + + let outgoing = outgoingEdges.get(node) || new Set(); + outgoing = Array.from(outgoing); + + // eslint-disable-next-line no-cond-assign + if (j = outgoing.length) { + predecessors.add(node); + do { + const child = outgoing[--j]; + visit(child, nodesHash.get(child), predecessors); + } while (j); + predecessors.delete(node); + } + + sorted[--cursor] = node; + } +} + +function makeOutgoingEdges(arr: Array>) { + const edges = new Map(); + for (let i = 0, len = arr.length; i < len; i++) { + const edge = arr[i]; + if (!edges.has(edge[0])) { + edges.set(edge[0], new Set()); + } + if (!edges.has(edge[1])) { + edges.set(edge[1], new Set()); + } + edges.get(edge[0]).add(edge[1]); + } + return edges; +} + +function makeNodesHash(arr: Array) { + const res = new Map(); + for (let i = 0, len = arr.length; i < len; i++) { + res.set(arr[i], i); + } + return res; +} diff --git a/plugins/postcss-custom-media/src/transform-at-media/always-true-or-false.ts b/plugins/postcss-custom-media/src/transform-at-media/always-true-or-false.ts new file mode 100644 index 000000000..ec03674d1 --- /dev/null +++ b/plugins/postcss-custom-media/src/transform-at-media/always-true-or-false.ts @@ -0,0 +1,14 @@ +import { NumberType, TokenType } from '@csstools/css-tokenizer'; +import type { CSSToken } from '@csstools/css-tokenizer'; + +export const alwaysTrue: Array = [ + [TokenType.Ident, 'max-color', 0, 0, { value: 'max-color' }], + [TokenType.Colon, ':', 0, 0, undefined], + [TokenType.Number, '2147477350', 0, 0, { value: 2147477350, type: NumberType.Integer }], +]; + +export const neverTrue: Array = [ + [TokenType.Ident, 'color', 0, 0, { value: 'color' }], + [TokenType.Colon, ':', 0, 0, undefined], + [TokenType.Number, '2147477350', 0, 0, { value: 2147477350, type: NumberType.Integer }], +]; diff --git a/plugins/postcss-custom-media/src/transform-at-media/at-media-params-tokens.ts b/plugins/postcss-custom-media/src/transform-at-media/at-media-params-tokens.ts new file mode 100644 index 000000000..3b96058ad --- /dev/null +++ b/plugins/postcss-custom-media/src/transform-at-media/at-media-params-tokens.ts @@ -0,0 +1,20 @@ +import { tokenizer } from '@csstools/css-tokenizer'; +import type { CSSToken } from '@csstools/css-tokenizer'; + +export function atMediaParamsTokens(params: string): Array { + const t = tokenizer({ + css: params, + }, { + commentsAreTokens: true, + onParseError: () => { + throw new Error(`Unable to parse media query "${params}"`); + }, + }); + + const tokens: Array = []; + while (!t.endOfFile()) { + tokens.push(t.nextToken()); + } + + return tokens; +} diff --git a/plugins/postcss-custom-media/src/transform-at-media/custom-media.ts b/plugins/postcss-custom-media/src/transform-at-media/custom-media.ts new file mode 100644 index 000000000..bd9de7806 --- /dev/null +++ b/plugins/postcss-custom-media/src/transform-at-media/custom-media.ts @@ -0,0 +1,69 @@ +import { cloneTokens, stringify, TokenIdent, TokenType } from '@csstools/css-tokenizer'; +import { parseFromTokens, MediaQuery } from '@csstools/media-query-list-parser'; +import { atMediaParamsTokens } from '../transform-at-media/at-media-params-tokens'; +import { replaceTrueAndFalseTokens } from './true-and-false'; + +export function parseCustomMedia(params: string): { name: string, truthy: Array, falsy: Array, dependsOn: Array<[string, string]> } | false { + const tokens = atMediaParamsTokens(params); + + const customMediaReferences: Set = new Set(); + + let name = ''; + let remainder = tokens; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i][0] === TokenType.Comment) { + continue; + } + if (tokens[i][0] === TokenType.Whitespace) { + continue; + } + + if (tokens[i][0] === TokenType.Ident) { + const identToken = tokens[i] as TokenIdent; + if (identToken[4].value.startsWith('--')) { + name = identToken[4].value; + remainder = tokens.slice(i + 1); + break; + } + } + + return false; + } + + for (let i = 0; i < remainder.length; i++) { + if (remainder[i][0] === TokenType.Ident) { + const identToken = remainder[i] as TokenIdent; + if (identToken[4].value.startsWith('--')) { + customMediaReferences.add(identToken[4].value); + } + } + } + + remainder = replaceTrueAndFalseTokens(remainder); + + const mediaQueryListTruthy = parseFromTokens(cloneTokens(remainder), { + preserveInvalidMediaQueries: true, + onParseError: () => { + throw new Error(`Unable to parse media query "${stringify(...remainder)}"`); + }, + }); + const mediaQueryListFalsy = parseFromTokens(cloneTokens(remainder), { + preserveInvalidMediaQueries: true, + onParseError: () => { + throw new Error(`Unable to parse media query "${stringify(...remainder) }"`); + }, + }); + + for (let i = 0; i < mediaQueryListFalsy.length; i++) { + mediaQueryListFalsy[i] = mediaQueryListFalsy[i].negateQuery(); + } + + return { + name: name, + truthy: mediaQueryListTruthy, + falsy: mediaQueryListFalsy, + dependsOn: Array.from(customMediaReferences).map((x) => { + return [x, name]; + }), + }; +} diff --git a/plugins/postcss-custom-media/src/transform-at-media/transform-at-media.ts b/plugins/postcss-custom-media/src/transform-at-media/transform-at-media.ts new file mode 100644 index 000000000..65bea992a --- /dev/null +++ b/plugins/postcss-custom-media/src/transform-at-media/transform-at-media.ts @@ -0,0 +1,165 @@ +import { alwaysTrue, neverTrue } from './always-true-or-false'; +import { isGeneralEnclosed, isMediaAnd, isMediaConditionList, isMediaFeature, isMediaFeatureBoolean, isMediaNot, isMediaOr, isMediaQueryInvalid, isMediaQueryWithType, MediaQuery, newMediaFeaturePlain, parse } from '@csstools/media-query-list-parser'; + +export function transformAtMediaListTokens(params: string, replacements: Map, falsy: Array }>): Array<{ replaceWith: string, encapsulateWith?: string }> { + const mediaQueries = parse(params, { + preserveInvalidMediaQueries: true, onParseError: () => { + throw new Error(`Unable to parse media query "${params}"`); + }, + }); + + const stringQueries = mediaQueries.map((x) => x.toString()); + + for (let i = 0; i < mediaQueries.length; i++) { + const mediaQuery = mediaQueries[i]; + const original = stringQueries[i]; + + { + const transformedQuery = transformSimpleMediaQuery(mediaQuery, replacements); + if (transformedQuery && transformedQuery.replaceWith !== original) { + return stringQueries.map((query, index) => { + if (index === i) { + return transformedQuery; + } + + return { + replaceWith: query, + }; + }); + } + } + + const transformedQuery = transformComplexMediaQuery(mediaQuery, replacements); + if (!transformedQuery || transformedQuery.length === 0) { + continue; + } + + if (transformedQuery[0].replaceWith === original) { + continue; + } + + return stringQueries.flatMap((query, index) => { + if (index === i) { + return transformedQuery; + } + + return [{ + replaceWith: query, + }]; + }); + } + + return []; +} + +export function transformSimpleMediaQuery(mediaQuery: MediaQuery, replacements: Map, falsy: Array }>): { replaceWith: string, encapsulateWith?: string } | null { + if (!mediaQueryIsSimple(mediaQuery)) { + return null; + } + + let candidate: { replaceWith: string, encapsulateWith?: string } | null = null; + + mediaQuery.walk((entry) => { + const node = entry.node; + if (!isMediaFeatureBoolean(node)) { + return; + } + + const name = node.getName(); + if (!name.startsWith('--')) { + return false; + } + + const replacement = replacements.get(name); + if (replacement) { + candidate = { + replaceWith: replacement.truthy.map((x) => x.toString().trim()).join(','), + }; + + return false; + } + }); + + return candidate; +} + +export function transformComplexMediaQuery(mediaQuery: MediaQuery, replacements: Map, falsy: Array }>): Array<{ replaceWith: string, encapsulateWith?: string }> { + let candidate: Array<{ replaceWith: string, encapsulateWith?: string }> = []; + + mediaQuery.walk((entry) => { + const node = entry.node; + if (!isMediaFeatureBoolean(node)) { + return; + } + + const parent = entry.parent; + if (!isMediaFeature(parent)) { + return; + } + + const name = node.getName(); + if (!name.startsWith('--')) { + return false; + } + + const replacement = replacements.get(name); + if (replacement) { + const replaceWithTrue = newMediaFeaturePlain( + alwaysTrue[0][4].value as string, + alwaysTrue[2], + ); + + parent.feature = replaceWithTrue.feature; + const replaceWithTrueString = mediaQuery.toString(); + + const replaceWithFalse = newMediaFeaturePlain( + neverTrue[0][4].value as string, + neverTrue[2], + ); + + parent.feature = replaceWithFalse.feature; + const replaceWithFalseString = mediaQuery.toString(); + + candidate = [ + { + replaceWith: replaceWithTrueString, + encapsulateWith: replacement.truthy.map((x) => x.toString().trim()).join(','), + }, + { + replaceWith: replaceWithFalseString, + encapsulateWith: replacement.falsy.map((x) => x.toString().trim()).join(','), + }, + ]; + + return false; + } + }); + + return candidate; +} + +function mediaQueryIsSimple(mediaQuery: MediaQuery): boolean { + if (isMediaQueryInvalid(mediaQuery)) { + return false; + } + + if (isMediaQueryWithType(mediaQuery)) { + return false; + } + + let isSimple = true; + mediaQuery.walk((entry) => { + if ( + isMediaAnd(entry.node) || + isMediaOr(entry.node) || + isMediaNot(entry.node) || + isMediaConditionList(entry.node) || + isGeneralEnclosed(entry.node) + ) { + isSimple = false; + return false; + } + }); + + return isSimple; +} diff --git a/plugins/postcss-custom-media/src/transform-at-media/true-and-false.ts b/plugins/postcss-custom-media/src/transform-at-media/true-and-false.ts new file mode 100644 index 000000000..ebb56a460 --- /dev/null +++ b/plugins/postcss-custom-media/src/transform-at-media/true-and-false.ts @@ -0,0 +1,68 @@ +import { TokenType, TokenIdent } from '@csstools/css-tokenizer'; +import type { CSSToken } from '@csstools/css-tokenizer'; +import { alwaysTrue, neverTrue } from './always-true-or-false'; + +export function replaceTrueAndFalseTokens(tokens: Array): Array { + let booleanToken; + let remainder; + + for (let i = 0; i < tokens.length; i++) { + if (tokens[i][0] === TokenType.Comment) { + continue; + } + if (tokens[i][0] === TokenType.Whitespace) { + continue; + } + + if (tokens[i][0] === TokenType.Ident) { + const identToken = tokens[i] as TokenIdent; + if (identToken[4].value.toLowerCase() === 'true') { + booleanToken = 'true'; + remainder = tokens.slice(i + 1); + break; + } + + if (identToken[4].value.toLowerCase() === 'false') { + booleanToken = 'false'; + remainder = tokens.slice(i + 1); + break; + } + } + + return tokens; + } + + if (!booleanToken) { + return tokens; + } + + { + // Nothing is allowed after true|false except for comments and whitespace + for (let i = 0; i < remainder.length; i++) { + if (remainder[i][0] === TokenType.Comment) { + continue; + } + if (remainder[i][0] === TokenType.Whitespace) { + continue; + } + + return tokens; + } + } + + if (booleanToken === 'true') { + return [ + [TokenType.Whitespace, ' ', 0, 0, undefined], + [TokenType.OpenParen, '(', 0, 0, undefined], + ...alwaysTrue, + [TokenType.CloseParen, ')', 0, 0, undefined], + ]; + } + + return [ + [TokenType.Whitespace, ' ', 0, 0, undefined], + [TokenType.OpenParen, '(', 0, 0, undefined], + ...neverTrue, + [TokenType.CloseParen, ')', 0, 0, undefined], + ]; +} diff --git a/plugins/postcss-custom-media/src/transform-atrules.js b/plugins/postcss-custom-media/src/transform-atrules.js deleted file mode 100644 index 130e0bbb9..000000000 --- a/plugins/postcss-custom-media/src/transform-atrules.js +++ /dev/null @@ -1,25 +0,0 @@ -import transformMediaList from './transform-media-list'; -import mediaASTFromString from './media-ast-from-string'; - -// transform custom pseudo selectors with custom selectors -export default (atrule, customMedia, { preserve }) => { - if (atrule.params.indexOf('--') > -1) { - const mediaAST = mediaASTFromString(atrule.params); - const params = String(transformMediaList(mediaAST, customMedia)); - if (params === null) { - return; - } - - if (params === atrule.params) { - return; - } - - atrule.cloneBefore({ - params: params, - }); - - if (!preserve) { - atrule.remove(); - } - } -}; diff --git a/plugins/postcss-custom-media/src/transform-media-list.js b/plugins/postcss-custom-media/src/transform-media-list.js deleted file mode 100644 index 2eed88fdb..000000000 --- a/plugins/postcss-custom-media/src/transform-media-list.js +++ /dev/null @@ -1,88 +0,0 @@ -import { getCustomMediaNameReference } from './custom-media-name'; - -// return transformed medias, replacing custom pseudo medias with custom medias -export default function transformMediaList(mediaList, customMedias) { - let index = mediaList.nodes.length - 1; - - while (index >= 0) { - const transformedMedias = transformMedia(mediaList.nodes[index], customMedias); - - if (transformedMedias.length) { - mediaList.nodes.splice(index, 1, ...transformedMedias); - } - - --index; - } - - return mediaList; -} - -// return custom pseudo medias replaced with custom medias -function transformMedia(media, customMedias) { - const transpiledMedias = []; - - for (const index in media.nodes) { - const { value, nodes } = media.nodes[index]; - const key = getCustomMediaNameReference(value); - if (key && (key in customMedias)) { - for (const replacementMedia of customMedias[key].nodes) { - // use the first available modifier unless they cancel each other out - const modifier = media.modifier !== replacementMedia.modifier - ? media.modifier || replacementMedia.modifier - : ''; - const mediaClone = media.clone({ - modifier, - // conditionally use the raws from the first available modifier - raws: !modifier || media.modifier - ? { ...media.raws } - : { ...replacementMedia.raws }, - type: media.type || replacementMedia.type, - }); - - // conditionally include more replacement raws when the type is present - if (mediaClone.type === replacementMedia.type) { - Object.assign(mediaClone.raws, { - and: replacementMedia.raws.and, - beforeAnd: replacementMedia.raws.beforeAnd, - beforeExpression: replacementMedia.raws.beforeExpression, - }); - } - - mediaClone.nodes.splice(index, 1, ...replacementMedia.clone().nodes.map(node => { - // use raws and spacing from the current usage - if (media.nodes[index].raws.and) { - node.raws = { ...media.nodes[index].raws }; - } - - node.spaces = { ...media.nodes[index].spaces }; - - return node; - })); - - // remove the currently transformed key to prevent recursion - const nextCustomMedia = getCustomMediasWithoutKey(customMedias, key); - const retranspiledMedias = transformMedia(mediaClone, nextCustomMedia); - - if (retranspiledMedias.length) { - transpiledMedias.push(...retranspiledMedias); - } else { - transpiledMedias.push(mediaClone); - } - } - - return transpiledMedias; - } else if (nodes && nodes.length) { - transformMediaList(media.nodes[index], customMedias); - } - } - - return transpiledMedias; -} - -const getCustomMediasWithoutKey = (customMedias, key) => { - const nextCustomMedias = Object.assign({}, customMedias); - - delete nextCustomMedias[key]; - - return nextCustomMedias; -}; diff --git a/plugins/postcss-custom-media/src/write-custom-media-to-exports.js b/plugins/postcss-custom-media/src/write-custom-media-to-exports.js deleted file mode 100644 index 52754f3f5..000000000 --- a/plugins/postcss-custom-media/src/write-custom-media-to-exports.js +++ /dev/null @@ -1,129 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -/* Write Custom Media from CSS File -/* ========================================================================== */ - -async function writeCustomMediaToCssFile(to, customMedia) { - const cssContent = Object.keys(customMedia).reduce((cssLines, name) => { - cssLines.push(`@custom-media ${name} ${customMedia[name]};`); - - return cssLines; - }, []).join('\n'); - const css = `${cssContent}\n`; - - await writeFile(to, css); -} - -/* Write Custom Media from JSON file -/* ========================================================================== */ - -async function writeCustomMediaToJsonFile(to, customMedia) { - const jsonContent = JSON.stringify({ - 'custom-media': customMedia, - }, null, '\t'); - const json = `${jsonContent}\n`; - - await writeFile(to, json); -} - -/* Write Custom Media from Common JS file -/* ========================================================================== */ - -async function writeCustomMediaToCjsFile(to, customMedia) { - const jsContents = Object.keys(customMedia).reduce((jsLines, name) => { - jsLines.push(`\t\t'${escapeForJS(name)}': '${escapeForJS(customMedia[name])}'`); - - return jsLines; - }, []).join(',\n'); - const js = `module.exports = {\n\tcustomMedia: {\n${jsContents}\n\t}\n};\n`; - - await writeFile(to, js); -} - -/* Write Custom Media from Module JS file -/* ========================================================================== */ - -async function writeCustomMediaToMjsFile(to, customMedia) { - const mjsContents = Object.keys(customMedia).reduce((mjsLines, name) => { - mjsLines.push(`\t'${escapeForJS(name)}': '${escapeForJS(customMedia[name])}'`); - - return mjsLines; - }, []).join(',\n'); - const mjs = `export const customMedia = {\n${mjsContents}\n};\n`; - - await writeFile(to, mjs); -} - -/* Write Custom Media to Exports -/* ========================================================================== */ - -export default function writeCustomMediaToExports(customMedia, destinations) { - return Promise.all(destinations.map(async destination => { - if (destination instanceof Function) { - await destination(defaultCustomMediaToJSON(customMedia)); - } else { - // read the destination as an object - const opts = destination === Object(destination) ? destination : { to: String(destination) }; - - // transformer for custom media into a JSON-compatible object - const toJSON = opts.toJSON || defaultCustomMediaToJSON; - - if ('customMedia' in opts) { - // write directly to an object as customMedia - opts.customMedia = toJSON(customMedia); - } else if ('custom-media' in opts) { - // write directly to an object as custom-media - opts['custom-media'] = toJSON(customMedia); - } else { - // destination pathname - const to = String(opts.to || ''); - - // type of file being written to - const type = (opts.type || path.extname(to).slice(1)).toLowerCase(); - - // transformed custom media - const customMediaJSON = toJSON(customMedia); - - if (type === 'css') { - await writeCustomMediaToCssFile(to, customMediaJSON); - } - - if (type === 'js') { - await writeCustomMediaToCjsFile(to, customMediaJSON); - } - - if (type === 'json') { - await writeCustomMediaToJsonFile(to, customMediaJSON); - } - - if (type === 'mjs') { - await writeCustomMediaToMjsFile(to, customMediaJSON); - } - } - } - })); -} - -/* Helper utilities -/* ========================================================================== */ - -const defaultCustomMediaToJSON = customMedia => { - return Object.keys(customMedia).reduce((customMediaJSON, key) => { - customMediaJSON[key] = String(customMedia[key]); - - return customMediaJSON; - }, {}); -}; - -const writeFile = (to, text) => new Promise((resolve, reject) => { - fs.writeFile(to, text, error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); -}); - -const escapeForJS = string => string.replace(/\\([\s\S])|(')/g, '\\$1$2').replace(/\n/g, '\\n').replace(/\r/g, '\\r'); diff --git a/plugins/postcss-custom-media/test/and.css b/plugins/postcss-custom-media/test/and.css new file mode 100644 index 000000000..4d40c3bcd --- /dev/null +++ b/plugins/postcss-custom-media/test/and.css @@ -0,0 +1,17 @@ +/* Media queries with "and" */ +@custom-media --with-and (min-width: 300px) and (min-height: 300px); + +@media not screen and (--with-and) { + .a { + order: 1; + } +} + +@custom-media --not-screen not screen; +@custom-media --not-print not print; + +@media (--not-screen) and (--not-print) { + .a { + order: 2; + } +} diff --git a/plugins/postcss-custom-media/test/and.expect.css b/plugins/postcss-custom-media/test/and.expect.css new file mode 100644 index 000000000..9cbf54fd4 --- /dev/null +++ b/plugins/postcss-custom-media/test/and.expect.css @@ -0,0 +1,61 @@ +/* Media queries with "and" */ + +@media (min-width: 300px) and (min-height: 300px) { + +@media not screen and (max-color:2147477350) { + .a { + order: 1; + } +} +} + +@media not all and (min-width: 300px) and (min-height: 300px) { + +@media not screen and (color:2147477350) { + .a { + order: 1; + } +} +} + +@media not screen { + +@media not print { + +@media (max-color:2147477350) and (max-color:2147477350) { + .a { + order: 2; + } +} +} + +@media print { + +@media (max-color:2147477350) and (color:2147477350) { + .a { + order: 2; + } +} +} +} + +@media screen { + +@media not print { + +@media (color:2147477350) and (max-color:2147477350) { + .a { + order: 2; + } +} +} + +@media print { + +@media (color:2147477350) and (color:2147477350) { + .a { + order: 2; + } +} +} +} diff --git a/plugins/postcss-custom-media/test/basic-after-v9.css b/plugins/postcss-custom-media/test/basic-after-v9.css new file mode 100644 index 000000000..01fb549e9 --- /dev/null +++ b/plugins/postcss-custom-media/test/basic-after-v9.css @@ -0,0 +1,37 @@ +@custom-media --simple-feature-test (min-width: 300px); + +/* Most basic case */ +@media (--simple-feature-test) { + .a { + order: 1; + } +} + +@media ((--simple-feature-test)) { + .a { + order: 1.1; + } +} + +/* Also a type condition */ +@media screen and (--simple-feature-test) { + .a { + order: 2; + } +} + +/* Negation */ +@media not (--simple-feature-test) { + .a { + order: 3; + } +} + +/* LightningCSS example */ +@custom-media --modern (color), (hover); + +@media (--modern) and (width > 1024px) { + .a { + color: green; + } +} diff --git a/plugins/postcss-custom-media/test/basic-after-v9.expect.css b/plugins/postcss-custom-media/test/basic-after-v9.expect.css new file mode 100644 index 000000000..6d01623fe --- /dev/null +++ b/plugins/postcss-custom-media/test/basic-after-v9.expect.css @@ -0,0 +1,64 @@ +/* Most basic case */ +@media (min-width: 300px) { + .a { + order: 1; + } +} + +@media (min-width: 300px) { + .a { + order: 1.1; + } +} + +/* Also a type condition */ +@media (min-width: 300px) { +@media screen and (max-color:2147477350) { + .a { + order: 2; + } +} +} +@media not all and (min-width: 300px) { +@media screen and (color:2147477350) { + .a { + order: 2; + } +} +} + +/* Negation */ +@media (min-width: 300px) { +@media not (max-color:2147477350) { + .a { + order: 3; + } +} +} +@media not all and (min-width: 300px) { +@media not (color:2147477350) { + .a { + order: 3; + } +} +} + +/* LightningCSS example */ + +@media (color),(hover) { + +@media (max-color:2147477350) and (width > 1024px) { + .a { + color: green; + } +} +} + +@media not all and (color),not all and (hover) { + +@media (color:2147477350) and (width > 1024px) { + .a { + color: green; + } +} +} diff --git a/plugins/postcss-custom-media/test/basic-after-v9.preserve.expect.css b/plugins/postcss-custom-media/test/basic-after-v9.preserve.expect.css new file mode 100644 index 000000000..5fa31e9fd --- /dev/null +++ b/plugins/postcss-custom-media/test/basic-after-v9.preserve.expect.css @@ -0,0 +1,94 @@ +@custom-media --simple-feature-test (min-width: 300px); + +/* Most basic case */ +@media (min-width: 300px) { + .a { + order: 1; + } +} +@media (--simple-feature-test) { + .a { + order: 1; + } +} + +@media (min-width: 300px) { + .a { + order: 1.1; + } +} + +@media ((--simple-feature-test)) { + .a { + order: 1.1; + } +} + +/* Also a type condition */ +@media (min-width: 300px) { +@media screen and (max-color:2147477350) { + .a { + order: 2; + } +} +} +@media not all and (min-width: 300px) { +@media screen and (color:2147477350) { + .a { + order: 2; + } +} +} +@media screen and (--simple-feature-test) { + .a { + order: 2; + } +} + +/* Negation */ +@media (min-width: 300px) { +@media not (max-color:2147477350) { + .a { + order: 3; + } +} +} +@media not all and (min-width: 300px) { +@media not (color:2147477350) { + .a { + order: 3; + } +} +} +@media not (--simple-feature-test) { + .a { + order: 3; + } +} + +/* LightningCSS example */ +@custom-media --modern (color), (hover); + +@media (color),(hover) { + +@media (max-color:2147477350) and (width > 1024px) { + .a { + color: green; + } +} +} + +@media not all and (color),not all and (hover) { + +@media (color:2147477350) and (width > 1024px) { + .a { + color: green; + } +} +} + +@media (--modern) and (width > 1024px) { + .a { + color: green; + } +} diff --git a/plugins/postcss-custom-media/test/basic.css b/plugins/postcss-custom-media/test/basic.css index a992cfe2f..2b80609fe 100644 --- a/plugins/postcss-custom-media/test/basic.css +++ b/plugins/postcss-custom-media/test/basic.css @@ -10,31 +10,31 @@ @media (--mq-b) { body { - order: 1; + order: 2; } } @media (--mq-a), (--mq-a) { body { - order: 1; + order: 3; } } @media not all and (--mq-a) { body { - order: 2; + order: 4; } } @media (--not-mq-a) { body { - order: 1; + order: 5; } } @media not all and (--not-mq-a) { body { - order: 2; + order: 6; } } @@ -43,19 +43,19 @@ @media (--circular-mq-a) { body { - order: 3; + order: 7; } } @media (--circular-mq-b) { body { - order: 4; + order: 8; } } @media (--unresolved-mq) { body { - order: 5; + order: 9; } } @@ -64,7 +64,7 @@ @media (--min) and (--max) { body { - order: 6; + order: 10; } } @@ -72,13 +72,13 @@ @media (--concat) { body { - order: 7; + order: 11; } } @media (--concat) and (min-aspect-ratio: 16/9) { body { - order: 8; + order: 12; } } diff --git a/plugins/postcss-custom-media/test/basic.expect.css b/plugins/postcss-custom-media/test/basic.expect.css index ed797941c..97d82e390 100644 --- a/plugins/postcss-custom-media/test/basic.expect.css +++ b/plugins/postcss-custom-media/test/basic.expect.css @@ -6,69 +6,183 @@ @media screen and (max-width: 30em) { body { - order: 1; + order: 2; } } -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { +@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) { body { - order: 1; + order: 3; + } +} + +@media (max-width: 30em),(max-height: 30em) { + +@media not all and (max-color:2147477350) { + body { + order: 4; } } +} @media not all and (max-width: 30em),not all and (max-height: 30em) { + +@media not all and (color:2147477350) { body { - order: 2; + order: 4; } } +} + +@media (max-width: 30em),(max-height: 30em) { + +@media not all and (max-color:2147477350) { + body { + order: 5; + } +} +} @media not all and (max-width: 30em),not all and (max-height: 30em) { + +@media not all and (color:2147477350) { body { - order: 1; + order: 5; } } +} + +@media (max-width: 30em),(max-height: 30em) { + +@media not all and (max-color:2147477350) { -@media all and (max-width: 30em),all and (max-height: 30em) { +@media not all and (max-color:2147477350) { body { - order: 2; + order: 6; } } +} +} + +@media not all and (max-width: 30em),not all and (max-height: 30em) { -@media (--circular-mq-a) { +@media not all and (color:2147477350) { + +@media not all and (max-color:2147477350) { body { - order: 3; + order: 6; + } +} +} +} + +@media (max-width: 30em),(max-height: 30em) { + +@media all and (max-color:2147477350) { + +@media not all and (color:2147477350) { + body { + order: 6; + } +} +} +} + +@media not all and (max-width: 30em),not all and (max-height: 30em) { + +@media all and (color:2147477350) { + +@media not all and (color:2147477350) { + body { + order: 6; } } +} +} @media (--circular-mq-b) { body { - order: 4; + order: 7; + } +} + +@media (--circular-mq-b) { + body { + order: 8; } } @media (--unresolved-mq) { body { - order: 5; + order: 9; } } +@media (min-width: 320px) { + +@media (max-width: 640px) { + +@media (max-color:2147477350) and (max-color:2147477350) { + body { + order: 10; + } +} +} + +@media not all and (max-width: 640px) { + +@media (max-color:2147477350) and (color:2147477350) { + body { + order: 10; + } +} +} +} + +@media not all and (min-width: 320px) { + +@media (max-width: 640px) { + +@media (color:2147477350) and (max-color:2147477350) { + body { + order: 10; + } +} +} + +@media not all and (max-width: 640px) { + +@media (color:2147477350) and (color:2147477350) { + body { + order: 10; + } +} +} +} + @media (min-width: 320px) and (max-width: 640px) { body { - order: 6; + order: 11; } } @media (min-width: 320px) and (max-width: 640px) { + +@media (max-color:2147477350) and (min-aspect-ratio: 16/9) { body { - order: 7; + order: 12; } } +} -@media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) { +@media not all and (min-width: 320px) and (max-width: 640px) { + +@media (color:2147477350) and (min-aspect-ratio: 16/9) { body { - order: 8; + order: 12; } } +} @media (max-width: 30em),(max-height: 30em) { body { @@ -82,27 +196,25 @@ } } -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { +@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) { body { order: 1002; } } -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { +@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) { body { order: 1003; } } -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { +@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) { body { order: 1004; } } -@media (max-width: 30em),(max-height: 30em), -(max-width: 30em), -(max-height: 30em) { +@media (max-width: 30em),(max-height: 30em),(max-width: 30em),(max-height: 30em) { body { order: 1005; } diff --git a/plugins/postcss-custom-media/test/basic.import.expect.css b/plugins/postcss-custom-media/test/basic.import.expect.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/postcss-custom-media/test/basic.preserve.expect.css b/plugins/postcss-custom-media/test/basic.preserve.expect.css deleted file mode 100644 index 4c8aefc63..000000000 --- a/plugins/postcss-custom-media/test/basic.preserve.expect.css +++ /dev/null @@ -1,224 +0,0 @@ -@custom-media --mq-a (max-width: 30em), (max-height: 30em); -@custom-media --mq-b screen and (max-width: 30em); -@custom-media --not-mq-a not all and (--mq-a); - -@media (max-width: 30em),(max-height: 30em) { - body { - order: 1; - } -} - -@media (--mq-a) { - body { - order: 1; - } -} - -@media screen and (max-width: 30em) { - body { - order: 1; - } -} - -@media (--mq-b) { - body { - order: 1; - } -} - -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { - body { - order: 1; - } -} - -@media (--mq-a), (--mq-a) { - body { - order: 1; - } -} - -@media not all and (max-width: 30em),not all and (max-height: 30em) { - body { - order: 2; - } -} - -@media not all and (--mq-a) { - body { - order: 2; - } -} - -@media not all and (max-width: 30em),not all and (max-height: 30em) { - body { - order: 1; - } -} - -@media (--not-mq-a) { - body { - order: 1; - } -} - -@media all and (max-width: 30em),all and (max-height: 30em) { - body { - order: 2; - } -} - -@media not all and (--not-mq-a) { - body { - order: 2; - } -} - -@custom-media --circular-mq-a (--circular-mq-b); -@custom-media --circular-mq-b (--circular-mq-a); - -@media (--circular-mq-a) { - body { - order: 3; - } -} - -@media (--circular-mq-b) { - body { - order: 4; - } -} - -@media (--unresolved-mq) { - body { - order: 5; - } -} - -@custom-media --min (min-width: 320px); -@custom-media --max (max-width: 640px); - -@media (min-width: 320px) and (max-width: 640px) { - body { - order: 6; - } -} - -@media (--min) and (--max) { - body { - order: 6; - } -} - -@custom-media --concat (min-width: 320px) and (max-width: 640px); - -@media (min-width: 320px) and (max-width: 640px) { - body { - order: 7; - } -} - -@media (--concat) { - body { - order: 7; - } -} - -@media (min-width: 320px) and (max-width: 640px) and (min-aspect-ratio: 16/9) { - body { - order: 8; - } -} - -@media (--concat) and (min-aspect-ratio: 16/9) { - body { - order: 8; - } -} - -@media (max-width: 30em),(max-height: 30em) { - body { - order: 1000; - } -} - -@media ( --mq-a ) { - body { - order: 1000; - } -} - -@media (max-width: 30em),(max-height: 30em) { - body { - order: 1001; - } -} - -@media ( --mq-a ) { - body { - order: 1001; - } -} - -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { - body { - order: 1002; - } -} - -@media ( --mq-a ), ( --mq-a ) { - body { - order: 1002; - } -} - -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { - body { - order: 1003; - } -} - -@media ( --mq-a ), ( --mq-a ) { - body { - order: 1003; - } -} - -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { - body { - order: 1004; - } -} - -@media ( --mq-a ), ( --mq-a ) { - body { - order: 1004; - } -} - -@media (max-width: 30em),(max-height: 30em), -(max-width: 30em), -(max-height: 30em) { - body { - order: 1005; - } -} - -@media ( - --mq-a -), -( - --mq-a -) { - body { - order: 1005; - } -} - -@media (trailer--) { - body { - order: 1006; - } -} - -@custom-media trailer-- (min-width: 320px); diff --git a/plugins/postcss-custom-media/test/comma-1.css b/plugins/postcss-custom-media/test/comma-1.css new file mode 100644 index 000000000..962a50fed --- /dev/null +++ b/plugins/postcss-custom-media/test/comma-1.css @@ -0,0 +1,39 @@ +@custom-media --list-1 screen, print; + +@media (--list-1) { + .a { + order: 1; + } +} + +@custom-media --list-2 screen, (func(aa,bb,cc)); + +@media (--list-2) { + .a { + order: 2; + } +} + +@custom-media --list-3 screen, [aa,bb,cc]; + +@media (--list-3) { + .a { + order: 3; + } +} + +@custom-media --list-4 screen, ([aa,bb,cc]); + +@media (--list-4) { + .a { + order: 4; + } +} + +@custom-media --list-5 screen, ({aa,bb,cc}); + +@media (--list-5) { + .a { + order: 5; + } +} diff --git a/plugins/postcss-custom-media/test/comma-1.expect.css b/plugins/postcss-custom-media/test/comma-1.expect.css new file mode 100644 index 000000000..6a87b6d35 --- /dev/null +++ b/plugins/postcss-custom-media/test/comma-1.expect.css @@ -0,0 +1,29 @@ +@media screen,print { + .a { + order: 1; + } +} + +@media screen,(func(aa,bb,cc)) { + .a { + order: 2; + } +} + +@media screen,[aa,bb,cc] { + .a { + order: 3; + } +} + +@media screen,([aa,bb,cc]) { + .a { + order: 4; + } +} + +@media screen,({aa,bb,cc}) { + .a { + order: 5; + } +} diff --git a/plugins/postcss-custom-media/test/comma-2.css b/plugins/postcss-custom-media/test/comma-2.css new file mode 100644 index 000000000..6e7c20c54 --- /dev/null +++ b/plugins/postcss-custom-media/test/comma-2.css @@ -0,0 +1,50 @@ +@custom-media --list-1 screen; +@custom-media --list-2 print; + +@media (--list-1),(--list-2) { + .a { + order: 1; + } +} + +@media (--list-1), (func(aa,(--list-2),cc)) { + .a { + order: 2; + } +} + +@media (--list-1), [aa,(--list-2),cc] { + .a { + order: 3; + } +} + +@media (--list-1), ([aa,(--list-2),cc]) { + .a { + order: 4; + } +} + +@media (--list-1), ([aa,[(--list-2)],cc]) { + .a { + order: 5; + } +} + +@media (--list-1), ({aa,(--list-2),cc}) { + .a { + order: 6; + } +} + +@media (--list-1), ({aa,{(--list-2)},cc}) { + .a { + order: 7; + } +} + +@media ((--list-1) or (min-width: 300px)),((--list-2) and (min-height: 300px)) { + .a { + order: 8; + } +} diff --git a/plugins/postcss-custom-media/test/comma-2.expect.css b/plugins/postcss-custom-media/test/comma-2.expect.css new file mode 100644 index 000000000..820d9b2cb --- /dev/null +++ b/plugins/postcss-custom-media/test/comma-2.expect.css @@ -0,0 +1,77 @@ +@media screen,print { + .a { + order: 1; + } +} + +@media screen, (func(aa,(--list-2),cc)) { + .a { + order: 2; + } +} + +@media screen, [aa,(--list-2),cc] { + .a { + order: 3; + } +} + +@media screen, ([aa,(--list-2),cc]) { + .a { + order: 4; + } +} + +@media screen, ([aa,[(--list-2)],cc]) { + .a { + order: 5; + } +} + +@media screen, ({aa,(--list-2),cc}) { + .a { + order: 6; + } +} + +@media screen, ({aa,{(--list-2)},cc}) { + .a { + order: 7; + } +} + +@media screen { + +@media ((max-color:2147477350) or (min-width: 300px)) { + .a { + order: 8; + } +} +} + +@media not screen { + +@media ((color:2147477350) or (min-width: 300px)) { + .a { + order: 8; + } +} +} + +@media print { + +@media ((max-color:2147477350) and (min-height: 300px)) { + .a { + order: 8; + } +} +} + +@media not print { + +@media ((color:2147477350) and (min-height: 300px)) { + .a { + order: 8; + } +} +} diff --git a/plugins/postcss-custom-media/test/complex.css b/plugins/postcss-custom-media/test/complex.css index 4b427cbd0..ca21e8482 100644 --- a/plugins/postcss-custom-media/test/complex.css +++ b/plugins/postcss-custom-media/test/complex.css @@ -21,12 +21,11 @@ } /* #region https://github.com/csstools/postcss-custom-media/issues/51 */ -/* TODO: This is broken at the moment */ @custom-media --screen only screen; @custom-media --md-and-larger1 --screen and (width >= 570px); @custom-media --md-and-larger2 (--screen) and (width >= 570px); @custom-media --md-and-larger3 only screen and (width >= 570px); -@custom-media --md-larger4 (width >=570px); +@custom-media --md-larger4 (width >= 570px); @custom-media --md-smaller4 (width < 1000px); @media (--md-and-larger1) { diff --git a/plugins/postcss-custom-media/test/complex.expect.css b/plugins/postcss-custom-media/test/complex.expect.css index 486d7f515..792e80338 100644 --- a/plugins/postcss-custom-media/test/complex.expect.css +++ b/plugins/postcss-custom-media/test/complex.expect.css @@ -10,24 +10,45 @@ } } -@media (min-width: 3) and (width > 1024px),(min-width: 4) and (width > 1024px) { +@media (min-width: 3),(min-width: 4) { + +@media (max-color:2147477350) and (width > 1024px) { .a { order: 3; } } +} + +@media not all and (min-width: 3),not all and (min-width: 4) { + +@media (color:2147477350) and (width > 1024px) { + .a { order: 3; } +} +} /* #region https://github.com/csstools/postcss-custom-media/issues/51 */ -/* TODO: This is broken at the moment */ -@media only screen(width >= 570px) { +@media --screen and (width >= 570px) { body { background-color: red; } } -@media only screen(width >= 570px) { +@media only screen { + +@media (max-color:2147477350) and (width >= 570px) { + body { + background-color: yellow; + } +} +} + +@media not screen { + +@media (color:2147477350) and (width >= 570px) { body { background-color: yellow; } } +} @media only screen and (width >= 570px) { body { @@ -35,15 +56,87 @@ } } -@media only screen(width >=570px) { +@media only screen { + +@media (width >= 570px) { + +@media (max-color:2147477350) and (max-color:2147477350) { + body { + background-color: green; + } +} +} + +@media not all and (width >= 570px) { + +@media (max-color:2147477350) and (color:2147477350) { + body { + background-color: green; + } +} +} +} + +@media not screen { + +@media (width >= 570px) { + +@media (color:2147477350) and (max-color:2147477350) { + body { + background-color: green; + } +} +} + +@media not all and (width >= 570px) { + +@media (color:2147477350) and (color:2147477350) { + body { + background-color: green; + } +} +} +} + +@media (width >= 570px) { + +@media (width < 1000px) { + +@media (max-color:2147477350) and (max-color:2147477350) { body { background-color: green; } } +} -@media (width >=570px) and (width < 1000px) { +@media not all and (width < 1000px) { + +@media (max-color:2147477350) and (color:2147477350) { body { background-color: green; } } +} +} + +@media not all and (width >= 570px) { + +@media (width < 1000px) { + +@media (color:2147477350) and (max-color:2147477350) { + body { + background-color: green; + } +} +} + +@media not all and (width < 1000px) { + +@media (color:2147477350) and (color:2147477350) { + body { + background-color: green; + } +} +} +} /* #endregion https://github.com/csstools/postcss-custom-media/issues/51 */ diff --git a/plugins/postcss-custom-media/test/cyclic.css b/plugins/postcss-custom-media/test/cyclic.css new file mode 100644 index 000000000..c22b3a4e9 --- /dev/null +++ b/plugins/postcss-custom-media/test/cyclic.css @@ -0,0 +1,29 @@ +/* Simple cyclic */ +@custom-media --mq-a (--mq-b); +@custom-media --mq-b (--mq-a); + +@media (--mq-a) { + .a { + color: red; + } +} + +/* Indirect cyclic */ +@custom-media --mq-x (--mq-z); +@custom-media --mq-y (--mq-x); +@custom-media --mq-z (--mq-y); + +@media (--mq-z) { + .b { + color: red; + } +} + +/* Self referencing */ +@custom-media --mq-self (--mq-self); + +@media (--mq-self) { + .c { + color: red; + } +} diff --git a/plugins/postcss-custom-media/test/cyclic.expect.css b/plugins/postcss-custom-media/test/cyclic.expect.css new file mode 100644 index 000000000..6674fd4a8 --- /dev/null +++ b/plugins/postcss-custom-media/test/cyclic.expect.css @@ -0,0 +1,23 @@ +/* Simple cyclic */ + +@media (--mq-b) { + .a { + color: red; + } +} + +/* Indirect cyclic */ + +@media (--mq-z) { + .b { + color: red; + } +} + +/* Self referencing */ + +@media (--mq-self) { + .c { + color: red; + } +} diff --git a/plugins/postcss-custom-media/test/eof-1.css b/plugins/postcss-custom-media/test/eof-1.css new file mode 100644 index 000000000..424bd22f0 --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-1.css @@ -0,0 +1,4 @@ +@custom-media --one screen; + +@media (--one + diff --git a/plugins/postcss-custom-media/test/eof-1.expect.css b/plugins/postcss-custom-media/test/eof-1.expect.css new file mode 100644 index 000000000..f3ea0731e --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-1.expect.css @@ -0,0 +1,2 @@ +@media (--one + diff --git a/plugins/postcss-custom-media/test/eof-2.css b/plugins/postcss-custom-media/test/eof-2.css new file mode 100644 index 000000000..50cb010f7 --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-2.css @@ -0,0 +1,3 @@ +@custom-media --one screen; + +@media (calc(--one diff --git a/plugins/postcss-custom-media/test/eof-2.expect.css b/plugins/postcss-custom-media/test/eof-2.expect.css new file mode 100644 index 000000000..460b84e8e --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-2.expect.css @@ -0,0 +1 @@ +@media (calc(--one diff --git a/plugins/postcss-custom-media/test/eof-3.css b/plugins/postcss-custom-media/test/eof-3.css new file mode 100644 index 000000000..849c7fecc --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-3.css @@ -0,0 +1,3 @@ +@custom-media --one screen; + +@media ([--one diff --git a/plugins/postcss-custom-media/test/eof-3.expect.css b/plugins/postcss-custom-media/test/eof-3.expect.css new file mode 100644 index 000000000..f907f2f71 --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-3.expect.css @@ -0,0 +1 @@ +@media ([--one diff --git a/plugins/postcss-custom-media/test/eof-4.css b/plugins/postcss-custom-media/test/eof-4.css new file mode 100644 index 000000000..4ddee3de8 --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-4.css @@ -0,0 +1,3 @@ +@custom-media --one screen; + +@media ({--one diff --git a/plugins/postcss-custom-media/test/eof-4.expect.css b/plugins/postcss-custom-media/test/eof-4.expect.css new file mode 100644 index 000000000..20ae091c4 --- /dev/null +++ b/plugins/postcss-custom-media/test/eof-4.expect.css @@ -0,0 +1 @@ +@media ({--one diff --git a/plugins/postcss-custom-media/test/export-media.css b/plugins/postcss-custom-media/test/export-media.css deleted file mode 100644 index f51e88c82..000000000 --- a/plugins/postcss-custom-media/test/export-media.css +++ /dev/null @@ -1,8 +0,0 @@ -@custom-media --mq-a (max-width: 30em), (max-height: 30em); -@custom-media --mq-b screen and (max-width: 30em); -@custom-media --not-mq-a not all and (--mq-a); -@custom-media --circular-mq-a (--circular-mq-b); -@custom-media --circular-mq-b (--circular-mq-a); -@custom-media --min (min-width: 320px); -@custom-media --max (max-width: 640px); -@custom-media --concat (min-width: 320px) and (max-width: 640px); diff --git a/plugins/postcss-custom-media/test/export-media.js b/plugins/postcss-custom-media/test/export-media.js deleted file mode 100644 index acccd8408..000000000 --- a/plugins/postcss-custom-media/test/export-media.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - customMedia: { - '--mq-a': '(max-width: 30em), (max-height: 30em)', - '--mq-b': 'screen and (max-width: 30em)', - '--not-mq-a': 'not all and (--mq-a)', - '--circular-mq-a': '(--circular-mq-b)', - '--circular-mq-b': '(--circular-mq-a)', - '--min': '(min-width: 320px)', - '--max': '(max-width: 640px)', - '--concat': '(min-width: 320px) and (max-width: 640px)' - } -}; diff --git a/plugins/postcss-custom-media/test/export-media.json b/plugins/postcss-custom-media/test/export-media.json deleted file mode 100644 index 729bde28e..000000000 --- a/plugins/postcss-custom-media/test/export-media.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "custom-media": { - "--mq-a": "(max-width: 30em), (max-height: 30em)", - "--mq-b": "screen and (max-width: 30em)", - "--not-mq-a": "not all and (--mq-a)", - "--circular-mq-a": "(--circular-mq-b)", - "--circular-mq-b": "(--circular-mq-a)", - "--min": "(min-width: 320px)", - "--max": "(max-width: 640px)", - "--concat": "(min-width: 320px) and (max-width: 640px)" - } -} diff --git a/plugins/postcss-custom-media/test/export-media.mjs b/plugins/postcss-custom-media/test/export-media.mjs deleted file mode 100644 index ea36519d8..000000000 --- a/plugins/postcss-custom-media/test/export-media.mjs +++ /dev/null @@ -1,10 +0,0 @@ -export const customMedia = { - '--mq-a': '(max-width: 30em), (max-height: 30em)', - '--mq-b': 'screen and (max-width: 30em)', - '--not-mq-a': 'not all and (--mq-a)', - '--circular-mq-a': '(--circular-mq-b)', - '--circular-mq-b': '(--circular-mq-a)', - '--min': '(min-width: 320px)', - '--max': '(max-width: 640px)', - '--concat': '(min-width: 320px) and (max-width: 640px)' -}; diff --git a/plugins/postcss-custom-media/test/import-css.css b/plugins/postcss-custom-media/test/import-css.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/postcss-custom-media/test/import-media.css b/plugins/postcss-custom-media/test/import-media.css deleted file mode 100644 index e788f32ae..000000000 --- a/plugins/postcss-custom-media/test/import-media.css +++ /dev/null @@ -1,2 +0,0 @@ -@custom-media --mq-a (max-width: 30em), (max-height: 30em); -@custom-media --not-mq-a not all and (--mq-a); diff --git a/plugins/postcss-custom-media/test/import-media.js b/plugins/postcss-custom-media/test/import-media.js deleted file mode 100644 index 3f2e0401a..000000000 --- a/plugins/postcss-custom-media/test/import-media.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - customMedia: { - '--mq-a': '(max-width: 30em), (max-height: 30em)', - '--not-mq-a': 'not all and (--mq-a)' - } -} diff --git a/plugins/postcss-custom-media/test/import-media.json b/plugins/postcss-custom-media/test/import-media.json deleted file mode 100644 index 807d8dfdd..000000000 --- a/plugins/postcss-custom-media/test/import-media.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "customMedia": { - "--mq-a": "(max-width: 30em), (max-height: 30em)", - "--not-mq-a": "not all and (--mq-a)" - } -} diff --git a/plugins/postcss-custom-media/test/import.css b/plugins/postcss-custom-media/test/import.css deleted file mode 100644 index f37220e74..000000000 --- a/plugins/postcss-custom-media/test/import.css +++ /dev/null @@ -1,29 +0,0 @@ -@media (--mq-a) { - body { - order: 1; - } -} - -@media (--mq-a), (--mq-a) { - body { - order: 1; - } -} - -@media not all and (--mq-a) { - body { - order: 2; - } -} - -@media (--not-mq-a) { - body { - order: 1; - } -} - -@media not all and (--not-mq-a) { - body { - order: 2; - } -} diff --git a/plugins/postcss-custom-media/test/import.empty.expect.css b/plugins/postcss-custom-media/test/import.empty.expect.css deleted file mode 100644 index f37220e74..000000000 --- a/plugins/postcss-custom-media/test/import.empty.expect.css +++ /dev/null @@ -1,29 +0,0 @@ -@media (--mq-a) { - body { - order: 1; - } -} - -@media (--mq-a), (--mq-a) { - body { - order: 1; - } -} - -@media not all and (--mq-a) { - body { - order: 2; - } -} - -@media (--not-mq-a) { - body { - order: 1; - } -} - -@media not all and (--not-mq-a) { - body { - order: 2; - } -} diff --git a/plugins/postcss-custom-media/test/import.expect.css b/plugins/postcss-custom-media/test/import.expect.css deleted file mode 100644 index 0bc2bbf3b..000000000 --- a/plugins/postcss-custom-media/test/import.expect.css +++ /dev/null @@ -1,29 +0,0 @@ -@media (max-width: 30em),(max-height: 30em) { - body { - order: 1; - } -} - -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { - body { - order: 1; - } -} - -@media not all and (max-width: 30em),not all and (max-height: 30em) { - body { - order: 2; - } -} - -@media not all and (max-width: 30em),not all and (max-height: 30em) { - body { - order: 1; - } -} - -@media all and (max-width: 30em),all and (max-height: 30em) { - body { - order: 2; - } -} diff --git a/plugins/postcss-custom-media/test/import.plugin.expect.css b/plugins/postcss-custom-media/test/import.plugin.expect.css deleted file mode 100644 index 0bc2bbf3b..000000000 --- a/plugins/postcss-custom-media/test/import.plugin.expect.css +++ /dev/null @@ -1,29 +0,0 @@ -@media (max-width: 30em),(max-height: 30em) { - body { - order: 1; - } -} - -@media (max-width: 30em),(max-height: 30em), (max-width: 30em), (max-height: 30em) { - body { - order: 1; - } -} - -@media not all and (max-width: 30em),not all and (max-height: 30em) { - body { - order: 2; - } -} - -@media not all and (max-width: 30em),not all and (max-height: 30em) { - body { - order: 1; - } -} - -@media all and (max-width: 30em),all and (max-height: 30em) { - body { - order: 2; - } -} diff --git a/plugins/postcss-custom-media/test/list.css b/plugins/postcss-custom-media/test/list.css new file mode 100644 index 000000000..cd0b8e893 --- /dev/null +++ b/plugins/postcss-custom-media/test/list.css @@ -0,0 +1,30 @@ +/* Custom media with a list */ +@custom-media --mq-with-list-a (min-width: 101px), (min-height: 102px); + +@media screen and (--mq-with-list-a) { + .a { + order: 1; + } +} + +@media ((other: feature) and (--mq-with-list-a)) { + .a { + order: 2; + } +} + +/* Custom media used in a list */ +@custom-media --mq-without-a-list-a (min-width: 201px); +@custom-media --mq-without-a-list-b (min-width: 202px); + +@media (--mq-without-a-list-a), (--mq-without-a-list-b) { + .b { + order: 3; + } +} + +@media screen and (--mq-without-a-list-a), not (--mq-without-a-list-b) { + .b { + order: 4; + } +} diff --git a/plugins/postcss-custom-media/test/list.expect.css b/plugins/postcss-custom-media/test/list.expect.css new file mode 100644 index 000000000..3bf1138d3 --- /dev/null +++ b/plugins/postcss-custom-media/test/list.expect.css @@ -0,0 +1,81 @@ +/* Custom media with a list */ + +@media (min-width: 101px),(min-height: 102px) { + +@media screen and (max-color:2147477350) { + .a { + order: 1; + } +} +} + +@media not all and (min-width: 101px),not all and (min-height: 102px) { + +@media screen and (color:2147477350) { + .a { + order: 1; + } +} +} + +@media (min-width: 101px),(min-height: 102px) { + +@media ((other: feature) and (max-color:2147477350)) { + .a { + order: 2; + } +} +} + +@media not all and (min-width: 101px),not all and (min-height: 102px) { + +@media ((other: feature) and (color:2147477350)) { + .a { + order: 2; + } +} +} + +/* Custom media used in a list */ + +@media (min-width: 201px),(min-width: 202px) { + .b { + order: 3; + } +} + +@media (min-width: 201px) { + +@media screen and (max-color:2147477350) { + .b { + order: 4; + } +} +} + +@media not all and (min-width: 201px) { + +@media screen and (color:2147477350) { + .b { + order: 4; + } +} +} + +@media (min-width: 202px) { + +@media not (max-color:2147477350) { + .b { + order: 4; + } +} +} + +@media not all and (min-width: 202px) { + +@media not (color:2147477350) { + .b { + order: 4; + } +} +} diff --git a/plugins/postcss-custom-media/test/modifiers.css b/plugins/postcss-custom-media/test/modifiers.css new file mode 100644 index 000000000..c8216fad5 --- /dev/null +++ b/plugins/postcss-custom-media/test/modifiers.css @@ -0,0 +1,28 @@ +/* Media queries with modifiers (not|only) */ +@custom-media --mq-not-screen not screen; + +@media screen and (--mq-not-screen) { + .a { + order: 1; + } +} + +@media only print and (not (--mq-not-screen)) { + .a { + order: 2; + } +} + +@custom-media --mq-only-screen-min-width only screen and (min-width: 200px); + +@media print and (--mq-only-screen-min-width) { + .a { + order: 3; + } +} + +@media (--mq-only-screen-min-width), (not (--mq-only-screen-min-width)) { + .a { + order: 4; + } +} diff --git a/plugins/postcss-custom-media/test/modifiers.expect.css b/plugins/postcss-custom-media/test/modifiers.expect.css new file mode 100644 index 000000000..1b2574995 --- /dev/null +++ b/plugins/postcss-custom-media/test/modifiers.expect.css @@ -0,0 +1,79 @@ +/* Media queries with modifiers (not|only) */ + +@media not screen { + +@media screen and (max-color:2147477350) { + .a { + order: 1; + } +} +} + +@media screen { + +@media screen and (color:2147477350) { + .a { + order: 1; + } +} +} + +@media not screen { + +@media only print and (not (max-color:2147477350)) { + .a { + order: 2; + } +} +} + +@media screen { + +@media only print and (not (color:2147477350)) { + .a { + order: 2; + } +} +} + +@media only screen and (min-width: 200px) { + +@media print and (max-color:2147477350) { + .a { + order: 3; + } +} +} + +@media not screen and (min-width: 200px) { + +@media print and (color:2147477350) { + .a { + order: 3; + } +} +} + +@media only screen and (min-width: 200px) { + .a { + order: 4; + } +} + +@media only screen and (min-width: 200px) { + +@media (not (max-color:2147477350)) { + .a { + order: 4; + } +} +} + +@media not screen and (min-width: 200px) { + +@media (not (color:2147477350)) { + .a { + order: 4; + } +} +} diff --git a/plugins/postcss-custom-media/test/nesting.css b/plugins/postcss-custom-media/test/nesting.css new file mode 100644 index 000000000..4d48d760d --- /dev/null +++ b/plugins/postcss-custom-media/test/nesting.css @@ -0,0 +1,25 @@ +:root { + @custom-media --foo screen; +} + +.foo { + .bar { + @custom-media --foo print; + + @media screen { + @custom-media --foo not all; + } + } + + @media screen { + .bar { + @custom-media --foo not all; + } + } +} + +@media (--foo) { + .a { + order: 1; + } +} diff --git a/plugins/postcss-custom-media/test/nesting.expect.css b/plugins/postcss-custom-media/test/nesting.expect.css new file mode 100644 index 000000000..80d83ea0b --- /dev/null +++ b/plugins/postcss-custom-media/test/nesting.expect.css @@ -0,0 +1,20 @@ +.foo { + .bar { + + @media screen { + @custom-media --foo not all; + } + } + + @media screen { + .bar { + @custom-media --foo not all; + } + } +} + +@media print { + .a { + order: 1; + } +} diff --git a/plugins/postcss-custom-media/test/not-processable.css b/plugins/postcss-custom-media/test/not-processable.css new file mode 100644 index 000000000..9aa06e217 --- /dev/null +++ b/plugins/postcss-custom-media/test/not-processable.css @@ -0,0 +1,17 @@ +@media print { + @custom-media --foo screen; +} + +@custom-media --foo screen { + /* has children */ +} + +@custom-media --foo; +@custom-media screen; +@custom-media ; + +@media (--foo) { + .a { + order: 1; + } +} diff --git a/plugins/postcss-custom-media/test/not-processable.expect.css b/plugins/postcss-custom-media/test/not-processable.expect.css new file mode 100644 index 000000000..9aa06e217 --- /dev/null +++ b/plugins/postcss-custom-media/test/not-processable.expect.css @@ -0,0 +1,17 @@ +@media print { + @custom-media --foo screen; +} + +@custom-media --foo screen { + /* has children */ +} + +@custom-media --foo; +@custom-media screen; +@custom-media ; + +@media (--foo) { + .a { + order: 1; + } +} diff --git a/plugins/postcss-custom-media/test/not.css b/plugins/postcss-custom-media/test/not.css new file mode 100644 index 000000000..cb21c4a2e --- /dev/null +++ b/plugins/postcss-custom-media/test/not.css @@ -0,0 +1,17 @@ +/* Media queries with "and" */ +@custom-media --with-not not (min-height: 300px); + +@media not (--with-not) { + .a { + order: 1; + } +} + +@custom-media --not-screen not screen; +@custom-media --not-print not print; + +@media (--not-screen) and (not (--not-print)) { + .a { + order: 2; + } +} diff --git a/plugins/postcss-custom-media/test/not.expect.css b/plugins/postcss-custom-media/test/not.expect.css new file mode 100644 index 000000000..e1d749ba4 --- /dev/null +++ b/plugins/postcss-custom-media/test/not.expect.css @@ -0,0 +1,61 @@ +/* Media queries with "and" */ + +@media not (min-height: 300px) { + +@media not (max-color:2147477350) { + .a { + order: 1; + } +} +} + +@media (min-height: 300px) { + +@media not (color:2147477350) { + .a { + order: 1; + } +} +} + +@media not screen { + +@media not print { + +@media (max-color:2147477350) and (not (max-color:2147477350)) { + .a { + order: 2; + } +} +} + +@media print { + +@media (max-color:2147477350) and (not (color:2147477350)) { + .a { + order: 2; + } +} +} +} + +@media screen { + +@media not print { + +@media (color:2147477350) and (not (max-color:2147477350)) { + .a { + order: 2; + } +} +} + +@media print { + +@media (color:2147477350) and (not (color:2147477350)) { + .a { + order: 2; + } +} +} +} diff --git a/plugins/postcss-custom-media/test/or.css b/plugins/postcss-custom-media/test/or.css new file mode 100644 index 000000000..c428d9c3e --- /dev/null +++ b/plugins/postcss-custom-media/test/or.css @@ -0,0 +1,17 @@ +/* Media queries with "or" */ +@custom-media --with-or (min-width: 300px) or (min-height: 300px); + +@media not screen and (--with-or) { + .a { + order: 1; + } +} + +@custom-media --not-screen not screen; +@custom-media --not-print not print; + +@media (--not-screen) or (--not-print) { + .a { + order: 2; + } +} diff --git a/plugins/postcss-custom-media/test/or.expect.css b/plugins/postcss-custom-media/test/or.expect.css new file mode 100644 index 000000000..54b983ab5 --- /dev/null +++ b/plugins/postcss-custom-media/test/or.expect.css @@ -0,0 +1,61 @@ +/* Media queries with "or" */ + +@media (min-width: 300px) or (min-height: 300px) { + +@media not screen and (max-color:2147477350) { + .a { + order: 1; + } +} +} + +@media not all and ( (min-width: 300px) or (min-height: 300px)) { + +@media not screen and (color:2147477350) { + .a { + order: 1; + } +} +} + +@media not screen { + +@media not print { + +@media (max-color:2147477350) or (max-color:2147477350) { + .a { + order: 2; + } +} +} + +@media print { + +@media (max-color:2147477350) or (color:2147477350) { + .a { + order: 2; + } +} +} +} + +@media screen { + +@media not print { + +@media (color:2147477350) or (max-color:2147477350) { + .a { + order: 2; + } +} +} + +@media print { + +@media (color:2147477350) or (color:2147477350) { + .a { + order: 2; + } +} +} +} diff --git a/plugins/postcss-custom-media/test/override.css b/plugins/postcss-custom-media/test/override.css new file mode 100644 index 000000000..7b7fd09bc --- /dev/null +++ b/plugins/postcss-custom-media/test/override.css @@ -0,0 +1,30 @@ +@custom-media --mq-a (min-width: 101px); + +@scope (.a-scope) { + @custom-media --mq-a (min-width: 102px); +} + +@media (--mq-a) { + .a { + order: 1; + } +} + +@custom-media --mq-b (min-width: 201px); + +@media (--mq-b) { + @custom-media --mq-b (min-width: 202px); + + .b { + order: 2; + } +} + +@custom-media --mq-c (min-width: 301px); +@custom-media --mq-c (min-width: 302px); + +@media (--mq-c) { + .c { + order: 3; + } +} diff --git a/plugins/postcss-custom-media/test/override.expect.css b/plugins/postcss-custom-media/test/override.expect.css new file mode 100644 index 000000000..30f2040bd --- /dev/null +++ b/plugins/postcss-custom-media/test/override.expect.css @@ -0,0 +1,19 @@ +@media (min-width: 102px) { + .a { + order: 1; + } +} + +@media (min-width: 201px) { + @custom-media --mq-b (min-width: 202px); + + .b { + order: 2; + } +} + +@media (min-width: 302px) { + .c { + order: 3; + } +} diff --git a/plugins/postcss-custom-media/test/parser-checks.css b/plugins/postcss-custom-media/test/parser-checks.css new file mode 100644 index 000000000..5c5b625a5 --- /dev/null +++ b/plugins/postcss-custom-media/test/parser-checks.css @@ -0,0 +1,58 @@ +@custom-media/*a comment */--one/*a comment */screen; + +@media (--one) { + .one { + order: 1; + } +} + +@custom-media --two only screen; + +@media/*a comment */((/*a comment */--two/*a comment */))/*a comment */{ + .two { + order: 2; + } +} + +@custom-media --๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ (min-width: 300px); + +@media (--๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ) { + .three { + order: 3; + } +} + +/* Double space is needed */ +@custom-media --\66 (min-width: 300px); + +@media (--f) { + .four { + order: 4; + } +} + +@custom-media --five (min-width: 300px); + +@media (width: calc(--five * 5)) { + .five { + order: 5.0; + } +} + +@media calc(--five) { + .five { + order: 5.1; + } +} + +@media (width: calc((--five) * 5)) { + .five { + order: 5.2; + } +} + +@media (width: calc(env(--five) * 5)) { + .five { + order: 5.3; + } +} diff --git a/plugins/postcss-custom-media/test/parser-checks.expect.css b/plugins/postcss-custom-media/test/parser-checks.expect.css new file mode 100644 index 000000000..40af5f4d2 --- /dev/null +++ b/plugins/postcss-custom-media/test/parser-checks.expect.css @@ -0,0 +1,49 @@ +@media /*a comment */screen { + .one { + order: 1; + } +} + +@media/*a comment */only screen/*a comment */{ + .two { + order: 2; + } +} + +@media (min-width: 300px) { + .three { + order: 3; + } +} + +/* Double space is needed */ + +@media (min-width: 300px) { + .four { + order: 4; + } +} + +@media (width: calc(--five * 5)) { + .five { + order: 5.0; + } +} + +@media calc(--five) { + .five { + order: 5.1; + } +} + +@media (width: calc((--five) * 5)) { + .five { + order: 5.2; + } +} + +@media (width: calc(env(--five) * 5)) { + .five { + order: 5.3; + } +} diff --git a/plugins/postcss-custom-media/test/true-false.css b/plugins/postcss-custom-media/test/true-false.css new file mode 100644 index 000000000..3c717c050 --- /dev/null +++ b/plugins/postcss-custom-media/test/true-false.css @@ -0,0 +1,44 @@ +@custom-media --truthy tRUe; +@custom-media --falsy fAlsE; + +@media screen and (--truthy) { + .true { + order: 1; + } +} + +@media screen and (--falsy) { + .false { + order: 2; + } +} + +@custom-media --truthy-trailing-ws tRUe ; +@custom-media --falsy-trailing-comment fAlsE/* a comment */ /* another comment */; + +@media (--truthy-trailing-ws) { + .true { + order: 3; + } +} + +@media (--falsy-trailing-comment) { + .false { + order: 4; + } +} + +@custom-media --truthy-broken tRUe and (min-width: 300px); +@custom-media --falsy-broken fAlsE and (min-width: 300px); + +@media (--truthy-broken) { + .true { + order: 5; + } +} + +@media (--falsy-broken) { + .false { + order: 6; + } +} diff --git a/plugins/postcss-custom-media/test/true-false.expect.css b/plugins/postcss-custom-media/test/true-false.expect.css new file mode 100644 index 000000000..fdde69e1b --- /dev/null +++ b/plugins/postcss-custom-media/test/true-false.expect.css @@ -0,0 +1,53 @@ +@media (max-color:2147477350) {@media screen and (max-color:2147477350) { + .true { + order: 1; + } +} +}@media not all and (max-color:2147477350) {@media screen and (color:2147477350) { + .true { + order: 1; + } +} +} + +@media (color:2147477350) { + +@media screen and (max-color:2147477350) { + .false { + order: 2; + } +} +} + +@media not all and (color:2147477350) { + +@media screen and (color:2147477350) { + .false { + order: 2; + } +} +} + +@media (max-color:2147477350) { + .true { + order: 3; + } +} + +@media (color:2147477350) { + .false { + order: 4; + } +} + +@media tRUe and (min-width: 300px) { + .true { + order: 5; + } +} + +@media fAlsE and (min-width: 300px) { + .false { + order: 6; + } +} diff --git a/plugins/postcss-custom-media/tsconfig.json b/plugins/postcss-custom-media/tsconfig.json new file mode 100644 index 000000000..2e428a8c2 --- /dev/null +++ b/plugins/postcss-custom-media/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "module": "es2020" + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} diff --git a/rollup/configs/externals.js b/rollup/configs/externals.js index 44805e2c2..4c78033bb 100644 --- a/rollup/configs/externals.js +++ b/rollup/configs/externals.js @@ -4,6 +4,9 @@ export const externalsForCLI = [ 'url', 'vm', + '@csstools/css-parser-algorithms', + '@csstools/css-tokenizer', + '@csstools/media-query-list-parser', '@csstools/postcss-cascade-layers', '@csstools/postcss-color-function', '@csstools/postcss-font-format-keywords', @@ -70,6 +73,9 @@ export const externalsForPlugin = [ /^postcss\/lib\/*/, 'postcss-html', + '@csstools/css-parser-algorithms', + '@csstools/css-tokenizer', + '@csstools/media-query-list-parser', '@csstools/postcss-cascade-layers', '@csstools/postcss-color-function', '@csstools/postcss-font-format-keywords', diff --git a/rollup/presets/package-typescript.js b/rollup/presets/package-typescript.js index d765bf703..6242f32c5 100644 --- a/rollup/presets/package-typescript.js +++ b/rollup/presets/package-typescript.js @@ -21,7 +21,9 @@ export function packageTypescript() { extensions: ['.js', '.ts'], presets: packageBabelPreset, }), - terser(), + terser({ + keep_classnames: true, + }), ], }, ];