From f2d3c17bb467bf76ab63d86bc83a12c36f9bde9c Mon Sep 17 00:00:00 2001 From: Roman Dvornov Date: Mon, 25 Sep 2023 04:03:48 +0200 Subject: [PATCH] Restore missed changes for Ratio --- CHANGELOG.md | 1 + fixtures/ast/atrule/atrule/media.json | 10 +- fixtures/ast/mediaQuery/FeatureRange.json | 229 ++++++++++++++- fixtures/ast/mediaQuery/MediaQuery.json | 20 +- fixtures/ast/mediaQuery/Ratio.json | 337 ++++++++++++++++++++-- fixtures/stringify.ast | 36 ++- lib/syntax/node/Feature.js | 20 +- lib/syntax/node/FeatureRange.js | 18 +- lib/syntax/node/Ratio.js | 49 ++-- 9 files changed, 651 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3962e654..126f800d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added `TokenStream#lookupTypeNonSC()` method - Added `` to generic types - Changed `Ratio` parsing: + - Left and right parts contain nodes instead of strings - Both left and right parts of a ratio can now be any number; validation of number range is no longer within the parser's scope. - Both parts can now be functions. Although not explicitly mentioned in the specification, mathematical functions can replace numbers, addressing potential use cases (#162). - As per the [CSS Values and Units Level 4](https://drafts.csswg.org/css-values-4/#ratios) specification, the right part of `Ratio` can be omitted. While this can't be a parser output (which would produce a `Number` node), it's feasible during `Ratio` node construction or transformation. diff --git a/fixtures/ast/atrule/atrule/media.json b/fixtures/ast/atrule/atrule/media.json index 54212dcc..d81896cc 100644 --- a/fixtures/ast/atrule/atrule/media.json +++ b/fixtures/ast/atrule/atrule/media.json @@ -158,8 +158,14 @@ "name": "foo", "value": { "type": "Ratio", - "left": "1", - "right": "2" + "left": { + "type": "Number", + "value": "1" + }, + "right": { + "type": "Number", + "value": "2" + } } }, { diff --git a/fixtures/ast/mediaQuery/FeatureRange.json b/fixtures/ast/mediaQuery/FeatureRange.json index 6b699130..a0f8040d 100644 --- a/fixtures/ast/mediaQuery/FeatureRange.json +++ b/fixtures/ast/mediaQuery/FeatureRange.json @@ -58,8 +58,14 @@ "kind": "media", "left": { "type": "Ratio", - "left": "1", - "right": "2" + "left": { + "type": "Number", + "value": "1" + }, + "right": { + "type": "Number", + "value": "2" + } }, "leftComparison": "<", "middle": { @@ -97,6 +103,44 @@ ] } }, + "function first": { + "source": "(calc(1 + 2) 123 123)", diff --git a/fixtures/ast/mediaQuery/MediaQuery.json b/fixtures/ast/mediaQuery/MediaQuery.json index 959e2930..cfb84f44 100644 --- a/fixtures/ast/mediaQuery/MediaQuery.json +++ b/fixtures/ast/mediaQuery/MediaQuery.json @@ -105,8 +105,14 @@ "name": "foo", "value": { "type": "Ratio", - "left": "3", - "right": "4" + "left": { + "type": "Number", + "value": "3" + }, + "right": { + "type": "Number", + "value": "4" + } } } ] @@ -182,8 +188,14 @@ "name": "foo", "value": { "type": "Ratio", - "left": "3", - "right": "4" + "left": { + "type": "Number", + "value": "3" + }, + "right": { + "type": "Number", + "value": "4" + } } } ] diff --git a/fixtures/ast/mediaQuery/Ratio.json b/fixtures/ast/mediaQuery/Ratio.json index 824ac7fb..6a5a3490 100644 --- a/fixtures/ast/mediaQuery/Ratio.json +++ b/fixtures/ast/mediaQuery/Ratio.json @@ -11,8 +11,14 @@ "name": "foo", "value": { "type": "Ratio", - "left": "1", - "right": "2" + "left": { + "type": "Number", + "value": "1" + }, + "right": { + "type": "Number", + "value": "2" + } } } ] @@ -31,8 +37,14 @@ "name": "foo", "value": { "type": "Ratio", - "left": "2.5", - "right": "3" + "left": { + "type": "Number", + "value": "2.5" + }, + "right": { + "type": "Number", + "value": "3" + } } } ] @@ -50,8 +62,14 @@ "name": "foo", "value": { "type": "Ratio", - "left": "3", - "right": "1.6" + "left": { + "type": "Number", + "value": "3" + }, + "right": { + "type": "Number", + "value": "1.6" + } } } ] @@ -69,49 +87,310 @@ "name": "foo", "value": { "type": "Ratio", - "left": "2.5", - "right": "1.6" + "left": { + "type": "Number", + "value": "2.5" + }, + "right": { + "type": "Number", + "value": "1.6" + } } } ] } } ], - "error": [ + "out of range": [ { - "source": "(foo: 1/)", - "offset": " ^", - "error": "Number is expected" + "source": "(foo:0/1)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "0" + }, + "right": { + "type": "Number", + "value": "1" + } + } + } + ] + } }, { - "source": "(foo: 0/1)", - "offset": " ^", - "error": "Zero number is not allowed" + "source": "(foo:1e2/1)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "1e2" + }, + "right": { + "type": "Number", + "value": "1" + } + } + } + ] + } }, { - "source": "(foo: 1e2/1)", - "offset": " ^", - "error": "Unsigned number is expected" + "source": "(foo:-1/5)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "-1" + }, + "right": { + "type": "Number", + "value": "5" + } + } + } + ] + } }, { - "source": "(foo: -1/5)", - "offset": " ^", - "error": "Unsigned number is expected" + "source": "(foo:2/0)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "2" + }, + "right": { + "type": "Number", + "value": "0" + } + } + } + ] + } }, { - "source": "(foo: 2/0)", - "offset": " ^", - "error": "Zero number is not allowed" + "source": "(foo:1/1e2)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "1" + }, + "right": { + "type": "Number", + "value": "1e2" + } + } + } + ] + } + }, + { + "source": "(foo:1/-5)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "1" + }, + "right": { + "type": "Number", + "value": "-5" + } + } + } + ] + } + } + ], + "usingn with functions": [ + { + "source": "(foo:1/calc(2 + 3))", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Number", + "value": "1" + }, + "right": { + "type": "Function", + "name": "calc", + "children": [ + { + "type": "Number", + "value": "2" + }, + { + "type": "Operator", + "value": " + " + }, + { + "type": "Number", + "value": "3" + } + ] + } + } + } + ] + } }, { - "source": "(foo: 1/1e2)", - "offset": " ^", - "error": "Unsigned number is expected" + "source": "(foo:calc(2 + 3)/1)", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Function", + "name": "calc", + "children": [ + { + "type": "Number", + "value": "2" + }, + { + "type": "Operator", + "value": " + " + }, + { + "type": "Number", + "value": "3" + } + ] + }, + "right": { + "type": "Number", + "value": "1" + } + } + } + ] + } }, { - "source": "(foo: 1/-5)", + "source": "(foo:calc(2 + 3)/calc(2 + 3))", + "ast": { + "type": "Condition", + "kind": "media", + "children": [ + { + "type": "Feature", + "kind": "media", + "name": "foo", + "value": { + "type": "Ratio", + "left": { + "type": "Function", + "name": "calc", + "children": [ + { + "type": "Number", + "value": "2" + }, + { + "type": "Operator", + "value": " + " + }, + { + "type": "Number", + "value": "3" + } + ] + }, + "right": { + "type": "Function", + "name": "calc", + "children": [ + { + "type": "Number", + "value": "2" + }, + { + "type": "Operator", + "value": " + " + }, + { + "type": "Number", + "value": "3" + } + ] + } + } + } + ] + } + } + ], + "error": [ + { + "source": "(foo: 1/)", "offset": " ^", - "error": "Unsigned number is expected" + "error": "Number of function are expected" } ] } diff --git a/fixtures/stringify.ast b/fixtures/stringify.ast index aa0c7570..5f21abe1 100644 --- a/fixtures/stringify.ast +++ b/fixtures/stringify.ast @@ -2531,8 +2531,40 @@ "column": 34 } }, - "left": "16", - "right": "9" + "left": { + "type": "Number", + "loc": { + "source": "stringify.css", + "start": { + "offset": 786, + "line": 25, + "column": 30 + }, + "end": { + "offset": 788, + "line": 25, + "column": 32 + } + }, + "value": "16" + }, + "right": { + "type": "Number", + "loc": { + "source": "stringify.css", + "start": { + "offset": 789, + "line": 25, + "column": 33 + }, + "end": { + "offset": 790, + "line": 25, + "column": 34 + } + }, + "value": "9" + } } } ] diff --git a/lib/syntax/node/Feature.js b/lib/syntax/node/Feature.js index 79c87745..19dda66b 100644 --- a/lib/syntax/node/Feature.js +++ b/lib/syntax/node/Feature.js @@ -9,6 +9,8 @@ import { Delim } from '../../tokenizer/index.js'; +const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) + export const name = 'Feature'; export const structure = { kind: String, @@ -50,7 +52,23 @@ export function parse(kind) { break; case FunctionToken: - value = this.Function(this.readSequence, this.scope.Value); + value = this.parseWithFallback( + () => { + const res = this.Function(this.readSequence, this.scope.Value); + + this.skipSC(); + + if (this.isDelim(SOLIDUS)) { + this.error(); + } + + return res; + }, + (startIdx) => { + this.skip(startIdx - this.tokenIndex); + return this.Ratio(); + } + ); break; default: diff --git a/lib/syntax/node/FeatureRange.js b/lib/syntax/node/FeatureRange.js index 4eae217f..2da581dd 100644 --- a/lib/syntax/node/FeatureRange.js +++ b/lib/syntax/node/FeatureRange.js @@ -40,7 +40,23 @@ function readTerm() { return this.Identifier(); case FunctionToken: - return this.Function(this.readSequence, this.scope.Value); + return this.parseWithFallback( + () => { + const res = this.Function(this.readSequence, this.scope.Value); + + this.skipSC(); + + if (this.isDelim(SOLIDUS)) { + this.error(); + } + + return res; + }, + (startIdx) => { + this.skip(startIdx - this.tokenIndex); + return this.Ratio(); + } + ); default: this.error('Number, dimension, ratio or identifier is expected'); diff --git a/lib/syntax/node/Ratio.js b/lib/syntax/node/Ratio.js index 618bbe61..ae667b9c 100644 --- a/lib/syntax/node/Ratio.js +++ b/lib/syntax/node/Ratio.js @@ -1,7 +1,10 @@ -import { isDigit, Delim, Number as NumberToken } from '../../tokenizer/index.js'; +import { + Delim, + Number as NumberToken, + Function as FunctionToken +} from '../../tokenizer/index.js'; const SOLIDUS = 0x002F; // U+002F SOLIDUS (/) -const FULLSTOP = 0x002E; // U+002E FULL STOP (.) // Media Queries Level 3 defines terms of as a positive (not zero or negative) // integers (see https://drafts.csswg.org/mediaqueries-3/#values) @@ -13,40 +16,38 @@ const FULLSTOP = 0x002E; // U+002E FULL STOP (.) // any constrains on terms were removed. Parser also doesn't test numbers // in any way to make possible for linting and fixing them by the tools using CSSTree. // An additional syntax examination may be applied by a lexer. -function consumeNumber() { +function consumeTerm() { this.skipSC(); - const value = this.consume(NumberToken); + switch (this.tokenType) { + case NumberToken: + return this.Number(); - for (let i = 0; i < value.length; i++) { - const code = value.charCodeAt(i); - if (!isDigit(code) && code !== FULLSTOP) { - this.error('Unsigned number is expected', this.tokenStart - value.length + i); - } - } + case FunctionToken: + return this.Function(this.readSequence, this.scope.Value); - if (Number(value) === 0) { - this.error('Zero number is not allowed', this.tokenStart - value.length); + default: + this.error('Number of function are expected'); } - - return value; } export const name = 'Ratio'; export const structure = { - left: String, - right: String + left: ['Number', 'Function'], + right: ['Number', 'Function', null] }; // [ / ]? export function parse() { const start = this.tokenStart; - const left = consumeNumber.call(this); - let right; + const left = consumeTerm.call(this); + let right = null; this.skipSC(); - this.eatDelim(SOLIDUS); - right = consumeNumber.call(this); + if (this.isDelim(SOLIDUS)) { + this.eatDelim(SOLIDUS); + right = consumeTerm.call(this); + } return { type: 'Ratio', @@ -57,7 +58,11 @@ export function parse() { } export function generate(node) { - this.token(NumberToken, node.left); + this.node(node.left); this.token(Delim, '/'); - this.token(NumberToken, node.right); + if (node.right) { + this.node(node.right); + } else { + this.node(NumberToken, 1); + } }