Skip to content

Commit

Permalink
Add support for CSS range restrictions in CSS Grammar parser (#149)
Browse files Browse the repository at this point in the history
* Addition to the CSS Grammar in https://drafts.csswg.org/css-values-3/#numeric-ranges
See also w3c/csswg-drafts#2921 (comment)
* Add range to CSS Grammar Parsing output schema
* Test support for range restriction grammar parsing
  • Loading branch information
dontcallmedom authored and tidoust committed May 9, 2019
1 parent 54af7cf commit 0c20f69
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/lib/css-grammar-parse-tree.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"properties": {
"type": { "type": "string", "enum": ["primitive", "valuespace", "propertyref", "keyword" ] },
"name": { "type": "string"},
"optional": { "type": "boolean" }
"optional": { "type": "boolean" },
"range": { "type": "array", "items": {"type": "string"} }
},
"additionalProperties": false,
"required": ["type", "name"]
Expand Down
34 changes: 28 additions & 6 deletions src/lib/css-grammar-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ const applyMultiplier = (multiplier, modifiee) => {

const isMultiplier = s => typeof s === "string" && multipliersStarters.map(starter => s.startsWith(starter)).includes(true);

const primitiveMatch = (s, p) => s.match(new RegExp("<(" + p + ")( +\\\[[^\\\]]*\\\])?>"));

const parseBracketedRange = s => {
if (!s || !s.trim()) return undefined;
const range = s.trim().slice(1, s.length - 2).split(',').map(x => x.trim());
if (range.length != 2) throw new Error(`Unrecognized range descriptor ${s}`);
return range;
};

const parseTerminals = s => {
let m;
if ([...new Map(combinatorsMap).keys()].includes(s) || s === '[' || s.startsWith(']') || isMultiplier(s)) {
Expand All @@ -167,8 +176,10 @@ const parseTerminals = s => {
return {type: "string", content: m[1]};
} else if ((m = s.match(/^<\'([-_a-zA-Z][^\'>]*)\'>$/))) {
return {type: "propertyref", name: m[1]};
} else if ([...primitives.keys()].map(p => "<" + p + ">").includes(s)) {
return {type: "primitive", name: s.slice(1, s.length -1)};
} else if ((m = [...primitives.keys()].find(p => primitiveMatch(s, p)))) {
// TODO: parse bracketed range notation
const [, name, range] = primitiveMatch(s, m);
return Object.assign({type: "primitive", name}, range ? {range: parseBracketedRange(range)} : {});
} else if ((m = s.match(/^<[-_a-zA-Z]([^>]*)>$/))) {
return {type: "valuespace", name: s.slice(1, s.length -1)};
} else if ((m = s.match(/^[-_a-zA-Z][-_a-zA-Z0-9]*$/))) {
Expand All @@ -186,9 +197,13 @@ const tokenize = (value) => {
while(i < value.length) {
const c = value[i];
if (c.match(/\s/)) {
if (currentToken) tokens.push(currentToken);
currentToken = '';
state = 'new';
if (state === 'labracket') { // bracketed range notation
currentToken += c;
} else {
if (currentToken) tokens.push(currentToken);
currentToken = '';
state = 'new';
}
} else if (c === '<') {
if (delimiterStates.includes(state)) {
if (currentToken) tokens.push(currentToken);
Expand Down Expand Up @@ -235,6 +250,13 @@ const tokenize = (value) => {
state = 'new';
} else if (state === 'quote') {
currentToken += c;
} else if (state === 'labracket' && c === '[') {
// bracketed range notation
state = 'bracketedrange';
currentToken += c;
} else if (state === 'bracketedrange' && c === ']') {
currentToken += c;
state = 'labracket';
} else {
throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
}
Expand Down Expand Up @@ -265,7 +287,7 @@ const tokenize = (value) => {
tokens.push(c);
currentToken='';
state = 'new';
} else if (state === 'quote' || state === 'curlybracket') {
} else if (state === 'quote' || state === 'curlybracket' || state === 'bracketedrange') {
currentToken += c;
} else {
throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
Expand Down
4 changes: 2 additions & 2 deletions tests/css-grammar-parser/in
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<integer>
<integer [0,10]>
left | right | center | justify
<length> | <percentage>
<length [-∞,∞]> | <percentage>
<color> | invert
none | underline || overline || line-through || blink
[ <family-name> | <generic-family> ]#
Expand Down
6 changes: 4 additions & 2 deletions tests/css-grammar-parser/out.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[
{
"type": "primitive",
"name": "integer"
"name": "integer",
"range": ["0", "10"]
},
{
"oneOf": [
Expand All @@ -23,7 +24,8 @@
"oneOf": [
{
"type": "primitive",
"name": "length"
"name": "length",
"range": ["-∞", ""]
},
{
"type": "primitive",
Expand Down
12 changes: 7 additions & 5 deletions tests/css-grammar-parser/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ const propDefs = fs.readFileSync("tests/css-grammar-parser/in", "utf-8").split("
const propDefsOut = JSON.parse(fs.readFileSync("tests/css-grammar-parser/out.json", "utf-8"));

const results = propDefs.map(css.parsePropDefValue);
for(let i in results) {
describe(`Parse property definition ${propDefs[i]} as expected`, () => {
expect(results[i]).to.deep.equal(propDefsOut[i], `Parsing ${propDefs[i]} got ${JSON.stringify(results[i], null, 2)} instead of ${JSON.stringify(propDefsOut[i], null, 2)}`);
});
}
describe('Parser correctly parses grammar instances', () => {
for(let i in results) {
it(`parses property definition ${propDefs[i]} as expected`, () => {
expect(results[i]).to.deep.equal(propDefsOut[i], `Parsing ${propDefs[i]} got ${JSON.stringify(results[i], null, 2)} instead of ${JSON.stringify(propDefsOut[i], null, 2)}`);
});
}
});

0 comments on commit 0c20f69

Please sign in to comment.