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 +5 -5
- package/schemas/browserlib/extract-cssdfn.json +49 -19
- package/schemas/common.json +17 -0
- package/src/browserlib/extract-cssdfn.mjs +499 -223
- package/src/browserlib/extract-dfns.mjs +36 -6
- package/src/cli/check-missing-dfns.js +10 -53
- package/src/lib/nock-server.js +1 -1
- package/src/lib/specs-crawler.js +5 -4
- package/src/postprocessing/csscomplete.js +7 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reffy",
|
|
3
|
-
"version": "
|
|
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.
|
|
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.
|
|
40
|
+
"puppeteer": "19.3.0",
|
|
41
41
|
"semver": "^7.3.5",
|
|
42
|
-
"web-specs": "2.
|
|
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.
|
|
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", "
|
|
7
|
+
"required": ["properties", "atrules", "selectors", "values"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"properties": {
|
|
10
|
-
"type": "
|
|
11
|
-
"
|
|
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": "
|
|
29
|
-
"
|
|
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
|
-
"
|
|
55
|
-
"type": "
|
|
56
|
-
"
|
|
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
|
-
"
|
|
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
|
}
|
package/schemas/common.json
CHANGED
|
@@ -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 "
|
|
11
|
+
* "descriptors", "selectors" and "values".
|
|
12
12
|
*/
|
|
13
13
|
export default function () {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
29
|
-
for (const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 = (
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
173
|
-
.map(dfn => dfn
|
|
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 (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
*
|
|
199
|
-
*
|
|
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
|
|
202
|
-
extractDfns(doc, 'table.' + className + ':not(.attrdef)', extractTableDfn, options);
|
|
496
|
+
const reSplitRules = /\s(?=[^\s]+?\s*?=[^'])/;
|
|
203
497
|
|
|
204
498
|
|
|
205
499
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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
|
|
210
|
-
|
|
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
|
|
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
|
|
547
|
+
const extractTypedDfn = dfn => {
|
|
228
548
|
let res = {};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
273
|
-
//
|
|
274
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
328
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
.
|
|
684
|
+
.forEach(text => {
|
|
406
685
|
if (text.match(/\s?=\s/)) {
|
|
407
|
-
|
|
686
|
+
parseProductionRule(text, { res: rules, pureSyntax: true });
|
|
408
687
|
}
|
|
409
688
|
else if (text.startsWith('@')) {
|
|
410
689
|
const name = text.split(' ')[0];
|
|
411
|
-
|
|
690
|
+
parseProductionRule(`${name} = ${text}`, { res: rules, pureSyntax: true });
|
|
412
691
|
}
|
|
413
692
|
});
|
|
414
693
|
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/nock-server.js
CHANGED
|
@@ -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: {
|
package/src/lib/specs-crawler.js
CHANGED
|
@@ -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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
24
|
-
spec.css.properties
|
|
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
|
-
|
|
35
|
-
dfn.styleDeclaration = getGeneratedIDLNamesByCSSProperty(
|
|
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
|
-
|
|
42
|
-
|
|
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;
|