reffy 10.2.3 → 11.0.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reffy",
3
- "version": "10.2.3",
3
+ "version": "11.0.0",
4
4
  "description": "W3C/WHATWG spec dependencies exploration companion. Features a short set of tools to study spec references as well as WebIDL term definitions and references found in W3C specifications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,13 +33,13 @@
33
33
  "bin": "./reffy.js",
34
34
  "dependencies": {
35
35
  "abortcontroller-polyfill": "1.7.5",
36
- "ajv": "8.11.0",
36
+ "ajv": "8.11.2",
37
37
  "ajv-formats": "2.1.1",
38
38
  "commander": "9.4.1",
39
39
  "fetch-filecache-for-crawling": "4.1.0",
40
- "puppeteer": "19.2.2",
40
+ "puppeteer": "19.3.0",
41
41
  "semver": "^7.3.5",
42
- "web-specs": "2.35.0",
42
+ "web-specs": "2.36.0",
43
43
  "webidl2": "24.2.2"
44
44
  },
45
45
  "devDependencies": {
@@ -48,7 +48,7 @@
48
48
  "nock": "13.2.9",
49
49
  "respec": "32.3.0",
50
50
  "respec-hljs": "2.1.1",
51
- "rollup": "3.2.5"
51
+ "rollup": "3.5.0"
52
52
  },
53
53
  "scripts": {
54
54
  "test": "mocha --recursive tests/"
@@ -4,17 +4,18 @@
4
4
 
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["properties", "atrules", "valuespaces"],
7
+ "required": ["properties", "atrules", "selectors", "values"],
8
8
  "properties": {
9
9
  "properties": {
10
- "type": "object",
11
- "propertyNames": { "$ref": "../common.json#/$defs/cssPropertyName" },
12
- "additionalProperties": {
10
+ "type": "array",
11
+ "items": {
13
12
  "type": "object",
14
13
  "additionalProperties": true,
14
+ "required": ["name"],
15
15
  "properties": {
16
16
  "name": { "$ref": "../common.json#/$defs/cssPropertyName" },
17
17
  "value": { "$ref": "../common.json#/$defs/cssValue" },
18
+ "values": { "$ref": "../common.json#/$defs/cssValues" },
18
19
  "styleDeclaration": {
19
20
  "type": "array",
20
21
  "items": { "type": "string" },
@@ -25,25 +26,26 @@
25
26
  },
26
27
 
27
28
  "atrules": {
28
- "type": "object",
29
- "propertyNames": {
30
- "type": "string",
31
- "pattern": "^@"
32
- },
33
- "additionalProperties": {
29
+ "type": "array",
30
+ "items": {
34
31
  "type": "object",
32
+ "required": ["name", "descriptors"],
35
33
  "additionalProperties": false,
36
34
  "properties": {
35
+ "name": { "type": "string", "pattern": "^@" },
37
36
  "value": { "$ref": "../common.json#/$defs/cssValue" },
37
+ "prose": { "type": "string" },
38
38
  "descriptors": {
39
39
  "type": "array",
40
40
  "items": {
41
41
  "type": "object",
42
+ "required": ["name", "for"],
42
43
  "additionalProperties": true,
43
44
  "properties": {
44
45
  "name": { "type": "string" },
45
46
  "for": { "type": "string" },
46
- "value": { "$ref": "../common.json#/$defs/cssValue" }
47
+ "value": { "$ref": "../common.json#/$defs/cssValue" },
48
+ "values": { "$ref": "../common.json#/$defs/cssValues" }
47
49
  }
48
50
  }
49
51
  }
@@ -51,21 +53,49 @@
51
53
  }
52
54
  },
53
55
 
54
- "valuespaces": {
55
- "type": "object",
56
- "propertyNames": {
57
- "type": "string",
58
- "pattern": "^<[^>]+>$"
59
- },
60
- "additionalProperties": {
56
+ "selectors": {
57
+ "type": "array",
58
+ "items": {
61
59
  "type": "object",
60
+ "required": ["name"],
62
61
  "additionalProperties": false,
63
62
  "properties": {
63
+ "name": { "$ref": "../common.json#/$defs/cssPropertyName" },
64
64
  "prose": { "type": "string" },
65
65
  "value": { "$ref": "../common.json#/$defs/cssValue" },
66
- "legacyValue": { "$ref": "../common.json#/$defs/cssValue" }
66
+ "values": { "$ref": "../common.json#/$defs/cssValues" }
67
67
  }
68
68
  }
69
+ },
70
+
71
+ "values": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "object",
75
+ "required": ["name", "type"],
76
+ "additionalProperties": false,
77
+ "properties": {
78
+ "name": { "type": "string", "pattern": "^<[^>]+>$|^.*()$" },
79
+ "type": { "type": "string", "enum": ["type", "function"] },
80
+ "prose": { "type": "string" },
81
+ "value": { "$ref": "../common.json#/$defs/cssValue" },
82
+ "legacyValue": { "$ref": "../common.json#/$defs/cssValue" },
83
+ "values": { "$ref": "../common.json#/$defs/cssValues" }
84
+ }
85
+ }
86
+ },
87
+
88
+ "warnings": {
89
+ "type": "array",
90
+ "items": {
91
+ "type": "object",
92
+ "required": ["msg", "name"],
93
+ "properties": {
94
+ "msg": { "type": "string" },
95
+ "name": { "type": "string" }
96
+ }
97
+ },
98
+ "minItems": 1
69
99
  }
70
100
  }
71
101
  }
@@ -39,6 +39,23 @@
39
39
  "minLength": 1
40
40
  },
41
41
 
42
+ "cssValues": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "object",
46
+ "required": ["name", "type", "value"],
47
+ "additionalProperties": false,
48
+ "properties": {
49
+ "name": { "$ref": "#/$defs/cssValue" },
50
+ "type": { "type": "string", "enum": ["type", "function", "value"] },
51
+ "prose": { "type": "string" },
52
+ "value": { "$ref": "#/$defs/cssValue" },
53
+ "legacyValue": { "$ref": "#/$defs/cssValue" },
54
+ "values": { "$ref": "#/$defs/cssValues" }
55
+ }
56
+ }
57
+ },
58
+
42
59
  "interface": {
43
60
  "type": "string",
44
61
  "pattern": "^[A-Z]([A-Za-z0-9_])*$|^console$",
@@ -8,44 +8,306 @@ import informativeSelector from './informative-selector.mjs';
8
8
  * @return {Promise} The promise to get an extract of the CSS definitions, or
9
9
  * an empty CSS description object if the spec does not contain any CSS
10
10
  * definition. The return object will have properties named "properties",
11
- * "descriptors", and "valuespaces".
11
+ * "descriptors", "selectors" and "values".
12
12
  */
13
13
  export default function () {
14
- let res = {
15
- properties: extractTableDfns(document, 'propdef', { unique: true }),
16
- atrules: {},
17
- valuespaces: extractValueSpaces(document)
14
+ // List of inconsistencies and errors found in the spec while trying to make
15
+ // sense of the CSS definitions and production rules it contains
16
+ const warnings = [];
17
+
18
+ const res = {
19
+ // Properties are always defined in dedicated tables in modern CSS specs
20
+ properties: extractDfns({
21
+ selector: 'table.propdef:not(.attrdef)',
22
+ extractor: extractTableDfn,
23
+ duplicates: 'merge',
24
+ mayReturnMultipleDfns: true,
25
+ warnings
26
+ }),
27
+
28
+ // At-rules, selectors, functions and types are defined through dfns with
29
+ // the right "data-dfn-type" attribute
30
+ // Note some selectors are re-defined locally in HTML and Fullscreen. We
31
+ // won't import them.
32
+ atrules: extractDfns({
33
+ selector: 'dfn[data-dfn-type=at-rule]',
34
+ extractor: extractTypedDfn,
35
+ duplicates: 'reject',
36
+ warnings
37
+ }),
38
+ selectors: extractDfns({
39
+ selector: 'dfn[data-dfn-type=selector][data-export]',
40
+ extractor: extractTypedDfn,
41
+ duplicates: 'reject',
42
+ warnings
43
+ }),
44
+ values: extractDfns({
45
+ selector: ['dfn[data-dfn-type=function]:not([data-dfn-for])',
46
+ 'dfn[data-dfn-type=function][data-dfn-for=""]',
47
+ 'dfn[data-dfn-type=type]:not([data-dfn-for])',
48
+ 'dfn[data-dfn-type=type][data-dfn-for=""]'
49
+ ].join(','),
50
+ extractor: extractTypedDfn,
51
+ duplicates: 'reject',
52
+ keepDfnType: true,
53
+ warnings
54
+ })
18
55
  };
19
- let descriptors = extractTableDfns(document, 'descdef', { unique: false });
20
56
 
21
- // Try old recipes if we couldn't extract anything
22
- if ((Object.keys(res.properties).length === 0) &&
23
- (Object.keys(descriptors).length === 0)) {
24
- res.properties = extractDlDfns(document, 'propdef', { unique: true });
25
- descriptors = extractDlDfns(document, 'descdef', { unique: false });
57
+ // At-rules have descriptors, defined in dedicated tables in modern CSS specs
58
+ // Note some of the descriptors are defined with a "type" property set to
59
+ // "range". Not sure what the type of a descriptor is supposed to mean, but
60
+ // let's keep that information around. One such example is the
61
+ // "-webkit-device-pixel-ratio" descriptor for "@media" at-rule in compat:
62
+ // https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio
63
+ let descriptors = extractDfns({
64
+ selector: 'table.descdef:not(.attrdef)',
65
+ extractor: extractTableDfn,
66
+ duplicates: 'push',
67
+ mayReturnMultipleDfns: true,
68
+ keepDfnType: true,
69
+ warnings
70
+ });
71
+
72
+ // Older specs may follow older recipes, let's give them a try if we couldn't
73
+ // extract properties or descriptors
74
+ if (res.properties.length === 0 && descriptors.length === 0) {
75
+ res.properties = extractDfns({
76
+ selector: 'div.propdef dl',
77
+ extractor: extractDlDfn,
78
+ duplicates: 'merge',
79
+ mayReturnMultipleDfns: true,
80
+ warnings
81
+ });
82
+ descriptors = extractDfns({
83
+ selector: 'div.descdef dl',
84
+ extractor: extractDlDfn,
85
+ duplicates: 'push',
86
+ mayReturnMultipleDfns: true,
87
+ warnings
88
+ });
26
89
  }
27
90
 
28
- // Move at-rules definitions from valuespaces to at-rules structure
29
- for (const [name, dfn] of Object.entries(res.valuespaces)) {
30
- if (name.startsWith('@')) {
31
- if (!res.atrules[name]) {
32
- res.atrules[name] = Object.assign(dfn, { descriptors: [] });
91
+ // Move descriptors to at-rules structure
92
+ for (const desclist of descriptors) {
93
+ for (const desc of desclist) {
94
+ let rule = res.atrules.find(r => r.name === desc.for);
95
+ if (rule) {
96
+ if (!rule.descriptors) {
97
+ rule.descriptors = [];
98
+ }
99
+ }
100
+ else {
101
+ rule = { name: desc.for, descriptors: [] };
102
+ res.atrules.push(rule);
33
103
  }
34
- delete res.valuespaces[name];
104
+ rule.descriptors.push(desc);
105
+ }
106
+ }
107
+ for (const rule of res.atrules) {
108
+ if (!rule.descriptors) {
109
+ rule.descriptors = [];
35
110
  }
36
111
  }
37
112
 
38
- // Move descriptors to at-rules structure
39
- for (const [name, desclist] of Object.entries(descriptors)) {
113
+ // Keep an index of "root" (non-namespaced + descriptors) dfns
114
+ const rootDfns = Object.values(res).flat();
115
+ for (const desclist of descriptors) {
40
116
  for (const desc of desclist) {
41
- const rule = desc.for;
42
- if (!res.atrules[rule]) {
43
- res.atrules[rule] = { descriptors: [] };
117
+ rootDfns.push(desc);
118
+ }
119
+ }
120
+
121
+ // Extract value dfns.
122
+ // Note some of the values can be namespaced "function" or "type" dfns, such
123
+ // as "<content-replacement>" in css-content-3:
124
+ // https://drafts.csswg.org/css-content-3/#typedef-content-content-replacement
125
+ const values = extractDfns({
126
+ selector: ['dfn[data-dfn-type=value][data-dfn-for]:not([data-dfn-for=""])',
127
+ 'dfn[data-dfn-type=function][data-dfn-for]:not([data-dfn-for=""])',
128
+ 'dfn[data-dfn-type=type][data-dfn-for]:not([data-dfn-for=""])'
129
+ ].join(','),
130
+ extractor: extractTypedDfn,
131
+ duplicates: 'push',
132
+ keepDfnType: true,
133
+ warnings
134
+ }).flat();
135
+
136
+ const matchName = (name, { approx = false } = {}) => dfn => {
137
+ let res = dfn.name === name;
138
+ if (!res && name.match(/^@.+\/.+$/)) {
139
+ // Value reference might be for an at-rule descriptor:
140
+ // https://tabatkins.github.io/bikeshed/#dfn-for
141
+ const parts = name.split('/');
142
+ res = dfn.name === parts[1] && dfn.for === parts[0];
143
+ }
144
+ if (!res && approx) {
145
+ res = `<${dfn.name}>` === name;
146
+ }
147
+ return res;
148
+ };
149
+
150
+ // Extract production rules from pre.prod contructs
151
+ // and complete result structure accordingly
152
+ const rules = extractProductionRules(document);
153
+ for (const rule of rules) {
154
+ const dfn = rootDfns.find(matchName(rule.name)) ??
155
+ rootDfns.find(matchName(rule.name, { approx: true }));
156
+ if (dfn) {
157
+ dfn.value = rule.value;
158
+ if (rule.legacyValue) {
159
+ dfn.legacyValue = rule.legacyValue;
160
+ }
161
+ }
162
+ else {
163
+ let matchingValues = values.filter(matchName(rule.name));
164
+ if (matchingValues.length === 0) {
165
+ matchingValues = values.filter(matchName(rule.name, { approx: true }));
166
+ }
167
+ for (const matchingValue of matchingValues) {
168
+ matchingValue.value = rule.value;
169
+ if (rule.legacyValue) {
170
+ matchingValue.legacyValue = rule.legacyValue;
171
+ }
172
+ }
173
+ if (matchingValues.length === 0) {
174
+ // Dangling production rule. That should never happen for properties,
175
+ // at-rules, descriptors and functions, since they should always be
176
+ // defined somewhere. That happens from time to time for types that are
177
+ // described in prose and that don't have a dfn. One could perhaps argue
178
+ // that these constructs ought to have a dfn too.
179
+ if (!res.warnings) {
180
+ res.warnings = []
181
+ }
182
+ const warning = Object.assign({ msg: 'Missing definition' }, rule);
183
+ warnings.push(warning);
184
+ rootDfns.push(warning);
44
185
  }
45
- res.atrules[rule].descriptors.push(desc);
46
186
  }
47
187
  }
48
188
 
189
+ // We now need to associate values with dfns. CSS specs tend to list in
190
+ // "data-dfn-for" attributes of values the construct to which the value
191
+ // applies directly but also the constructs to which the value indirectly
192
+ // applies. For instance, "open-quote" in css-content-3 directly applies to
193
+ // "<quote>" and indirectly applies to "<content-list>" (which has "<quote>"
194
+ // in its value syntax) and to "content" (which has "<content-list>" in its
195
+ // value syntax), and all 3 appear in the "data-dfn-for" attribute:
196
+ // https://drafts.csswg.org/css-content-3/#valdef-content-open-quote
197
+ //
198
+ // To make it easier to make sense of the extracted data and avoid duplicates
199
+ // in the resulting structure, the goal is to only keep the constructs to
200
+ // which the value applies directly. In the previous example, the goal is to
201
+ // list "open-quote" under "<quote>" but not under "<content-list>" and
202
+ // "<content>".
203
+
204
+ // Start by looking at values that are "for" something. Their list of parents
205
+ // appears in the "data-dfn-for" attribute of their definition.
206
+ const parents = {};
207
+ for (const value of values) {
208
+ if (!parents[value.name]) {
209
+ parents[value.name] = [];
210
+ }
211
+ parents[value.name].push(...value.for.split(',').map(ref => ref.trim()));
212
+ }
213
+
214
+ // Then look at non-namespaced types and functions. Their list of parents
215
+ // are all the definitions whose values reference them (for instance,
216
+ // "<quote>" has "<content-list>" as parent because "<content-list>" has
217
+ // "<quote>" in its value syntax).
218
+ for (const type of res.values) {
219
+ if (!parents[type.name]) {
220
+ parents[type.name] = [];
221
+ }
222
+ for (const value of values) {
223
+ if (value.value?.includes(type.name)) {
224
+ parents[type.name].push(value.name);
225
+ }
226
+ }
227
+ for (const dfn of rootDfns) {
228
+ if (dfn.value?.includes(type.name)) {
229
+ parents[type.name].push(dfn.name);
230
+ }
231
+ }
232
+ }
233
+
234
+ // Helper functions to reason on the parents index we just created.
235
+ // Note there may be cycles. For instance, in CSS Images 4, <image> references
236
+ // <image-set()>, which references <image-set-option>, which references
237
+ // <image> again:
238
+ // https://drafts.csswg.org/css-images-4/#typedef-image
239
+ const isAncestorOf = (ancestor, child) => {
240
+ let seen = [];
241
+ const checkChild = c => {
242
+ let res = ancestor === c;
243
+ if (!res) {
244
+ res = parents[c]
245
+ ?.filter(p => !seen.includes(p))
246
+ ?.find(p => checkChild(ancestor, p));
247
+ }
248
+ seen = seen.concat(parents[c]);
249
+ return res;
250
+ }
251
+ return checkChild(child);
252
+ };
253
+ const isDeepestConstruct = (name, list) =>
254
+ list.every(p => p === name || !isAncestorOf(name, p));
255
+
256
+ // We may now associate values with dfns
257
+ for (const value of values) {
258
+ value.for.split(',')
259
+ .map(ref => ref.trim())
260
+ .filter((ref, _, arr) => isDeepestConstruct(ref, arr))
261
+ .forEach(ref => {
262
+ // Look for the referenced definition in root dfns
263
+ const dfn = rootDfns.find(matchName(ref)) ??
264
+ rootDfns.find(matchName(ref, { approx: true }));
265
+ if (dfn) {
266
+ if (!dfn.values) {
267
+ dfn.values = [];
268
+ }
269
+ dfn.values.push(value);
270
+ }
271
+ else {
272
+ // If the referenced definition is not in root dfns, look in
273
+ // namespaced dfns as functions/types are sometimes namespaced to a
274
+ // property, and values may reference these functions/types.
275
+ let referencedValues = values.filter(matchName(ref));
276
+ if (referencedValues.length === 0) {
277
+ referencedValues = values.filter(matchName(ref, { approx: true }));
278
+ }
279
+ for (const referencedValue of referencedValues) {
280
+ if (!referencedValue.values) {
281
+ referencedValue.values = [];
282
+ }
283
+ referencedValue.values.push(value);
284
+ }
285
+
286
+ if (referencedValues.length === 0) {
287
+ warnings.push(Object.assign(
288
+ { msg: 'Dangling value' },
289
+ value,
290
+ { for: ref }
291
+ ));
292
+ }
293
+ }
294
+ });
295
+ }
296
+
297
+ // Don't keep the info on whether value comes from a pure syntax section
298
+ for (const dfn of rootDfns) {
299
+ delete dfn.pureSyntax;
300
+ }
301
+ for (const value of values) {
302
+ delete value.for;
303
+ delete value.pureSyntax;
304
+ }
305
+
306
+ // Report warnings
307
+ if (warnings.length > 0) {
308
+ res.warnings = warnings;
309
+ }
310
+
49
311
  return res;
50
312
  }
51
313
 
@@ -162,32 +424,63 @@ const mergeDfns = (dfn1, dfn2) => {
162
424
 
163
425
  /**
164
426
  * Extract CSS definitions in a spec using the given CSS selector and extractor
427
+ *
428
+ * The "duplicates" option controls the behavior of the function when it
429
+ * encounters a duplicate (or similar) definition for the same thing. Values
430
+ * may be "reject" to report a warning, "merge" to merge the definitions, and
431
+ * "push" to keep both definitions separated.
432
+ *
433
+ * Merge issues and unexpected duplicates get reported as warnings in the
434
+ * "warnings" array passed as parameter.
165
435
  */
166
- const extractDfns = (doc, selector, extractor, { unique } = { unique: true }) => {
167
- let res = {};
168
- [...doc.querySelectorAll(selector)]
436
+ const extractDfns = ({ root = document,
437
+ selector,
438
+ extractor,
439
+ duplicates = 'reject',
440
+ mayReturnMultipleDfns = false,
441
+ keepDfnType = false,
442
+ warnings = [] }) => {
443
+ const res = [];
444
+ [...root.querySelectorAll(selector)]
169
445
  .filter(el => !el.closest(informativeSelector))
170
446
  .filter(el => !el.querySelector('ins, del'))
171
447
  .map(extractor)
172
- .filter(dfn => !!dfn.name)
173
- .map(dfn => dfn.name.split(',').map(name => Object.assign({},
174
- dfn, { name: name.trim() })))
448
+ .filter(dfn => !!dfn?.name)
449
+ .map(dfn => !mayReturnMultipleDfns ? [dfn] :
450
+ dfn.name.split(',').map(name => Object.assign({}, dfn, { name: name.trim() })))
175
451
  .reduce((acc, val) => acc.concat(val), [])
176
452
  .forEach(dfn => {
177
- if (res[dfn.name]) {
178
- if (unique) {
179
- const merged = mergeDfns(res[dfn.name], dfn);
180
- if (!merged) {
181
- throw new Error(`More than one dfn found for CSS property "${dfn.name}" and dfns cannot be merged`);
453
+ if (dfn.type && !keepDfnType) {
454
+ delete dfn.type;
455
+ }
456
+ const idx = res.findIndex(e => e.name === dfn.name);
457
+ if (idx >= 0) {
458
+ switch (duplicates) {
459
+ case 'merge':
460
+ const merged = mergeDfns(res[idx], dfn);
461
+ if (merged) {
462
+ res[idx] = merged;
182
463
  }
183
- res[dfn.name] = merged;
184
- }
185
- else {
186
- res[dfn.name].push(dfn);
464
+ else {
465
+ warnings.push(Object.assign(
466
+ { msg: 'Unmergeable definition' },
467
+ dfn
468
+ ));
469
+ }
470
+ break;
471
+
472
+ case 'push':
473
+ res[idx].push(dfn);
474
+
475
+ default:
476
+ warnings.push(Object.assign(
477
+ { msg: 'Duplicate definition' },
478
+ dfn
479
+ ));
187
480
  }
188
481
  }
189
482
  else {
190
- res[dfn.name] = unique ? dfn : [dfn];
483
+ res.push(duplicates !== 'push' ? dfn : [dfn]);
191
484
  }
192
485
  });
193
486
  return res;
@@ -195,201 +488,187 @@ const extractDfns = (doc, selector, extractor, { unique } = { unique: true }) =>
195
488
 
196
489
 
197
490
  /**
198
- * Extract CSS definitions in tables for the given class name
199
- * (typically one of `propdef` or `descdef`)
491
+ * Regular expression used to split production rules:
492
+ * Split on the space that precedes a term immediately before an equal sign
493
+ * that is not wrapped in quotes (an equal sign wrapped in quotes is part of
494
+ * actual value syntax)
200
495
  */
201
- const extractTableDfns = (doc, className, options) =>
202
- extractDfns(doc, 'table.' + className + ':not(.attrdef)', extractTableDfn, options);
496
+ const reSplitRules = /\s(?=[^\s]+?\s*?=[^'])/;
203
497
 
204
498
 
205
499
  /**
206
- * Extract CSS definitions in a dl list for the given class name
207
- * (typically one of `propdef` or `descdef`)
500
+ * Helper function to parse a production rule. The "pureSyntax" parameter
501
+ * should be set to indicate that the rule comes from a pure syntactic block
502
+ * and should have precedence over another value definition that may be
503
+ * extracted from the prose. For instance, this makes it possible to extract
504
+ * `<abs()> = abs( <calc-sum> )` from the syntax part in CSS Values instead
505
+ * of `<abs()> = abs(A)` which is how the function is defined in prose.
208
506
  */
209
- const extractDlDfns = (doc, className, options) =>
210
- extractDfns(doc, 'div.' + className + ' dl', extractDlDfn, options);
507
+ const parseProductionRule = (rule, { res = [], pureSyntax = false }) => {
508
+ const nameAndValue = rule
509
+ .replace(/\/\*[^]*?\*\//gm, '') // Drop comments
510
+ .split(/\s?=\s/)
511
+ .map(s => s.trim().replace(/\s+/g, ' '));
512
+
513
+ const name = nameAndValue[0];
514
+ const value = nameAndValue[1];
515
+
516
+ const normalizedValue = normalize(value);
517
+ let entry = res.find(e => e.name === name);
518
+ if (!entry) {
519
+ entry = { name };
520
+ res.push(entry);
521
+ }
522
+ if (!entry.value || (pureSyntax && !entry.pureSyntax)) {
523
+ entry.value = normalizedValue;
524
+ entry.pureSyntax = pureSyntax;
525
+ }
526
+ else if (entry.value !== normalizedValue) {
527
+ // Second definition found. Typically happens for the statement and
528
+ // block @layer definitions in css-cascade-5. We'll combine the values
529
+ // as alternative.
530
+ // Hardcoded exception: re-definitions of rgb() and hsl() are legacy
531
+ // constructs, stored separately not to pollute `value`.
532
+ if (name === '<rgb()>' || name === '<hsl()>') {
533
+ entry.legacyValue = normalizedValue;
534
+ }
535
+ else {
536
+ entry.value += ` | ${normalizedValue}`;
537
+ }
538
+ }
539
+
540
+ return entry;
541
+ };
211
542
 
212
543
 
213
544
  /**
214
- * Extract value spaces (non-terminal values) defined in the specification
215
- *
216
- * From a definitions data model perspective, non-terminal values are those
217
- * defined with a `data-dfn-type` attribute equal to `type` or `function`. They
218
- * form (at least in theory) a single namespace across CSS specs.
219
- *
220
- * Definitions with `data-dfn-type` attribute set to `value` are not extracted
221
- * on purpose as they are typically namespaced to another construct (through a
222
- * `data-dfn-for` attribute.
223
- *
224
- * The function also extracts syntax of at-rules (name starts with '@') defined
225
- * in "pre.prod" blocks.
545
+ * Extract the given dfn
226
546
  */
227
- const extractValueSpaces = doc => {
547
+ const extractTypedDfn = dfn => {
228
548
  let res = {};
229
-
230
- // Helper function to parse a production rule. The "pureSyntax" parameter
231
- // should be set to indicate that the rule comes from a pure syntactic block
232
- // and should have precedence over another value definition that may be
233
- // extracted from the prose. For instance, this makes it possible to extract
234
- // `<abs()> = abs( <calc-sum> )` from the syntax part in CSS Values instead
235
- // of `<abs()> = abs(A)` which is how the function is defined in prose.
236
- const parseProductionRule = (rule, { pureSyntax = false }) => {
237
- const nameAndValue = rule
238
- .replace(/\/\*[^]*?\*\//gm, '') // Drop comments
239
- .split(/\s?=\s/)
240
- .map(s => s.trim().replace(/\s+/g, ' '));
241
-
242
- function addValuespace(name, value) {
243
- const normalizedValue = normalize(value);
244
- if (!(name in res)) {
245
- res[name] = {};
246
- }
247
- if (!res[name].value || (pureSyntax && !res[name].pureSyntax)) {
248
- res[name].value = normalizedValue;
249
- res[name].pureSyntax = pureSyntax;
250
- }
251
- else if (res[name].value !== normalizedValue) {
252
- // Second definition found. Typically happens for the statement and
253
- // block @layer definitions in css-cascade-5. We'll combine the values
254
- // as alternative.
255
- // Hardcoded exception: re-definitions of rgb() and hsl() are legacy
256
- // constructs, stored separately not to pollute `value`.
257
- if (name === '<rgb()>' || name === '<hsl()>') {
258
- res[name].legacyValue = normalizedValue;
259
- }
260
- else {
261
- res[name].value += ` | ${normalizedValue}`;
262
- }
263
- }
549
+ const arr = [];
550
+ const dfnType = dfn.getAttribute('data-dfn-type');
551
+ const dfnFor = dfn.getAttribute('data-dfn-for');
552
+ const parent = dfn.parentNode.cloneNode(true);
553
+
554
+ // Remove note references as in:
555
+ // https://drafts.csswg.org/css-syntax-3/#the-anb-type
556
+ // and remove MDN annotations as well
557
+ [...parent.querySelectorAll('sup')]
558
+ .map(sup => sup.parentNode.removeChild(sup));
559
+ [...parent.querySelectorAll('aside, .mdn-anno')]
560
+ .map(annotation => annotation.parentNode.removeChild(annotation));
561
+
562
+ const text = parent.textContent.trim();
563
+ if (text.match(/\s?=\s/)) {
564
+ // Definition appears in a "prod = foo" text, that's all good
565
+ // ... except in css-easing-2 draft where text also contains another
566
+ // production rule as a child of the first one:
567
+ // https://drafts.csswg.org/css-easing-2/#typedef-step-easing-function
568
+ const prod = text.split(reSplitRules)
569
+ .find(p => p.trim().startsWith(dfn.textContent.trim()));
570
+ if (dfn.closest('pre')) {
571
+ // Don't attempt to parse pre tags at this stage, they are tricky to
572
+ // split, we'll parse them as text and map them to the right definitions
573
+ // afterwards.
574
+ const name = (dfn.getAttribute('data-lt') ?? dfn.textContent).trim();
575
+ res = { name };
264
576
  }
265
-
266
- if (nameAndValue[0].match(/^<.*>$|^.*\(\)$/)) {
267
- // Regular valuespace
268
- addValuespace(
269
- nameAndValue[0].replace(/^(.*\(\))$/, '<$1>'),
270
- nameAndValue[1]);
577
+ else if (prod) {
578
+ res = parseProductionRule(prod, { pureSyntax: true });
271
579
  }
272
- else if (nameAndValue[0].match(/^@[a-z\-]+$/)) {
273
- // At-rule syntax
274
- addValuespace(nameAndValue[0], nameAndValue[1]);
580
+ else {
581
+ // "=" may appear in another formula in the body of the text, as in:
582
+ // https://drafts.csswg.org/css-speech-1/#typedef-voice-volume-decibel
583
+ // It may be worth checking but not an error per se.
584
+ console.warn('[reffy]', `Found "=" next to definition of ${dfn.textContent.trim()} but no production rule. Did I miss something?`);
585
+ const name = (dfn.getAttribute('data-lt') ?? dfn.textContent).trim();
586
+ res = { name, prose: text.replace(/\s+/g, ' ') };
275
587
  }
276
- };
277
-
278
- // Regular expression to use to split production rules:
279
- // Split on the space that precedes a term immediately before an equal sign
280
- // that is not wrapped in quotes (an equal sign wrapped in quotes is part of
281
- // actual value syntax)
282
- const reSplitRules = /\s(?=[^\s]+?\s*?=[^'])/;
283
-
284
- // Extract all dfns with data-dfn-type="type" or data-dfn-type="function"
285
- // but ignore definitions in <pre> as they do not always use dfns, as in
286
- // https://drafts.csswg.org/css-values-4/#calc-syntax
287
- [...doc.querySelectorAll(
288
- 'dfn[data-dfn-type=type],dfn[data-dfn-type=function]')]
289
- .filter(el => !el.closest(informativeSelector))
290
- .filter(el => !el.closest('pre'))
291
- .forEach(dfn => {
292
- const parent = dfn.parentNode.cloneNode(true);
293
-
294
- // Remove note references as in:
295
- // https://drafts.csswg.org/css-syntax-3/#the-anb-type
296
- // and remove MDN annotations as well
297
- [...parent.querySelectorAll('sup')]
298
- .map(sup => sup.parentNode.removeChild(sup));
299
- [...parent.querySelectorAll('aside, .mdn-anno')]
300
- .map(annotation => annotation.parentNode.removeChild(annotation));
301
-
302
- const text = parent.textContent.trim();
303
- if (text.match(/\s?=\s/)) {
304
- // Definition appears in a "prod = foo" text, that's all good
305
- // ... except in css-easing-2 draft where text also contains another
306
- // production rule as a child of the first one:
307
- // https://drafts.csswg.org/css-easing-2/#typedef-step-easing-function
308
- const prod = text.split(reSplitRules)
309
- .find(p => p.trim().startsWith(dfn.textContent.trim()));
310
- if (prod) {
311
- parseProductionRule(prod, { pureSyntax: true });
312
- }
313
- else {
314
- // "=" may appear in another formula in the body of the text, as in:
315
- // https://drafts.csswg.org/css-speech-1/#typedef-voice-volume-decibel
316
- // It may be worth checking but not an error per se.
317
- console.warn('[reffy]', `Found "=" next to definition of ${dfn.textContent.trim()} but no production rule. Did I miss something?`);
318
- const name = (dfn.getAttribute('data-lt') ?? dfn.textContent)
319
- .trim().replace(/^<?(.*?)>?$/, '<$1>');
320
- if (!(name in res)) {
321
- res[name] = {
322
- prose: parent.textContent.trim().replace(/\s+/g, ' ')
323
- };
324
- }
325
- }
588
+ }
589
+ else if (dfn.textContent.trim().match(/^[a-zA-Z_][a-zA-Z0-9_\-]+\([^\)]+\)$/)) {
590
+ // Definition is "prod(foo bar)", create a "prod() = prod(foo bar)" entry
591
+ const fn = dfn.textContent.trim().match(/^([a-zA-Z_][a-zA-Z0-9_\-]+)\([^\)]+\)$/)[1];
592
+ res = parseProductionRule(`${fn}() = ${dfn.textContent.trim()}`, { pureSyntax: false });
593
+ }
594
+ else if (parent.nodeName === 'DT') {
595
+ // Definition is in a <dt>, look for value in following <dd>
596
+ let dd = dfn.parentNode;
597
+ while (dd && (dd.nodeName !== 'DD')) {
598
+ dd = dd.nextSibling;
599
+ }
600
+ if (!dd) {
601
+ return;
602
+ }
603
+ let code = dd.querySelector('p > code, pre.prod');
604
+ if (code) {
605
+ if (code.textContent.startsWith(`${text} = `) ||
606
+ code.textContent.startsWith(`<${text}> = `)) {
607
+ res = parseProductionRule(code.textContent, { pureSyntax: true });
326
608
  }
327
- else if (dfn.textContent.trim().match(/^[a-zA-Z_][a-zA-Z0-9_\-]+\([^\)]+\)$/)) {
328
- // Definition is "prod(foo bar)", create a "prod() = prod(foo bar)" entry
329
- const fn = dfn.textContent.trim().match(/^([a-zA-Z_][a-zA-Z0-9_\-]+)\([^\)]+\)$/)[1];
330
- parseProductionRule(`${fn}() = ${dfn.textContent.trim()}`, { pureSyntax: false });
609
+ else {
610
+ res = parseProductionRule(`${text} = ${code.textContent}`, { pureSyntax: false });
331
611
  }
332
- else if (parent.nodeName === 'DT') {
333
- // Definition is in a <dt>, look for value in following <dd>
334
- let dd = dfn.parentNode;
335
- while (dd && (dd.nodeName !== 'DD')) {
336
- dd = dd.nextSibling;
337
- }
338
- if (!dd) {
339
- return;
340
- }
341
- let code = dd.querySelector('p > code, pre.prod');
342
- if (code) {
343
- if (code.textContent.startsWith(`${text} = `) ||
344
- code.textContent.startsWith(`<${text}> = `)) {
345
- parseProductionRule(code.textContent, { pureSyntax: true });
346
- }
347
- else {
348
- parseProductionRule(`${text} = ${code.textContent}`, { pureSyntax: false });
349
- }
612
+ }
613
+ else {
614
+ // Remove notes, details sections that link to tests, and subsections
615
+ // that go too much into details
616
+ dd = dd.cloneNode(true);
617
+ [...dd.children].forEach(c => {
618
+ if (c.tagName === 'DETAILS' ||
619
+ c.tagName === 'DL' ||
620
+ c.classList.contains('note')) {
621
+ c.remove();
350
622
  }
351
- else {
352
- // Remove notes, details sections that link to tests, and subsections
353
- // that go too much into details
354
- dd = dd.cloneNode(true);
355
- [...dd.children].forEach(c => {
356
- if (c.tagName === 'DETAILS' ||
357
- c.tagName === 'DL' ||
358
- c.classList.contains('note')) {
359
- c.remove();
360
- }
361
- });
623
+ });
362
624
 
363
- const name = (dfn.getAttribute('data-lt') ?? dfn.textContent)
364
- .trim().replace(/^<?(.*?)>?$/, '<$1>');
365
- if (!(name in res)) {
366
- res[name] = {};
367
- }
368
- if (!res[name].prose) {
369
- res[name].prose = dd.textContent.trim().replace(/\s+/g, ' ');
370
- }
371
- }
372
- }
373
- else if (parent.nodeName === 'P') {
374
- // Definition is in regular prose, extract value from prose.
375
- const name = (dfn.getAttribute('data-lt') ?? dfn.textContent)
376
- .trim().replace(/^<?(.*?)>?$/, '<$1>');
377
- if (!(name in res)) {
378
- res[name] = {
379
- prose: parent.textContent.trim().replace(/\s+/g, ' ')
380
- };
381
- }
382
- }
383
- });
625
+ const name = (dfn.getAttribute('data-lt') ?? dfn.textContent).trim();
626
+ res = { name, prose: dd.textContent.trim().replace(/\s+/g, ' ') };
627
+ }
628
+ }
629
+ else if (parent.nodeName === 'P') {
630
+ // Definition is in regular prose, extract value from prose.
631
+ const name = (dfn.getAttribute('data-lt') ?? dfn.textContent).trim();
632
+ res = { name, prose: parent.textContent.trim().replace(/\s+/g, ' ') };
633
+ }
634
+ else {
635
+ // Definition is in a heading or a more complex structure, just list the
636
+ // name for now.
637
+ const name = (dfn.getAttribute('data-lt') ?? dfn.textContent).trim();
638
+ res = { name };
639
+ }
640
+
641
+ res.type = dfnType;
642
+ if (dfnType === 'value') {
643
+ res.value = normalize(res.name);
644
+ }
645
+ if (dfnFor) {
646
+ res.for = dfnFor;
647
+ }
384
648
 
385
- // Complete with production rules defined in <pre class=prod> tags (some of
386
- // which use dfns, while others don't, but all of them are actual production
387
- // rules). For <pre> tags that don't have a "prod" class (e.g. in HTML and
388
- // css-namespaces), make sure they contain a <dfn> to avoid parsing things
389
- // that are not production rules
390
- [...doc.querySelectorAll('pre.prod')]
391
- .concat([...doc.querySelectorAll('pre:not(.idl)')]
392
- .filter(el => el.querySelector('dfn')))
649
+ return res;
650
+ };
651
+
652
+ /**
653
+ * Extract production rules defined in the specification in "<pre>" tags and
654
+ * complete the result structure received as parameter accordingly.
655
+ */
656
+ const extractProductionRules = root => {
657
+ // For <pre> tags that don't have a "prod" class (e.g. in HTML and
658
+ // css-namespaces), make sure they contain a <dfn> with a valid CSS
659
+ // data-dfn-type attribute to avoid parsing things that are not production
660
+ // rules. In all cases, make sure we're not in a changelog with details as in:
661
+ // https://drafts.csswg.org/css-backgrounds-3/#changes-2017-10
662
+ const rules = [];
663
+ [...root.querySelectorAll('pre.prod:not(:has(del)):not(:has(ins))')]
664
+ .concat([...root.querySelectorAll('pre:not(.idl):not(:has(.idl)):not(:has(del)):not(:has(ins))')]
665
+ .filter(el => el.querySelector([
666
+ 'dfn[data-dfn-type=at-rule]',
667
+ 'dfn[data-dfn-type=selector]',
668
+ 'dfn[data-dfn-type=value]',
669
+ 'dfn[data-dfn-type=function]',
670
+ 'dfn[data-dfn-type=type]'
671
+ ].join(','))))
393
672
  .filter(el => !el.closest(informativeSelector))
394
673
  .map(el => el.cloneNode(true))
395
674
  .map(el => {
@@ -402,18 +681,15 @@ const extractValueSpaces = doc => {
402
681
  .map(val => val.split(reSplitRules)) // Separate definitions
403
682
  .flat()
404
683
  .map(text => text.trim())
405
- .map(text => {
684
+ .forEach(text => {
406
685
  if (text.match(/\s?=\s/)) {
407
- return parseProductionRule(text, { pureSyntax: true });
686
+ parseProductionRule(text, { res: rules, pureSyntax: true });
408
687
  }
409
688
  else if (text.startsWith('@')) {
410
689
  const name = text.split(' ')[0];
411
- return parseProductionRule(`${name} = ${text}`, { pureSyntax: true });
690
+ parseProductionRule(`${name} = ${text}`, { res: rules, pureSyntax: true });
412
691
  }
413
692
  });
414
693
 
415
- // Don't keep the info on whether value comes from a pure syntax section
416
- Object.values(res).map(value => delete value.pureSyntax);
417
-
418
- return res;
694
+ return rules;
419
695
  }
@@ -24,6 +24,13 @@ import {parse} from "../../node_modules/webidl2/index.js";
24
24
  * "prose" (last one indicates that definition appears in the main body of
25
25
  * the spec)
26
26
  *
27
+ * The extraction ignores definitions with an unknown type. A warning is issued
28
+ * to the console when that happens.
29
+ *
30
+ * The extraction uses the first definition it finds when it bumps into a term
31
+ * that is defined more than once (same "linkingText", same "type", same "for").
32
+ * A warning is issued to the console when that happens.
33
+ *
27
34
  * @function
28
35
  * @public
29
36
  * @return {Array(Object)} An Array of definitions
@@ -99,8 +106,22 @@ function hasValidType(el) {
99
106
  return isValid;
100
107
  }
101
108
 
109
+ // Return true when definition is not already defined in the list,
110
+ // Return false and issue a warning when it is already defined.
111
+ function isNotAlreadyDefined(dfn, idx, list) {
112
+ const first = list.find(d => d === dfn ||
113
+ (d.type === dfn.type &&
114
+ d.linkingText.length === dfn.linkingText.length &&
115
+ d.linkingText.every(lt => dfn.linkingText.find(t => t == lt)) &&
116
+ d.for.length === dfn.for.length &&
117
+ d.for.every(lt => dfn.for.find(t => t === lt))));
118
+ if (first !== dfn) {
119
+ console.warn('[reffy]', `Duplicate dfn found for "${dfn.linkingText[0]}", type="${dfn.type}", for="${dfn.for[0]}"`);
120
+ }
121
+ return first === dfn;
122
+ }
102
123
 
103
- function definitionMapper(el, idToHeading) {
124
+ function definitionMapper(el, idToHeading, usesDfnDataModel) {
104
125
  let definedIn = 'prose';
105
126
  const enclosingEl = el.closest('dt,pre,table,h1,h2,h3,h4,h5,h6,.note,.example') || el;
106
127
  switch (enclosingEl.nodeName) {
@@ -165,8 +186,10 @@ function definitionMapper(el, idToHeading) {
165
186
  [],
166
187
 
167
188
  // Definition is public if explicitly marked as exportable or if export has
168
- // not been explicitly disallowed and its type is not "dfn"
169
- access: (el.hasAttribute('data-export') ||
189
+ // not been explicitly disallowed and its type is not "dfn", or if the spec
190
+ // is an old spec that does not use the "data-dfn-type" convention.
191
+ access: (!usesDfnDataModel ||
192
+ el.hasAttribute('data-export') ||
170
193
  (!el.hasAttribute('data-noexport') &&
171
194
  el.hasAttribute('data-dfn-type') &&
172
195
  el.getAttribute('data-dfn-type') !== 'dfn')) ?
@@ -200,7 +223,6 @@ export default function (spec, idToHeading = {}) {
200
223
  'h6[id][data-dfn-type]:not([data-lt=""])'
201
224
  ].join(',');
202
225
 
203
- let extraDefinitions = [];
204
226
  const shortname = (typeof spec === 'string') ? spec : spec.shortname;
205
227
  switch (shortname) {
206
228
  case "html":
@@ -214,7 +236,14 @@ export default function (spec, idToHeading = {}) {
214
236
  break;
215
237
  }
216
238
 
217
- return [...document.querySelectorAll(definitionsSelector)]
239
+ const definitions = [...document.querySelectorAll(definitionsSelector)];
240
+ const usesDfnDataModel = definitions.some(dfn =>
241
+ dfn.hasAttribute('data-dfn-type') ||
242
+ dfn.hasAttribute('data-dfn-for') ||
243
+ dfn.hasAttribute('data-export') ||
244
+ dfn.hasAttribute('data-noexport'));
245
+
246
+ return definitions
218
247
  .map(node => {
219
248
  // 2021-06-21: Temporary preprocessing of invalid "idl" dfn type (used for
220
249
  // internal slots) while fix for https://github.com/w3c/respec/issues/3644
@@ -237,7 +266,8 @@ export default function (spec, idToHeading = {}) {
237
266
  const link = node.querySelector('a[href^="http"]');
238
267
  return !link || (node.textContent.trim() !== link.textContent.trim());
239
268
  })
240
- .map(node => definitionMapper(node, idToHeading));
269
+ .map(node => definitionMapper(node, idToHeading, usesDfnDataModel))
270
+ .filter(isNotAlreadyDefined);
241
271
  }
242
272
 
243
273
  function preProcessEcmascript() {
@@ -16,6 +16,9 @@
16
16
  * - `format` is the optional output format. Either `json` or `markdown` with
17
17
  * `markdown` being the default.
18
18
  *
19
+ * Note: CSS extraction already relies on dfns and reports missing dfns in a
20
+ * "warnings" property. This checker simply looks at that list.
21
+ *
19
22
  * @module checker
20
23
  */
21
24
 
@@ -59,61 +62,15 @@ function arraysEqual(a, b) {
59
62
  * @return {Array} An array of expected definitions
60
63
  */
61
64
  function getExpectedDfnsFromCSS(css) {
62
- let expected = [];
63
-
64
- // Add the list of expected properties, filtering out properties that define
65
- // new values to an existing property (defined elsewhere)
66
- expected = expected.concat(
67
- Object.values(css.properties || {})
68
- .filter(desc => !desc.newValues)
69
- .map(desc => {
70
- return {
71
- linkingText: [desc.name],
72
- type: 'property',
73
- 'for': []
74
- };
75
- })
76
- );
77
-
78
- // Add the list of expected at-rules
79
- expected = expected.concat(
80
- Object.entries(css.atrules || {}).map(([name, rule]) => {
81
- if (rule.value) {
82
- return {
83
- linkingText: [name],
84
- type: 'at-rule',
85
- 'for': []
86
- };
87
- }
88
- }).filter(dfn => !!dfn)
89
- );
90
-
91
- // Add the list of expected descriptors
92
- expected = expected.concat(
93
- Object.entries(css.atrules || {}).map(([name, rule]) =>
94
- (rule.descriptors || []).map(desc => {
95
- return {
96
- linkingText: [desc.name],
97
- type: 'descriptor',
98
- 'for': [name]
99
- };
100
- })
101
- ).flat()
102
- );
103
-
104
- // Add the list of expected "values".
105
- // Note: we don't qualify the "type" of values in valuespaces and don't store
106
- // the scope of values either (the "for" property). Definition types can be
107
- // "type", "function", "value", etc. in practice. The comparison cannot be
108
- // perfect as a result.
109
- expected = expected.concat(
110
- Object.entries(css.valuespaces || {}).map(([name, desc]) => {
65
+ const expected = (css.warnings ?? [])
66
+ .filter(warning => warning.msg === 'Missing definition')
67
+ .map(warning => {
111
68
  return {
112
- linkingText: [name],
113
- value: desc.value
69
+ linkingText: [warning.name],
70
+ type: warning.type,
71
+ 'for': warning.for
114
72
  };
115
- })
116
- );
73
+ });
117
74
 
118
75
  return expected;
119
76
  }
@@ -32,7 +32,7 @@ const mockSpecs = {
32
32
  html: `
33
33
  <title>WOFF2</title>
34
34
  <body>
35
- <dfn id='foo'>Foo</dfn>
35
+ <dfn id='foo' data-dfn-type="dfn">Foo</dfn>
36
36
  <a href="https://www.w3.org/TR/bar/#baz">bar</a>
37
37
  <ul class='toc'><li><a href='page.html'>page</a></ul>`,
38
38
  pages: {
@@ -241,10 +241,11 @@ async function saveSpecResults(spec, settings) {
241
241
 
242
242
  // Save CSS dumps
243
243
  function defineCSSContent(spec) {
244
- return spec.css && (
245
- (Object.keys(spec.css.properties || {}).length > 0) ||
246
- (Object.keys(spec.css.atrules || {}).length > 0) ||
247
- (Object.keys(spec.css.valuespaces || {}).length > 0));
244
+ return (spec.css?.properties?.length > 0) ||
245
+ (spec.css?.atrules?.length > 0) ||
246
+ (spec.css?.selectors?.length > 0) ||
247
+ (spec.css?.values?.length > 0) ||
248
+ (spec.css?.warnings?.length > 0);
248
249
  }
249
250
  if (defineCSSContent(spec)) {
250
251
  spec.css = await saveCss(spec);
@@ -20,10 +20,10 @@ module.exports = {
20
20
  .filter(dfn => dfn.type == "property" && !dfn.informative)
21
21
  .forEach(propDfn => {
22
22
  propDfn.linkingText.forEach(lt => {
23
- if (!spec.css.properties.hasOwnProperty(lt)) {
24
- spec.css.properties[lt] = {
23
+ if (!spec.css.properties.find(p => p.name === lt)) {
24
+ spec.css.properties.push({
25
25
  name: lt
26
- };
26
+ });
27
27
  }
28
28
  });
29
29
  });
@@ -31,18 +31,15 @@ module.exports = {
31
31
 
32
32
  if (spec.css) {
33
33
  // Add generated IDL attribute names
34
- Object.entries(spec.css.properties || {}).forEach(([prop, dfn]) => {
35
- dfn.styleDeclaration = getGeneratedIDLNamesByCSSProperty(prop);
34
+ spec.css.properties.forEach(dfn => {
35
+ dfn.styleDeclaration = getGeneratedIDLNamesByCSSProperty(dfn.name);
36
36
  });
37
37
 
38
38
  // Drop the sample definition (property-name) in CSS2 and the custom
39
39
  // property definition (--*) in CSS Variables that specs incorrectly flag
40
40
  // as real CSS properties.
41
- ['property-name', '--*'].forEach(prop => {
42
- if ((spec.css.properties || {})[prop]) {
43
- delete spec.css.properties[prop];
44
- }
45
- });
41
+ spec.css.properties = spec.css.properties.filter(p =>
42
+ !['property-name', '--*'].includes(p.name));
46
43
  }
47
44
 
48
45
  return spec;