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 702f57bf..659ca493 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 @@ -1,6 +1,5 @@ import { parseValueSyntax } from '@tokey/css-value-parser'; import { expect } from 'chai'; -//TODO: fixme import { bar, dataType, @@ -15,11 +14,6 @@ import { import specs from '@webref/css/css.json' with { type: 'json' }; describe(`sanity`, () => { - const knownProblemticValuespacesCases = [ - `custom-selector: ? : [ ( +#? ) ]?`, - `if-condition: ]> | else`, - `cursor-image: [ | ] {2}?`, - ]; for (const [specName, data] of Object.entries(specs)) { describe(specName, () => { for (const { name, syntax } of data) { @@ -27,9 +21,6 @@ describe(`sanity`, () => { continue; } const title = `${name}: ${syntax}`; - if (knownProblemticValuespacesCases.includes(title)) { - continue; - } it(title, () => { parseValueSyntax(syntax); }); @@ -62,6 +53,12 @@ describe('value-syntax-parser', () => { property('name', [-Infinity, Infinity]), ); }); + + it('should parse data-type with type constraint', () => { + expect(parseValueSyntax(` ]>`)).to.eql( + dataType('boolean-expr[ ]'), + ); + }); }); describe('literals/keyword', () => { @@ -166,6 +163,14 @@ describe('value-syntax-parser', () => { expect(parseValueSyntax(`[a]{2}`)).to.eql(group([keyword('a')], { range: [2, 2] })); expect(parseValueSyntax(`[a]{2, 4}`)).to.eql(group([keyword('a')], { range: [2, 4] })); }); + it('optional modifier after range multiplier', () => { + expect(parseValueSyntax(`{2}?`)).to.eql( + dataType('name', undefined, { range: [0, 2] }), + ); + expect(parseValueSyntax(`+#?`)).to.eql( + dataType('name', undefined, { range: [0, Infinity], list: true }), + ); + }); }); describe('combinators', () => { diff --git a/packages/css-value-parser/src/value-syntax-parser.ts b/packages/css-value-parser/src/value-syntax-parser.ts index a91f632c..24b536e7 100644 --- a/packages/css-value-parser/src/value-syntax-parser.ts +++ b/packages/css-value-parser/src/value-syntax-parser.ts @@ -194,7 +194,7 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { throw new Error('missing data type name'); } s.back(); - const name = getText(nameBlock, undefined, undefined, source); + let name = getText(nameBlock, undefined, undefined, source); const type = getLiteralValueType(name); let range: Range | undefined; @@ -205,18 +205,49 @@ 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)]; + // Check if content after [ is a numeric range or a type constraint + let peekOffset = 1; + while (s.peek(peekOffset).type === 'space') peekOffset++; + const firstContentToken = s.peek(peekOffset); + + if (firstContentToken.type === 'text') { + // Numeric range: [min, max] + 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 closeAngle = s.eat('space').take('>'); + if (closeAngle) { + closed = true; + } } else { - throw new Error('Invalid range'); - } - const t = s.eat('space').take('>'); - if (t) { - closed = true; + // Type constraint like [ ] + let depth = 1; + const bracketStart = t.start; + let bracketEnd = t.end; + while (depth > 0) { + const tok = s.next(); + if (!tok.type) { + throw new Error('missing "]"'); + } + if (tok.type === '[') depth++; + if (tok.type === ']') { + depth--; + if (depth === 0) { + bracketEnd = tok.end; + } + } + } + name = `${name}${source.substring(bracketStart, bracketEnd)}`; + const closeAngle = s.eat('space').take('>'); + if (closeAngle) { + closed = true; + } } } } @@ -280,9 +311,15 @@ function parseTokens(tokens: ValueSyntaxToken[], source: string) { } node.multipliers ??= {}; if (node.multipliers.range) { - throw new Error('multiple multipliers on same node'); + if (token.type === '?') { + // ? after existing range modifier makes it optional (min becomes 0) + node.multipliers.range = [0, node.multipliers.range[1]]; + } else { + throw new Error('multiple multipliers on same node'); + } + } else { + node.multipliers.range = typeToRange(token.type); } - node.multipliers.range = typeToRange(token.type); } else if (token.type === '{') { if (s.peekBack().type === 'space') { ast.push(literal(token.value, false));