reffy 6.2.0 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +158 -158
  3. package/index.js +11 -11
  4. package/package.json +53 -53
  5. package/reffy.js +248 -248
  6. package/src/browserlib/canonicalize-url.mjs +50 -50
  7. package/src/browserlib/create-outline.mjs +352 -352
  8. package/src/browserlib/extract-cssdfn.mjs +319 -319
  9. package/src/browserlib/extract-dfns.mjs +686 -686
  10. package/src/browserlib/extract-elements.mjs +205 -205
  11. package/src/browserlib/extract-headings.mjs +48 -48
  12. package/src/browserlib/extract-ids.mjs +28 -28
  13. package/src/browserlib/extract-links.mjs +28 -28
  14. package/src/browserlib/extract-references.mjs +203 -203
  15. package/src/browserlib/extract-webidl.mjs +134 -134
  16. package/src/browserlib/get-absolute-url.mjs +21 -21
  17. package/src/browserlib/get-generator.mjs +26 -26
  18. package/src/browserlib/get-lastmodified-date.mjs +13 -13
  19. package/src/browserlib/get-title.mjs +11 -11
  20. package/src/browserlib/informative-selector.mjs +16 -16
  21. package/src/browserlib/map-ids-to-headings.mjs +136 -136
  22. package/src/browserlib/reffy.json +53 -53
  23. package/src/cli/check-missing-dfns.js +609 -609
  24. package/src/cli/generate-idlnames.js +430 -430
  25. package/src/cli/generate-idlparsed.js +139 -139
  26. package/src/cli/merge-crawl-results.js +128 -128
  27. package/src/cli/parse-webidl.js +430 -430
  28. package/src/lib/css-grammar-parse-tree.schema.json +109 -109
  29. package/src/lib/css-grammar-parser.js +440 -440
  30. package/src/lib/fetch.js +55 -55
  31. package/src/lib/nock-server.js +119 -119
  32. package/src/lib/specs-crawler.js +605 -603
  33. package/src/lib/util.js +898 -898
  34. package/src/specs/missing-css-rules.json +197 -197
  35. package/src/specs/spec-equivalents.json +149 -149
  36. package/src/browserlib/extract-editors.mjs~ +0 -14
  37. package/src/browserlib/generate-es-dfn-report.sh~ +0 -4
  38. package/src/cli/csstree-grammar-check.js +0 -28
  39. package/src/cli/csstree-grammar-check.js~ +0 -10
  40. package/src/cli/csstree-grammar-parser.js +0 -11
  41. package/src/cli/csstree-grammar-parser.js~ +0 -1
  42. package/src/cli/extract-editors.js~ +0 -38
  43. package/src/cli/process-specs.js~ +0 -28
@@ -1,440 +1,440 @@
1
- // Based on https://drafts.csswg.org/css-values-4/#value-defs
2
-
3
-
4
- const primitives = new Map([
5
- ["ident", {}],
6
- ["ident-token", {}],
7
- ["declaration-value", {}],
8
- // the subset below is only used in selectors / MQ
9
- // probably ought to be removed
10
- ["number-token", {}],
11
- ["hash-token", {}],
12
- ["any-value", {}],
13
- ["string-token", {}],
14
- ["function-token", {}],
15
- ["dimension-token", {}],
16
- // ----
17
- ["zero", {url: ""}],
18
- ["custom-ident", {url: "https://drafts.csswg.org/css-values-4/#custom-idents"}],
19
- ["string", {url: "https://drafts.csswg.org/css-values-4/#strings"}],
20
- ["url", {url: "https://drafts.csswg.org/css-values-4/#urls"}],
21
- ["integer", {url: "https://drafts.csswg.org/css-values-4/#integers"}],
22
- ["number", {url: "https://drafts.csswg.org/css-values-4/#numbers"}],
23
- ["percentage", {url: "https://drafts.csswg.org/css-values-4/#percentages"}],
24
- ["number-percentage", {url: "https://drafts.csswg.org/css-values-4/#number-percentage"}],
25
- ["length-percentage", {url: "https://drafts.csswg.org/css-values-4/#length-percentage"}],
26
- ["frequency-percentage", {url: "https://drafts.csswg.org/css-values-4/#frequency-percentage"}],
27
- ["angle-percentage", {url: "https://drafts.csswg.org/css-values-4/#angle-percentage"}],
28
- ["time-percentage", {url: "https://drafts.csswg.org/css-values-4/#time-percentage"}],
29
- ["dimension", {url: "https://drafts.csswg.org/css-values-4/#dimensions"}],
30
- ["length", {url: "https://drafts.csswg.org/css-values-4/#lengths"}],
31
- ["angle", {url: "https://drafts.csswg.org/css-values-4/#angles"}],
32
- ["time", {url: "https://drafts.csswg.org/css-values-4/#time"}],
33
- ["frequency", {url: "https://drafts.csswg.org/css-values-4/#frequency"}],
34
- ["resolution", {url: "https://drafts.csswg.org/css-values-4/#frequency"}],
35
- ["color", {url: "https://drafts.csswg.org/css-color-3/#valuea-def-color"}],
36
- ["image", {url: "https://drafts.csswg.org/css-images-3/#typedef-image"}],
37
- ["position", {url: "https://drafts.csswg.org/css-values-4/#typedef-position"}]
38
- ]);
39
-
40
- const combinatorsMap = [['&&', 'allOf'],
41
- ['||', 'anyOf'],
42
- ['|', 'oneOf']];
43
-
44
- const multipliersStarters = ['{', '+', '#', '!', '?', '*'];
45
-
46
- const unquotedTokens = ['/', ',', '(', ')'];
47
-
48
- const componentizeByCombinators = (parts, combinators = new Map(combinatorsMap)) => {
49
- const res = {};
50
- let combinatorFound = false;
51
- const combinatorIterator = combinators.entries();
52
- while (!combinatorFound) {
53
- const {value: entry, done} = combinatorIterator.next();
54
- if (done) break;
55
- const [c,t] = entry;
56
- if (Array.isArray(parts) && parts.includes(c)) {
57
- combinatorFound = true;
58
- // going down into the list of combinators by order of precedence
59
- const lowerCombinators = new Map(combinators);
60
- lowerCombinators.delete(c);
61
- let components = splitByCombinator(parts, c);
62
- res[t] = components.map(p => componentizeByCombinators(p, lowerCombinators));
63
- }
64
- }
65
- if (!combinatorFound) {
66
- if (Array.isArray(parts)) {
67
- if (parts.length > 1) {
68
- return {type: "array", items: parts.map(p => componentizeByCombinators(p))};
69
- } else {
70
- return componentizeByCombinators(parts[0]);
71
- }
72
- }
73
- if (parts && parts.type && parts.type === "array")
74
- return {...parts, items: componentizeByCombinators(parts.items)};
75
- return parts;
76
- }
77
- return res;
78
- };
79
-
80
- const splitByCombinator = (parts, combinator) => {
81
- const {components} = parts.reduce((a, b, i) => {
82
- if (b === combinator) {
83
- a.components.push(a.head.length === 1 ? a.head[0] : a.head);
84
- a.head = [];
85
- } else {
86
- if (Array.isArray(b)) {
87
- a.head.push(componentizeByCombinators(b));
88
- } else {
89
- a.head.push(b);
90
- }
91
- }
92
- if (i === parts.length - 1) {
93
- a.components.push(a.head.length === 1 ? a.head[0] : a.head);
94
- }
95
- return a;
96
- }, {head: [], components: []});
97
- return components;
98
- };
99
-
100
- const parseMultiplierRange = range => {
101
- if (range[0] !== '{')
102
- throw new Error(`Expected { at the start of multiplier range ${range}`);
103
- if (range[range.length - 1] !== '}')
104
- throw new Error(`Expected } at the end of multiplier range ${range}`);
105
- const values = range.slice(1,range.length - 1);
106
- if (values.match(/^[0-9]+$/)) {
107
- return {minItems: parseInt(values, 10), maxItems: parseInt(values, 10)};
108
- } else if (values.match(/^[0-9]+,([0-9]+)?$/)) {
109
- const [min,max] = values.split(',');
110
- return { ...{minItems: parseInt(min, 10)}, ...(max ? { maxItems: parseInt(max, 10)} : {})};
111
- } else {
112
- throw new Error(`Unrecognized range format in multiplier ${range}`);
113
- }
114
- };
115
-
116
- const applyMultiplier = (multiplier, modifiee) => {
117
- let ret;
118
- if (multiplier === '*') {
119
- return {
120
- type: "array",
121
- items: modifiee
122
- };
123
- } else if (multiplier === '+') {
124
- return {
125
- type: "array",
126
- items: modifiee,
127
- minItems: 1
128
- };
129
- } else if (multiplier === '#') {
130
- return {
131
- type: "array",
132
- items: modifiee,
133
- separator: ","
134
- };
135
- } else if (multiplier.startsWith('{')) {
136
- return {
137
- type: "array",
138
- items: modifiee,
139
- ...parseMultiplierRange(multiplier)
140
- };
141
- } else if (multiplier === '?') {
142
- if (Array.isArray(modifiee)) {
143
- return {type: "array", items: modifiee, maxItems: 1};
144
- } else {
145
- return {...modifiee, optional: true};
146
- }
147
- } else if (multiplier === '!') {
148
- if (Array.isArray(modifiee)) {
149
- return {type: "array", items: modifiee, minItems: 1};
150
- } else {
151
- throw new Error(`Multiplier "!" applied to non-group ${modifiee}`);
152
- }
153
- } else {
154
- throw new Error(`Unrecognized multiplier ${multiplier}`);
155
- }
156
- };
157
-
158
- const isMultiplier = s => typeof s === "string" && multipliersStarters.map(starter => s.startsWith(starter)).includes(true);
159
-
160
- const primitiveMatch = (s, p) => s.match(new RegExp("<(" + p + ")( +\\\[[^\\\]]*\\\])?>"));
161
-
162
- const parseBracketedRange = s => {
163
- if (!s || !s.trim()) return undefined;
164
- const range = s.trim().slice(1, s.length - 2).split(',').map(x => x.trim());
165
- if (range.length != 2) throw new Error(`Unrecognized range descriptor ${s}`);
166
- return range;
167
- };
168
-
169
- const parseTerminals = s => {
170
- let m;
171
- if ([...new Map(combinatorsMap).keys()].includes(s) || s === '[' || s.startsWith(']') || isMultiplier(s)) {
172
- return s;
173
- } else if (unquotedTokens.includes(s)) {
174
- return {type: "string", content: s};
175
- } else if ((m = s.match(/^\'([^\']*)\'$/))) {
176
- return {type: "string", content: m[1]};
177
- } else if ((m = s.match(/^<\'([-_a-zA-Z][^\'>]*)\'>$/))) {
178
- return {type: "propertyref", name: m[1]};
179
- } else if ((m = [...primitives.keys()].find(p => primitiveMatch(s, p)))) {
180
- // TODO: parse bracketed range notation
181
- const [, name, range] = primitiveMatch(s, m);
182
- return Object.assign({type: "primitive", name}, range ? {range: parseBracketedRange(range)} : {});
183
- } else if ((m = s.match(/^<[-_a-zA-Z]([^>]*)>$/))) {
184
- return {type: "valuespace", name: s.slice(1, s.length -1)};
185
- } else if ((m = s.match(/^[-_a-zA-Z][-_a-zA-Z0-9]*$/))) {
186
- return {type: "keyword", name: s};
187
- } else if ((m = s.match(/^[-_a-zA-Z][-_a-zA-Z0-9]*\($/))) {
188
- return {type: "functionstart", name: s};
189
- } else { // TODO: add support for functional notations https://drafts.csswg.org/css-values-4/#functional-notation even though they're not recognized as top-level items in the grammar
190
- throw new Error(`Unrecognized token ${s}`);
191
- }
192
- };
193
-
194
- const tokenize = (value) => {
195
- let i = 0, currentToken='', tokens=[], state = 'new';
196
- const delimiterStates = ['new', 'keyword', 'pipe'];
197
- while(i < value.length) {
198
- const c = value[i];
199
- if (c.match(/\s/)) {
200
- if (state === 'labracket') { // bracketed range notation
201
- currentToken += c;
202
- } else {
203
- if (currentToken) tokens.push(currentToken);
204
- currentToken = '';
205
- state = 'new';
206
- }
207
- } else if (c === '<') {
208
- if (delimiterStates.includes(state)) {
209
- if (currentToken) tokens.push(currentToken);
210
- currentToken = c;
211
- state = 'labracket';
212
- } else if (state === 'quote') {
213
- currentToken += c;
214
- } else {
215
- throw new Error(`Unexpected < in ${currentToken} while parsing ${value} in state ${state}`);
216
- }
217
- } else if (c === ">") {
218
- if (state === 'quote') {
219
- currentToken += c;
220
- } else if (state === 'rabracket' || state === 'labracket') {
221
- currentToken += c;
222
- tokens.push(currentToken);
223
- currentToken = '';
224
- state = 'new';
225
- } else {
226
- throw new Error(`Unexpected > in ${currentToken} while parsing ${value} in state ${state}`);
227
- }
228
- } else if (c === "'") {
229
- if (state === 'quote') {
230
- currentToken += c;
231
- tokens.push(currentToken);
232
- currentToken = '';
233
- state = 'new';
234
- } else if (state === 'labracket') {
235
- currentToken += c;
236
- state = 'labracketquote';
237
- } else if (state === 'labracketquote') {
238
- currentToken += c;
239
- state = 'rabracket';
240
- } else {
241
- if (currentToken) tokens.push(currentToken);
242
- currentToken = c;
243
- state = 'quote';
244
- }
245
- } else if (c === "[" || c === "]" || c === "+" || c === "*" || c === "#" || c === "!" || c === '?' || c === '/') {
246
- if (delimiterStates.includes(state)) {
247
- if (currentToken) tokens.push(currentToken);
248
- tokens.push(c);
249
- currentToken='';
250
- state = 'new';
251
- } else if (state === 'quote') {
252
- currentToken += c;
253
- } else if (state === 'labracket' && c === '[') {
254
- // bracketed range notation
255
- state = 'bracketedrange';
256
- currentToken += c;
257
- } else if (state === 'bracketedrange' && c === ']') {
258
- currentToken += c;
259
- state = 'labracket';
260
- } else {
261
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
262
- }
263
- } else if ( c === '{' ) {
264
- if (state === 'quote') {
265
- currentToken += c;
266
- } else if (delimiterStates.includes(state)) {
267
- if (currentToken) tokens.push(currentToken);
268
- currentToken = c;
269
- state = 'curlybracket';
270
- } else {
271
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
272
- }
273
- } else if ( c === '}' ) {
274
- if (state === 'quote') {
275
- currentToken += c;
276
- } else if (state === 'curlybracket') {
277
- currentToken += c;
278
- tokens.push(currentToken);
279
- currentToken = '';
280
- state = 'new';
281
- } else {
282
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
283
- }
284
- } else if ( c === ',') {
285
- if (delimiterStates.includes(state)) {
286
- if (currentToken) tokens.push(currentToken);
287
- tokens.push(c);
288
- currentToken='';
289
- state = 'new';
290
- } else if (state === 'quote' || state === 'curlybracket' || state === 'bracketedrange') {
291
- currentToken += c;
292
- } else {
293
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
294
- }
295
- } else if ( c === '(') {
296
- if (state === 'new' || state === 'pipe') {
297
- if (currentToken) tokens.push(currentToken);
298
- tokens.push(c);
299
- currentToken='';
300
- state = 'new';
301
- } else if (state === 'quote' || state === 'labracket' || state === 'labracketquote') {
302
- currentToken += c;
303
- } else if (state === 'keyword') {
304
- currentToken += c;
305
- tokens.push(currentToken);
306
- currentToken ='';
307
- state = 'new';
308
- } else {
309
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
310
- }
311
- } else if (c === ")") {
312
- if (delimiterStates.includes(state)) {
313
- if (currentToken) tokens.push(currentToken);
314
- tokens.push(c);
315
- currentToken='';
316
- state = 'new';
317
- } else if (state === 'quote' || state === 'labracket' || state === 'labracketquote') {
318
- currentToken += c;
319
- } else {
320
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
321
- }
322
- } else if ( c === '&') {
323
- if (state === 'new' || state === 'keyword') { // 'pipe' can't appear just before ampersand
324
- if (currentToken) tokens.push(currentToken);
325
- currentToken=c;
326
- state = 'ampersand';
327
- } else if (state === 'quote') {
328
- currentToken += c;
329
- } else if (state === 'ampersand') {
330
- currentToken += c;
331
- tokens.push(currentToken);
332
- currentToken='';
333
- state = 'new';
334
- } else {
335
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
336
- }
337
- } else if (c === '|') {
338
- if (state === 'new' || state === 'keyword') {
339
- if (currentToken) tokens.push(currentToken);
340
- currentToken=c;
341
- state = 'pipe';
342
- } else if (state === 'quote') {
343
- currentToken += c;
344
- } else if (state === 'pipe') {
345
- currentToken += c;
346
- tokens.push(currentToken);
347
- currentToken='';
348
- state = 'new';
349
- } else {
350
- throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
351
- }
352
- } else {
353
- if (state === 'pipe') {
354
- tokens.push(currentToken);
355
- currentToken = 'c';
356
- state = 'keyword';
357
- } else {
358
- currentToken += c;
359
- if (state === 'new') state = 'keyword';
360
- }
361
- }
362
- i++;
363
- }
364
- if (state === 'new' || state === 'keyword') {
365
- if (currentToken) tokens.push(currentToken);
366
- } else {
367
- throw new Error(`Unexpected EOF while parsing ${value} in state ${state}`);
368
- }
369
- return tokens;
370
- };
371
-
372
- const parsePropDefValue = (value) => {
373
- value = value.trim();
374
- const tokens = tokenize(value);
375
- let parts = tokens.filter(x => x)
376
- .map(parseTerminals);
377
-
378
- // Applying multipliers on terminals
379
- parts = parts.reduce((arr, item, i) => {
380
- if (!isMultiplier(item)) {
381
- arr.push(item);
382
- return arr;
383
- }
384
- if (i === 0)
385
- throw new Error(`Unexpected multiplier ${item} at the start of ${value}`);
386
- const prevItem = arr.pop();
387
- if (prevItem !== ']') {
388
- arr.push(applyMultiplier(item, prevItem));
389
- } else {
390
- arr.push(prevItem);
391
- arr.push(item);
392
- }
393
- return arr;
394
- }, []);
395
-
396
- // matching functional notations
397
- while(parts.findIndex(p => p.type === 'functionstart') !== -1) {
398
- const funcIdx = parts.findIndex(p => p.type === 'functionstart');
399
- const matchingClosingFuncIdx = parts.findIndex((p, i) => p.content === ')' && i > funcIdx);
400
- if (matchingClosingFuncIdx === -1) {
401
- throw new Error(`Unterminated function notation in ${value}`);
402
- }
403
- const name = parts[funcIdx].name;
404
- const func = { type: "function",
405
- name: name.slice(0, name.length - 1),
406
- arguments: parts.slice(funcIdx + 1, matchingClosingFuncIdx)
407
- };
408
- parts = parts.slice(0, funcIdx)
409
- .concat([func])
410
- .concat(parts.slice(matchingClosingFuncIdx + 1));
411
- }
412
-
413
- // matching potentially nested bracket-groups
414
- while(parts.lastIndexOf('[') !== -1) {
415
- const bracketIdx = parts.lastIndexOf('[');
416
-
417
- // closing bracket may be more than just ']'
418
- // since it can be accompanied with multipliers
419
- const matchingBracketIdx = parts.findIndex((p, i) => p === ']' && i > bracketIdx);
420
-
421
- if (matchingBracketIdx === -1) {
422
- throw new Error(`Unterminated bracket-group in ${value}`);
423
- }
424
- let group = parts.slice(bracketIdx + 1, matchingBracketIdx);
425
- let multiplier, i = 0, multiplied = false;
426
- while ((multiplier = parts.slice(matchingBracketIdx + 1)[i]) && isMultiplier(multiplier)) {
427
- group = applyMultiplier(multiplier, group);
428
- multiplied = true;
429
- i++;
430
- }
431
- const multipliedGroup = multiplied ? group : [ group ] ;
432
- parts = parts.slice(0, bracketIdx)
433
- .concat(multipliedGroup)
434
- .concat(parts.slice(matchingBracketIdx + 1 + i));
435
- }
436
- const res = componentizeByCombinators(parts);
437
- return res.length === 1 ? res[0] : res;
438
- };
439
-
440
- module.exports.parsePropDefValue = parsePropDefValue;
1
+ // Based on https://drafts.csswg.org/css-values-4/#value-defs
2
+
3
+
4
+ const primitives = new Map([
5
+ ["ident", {}],
6
+ ["ident-token", {}],
7
+ ["declaration-value", {}],
8
+ // the subset below is only used in selectors / MQ
9
+ // probably ought to be removed
10
+ ["number-token", {}],
11
+ ["hash-token", {}],
12
+ ["any-value", {}],
13
+ ["string-token", {}],
14
+ ["function-token", {}],
15
+ ["dimension-token", {}],
16
+ // ----
17
+ ["zero", {url: ""}],
18
+ ["custom-ident", {url: "https://drafts.csswg.org/css-values-4/#custom-idents"}],
19
+ ["string", {url: "https://drafts.csswg.org/css-values-4/#strings"}],
20
+ ["url", {url: "https://drafts.csswg.org/css-values-4/#urls"}],
21
+ ["integer", {url: "https://drafts.csswg.org/css-values-4/#integers"}],
22
+ ["number", {url: "https://drafts.csswg.org/css-values-4/#numbers"}],
23
+ ["percentage", {url: "https://drafts.csswg.org/css-values-4/#percentages"}],
24
+ ["number-percentage", {url: "https://drafts.csswg.org/css-values-4/#number-percentage"}],
25
+ ["length-percentage", {url: "https://drafts.csswg.org/css-values-4/#length-percentage"}],
26
+ ["frequency-percentage", {url: "https://drafts.csswg.org/css-values-4/#frequency-percentage"}],
27
+ ["angle-percentage", {url: "https://drafts.csswg.org/css-values-4/#angle-percentage"}],
28
+ ["time-percentage", {url: "https://drafts.csswg.org/css-values-4/#time-percentage"}],
29
+ ["dimension", {url: "https://drafts.csswg.org/css-values-4/#dimensions"}],
30
+ ["length", {url: "https://drafts.csswg.org/css-values-4/#lengths"}],
31
+ ["angle", {url: "https://drafts.csswg.org/css-values-4/#angles"}],
32
+ ["time", {url: "https://drafts.csswg.org/css-values-4/#time"}],
33
+ ["frequency", {url: "https://drafts.csswg.org/css-values-4/#frequency"}],
34
+ ["resolution", {url: "https://drafts.csswg.org/css-values-4/#frequency"}],
35
+ ["color", {url: "https://drafts.csswg.org/css-color-3/#valuea-def-color"}],
36
+ ["image", {url: "https://drafts.csswg.org/css-images-3/#typedef-image"}],
37
+ ["position", {url: "https://drafts.csswg.org/css-values-4/#typedef-position"}]
38
+ ]);
39
+
40
+ const combinatorsMap = [['&&', 'allOf'],
41
+ ['||', 'anyOf'],
42
+ ['|', 'oneOf']];
43
+
44
+ const multipliersStarters = ['{', '+', '#', '!', '?', '*'];
45
+
46
+ const unquotedTokens = ['/', ',', '(', ')'];
47
+
48
+ const componentizeByCombinators = (parts, combinators = new Map(combinatorsMap)) => {
49
+ const res = {};
50
+ let combinatorFound = false;
51
+ const combinatorIterator = combinators.entries();
52
+ while (!combinatorFound) {
53
+ const {value: entry, done} = combinatorIterator.next();
54
+ if (done) break;
55
+ const [c,t] = entry;
56
+ if (Array.isArray(parts) && parts.includes(c)) {
57
+ combinatorFound = true;
58
+ // going down into the list of combinators by order of precedence
59
+ const lowerCombinators = new Map(combinators);
60
+ lowerCombinators.delete(c);
61
+ let components = splitByCombinator(parts, c);
62
+ res[t] = components.map(p => componentizeByCombinators(p, lowerCombinators));
63
+ }
64
+ }
65
+ if (!combinatorFound) {
66
+ if (Array.isArray(parts)) {
67
+ if (parts.length > 1) {
68
+ return {type: "array", items: parts.map(p => componentizeByCombinators(p))};
69
+ } else {
70
+ return componentizeByCombinators(parts[0]);
71
+ }
72
+ }
73
+ if (parts && parts.type && parts.type === "array")
74
+ return {...parts, items: componentizeByCombinators(parts.items)};
75
+ return parts;
76
+ }
77
+ return res;
78
+ };
79
+
80
+ const splitByCombinator = (parts, combinator) => {
81
+ const {components} = parts.reduce((a, b, i) => {
82
+ if (b === combinator) {
83
+ a.components.push(a.head.length === 1 ? a.head[0] : a.head);
84
+ a.head = [];
85
+ } else {
86
+ if (Array.isArray(b)) {
87
+ a.head.push(componentizeByCombinators(b));
88
+ } else {
89
+ a.head.push(b);
90
+ }
91
+ }
92
+ if (i === parts.length - 1) {
93
+ a.components.push(a.head.length === 1 ? a.head[0] : a.head);
94
+ }
95
+ return a;
96
+ }, {head: [], components: []});
97
+ return components;
98
+ };
99
+
100
+ const parseMultiplierRange = range => {
101
+ if (range[0] !== '{')
102
+ throw new Error(`Expected { at the start of multiplier range ${range}`);
103
+ if (range[range.length - 1] !== '}')
104
+ throw new Error(`Expected } at the end of multiplier range ${range}`);
105
+ const values = range.slice(1,range.length - 1);
106
+ if (values.match(/^[0-9]+$/)) {
107
+ return {minItems: parseInt(values, 10), maxItems: parseInt(values, 10)};
108
+ } else if (values.match(/^[0-9]+,([0-9]+)?$/)) {
109
+ const [min,max] = values.split(',');
110
+ return { ...{minItems: parseInt(min, 10)}, ...(max ? { maxItems: parseInt(max, 10)} : {})};
111
+ } else {
112
+ throw new Error(`Unrecognized range format in multiplier ${range}`);
113
+ }
114
+ };
115
+
116
+ const applyMultiplier = (multiplier, modifiee) => {
117
+ let ret;
118
+ if (multiplier === '*') {
119
+ return {
120
+ type: "array",
121
+ items: modifiee
122
+ };
123
+ } else if (multiplier === '+') {
124
+ return {
125
+ type: "array",
126
+ items: modifiee,
127
+ minItems: 1
128
+ };
129
+ } else if (multiplier === '#') {
130
+ return {
131
+ type: "array",
132
+ items: modifiee,
133
+ separator: ","
134
+ };
135
+ } else if (multiplier.startsWith('{')) {
136
+ return {
137
+ type: "array",
138
+ items: modifiee,
139
+ ...parseMultiplierRange(multiplier)
140
+ };
141
+ } else if (multiplier === '?') {
142
+ if (Array.isArray(modifiee)) {
143
+ return {type: "array", items: modifiee, maxItems: 1};
144
+ } else {
145
+ return {...modifiee, optional: true};
146
+ }
147
+ } else if (multiplier === '!') {
148
+ if (Array.isArray(modifiee)) {
149
+ return {type: "array", items: modifiee, minItems: 1};
150
+ } else {
151
+ throw new Error(`Multiplier "!" applied to non-group ${modifiee}`);
152
+ }
153
+ } else {
154
+ throw new Error(`Unrecognized multiplier ${multiplier}`);
155
+ }
156
+ };
157
+
158
+ const isMultiplier = s => typeof s === "string" && multipliersStarters.map(starter => s.startsWith(starter)).includes(true);
159
+
160
+ const primitiveMatch = (s, p) => s.match(new RegExp("<(" + p + ")( +\\\[[^\\\]]*\\\])?>"));
161
+
162
+ const parseBracketedRange = s => {
163
+ if (!s || !s.trim()) return undefined;
164
+ const range = s.trim().slice(1, s.length - 2).split(',').map(x => x.trim());
165
+ if (range.length != 2) throw new Error(`Unrecognized range descriptor ${s}`);
166
+ return range;
167
+ };
168
+
169
+ const parseTerminals = s => {
170
+ let m;
171
+ if ([...new Map(combinatorsMap).keys()].includes(s) || s === '[' || s.startsWith(']') || isMultiplier(s)) {
172
+ return s;
173
+ } else if (unquotedTokens.includes(s)) {
174
+ return {type: "string", content: s};
175
+ } else if ((m = s.match(/^\'([^\']*)\'$/))) {
176
+ return {type: "string", content: m[1]};
177
+ } else if ((m = s.match(/^<\'([-_a-zA-Z][^\'>]*)\'>$/))) {
178
+ return {type: "propertyref", name: m[1]};
179
+ } else if ((m = [...primitives.keys()].find(p => primitiveMatch(s, p)))) {
180
+ // TODO: parse bracketed range notation
181
+ const [, name, range] = primitiveMatch(s, m);
182
+ return Object.assign({type: "primitive", name}, range ? {range: parseBracketedRange(range)} : {});
183
+ } else if ((m = s.match(/^<[-_a-zA-Z]([^>]*)>$/))) {
184
+ return {type: "valuespace", name: s.slice(1, s.length -1)};
185
+ } else if ((m = s.match(/^[-_a-zA-Z][-_a-zA-Z0-9]*$/))) {
186
+ return {type: "keyword", name: s};
187
+ } else if ((m = s.match(/^[-_a-zA-Z][-_a-zA-Z0-9]*\($/))) {
188
+ return {type: "functionstart", name: s};
189
+ } else { // TODO: add support for functional notations https://drafts.csswg.org/css-values-4/#functional-notation even though they're not recognized as top-level items in the grammar
190
+ throw new Error(`Unrecognized token ${s}`);
191
+ }
192
+ };
193
+
194
+ const tokenize = (value) => {
195
+ let i = 0, currentToken='', tokens=[], state = 'new';
196
+ const delimiterStates = ['new', 'keyword', 'pipe'];
197
+ while(i < value.length) {
198
+ const c = value[i];
199
+ if (c.match(/\s/)) {
200
+ if (state === 'labracket') { // bracketed range notation
201
+ currentToken += c;
202
+ } else {
203
+ if (currentToken) tokens.push(currentToken);
204
+ currentToken = '';
205
+ state = 'new';
206
+ }
207
+ } else if (c === '<') {
208
+ if (delimiterStates.includes(state)) {
209
+ if (currentToken) tokens.push(currentToken);
210
+ currentToken = c;
211
+ state = 'labracket';
212
+ } else if (state === 'quote') {
213
+ currentToken += c;
214
+ } else {
215
+ throw new Error(`Unexpected < in ${currentToken} while parsing ${value} in state ${state}`);
216
+ }
217
+ } else if (c === ">") {
218
+ if (state === 'quote') {
219
+ currentToken += c;
220
+ } else if (state === 'rabracket' || state === 'labracket') {
221
+ currentToken += c;
222
+ tokens.push(currentToken);
223
+ currentToken = '';
224
+ state = 'new';
225
+ } else {
226
+ throw new Error(`Unexpected > in ${currentToken} while parsing ${value} in state ${state}`);
227
+ }
228
+ } else if (c === "'") {
229
+ if (state === 'quote') {
230
+ currentToken += c;
231
+ tokens.push(currentToken);
232
+ currentToken = '';
233
+ state = 'new';
234
+ } else if (state === 'labracket') {
235
+ currentToken += c;
236
+ state = 'labracketquote';
237
+ } else if (state === 'labracketquote') {
238
+ currentToken += c;
239
+ state = 'rabracket';
240
+ } else {
241
+ if (currentToken) tokens.push(currentToken);
242
+ currentToken = c;
243
+ state = 'quote';
244
+ }
245
+ } else if (c === "[" || c === "]" || c === "+" || c === "*" || c === "#" || c === "!" || c === '?' || c === '/') {
246
+ if (delimiterStates.includes(state)) {
247
+ if (currentToken) tokens.push(currentToken);
248
+ tokens.push(c);
249
+ currentToken='';
250
+ state = 'new';
251
+ } else if (state === 'quote') {
252
+ currentToken += c;
253
+ } else if (state === 'labracket' && c === '[') {
254
+ // bracketed range notation
255
+ state = 'bracketedrange';
256
+ currentToken += c;
257
+ } else if (state === 'bracketedrange' && c === ']') {
258
+ currentToken += c;
259
+ state = 'labracket';
260
+ } else {
261
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
262
+ }
263
+ } else if ( c === '{' ) {
264
+ if (state === 'quote') {
265
+ currentToken += c;
266
+ } else if (delimiterStates.includes(state)) {
267
+ if (currentToken) tokens.push(currentToken);
268
+ currentToken = c;
269
+ state = 'curlybracket';
270
+ } else {
271
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
272
+ }
273
+ } else if ( c === '}' ) {
274
+ if (state === 'quote') {
275
+ currentToken += c;
276
+ } else if (state === 'curlybracket') {
277
+ currentToken += c;
278
+ tokens.push(currentToken);
279
+ currentToken = '';
280
+ state = 'new';
281
+ } else {
282
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
283
+ }
284
+ } else if ( c === ',') {
285
+ if (delimiterStates.includes(state)) {
286
+ if (currentToken) tokens.push(currentToken);
287
+ tokens.push(c);
288
+ currentToken='';
289
+ state = 'new';
290
+ } else if (state === 'quote' || state === 'curlybracket' || state === 'bracketedrange') {
291
+ currentToken += c;
292
+ } else {
293
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
294
+ }
295
+ } else if ( c === '(') {
296
+ if (state === 'new' || state === 'pipe') {
297
+ if (currentToken) tokens.push(currentToken);
298
+ tokens.push(c);
299
+ currentToken='';
300
+ state = 'new';
301
+ } else if (state === 'quote' || state === 'labracket' || state === 'labracketquote') {
302
+ currentToken += c;
303
+ } else if (state === 'keyword') {
304
+ currentToken += c;
305
+ tokens.push(currentToken);
306
+ currentToken ='';
307
+ state = 'new';
308
+ } else {
309
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
310
+ }
311
+ } else if (c === ")") {
312
+ if (delimiterStates.includes(state)) {
313
+ if (currentToken) tokens.push(currentToken);
314
+ tokens.push(c);
315
+ currentToken='';
316
+ state = 'new';
317
+ } else if (state === 'quote' || state === 'labracket' || state === 'labracketquote') {
318
+ currentToken += c;
319
+ } else {
320
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
321
+ }
322
+ } else if ( c === '&') {
323
+ if (state === 'new' || state === 'keyword') { // 'pipe' can't appear just before ampersand
324
+ if (currentToken) tokens.push(currentToken);
325
+ currentToken=c;
326
+ state = 'ampersand';
327
+ } else if (state === 'quote') {
328
+ currentToken += c;
329
+ } else if (state === 'ampersand') {
330
+ currentToken += c;
331
+ tokens.push(currentToken);
332
+ currentToken='';
333
+ state = 'new';
334
+ } else {
335
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
336
+ }
337
+ } else if (c === '|') {
338
+ if (state === 'new' || state === 'keyword') {
339
+ if (currentToken) tokens.push(currentToken);
340
+ currentToken=c;
341
+ state = 'pipe';
342
+ } else if (state === 'quote') {
343
+ currentToken += c;
344
+ } else if (state === 'pipe') {
345
+ currentToken += c;
346
+ tokens.push(currentToken);
347
+ currentToken='';
348
+ state = 'new';
349
+ } else {
350
+ throw new Error(`Unexpected ${c} in ${currentToken} while parsing ${value} in state ${state}`);
351
+ }
352
+ } else {
353
+ if (state === 'pipe') {
354
+ tokens.push(currentToken);
355
+ currentToken = 'c';
356
+ state = 'keyword';
357
+ } else {
358
+ currentToken += c;
359
+ if (state === 'new') state = 'keyword';
360
+ }
361
+ }
362
+ i++;
363
+ }
364
+ if (state === 'new' || state === 'keyword') {
365
+ if (currentToken) tokens.push(currentToken);
366
+ } else {
367
+ throw new Error(`Unexpected EOF while parsing ${value} in state ${state}`);
368
+ }
369
+ return tokens;
370
+ };
371
+
372
+ const parsePropDefValue = (value) => {
373
+ value = value.trim();
374
+ const tokens = tokenize(value);
375
+ let parts = tokens.filter(x => x)
376
+ .map(parseTerminals);
377
+
378
+ // Applying multipliers on terminals
379
+ parts = parts.reduce((arr, item, i) => {
380
+ if (!isMultiplier(item)) {
381
+ arr.push(item);
382
+ return arr;
383
+ }
384
+ if (i === 0)
385
+ throw new Error(`Unexpected multiplier ${item} at the start of ${value}`);
386
+ const prevItem = arr.pop();
387
+ if (prevItem !== ']') {
388
+ arr.push(applyMultiplier(item, prevItem));
389
+ } else {
390
+ arr.push(prevItem);
391
+ arr.push(item);
392
+ }
393
+ return arr;
394
+ }, []);
395
+
396
+ // matching functional notations
397
+ while(parts.findIndex(p => p.type === 'functionstart') !== -1) {
398
+ const funcIdx = parts.findIndex(p => p.type === 'functionstart');
399
+ const matchingClosingFuncIdx = parts.findIndex((p, i) => p.content === ')' && i > funcIdx);
400
+ if (matchingClosingFuncIdx === -1) {
401
+ throw new Error(`Unterminated function notation in ${value}`);
402
+ }
403
+ const name = parts[funcIdx].name;
404
+ const func = { type: "function",
405
+ name: name.slice(0, name.length - 1),
406
+ arguments: parts.slice(funcIdx + 1, matchingClosingFuncIdx)
407
+ };
408
+ parts = parts.slice(0, funcIdx)
409
+ .concat([func])
410
+ .concat(parts.slice(matchingClosingFuncIdx + 1));
411
+ }
412
+
413
+ // matching potentially nested bracket-groups
414
+ while(parts.lastIndexOf('[') !== -1) {
415
+ const bracketIdx = parts.lastIndexOf('[');
416
+
417
+ // closing bracket may be more than just ']'
418
+ // since it can be accompanied with multipliers
419
+ const matchingBracketIdx = parts.findIndex((p, i) => p === ']' && i > bracketIdx);
420
+
421
+ if (matchingBracketIdx === -1) {
422
+ throw new Error(`Unterminated bracket-group in ${value}`);
423
+ }
424
+ let group = parts.slice(bracketIdx + 1, matchingBracketIdx);
425
+ let multiplier, i = 0, multiplied = false;
426
+ while ((multiplier = parts.slice(matchingBracketIdx + 1)[i]) && isMultiplier(multiplier)) {
427
+ group = applyMultiplier(multiplier, group);
428
+ multiplied = true;
429
+ i++;
430
+ }
431
+ const multipliedGroup = multiplied ? group : [ group ] ;
432
+ parts = parts.slice(0, bracketIdx)
433
+ .concat(multipliedGroup)
434
+ .concat(parts.slice(matchingBracketIdx + 1 + i));
435
+ }
436
+ const res = componentizeByCombinators(parts);
437
+ return res.length === 1 ? res[0] : res;
438
+ };
439
+
440
+ module.exports.parsePropDefValue = parsePropDefValue;