reffy 18.7.3 → 18.8.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": "18.7.3",
3
+ "version": "18.8.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",
@@ -37,9 +37,9 @@
37
37
  "ajv-formats": "3.0.1",
38
38
  "commander": "14.0.0",
39
39
  "fetch-filecache-for-crawling": "5.1.1",
40
- "puppeteer": "24.9.0",
40
+ "puppeteer": "24.10.0",
41
41
  "semver": "^7.3.5",
42
- "web-specs": "3.52.0",
42
+ "web-specs": "3.53.0",
43
43
  "webidl2": "24.4.1"
44
44
  },
45
45
  "devDependencies": {
@@ -0,0 +1,103 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema#",
3
+ "$id": "https://github.com/w3c/reffy/blob/main/schemas/postprocessing/css.json",
4
+
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["atrules", "functions", "properties", "selectors", "types"],
8
+ "properties": {
9
+ "atrules": {
10
+ "type": "array",
11
+ "items": {
12
+ "type": "object",
13
+ "required": ["name", "descriptors"],
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "name": { "type": "string", "pattern": "^@" },
17
+ "href": { "$ref": "../common.json#/$defs/url" },
18
+ "value": { "$ref": "../common.json#/$defs/cssValue" },
19
+ "prose": { "type": "string" },
20
+ "descriptors": {
21
+ "type": "array",
22
+ "items": {
23
+ "type": "object",
24
+ "required": ["name", "for"],
25
+ "additionalProperties": true,
26
+ "properties": {
27
+ "name": { "type": "string" },
28
+ "for": { "type": "string" },
29
+ "href": { "$ref": "../common.json#/$defs/url" },
30
+ "value": { "$ref": "../common.json#/$defs/cssValue" }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ },
37
+ "functions": {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "object",
41
+ "required": ["name", "type"],
42
+ "additionalProperties": false,
43
+ "properties": {
44
+ "name": { "type": "string", "pattern": "^<[^>]+>$|^.*()$" },
45
+ "for": { "type": "string" },
46
+ "href": { "$ref": "../common.json#/$defs/url" },
47
+ "type": { "type": "string", "enum": ["function"] },
48
+ "prose": { "type": "string" },
49
+ "value": { "$ref": "../common.json#/$defs/cssValue" }
50
+ }
51
+ }
52
+ },
53
+ "properties": {
54
+ "type": "array",
55
+ "items": {
56
+ "type": "object",
57
+ "additionalProperties": true,
58
+ "required": ["name"],
59
+ "properties": {
60
+ "name": { "$ref": "../common.json#/$defs/cssPropertyName" },
61
+ "href": { "$ref": "../common.json#/$defs/url" },
62
+ "value": { "$ref": "../common.json#/$defs/cssValue" },
63
+ "legacyAliasOf": { "$ref": "../common.json#/$defs/cssPropertyName" },
64
+ "styleDeclaration": {
65
+ "type": "array",
66
+ "items": { "type": "string" },
67
+ "minItems": 1
68
+ }
69
+ }
70
+ }
71
+ },
72
+ "selectors": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "object",
76
+ "required": ["name"],
77
+ "additionalProperties": false,
78
+ "properties": {
79
+ "name": { "$ref": "../common.json#/$defs/cssPropertyName" },
80
+ "href": { "$ref": "../common.json#/$defs/url" },
81
+ "prose": { "type": "string" },
82
+ "value": { "$ref": "../common.json#/$defs/cssValue" }
83
+ }
84
+ }
85
+ },
86
+ "types": {
87
+ "type": "array",
88
+ "items": {
89
+ "type": "object",
90
+ "required": ["name", "type"],
91
+ "additionalProperties": false,
92
+ "properties": {
93
+ "name": { "type": "string", "pattern": "^<[^>]+>$|^.*()$" },
94
+ "for": { "type": "string" },
95
+ "href": { "$ref": "../common.json#/$defs/url" },
96
+ "type": { "type": "string", "enum": ["type"] },
97
+ "prose": { "type": "string" },
98
+ "value": { "$ref": "../common.json#/$defs/cssValue" }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
@@ -306,6 +306,21 @@ export default function () {
306
306
  delete value.pureSyntax;
307
307
  }
308
308
 
309
+ // Specs typically do not make the syntax of selectors such as `:visited`
310
+ // explicit because it essentially goes without saying: the syntax is the
311
+ // selector's name itself. Note that the syntax of selectors that are
312
+ // function-like such as `:nth-child()` cannot be inferred in the same way.
313
+ for (const selector of res.selectors) {
314
+ if (!selector.value && !selector.name.match(/\(/)) {
315
+ selector.value = selector.name;
316
+ }
317
+ for (const subSelector of selector.values ?? []) {
318
+ if (!subSelector.value && !subSelector.name.match(/\(/)) {
319
+ subSelector.value = subSelector.name;
320
+ }
321
+ }
322
+ }
323
+
309
324
  // Report warnings
310
325
  if (warnings.length > 0) {
311
326
  res.warnings = warnings;
@@ -52,6 +52,7 @@ import path from 'node:path';
52
52
  import { pathToFileURL } from 'node:url';
53
53
  import { createFolderIfNeeded, shouldSaveToFile } from './util.js';
54
54
  import csscomplete from '../postprocessing/csscomplete.js';
55
+ import cssmerge from '../postprocessing/cssmerge.js';
55
56
  import events from '../postprocessing/events.js';
56
57
  import idlnames from '../postprocessing/idlnames.js';
57
58
  import idlparsed from '../postprocessing/idlparsed.js';
@@ -64,6 +65,7 @@ import patchdfns from '../postprocessing/patch-dfns.js';
64
65
  */
65
66
  const modules = {
66
67
  csscomplete,
68
+ cssmerge,
67
69
  events,
68
70
  idlnames,
69
71
  idlparsed,
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Post-processing module that consolidates CSS extracts into a single
3
+ * structure. That structure is an object whose keys are `atrules`,
4
+ * `functions`, `properties`, `selectors`, and `types`. Values are lists of CSS
5
+ * constructs whose type matches the key.
6
+ *
7
+ * CSS constructs follow the same structure as that in individual CSS extracts
8
+ * except that values that are listed under `values` in the CSS extracts are
9
+ * not reported in the resulting structure because these values are a mix bag
10
+ * of things in practice and specs do not consistently define values that a CSS
11
+ * construct may take in any case.
12
+ *
13
+ * In CSS extracts, functions and types that are defined for another construct
14
+ * appear under the `values` key of that construct entry. In the resulting
15
+ * construct, they get copied to the root lists under `functions` or `types`,
16
+ * and get a `for` key that contains the name of the construct that they are
17
+ * defined for.
18
+ *
19
+ * CSS properties that are defined in one spec and extended in other specs get
20
+ * consolidated into a single entry in the resulting structure. The syntax of
21
+ * that single entry is the union (using `|`) of the syntaxes of each
22
+ * definition.
23
+ *
24
+ * Similarly, at-rules that are defined in one spec and for which additional
25
+ * descriptors get defined in other specs get consolidated into a single entry
26
+ * in the resulting structure. The list of descriptors gets merged accordingly
27
+ * (the order of descriptors is essentially arbitrary but then it is already
28
+ * somewhat arbitrary in the initial CSS extracts).
29
+ *
30
+ * When the syntax of an at-rule is defined in terms of `<declaration-list>` or
31
+ * `<declaration-rule-list>`, the resulting syntax is "expanded" using the
32
+ * syntax of the individual descriptors. For example, the syntax:
33
+ *
34
+ * `@property <custom-property-name> { <declaration-list> }`
35
+ *
36
+ * becomes:
37
+ *
38
+ * `@property <custom-property-name> {
39
+ * [ syntax: [ <string> ]; ] ||
40
+ * [ inherits: [ true | false ]; ] ||
41
+ * [ initial-value: [ <declaration-value>? ]; ]
42
+ * }`
43
+ *
44
+ * When a CSS property is defined as a legacy alias of another one, its syntax
45
+ * gets set to that of the other CSS property in the resulting structure.
46
+ *
47
+ * The structure roughly aligns with the structure followed in the MDN data
48
+ * project at https://github.com/mdn/data on purpose, to ease comparison and
49
+ * possible transition to Webref data. Main differences are:
50
+ * - This code reports at-rules under `atrules`, MDN data uses `atRules`.
51
+ * - This code uses arrays for lists, MDN data uses indexed objects.
52
+ * - This code lists scoped definitions with a `for` key. MDN data only has
53
+ * unscoped definitions.
54
+ * - This code stores syntaxes in a `value` key, MDN data uses a `syntax` key.
55
+ * - This code stores syntaxes of functions and types directly in the
56
+ * `functions` and `types` lists. MDN data stores them in a separate `syntaxes`
57
+ * category. The `syntaxes` view can be built by merging the `functions` and
58
+ * `types` lists.
59
+ * - This code keeps the surrounding `<>` for type names, MDN data does not.
60
+ *
61
+ * Module runs at the crawl level to create a `css.json` file.
62
+ */
63
+
64
+ /**
65
+ * CSS extracts have almost the right structure but mix functions and types
66
+ * into a values namespace.
67
+ */
68
+ const extractCategories = [
69
+ 'atrules',
70
+ 'properties',
71
+ 'selectors',
72
+ 'values'
73
+ ];
74
+
75
+ export default {
76
+ dependsOn: ['css'],
77
+ input: 'crawl',
78
+ property: 'css',
79
+
80
+ run: async function (crawl, options) {
81
+ // Final structure we're going to create
82
+ const categorized = {
83
+ atrules: [],
84
+ functions: [],
85
+ properties: [],
86
+ selectors: [],
87
+ types: []
88
+ };
89
+ const categories = Object.keys(categorized);
90
+
91
+ // Let's fill out the final structure based on data from the CSS extracts
92
+ for (const spec of crawl.results) {
93
+ // Only consider specs that define some CSS
94
+ if (!spec.css) {
95
+ continue;
96
+ }
97
+ const data = spec.css;
98
+
99
+ // We're going to merge features across specs, save the link back to
100
+ // individual specs, we'll need that to de-duplicate entries
101
+ decorateFeaturesWithSpec(data, spec);
102
+
103
+ // Same categorization for at-rules, properties, and selectors
104
+ categorized.atrules.push(...data.atrules);
105
+ categorized.properties.push(...data.properties);
106
+ categorized.selectors.push(...data.selectors);
107
+
108
+ // Functions and types are merged in CSS extracts
109
+ categorized.functions.push(...data.values.filter(v => v.type === 'function'));
110
+ categorized.types.push(...data.values.filter(v => v.type === 'type'));
111
+
112
+ // Copy scoped functions and types to the root level with a `for` key
113
+ // to link back to the scoping feature
114
+ for (const category of extractCategories) {
115
+ for (const feature of data[category]) {
116
+ if (feature.values) {
117
+ const values = feature.values
118
+ .map(v => Object.assign({ for: feature.name }, v));
119
+ categorized.functions.push(
120
+ ...values.filter(v => v.type === 'function'));
121
+ categorized.types.push(
122
+ ...values.filter(v => v.type === 'type'));
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ // The job is "almost" done but we now need to de-duplicate entries.
129
+ // Duplicated entries exist when:
130
+ // - A property is defined in one spec and extended in other specs. We'll
131
+ // consolidate the entries (and syntaxes) to get back to a single entry.
132
+ // - An at-rule is defined in one spec. Additional descriptors are defined
133
+ // in other specs. We'll consolidate the entries similarly.
134
+ // - A feature is defined in one level of a spec series, and re-defined in
135
+ // a subsequent level.
136
+ //
137
+ // And then, from time to time, specs define a function or type scoped to
138
+ // another construct while a similar unscoped definition already exists.
139
+ // The specs should get fixed (Strudy reports these problems already).
140
+ // We'll ignore the scoped definitions here when an unscoped definition can
141
+ // be used.
142
+ //
143
+ // To de-duplicate, we're going to take a live-on-the-edge perspective
144
+ // and use definitions from the latest level in a series when there's a
145
+ // choice.
146
+ //
147
+ // Notes:
148
+ // - The code assumes that the possibility that a CSS construct gets
149
+ // defined in multiple unrelated (i.e., not in the same series) specs has
150
+ // already been taken care of through some sort of curation. It will pick
151
+ // up a winner randomly if that happens.
152
+ // - There is no duplication for scoped functions and types provided that
153
+ // that the `for` key gets taken into account!)
154
+ for (const category of categories) {
155
+ // Create an index of feature definitions
156
+ const featureDfns = {};
157
+ for (const feature of categorized[category]) {
158
+ // ... and since we're looping through features, let's get rid
159
+ // of inner value definitions, which we no longer need
160
+ // (interesting ones were already copied to the root level)
161
+ if (feature.values) {
162
+ delete feature.values;
163
+ }
164
+ for (const descriptor of feature.descriptors ?? []) {
165
+ if (descriptor.values) {
166
+ delete descriptor.values;
167
+ }
168
+ }
169
+
170
+ const featureId = getFeatureId(feature);
171
+ if (!featureDfns[featureId]) {
172
+ featureDfns[featureId] = [];
173
+ }
174
+ featureDfns[featureId].push(feature);
175
+ }
176
+
177
+ // Identify the base definition for each feature, using the definition
178
+ // (that has some known syntax) in the most recent level. Move that base
179
+ // definition to the beginning of the array and get rid of other base
180
+ // definitions.
181
+ // (Note: the code throws an error if duplicates of base definitions in
182
+ // unrelated specs still exist)
183
+ for (const [name, dfns] of Object.entries(featureDfns)) {
184
+ let actualDfns = dfns.filter(dfn => dfn.value);
185
+ if (actualDfns.length === 0) {
186
+ actualDfns = dfns.filter(dfn => !dfn.newValues);
187
+ }
188
+ const best = actualDfns.reduce((dfn1, dfn2) => {
189
+ if (dfn1.spec.series.shortname !== dfn2.spec.series.shortname) {
190
+ console.warn(`${name} is defined in unrelated specs ${dfn1.spec.shortname}, ${dfn2.spec.shortname}`);
191
+ return dfn2;
192
+ }
193
+ if (dfn1.spec.seriesVersion < dfn2.spec.seriesVersion) {
194
+ return dfn2;
195
+ }
196
+ else {
197
+ return dfn1;
198
+ }
199
+ });
200
+ featureDfns[name] = [best].concat(
201
+ dfns.filter(dfn => !actualDfns.includes(dfn))
202
+ );
203
+ }
204
+
205
+ // Apply extensions for properties and at-rules descriptors
206
+ // (no extension mechanism for functions, selectors and types for now)
207
+ // Note: there are delta specs of delta specs from time to time (e.g.,
208
+ // `css-color`) and delta is not always a pure delta. In other words,
209
+ // extension definitions may themselves be duplicated, we'll again
210
+ // prefer the latest level in such cases.
211
+ for (const [name, dfns] of Object.entries(featureDfns)) {
212
+ const baseDfn = dfns[0];
213
+ for (const dfn of dfns) {
214
+ if (dfn === baseDfn) {
215
+ continue;
216
+ }
217
+ if (baseDfn.value && dfn.newValues) {
218
+ const newerDfn = dfns.find(d =>
219
+ d !== dfn &&
220
+ d.newValues === dfn.newValues &&
221
+ d.spec.seriesVersion > dfn.spec.seriesVersion);
222
+ if (newerDfn) {
223
+ // The extension is redefined in a newer level, let's ignore
224
+ // the older one
225
+ continue;
226
+ }
227
+ baseDfn.value += ' | ' + dfn.newValues;
228
+ }
229
+ if (baseDfn.descriptors && dfn.descriptors?.length > 0) {
230
+ baseDfn.descriptors.push(...dfn.descriptors.filter(desc => {
231
+ // Look for a possible newer definition of the descriptor
232
+ const newerDfn = dfns.find(d =>
233
+ d !== dfn &&
234
+ d.descriptors?.find(ddesc => ddesc.name === desc.name) &&
235
+ d.spec.seriesVersion > dfn.spec.seriesVersion);
236
+ return !newerDfn;
237
+ }));
238
+ }
239
+ }
240
+ }
241
+
242
+ // All duplicates should have been treated somehow and merged into the
243
+ // base definition. Use the base definition and get rid of the rest!
244
+ // We will also generate an expanded syntax when possible for at-rules,
245
+ // and drop scoped definitions when a suitable unscoped definition
246
+ // already exists.
247
+ categorized[category] = Object.entries(featureDfns)
248
+ .map(([name, features]) => features[0])
249
+ .filter(feature => {
250
+ if (feature.for) {
251
+ const unscoped = categorized[category].find(f =>
252
+ f.name === feature.name && !f.for);
253
+ if (unscoped) {
254
+ // Only keep the scoped feature if it has a known syntax that
255
+ // differs from the unscoped feature
256
+ return feature.value && feature.value !== unscoped.value;
257
+ }
258
+ }
259
+ return true;
260
+ })
261
+ .map(feature => {
262
+ if (feature.descriptors?.length > 0 &&
263
+ feature.value?.match(/{ <declaration-(rule-)?list> }/)) {
264
+ // Note: More advanced logic would allow to get rid of enclosing
265
+ // grouping constructs when there's no ambiguity. We'll stick to
266
+ // simple logic for now.
267
+ const syntax = feature.descriptors
268
+ .map(desc => {
269
+ if (desc.name.startsWith('@')) {
270
+ return `[ ${desc.value} ]`;
271
+ }
272
+ else {
273
+ return `[ ${desc.name}: [ ${desc.value} ]; ]`;
274
+ }
275
+ })
276
+ .join(' ||\n ');
277
+ feature.value = feature.value.replace(
278
+ /{ <declaration-(rule-)?list> }/,
279
+ '{\n ' + syntax + '\n}');
280
+ }
281
+
282
+ delete feature.spec;
283
+ return feature;
284
+ });
285
+
286
+ // Various CSS properties are "legacy aliases of" another property. Use the
287
+ // syntax of the other property for these.
288
+ for (const feature of categorized[category]) {
289
+ if (feature.legacyAliasOf && !feature.value) {
290
+ const target = categorized[category].find(f =>
291
+ f.name === feature.legacyAliasOf && !f.for);
292
+ if (!target) {
293
+ throw new Error(`${feature.name} is a legacy alias of unknown ${f.legacyAliasOf}`);
294
+ }
295
+ feature.value = target.value;
296
+ }
297
+ }
298
+
299
+ // Let's sort lists before we return to ease human-readability and
300
+ // avoid non-substantive diff
301
+ for (const feature of categorized[category]) {
302
+ if (feature.descriptors) {
303
+ feature.descriptors.sort((d1, d2) => d1.name.localeCompare(d2.name));
304
+ }
305
+ }
306
+ categorized[category].sort((f1, f2) =>
307
+ getFeatureId(f1).localeCompare(getFeatureId(f2)));
308
+ }
309
+
310
+ return categorized;
311
+ }
312
+ };
313
+
314
+
315
+ /**
316
+ * Return the identifier of a feature, taking scoping construct into account
317
+ * when needed.
318
+ */
319
+ function getFeatureId(feature) {
320
+ let featureId = feature.name;
321
+ if (feature.for) {
322
+ featureId += ' for ' + feature.for;
323
+ }
324
+ return featureId;
325
+ }
326
+
327
+
328
+ /**
329
+ * Decorate all CSS features in the extract with the spec
330
+ */
331
+ function decorateFeaturesWithSpec(data, spec) {
332
+ for (const category of extractCategories) {
333
+ for (const feature of data[category]) {
334
+ feature.spec = spec;
335
+ for (const value of feature.values ?? []) {
336
+ value.spec = spec;
337
+ }
338
+ }
339
+ }
340
+ }