diff --git a/package-lock.json b/package-lock.json index 71ade99..0f3f3b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", "@types/node": "22", - "@webref/css": "~6.16.1", + "@webref/css": "^6.18.0", "chai": "^4.3.10", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", @@ -592,13 +592,13 @@ } }, "node_modules/@webref/css": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/@webref/css/-/css-6.16.1.tgz", - "integrity": "sha512-35QpBnNHloWvNmHU4aHH36/40z7RhJf19dgeqr/O0jtbN0plg5xPmlE6MRxdSYItjb1ffh/AM4mA/hpud2E4Nw==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@webref/css/-/css-6.18.0.tgz", + "integrity": "sha512-2AFAkzJuNykYJox3EnDLQNTjJXgvMSMFV6Z+roxlWcn94Uo2torGkQuPw81u7q9qsVCpdazGBZU5FLqrvXr2nA==", "dev": true, "license": "MIT", "peerDependencies": { - "css-tree": "^2.3.1" + "css-tree": "^3.1.0" } }, "node_modules/acorn": { @@ -997,14 +997,14 @@ } }, "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "mdn-data": "2.0.30", + "mdn-data": "2.12.2", "source-map-js": "^1.0.1" }, "engines": { @@ -1828,9 +1828,9 @@ "license": "ISC" }, "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "dev": true, "license": "CC0-1.0", "peer": true diff --git a/package.json b/package.json index f8b5341..af4c274 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", "@types/node": "22", - "@webref/css": "~6.16.1", + "@webref/css": "^6.18.0", "chai": "^4.3.10", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", diff --git a/packages/css-value-parser/src/test/css-value-syntax.spec.ts b/packages/css-value-parser/src/test/css-value-syntax.spec.ts index bc1eeb4..aacffb6 100644 --- a/packages/css-value-parser/src/test/css-value-syntax.spec.ts +++ b/packages/css-value-parser/src/test/css-value-syntax.spec.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; //TODO: fixme import { bar, + booleanExpr, dataType, doubleAmpersand, doubleBar, @@ -249,4 +250,15 @@ describe('value-syntax-parser', () => { ); }); }); + + describe('boolean-expr', () => { + // https://drafts.csswg.org/css-values-5/#boolean + it('should parse with inner test nodes', () => { + const x = parseValueSyntax(`]>`); + expect(x).to.eql(booleanExpr([dataType('if-test')])); + }); + it('should fail for missing nodes', () => { + expect(() => parseValueSyntax(``)).to.throw('missing boolean expression'); + }); + }); }); diff --git a/packages/css-value-parser/src/value-syntax-parser.ts b/packages/css-value-parser/src/value-syntax-parser.ts index 853ddb4..5336afc 100644 --- a/packages/css-value-parser/src/value-syntax-parser.ts +++ b/packages/css-value-parser/src/value-syntax-parser.ts @@ -84,6 +84,11 @@ interface PropertyNode { multipliers?: Multipliers; } +interface BooleanExpr { + type: 'boolean-expr'; + test: ValueSyntaxAstNode[]; +} + interface LiteralNode { type: 'literal'; name: string; @@ -125,7 +130,7 @@ interface GroupNode extends CombinatorGroup { type Combinators = GroupNode | JuxtaposingNode | DoubleAmpersandNode | DoubleBarNode | BarNode; type Components = DataTypeNode | PropertyNode; -export type ValueSyntaxAstNode = Components | KeywordNode | LiteralNode | Combinators; +export type ValueSyntaxAstNode = Components | KeywordNode | LiteralNode | Combinators | BooleanExpr; export function literal(name: string, enclosed = false, multipliers?: Multipliers): LiteralNode { return { type: 'literal', name, enclosed, multipliers }; @@ -143,6 +148,10 @@ export function dataType(name: string, range?: Range, multipliers?: Multipliers) return { type: 'data-type', name, range, multipliers }; } +export function booleanExpr(test: ValueSyntaxAstNode[]): BooleanExpr { + return { type: 'boolean-expr', test }; +} + export function group(nodes: ValueSyntaxAstNode[], multipliers?: Multipliers): GroupNode { return { type: 'group', nodes, multipliers }; } @@ -198,6 +207,7 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { const type = getLiteralValueType(name); let range: Range | undefined; + let booleanTest: ValueSyntaxAstNode[] | undefined; if (type === 'invalid') { throw new Error('missing data type name'); } else { @@ -205,14 +215,20 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { if (t.type === '>') { closed = true; } else if (t.type === '[') { - const min = s.eat('space').take('text'); - const sep = s.eat('space').take(','); - const max = s.eat('space').take('text'); - const end = s.eat('space').take(']'); - if (min && sep && max && end) { - range = [parseNumber(min.value), parseNumber(max.value)]; + if (name === 'boolean-expr') { + // https://drafts.csswg.org/css-values-5/#boolean + const _test = s.run(handleToken, { ast: [] }, _source); + booleanTest = _test.ast; } else { - throw new Error('Invalid range'); + const min = s.eat('space').take('text'); + const sep = s.eat('space').take(','); + const max = s.eat('space').take('text'); + const end = s.eat('space').take(']'); + if (min && sep && max && end) { + range = [parseNumber(min.value), parseNumber(max.value)]; + } else { + throw new Error('Invalid range'); + } } const t = s.eat('space').take('>'); if (t) { @@ -223,7 +239,12 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { if (!closed) { throw new Error('missing ">"'); } - if (type === 'quoted') { + if (name === 'boolean-expr') { + if (!booleanTest) { + throw new Error('missing boolean expression'); + } + ast.push(booleanExpr(booleanTest)); + } else if (type === 'quoted') { ast.push(property(name.slice(1, -1), range)); } else { ast.push(dataType(name, range)); @@ -275,7 +296,12 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { s.eat('space'); } else if (isRangeMultiplier(token)) { const node = lastParsedNode(ast); - if (!node || node.type === 'juxtaposing' || isLowLevelGroup(node)) { + if ( + !node || + node.type === 'juxtaposing' || + node.type === 'boolean-expr' || + isLowLevelGroup(node) + ) { throw new Error('unexpected modifier'); } node.multipliers ??= {}; @@ -289,7 +315,12 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { } else { const node = lastParsedNode(ast); - if (!node || isLowLevelGroup(node) || node.type === 'juxtaposing') { + if ( + !node || + isLowLevelGroup(node) || + node.type === 'juxtaposing' || + node.type === 'boolean-expr' + ) { throw new Error('unexpected range modifier'); } @@ -327,7 +358,12 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { } } else if (token.type === '#') { const node = lastParsedNode(ast); - if (!node || node.type === 'juxtaposing' || isLowLevelGroup(node)) { + if ( + !node || + node.type === 'juxtaposing' || + isLowLevelGroup(node) || + node.type === 'boolean-expr' + ) { throw new Error('unexpected list modifier'); } node.multipliers ??= {};