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.
|
|
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.
|
|
40
|
+
"puppeteer": "24.10.0",
|
|
41
41
|
"semver": "^7.3.5",
|
|
42
|
-
"web-specs": "3.
|
|
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
|
+
}
|