reffy 6.2.0 → 6.4.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/LICENSE +21 -21
- package/README.md +158 -158
- package/index.js +11 -11
- package/package.json +53 -53
- package/reffy.js +248 -248
- package/src/browserlib/canonicalize-url.mjs +50 -50
- package/src/browserlib/create-outline.mjs +352 -352
- package/src/browserlib/extract-cssdfn.mjs +319 -319
- package/src/browserlib/extract-dfns.mjs +686 -686
- package/src/browserlib/extract-elements.mjs +205 -205
- package/src/browserlib/extract-headings.mjs +48 -48
- package/src/browserlib/extract-ids.mjs +28 -28
- package/src/browserlib/extract-links.mjs +28 -28
- package/src/browserlib/extract-references.mjs +203 -203
- package/src/browserlib/extract-webidl.mjs +134 -134
- package/src/browserlib/get-absolute-url.mjs +21 -21
- package/src/browserlib/get-generator.mjs +26 -26
- package/src/browserlib/get-lastmodified-date.mjs +13 -13
- package/src/browserlib/get-title.mjs +11 -11
- package/src/browserlib/informative-selector.mjs +16 -16
- package/src/browserlib/map-ids-to-headings.mjs +136 -136
- package/src/browserlib/reffy.json +53 -53
- package/src/cli/check-missing-dfns.js +609 -609
- package/src/cli/generate-idlnames.js +430 -430
- package/src/cli/generate-idlparsed.js +139 -139
- package/src/cli/merge-crawl-results.js +128 -128
- package/src/cli/parse-webidl.js +430 -430
- package/src/lib/css-grammar-parse-tree.schema.json +109 -109
- package/src/lib/css-grammar-parser.js +440 -440
- package/src/lib/fetch.js +56 -56
- package/src/lib/nock-server.js +127 -120
- package/src/lib/specs-crawler.js +622 -603
- package/src/lib/util.js +943 -898
- package/src/specs/missing-css-rules.json +197 -197
- package/src/specs/spec-equivalents.json +149 -149
- package/src/browserlib/extract-editors.mjs~ +0 -14
- package/src/browserlib/generate-es-dfn-report.sh~ +0 -4
- package/src/cli/csstree-grammar-check.js +0 -28
- package/src/cli/csstree-grammar-check.js~ +0 -10
- package/src/cli/csstree-grammar-parser.js +0 -11
- package/src/cli/csstree-grammar-parser.js~ +0 -1
- package/src/cli/extract-editors.js~ +0 -38
- package/src/cli/process-specs.js~ +0 -28
|
@@ -1,319 +1,319 @@
|
|
|
1
|
-
import informativeSelector from './informative-selector.mjs';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extract the list of CSS definitions in the current spec
|
|
5
|
-
*
|
|
6
|
-
* @function
|
|
7
|
-
* @public
|
|
8
|
-
* @return {Promise} The promise to get an extract of the CSS definitions, or
|
|
9
|
-
* an empty CSS description object if the spec does not contain any CSS
|
|
10
|
-
* definition. The return object will have properties named "properties",
|
|
11
|
-
* "descriptors", and "valuespaces".
|
|
12
|
-
*/
|
|
13
|
-
export default function () {
|
|
14
|
-
let res = {
|
|
15
|
-
properties: extractTableDfns(document, 'propdef', { unique: true }),
|
|
16
|
-
descriptors: extractTableDfns(document, 'descdef', { unique: false }),
|
|
17
|
-
valuespaces: extractValueSpaces(document)
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Try old recipes if we couldn't extract anything
|
|
21
|
-
if ((Object.keys(res.properties).length === 0) &&
|
|
22
|
-
(Object.keys(res.descriptors).length === 0)) {
|
|
23
|
-
res.properties = extractDlDfns(document, 'propdef', { unique: true });
|
|
24
|
-
res.descriptors = extractDlDfns(document, 'descdef', { unique: false });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return res;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Converts a definition label as it appears in a CSS spec to a lower camel
|
|
33
|
-
* case property name.
|
|
34
|
-
*
|
|
35
|
-
* @param {String} label Definition label
|
|
36
|
-
* @return {String} lower camel case property name for the label
|
|
37
|
-
*/
|
|
38
|
-
const dfnLabel2Property = label => label.trim()
|
|
39
|
-
.replace(/:/, '')
|
|
40
|
-
.split(' ')
|
|
41
|
-
.map((str, idx) => (idx === 0) ?
|
|
42
|
-
str.toLowerCase() :
|
|
43
|
-
str.charAt(0).toUpperCase() + str.slice(1))
|
|
44
|
-
.join('');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Extract a CSS definition from a table
|
|
49
|
-
*
|
|
50
|
-
* All recent CSS specs should follow that pattern
|
|
51
|
-
*/
|
|
52
|
-
const extractTableDfn = table => {
|
|
53
|
-
let res = {};
|
|
54
|
-
const lines = [...table.querySelectorAll('tr')]
|
|
55
|
-
.map(line => {
|
|
56
|
-
const cleanedLine = line.cloneNode(true);
|
|
57
|
-
const annotations = cleanedLine.querySelectorAll("aside, .mdn-anno");
|
|
58
|
-
annotations.forEach(n => n.remove());
|
|
59
|
-
return {
|
|
60
|
-
name: dfnLabel2Property(cleanedLine.querySelector(':first-child').textContent),
|
|
61
|
-
value: cleanedLine.querySelector('td:last-child').textContent.trim().replace(/\s+/g, ' ')
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
for (let prop of lines) {
|
|
65
|
-
res[prop.name] = prop.value;
|
|
66
|
-
}
|
|
67
|
-
return res;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Extract a CSS definition from a dl list
|
|
73
|
-
*
|
|
74
|
-
* Used in "old" CSS specs
|
|
75
|
-
*/
|
|
76
|
-
const extractDlDfn = dl => {
|
|
77
|
-
let res = {};
|
|
78
|
-
res.name = dl.querySelector('dt').textContent.replace(/'/g, '').trim();
|
|
79
|
-
const lines = [...dl.querySelectorAll('dd table tr')]
|
|
80
|
-
.map(line => Object.assign({
|
|
81
|
-
name: dfnLabel2Property(line.querySelector(':first-child').textContent),
|
|
82
|
-
value: line.querySelector('td:last-child').textContent.trim().replace(/\s+/g, ' ')
|
|
83
|
-
}));
|
|
84
|
-
for (let prop of lines) {
|
|
85
|
-
res[prop.name] = prop.value;
|
|
86
|
-
}
|
|
87
|
-
return res;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Merges CSS definitions for the same property into one
|
|
93
|
-
*
|
|
94
|
-
* The function runs a few simple sanity checks on the definitions. More checks
|
|
95
|
-
* would be needed to fully validate that merging is fine.
|
|
96
|
-
*
|
|
97
|
-
* The function returns null if CSS definitions cannot be merged.
|
|
98
|
-
*/
|
|
99
|
-
const mergeDfns = (dfn1, dfn2) => {
|
|
100
|
-
// Definitions should be about the same property
|
|
101
|
-
if (dfn1.name !== dfn2.name) {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// There should never be two base definitions for the same CSS property
|
|
106
|
-
if (dfn1.value && dfn2.value) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const [baseDfn, partialDfn] = dfn2.value ? [dfn2, dfn1] : [dfn1, dfn2];
|
|
111
|
-
if ((!baseDfn.value && !baseDfn.newValues) ||
|
|
112
|
-
!partialDfn.newValues ||
|
|
113
|
-
(partialDfn.initial && partialDfn.initial !== baseDfn.initial)) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const merged = Object.assign(baseDfn);
|
|
118
|
-
if (merged.value) {
|
|
119
|
-
merged.value += ` | ${partialDfn.newValues}`;
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
merged.newValues += ` | ${partialDfn.newValues}`;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return merged;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Extract CSS definitions in a spec using the given CSS selector and extractor
|
|
131
|
-
*/
|
|
132
|
-
const extractDfns = (doc, selector, extractor, { unique } = { unique: true }) => {
|
|
133
|
-
let res = {};
|
|
134
|
-
[...doc.querySelectorAll(selector)]
|
|
135
|
-
.filter(el => !el.closest(informativeSelector))
|
|
136
|
-
.filter(el => !el.querySelector('ins, del'))
|
|
137
|
-
.map(extractor)
|
|
138
|
-
.filter(dfn => !!dfn.name)
|
|
139
|
-
.map(dfn => dfn.name.split(',').map(name => Object.assign({},
|
|
140
|
-
dfn, { name: name.trim() })))
|
|
141
|
-
.reduce((acc, val) => acc.concat(val), [])
|
|
142
|
-
.forEach(dfn => {
|
|
143
|
-
if (res[dfn.name]) {
|
|
144
|
-
if (unique) {
|
|
145
|
-
const merged = mergeDfns(res[dfn.name], dfn);
|
|
146
|
-
if (!merged) {
|
|
147
|
-
throw new Error(`More than one dfn found for CSS property "${dfn.name}" and dfns cannot be merged`);
|
|
148
|
-
}
|
|
149
|
-
res[dfn.name] = merged;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
res[dfn.name].push(dfn);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
res[dfn.name] = unique ? dfn : [dfn];
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
return res;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Extract CSS definitions in tables for the given class name
|
|
165
|
-
* (typically one of `propdef` or `descdef`)
|
|
166
|
-
*/
|
|
167
|
-
const extractTableDfns = (doc, className, options) =>
|
|
168
|
-
extractDfns(doc, 'table.' + className + ':not(.attrdef)', extractTableDfn, options);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Extract CSS definitions in a dl list for the given class name
|
|
173
|
-
* (typically one of `propdef` or `descdef`)
|
|
174
|
-
*/
|
|
175
|
-
const extractDlDfns = (doc, className, options) =>
|
|
176
|
-
extractDfns(doc, 'div.' + className + ' dl', extractDlDfn, options);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Extract value spaces (non-terminal values) defined in the specification
|
|
181
|
-
*/
|
|
182
|
-
const extractValueSpaces = doc => {
|
|
183
|
-
let res = {};
|
|
184
|
-
|
|
185
|
-
const parseProductionRules = rules =>
|
|
186
|
-
rules
|
|
187
|
-
.map(val => val.split(/\n(?=[^\n]*\s?=\s)/m))
|
|
188
|
-
.reduce((acc, val) => acc.concat(val), [])
|
|
189
|
-
.map(line => line.split(/\s?=\s/).map(s => s.trim().replace(/\s+/g, ' ')))
|
|
190
|
-
.filter(val => val[0].match(/^<.*>$|^.*\(\)$/))
|
|
191
|
-
.filter(val => !!val[1])
|
|
192
|
-
.forEach(val => {
|
|
193
|
-
const name = val[0].replace(/^(.*\(\))$/, '<$1>')
|
|
194
|
-
if (!(name in res)) {
|
|
195
|
-
res[name] = { value: val[1] };
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Extract non-terminal value spaces defined in `pre` tags
|
|
200
|
-
// (remove note references as in:
|
|
201
|
-
// https://drafts.csswg.org/css-syntax-3/#the-anb-type)
|
|
202
|
-
parseProductionRules([...doc.querySelectorAll('pre.prod,span.prod')]
|
|
203
|
-
.filter(el => !el.closest(informativeSelector))
|
|
204
|
-
.map(el => {
|
|
205
|
-
[...el.querySelectorAll('sup')]
|
|
206
|
-
.map(sup => sup.parentNode.removeChild(sup));
|
|
207
|
-
return el;
|
|
208
|
-
})
|
|
209
|
-
.map(el => el.textContent));
|
|
210
|
-
|
|
211
|
-
// Complete with non-terminal value spaces defined in `pre` tags without
|
|
212
|
-
// an explicit class, as in:
|
|
213
|
-
// https://drafts.fxtf.org/compositing-1/#ltblendmodegt
|
|
214
|
-
parseProductionRules([...doc.querySelectorAll('pre:not(.idl)')]
|
|
215
|
-
.filter(el => !el.closest(informativeSelector))
|
|
216
|
-
.filter(el => el.querySelector('dfn'))
|
|
217
|
-
.map(el => el.textContent));
|
|
218
|
-
|
|
219
|
-
// Complete with non-terminal value spaces defined in `dt` tags, as in:
|
|
220
|
-
// https://drafts.csswg.org/css-shapes-1/#funcdef-inset
|
|
221
|
-
// https://drafts.csswg.org/css-transforms/#funcdef-transform-matrix
|
|
222
|
-
parseProductionRules([...doc.querySelectorAll('dt > dfn.css, dt > span.prod > dfn.css')]
|
|
223
|
-
.filter(el => !el.closest(informativeSelector))
|
|
224
|
-
.filter(el => el.parentNode.textContent.match(/\s?=\s/))
|
|
225
|
-
.map(el => el.parentNode.textContent));
|
|
226
|
-
|
|
227
|
-
// Complete with function values defined in `dt` tags where definition and
|
|
228
|
-
// value are mixed together, as in:
|
|
229
|
-
// https://drafts.csswg.org/css-overflow-4/#funcdef-text-overflow-fade
|
|
230
|
-
parseProductionRules([...doc.querySelectorAll('dt > dfn.css')]
|
|
231
|
-
.filter(el => !el.closest(informativeSelector))
|
|
232
|
-
.filter(el => el.parentNode.textContent.trim().match(/^[a-zA-Z_][a-zA-Z0-9_\-]+\([^\)]*\)$/))
|
|
233
|
-
.map(el => {
|
|
234
|
-
let fn = el.parentNode.textContent.trim()
|
|
235
|
-
.match(/^([a-zA-Z_][a-zA-Z0-9_\-]+)\([^\)]*\)$/)[1];
|
|
236
|
-
return fn + '() = ' + el.parentNode.textContent;
|
|
237
|
-
}));
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Complete with non-terminal value spaces defined in `.definition`
|
|
241
|
-
// paragraphs, as in:
|
|
242
|
-
// https://svgwg.org/svg2-draft/painting.html#DataTypeDasharray
|
|
243
|
-
parseProductionRules([...doc.querySelectorAll('.definition > dfn')]
|
|
244
|
-
.filter(el => !el.closest(informativeSelector))
|
|
245
|
-
.filter(el => el.parentNode.textContent.match(/\s?=\s/))
|
|
246
|
-
.map(el => el.parentNode.textContent));
|
|
247
|
-
|
|
248
|
-
// Complete with non-terminal value spaces defined in simple paragraphs,
|
|
249
|
-
// as in:
|
|
250
|
-
// https://drafts.csswg.org/css-animations-2/#typedef-single-animation-composition
|
|
251
|
-
// https://drafts.csswg.org/css-transitions/#single-transition-property
|
|
252
|
-
parseProductionRules([...doc.querySelectorAll('p > dfn, div.prod > dfn')]
|
|
253
|
-
.filter(el => !el.closest(informativeSelector))
|
|
254
|
-
.filter(el => el.parentNode.textContent.trim().match(/^<.*>\s?=\s/))
|
|
255
|
-
.map(el => el.parentNode.textContent));
|
|
256
|
-
|
|
257
|
-
// Complete with non-terminal value spaces defined in `dt` tags with
|
|
258
|
-
// production rules (or prose) in `dd` tags, as in:
|
|
259
|
-
// https://drafts.csswg.org/css-fonts/#absolute-size-value
|
|
260
|
-
// https://drafts.csswg.org/css-content/#typedef-content-content-list
|
|
261
|
-
[...doc.querySelectorAll('dt > dfn, dt > var')]
|
|
262
|
-
.filter(el => !el.closest(informativeSelector))
|
|
263
|
-
.filter(el => el.textContent.trim().match(/^<.*>$/))
|
|
264
|
-
.filter(el => {
|
|
265
|
-
let link = el.querySelector('a[href]');
|
|
266
|
-
if (!link) {
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
let href = (link ? link.getAttribute('href') : null);
|
|
270
|
-
return (href === '#' + el.getAttribute('id'));
|
|
271
|
-
})
|
|
272
|
-
.map(el => {
|
|
273
|
-
let dd = el.parentNode;
|
|
274
|
-
while (dd && (dd.nodeName !== 'DD')) {
|
|
275
|
-
dd = dd.nextSibling;
|
|
276
|
-
}
|
|
277
|
-
if (!dd) {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
let code = dd.querySelector('p > code, pre.prod');
|
|
281
|
-
if (code) {
|
|
282
|
-
return {
|
|
283
|
-
name: el.textContent.trim(),
|
|
284
|
-
value: code.textContent.trim().replace(/\s+/g, ' ')
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
// Remove notes, details sections that link to tests, and subsections
|
|
289
|
-
// that go too much into details
|
|
290
|
-
dd = dd.cloneNode(true);
|
|
291
|
-
[...dd.children].forEach(c => {
|
|
292
|
-
if (c.tagName === 'DETAILS' ||
|
|
293
|
-
c.tagName === 'DL' ||
|
|
294
|
-
c.classList.contains('note')) {
|
|
295
|
-
c.remove();
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
return {
|
|
299
|
-
name: el.textContent.trim(),
|
|
300
|
-
prose: dd.textContent.trim().replace(/\s+/g, ' ')
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
.filter(space => !!space)
|
|
305
|
-
.forEach(space => {
|
|
306
|
-
if (!(space.name in res)) {
|
|
307
|
-
res[space.name] = {};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!res[space.name].prose && space.prose) {
|
|
311
|
-
res[space.name].prose = space.prose;
|
|
312
|
-
}
|
|
313
|
-
if (!res[space.name].value && space.value) {
|
|
314
|
-
res[space.name].value = space.value;
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
return res;
|
|
319
|
-
}
|
|
1
|
+
import informativeSelector from './informative-selector.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract the list of CSS definitions in the current spec
|
|
5
|
+
*
|
|
6
|
+
* @function
|
|
7
|
+
* @public
|
|
8
|
+
* @return {Promise} The promise to get an extract of the CSS definitions, or
|
|
9
|
+
* an empty CSS description object if the spec does not contain any CSS
|
|
10
|
+
* definition. The return object will have properties named "properties",
|
|
11
|
+
* "descriptors", and "valuespaces".
|
|
12
|
+
*/
|
|
13
|
+
export default function () {
|
|
14
|
+
let res = {
|
|
15
|
+
properties: extractTableDfns(document, 'propdef', { unique: true }),
|
|
16
|
+
descriptors: extractTableDfns(document, 'descdef', { unique: false }),
|
|
17
|
+
valuespaces: extractValueSpaces(document)
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Try old recipes if we couldn't extract anything
|
|
21
|
+
if ((Object.keys(res.properties).length === 0) &&
|
|
22
|
+
(Object.keys(res.descriptors).length === 0)) {
|
|
23
|
+
res.properties = extractDlDfns(document, 'propdef', { unique: true });
|
|
24
|
+
res.descriptors = extractDlDfns(document, 'descdef', { unique: false });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts a definition label as it appears in a CSS spec to a lower camel
|
|
33
|
+
* case property name.
|
|
34
|
+
*
|
|
35
|
+
* @param {String} label Definition label
|
|
36
|
+
* @return {String} lower camel case property name for the label
|
|
37
|
+
*/
|
|
38
|
+
const dfnLabel2Property = label => label.trim()
|
|
39
|
+
.replace(/:/, '')
|
|
40
|
+
.split(' ')
|
|
41
|
+
.map((str, idx) => (idx === 0) ?
|
|
42
|
+
str.toLowerCase() :
|
|
43
|
+
str.charAt(0).toUpperCase() + str.slice(1))
|
|
44
|
+
.join('');
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract a CSS definition from a table
|
|
49
|
+
*
|
|
50
|
+
* All recent CSS specs should follow that pattern
|
|
51
|
+
*/
|
|
52
|
+
const extractTableDfn = table => {
|
|
53
|
+
let res = {};
|
|
54
|
+
const lines = [...table.querySelectorAll('tr')]
|
|
55
|
+
.map(line => {
|
|
56
|
+
const cleanedLine = line.cloneNode(true);
|
|
57
|
+
const annotations = cleanedLine.querySelectorAll("aside, .mdn-anno");
|
|
58
|
+
annotations.forEach(n => n.remove());
|
|
59
|
+
return {
|
|
60
|
+
name: dfnLabel2Property(cleanedLine.querySelector(':first-child').textContent),
|
|
61
|
+
value: cleanedLine.querySelector('td:last-child').textContent.trim().replace(/\s+/g, ' ')
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
for (let prop of lines) {
|
|
65
|
+
res[prop.name] = prop.value;
|
|
66
|
+
}
|
|
67
|
+
return res;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract a CSS definition from a dl list
|
|
73
|
+
*
|
|
74
|
+
* Used in "old" CSS specs
|
|
75
|
+
*/
|
|
76
|
+
const extractDlDfn = dl => {
|
|
77
|
+
let res = {};
|
|
78
|
+
res.name = dl.querySelector('dt').textContent.replace(/'/g, '').trim();
|
|
79
|
+
const lines = [...dl.querySelectorAll('dd table tr')]
|
|
80
|
+
.map(line => Object.assign({
|
|
81
|
+
name: dfnLabel2Property(line.querySelector(':first-child').textContent),
|
|
82
|
+
value: line.querySelector('td:last-child').textContent.trim().replace(/\s+/g, ' ')
|
|
83
|
+
}));
|
|
84
|
+
for (let prop of lines) {
|
|
85
|
+
res[prop.name] = prop.value;
|
|
86
|
+
}
|
|
87
|
+
return res;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Merges CSS definitions for the same property into one
|
|
93
|
+
*
|
|
94
|
+
* The function runs a few simple sanity checks on the definitions. More checks
|
|
95
|
+
* would be needed to fully validate that merging is fine.
|
|
96
|
+
*
|
|
97
|
+
* The function returns null if CSS definitions cannot be merged.
|
|
98
|
+
*/
|
|
99
|
+
const mergeDfns = (dfn1, dfn2) => {
|
|
100
|
+
// Definitions should be about the same property
|
|
101
|
+
if (dfn1.name !== dfn2.name) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// There should never be two base definitions for the same CSS property
|
|
106
|
+
if (dfn1.value && dfn2.value) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [baseDfn, partialDfn] = dfn2.value ? [dfn2, dfn1] : [dfn1, dfn2];
|
|
111
|
+
if ((!baseDfn.value && !baseDfn.newValues) ||
|
|
112
|
+
!partialDfn.newValues ||
|
|
113
|
+
(partialDfn.initial && partialDfn.initial !== baseDfn.initial)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const merged = Object.assign(baseDfn);
|
|
118
|
+
if (merged.value) {
|
|
119
|
+
merged.value += ` | ${partialDfn.newValues}`;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
merged.newValues += ` | ${partialDfn.newValues}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return merged;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract CSS definitions in a spec using the given CSS selector and extractor
|
|
131
|
+
*/
|
|
132
|
+
const extractDfns = (doc, selector, extractor, { unique } = { unique: true }) => {
|
|
133
|
+
let res = {};
|
|
134
|
+
[...doc.querySelectorAll(selector)]
|
|
135
|
+
.filter(el => !el.closest(informativeSelector))
|
|
136
|
+
.filter(el => !el.querySelector('ins, del'))
|
|
137
|
+
.map(extractor)
|
|
138
|
+
.filter(dfn => !!dfn.name)
|
|
139
|
+
.map(dfn => dfn.name.split(',').map(name => Object.assign({},
|
|
140
|
+
dfn, { name: name.trim() })))
|
|
141
|
+
.reduce((acc, val) => acc.concat(val), [])
|
|
142
|
+
.forEach(dfn => {
|
|
143
|
+
if (res[dfn.name]) {
|
|
144
|
+
if (unique) {
|
|
145
|
+
const merged = mergeDfns(res[dfn.name], dfn);
|
|
146
|
+
if (!merged) {
|
|
147
|
+
throw new Error(`More than one dfn found for CSS property "${dfn.name}" and dfns cannot be merged`);
|
|
148
|
+
}
|
|
149
|
+
res[dfn.name] = merged;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
res[dfn.name].push(dfn);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
res[dfn.name] = unique ? dfn : [dfn];
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return res;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract CSS definitions in tables for the given class name
|
|
165
|
+
* (typically one of `propdef` or `descdef`)
|
|
166
|
+
*/
|
|
167
|
+
const extractTableDfns = (doc, className, options) =>
|
|
168
|
+
extractDfns(doc, 'table.' + className + ':not(.attrdef)', extractTableDfn, options);
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract CSS definitions in a dl list for the given class name
|
|
173
|
+
* (typically one of `propdef` or `descdef`)
|
|
174
|
+
*/
|
|
175
|
+
const extractDlDfns = (doc, className, options) =>
|
|
176
|
+
extractDfns(doc, 'div.' + className + ' dl', extractDlDfn, options);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract value spaces (non-terminal values) defined in the specification
|
|
181
|
+
*/
|
|
182
|
+
const extractValueSpaces = doc => {
|
|
183
|
+
let res = {};
|
|
184
|
+
|
|
185
|
+
const parseProductionRules = rules =>
|
|
186
|
+
rules
|
|
187
|
+
.map(val => val.split(/\n(?=[^\n]*\s?=\s)/m))
|
|
188
|
+
.reduce((acc, val) => acc.concat(val), [])
|
|
189
|
+
.map(line => line.split(/\s?=\s/).map(s => s.trim().replace(/\s+/g, ' ')))
|
|
190
|
+
.filter(val => val[0].match(/^<.*>$|^.*\(\)$/))
|
|
191
|
+
.filter(val => !!val[1])
|
|
192
|
+
.forEach(val => {
|
|
193
|
+
const name = val[0].replace(/^(.*\(\))$/, '<$1>')
|
|
194
|
+
if (!(name in res)) {
|
|
195
|
+
res[name] = { value: val[1] };
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Extract non-terminal value spaces defined in `pre` tags
|
|
200
|
+
// (remove note references as in:
|
|
201
|
+
// https://drafts.csswg.org/css-syntax-3/#the-anb-type)
|
|
202
|
+
parseProductionRules([...doc.querySelectorAll('pre.prod,span.prod')]
|
|
203
|
+
.filter(el => !el.closest(informativeSelector))
|
|
204
|
+
.map(el => {
|
|
205
|
+
[...el.querySelectorAll('sup')]
|
|
206
|
+
.map(sup => sup.parentNode.removeChild(sup));
|
|
207
|
+
return el;
|
|
208
|
+
})
|
|
209
|
+
.map(el => el.textContent));
|
|
210
|
+
|
|
211
|
+
// Complete with non-terminal value spaces defined in `pre` tags without
|
|
212
|
+
// an explicit class, as in:
|
|
213
|
+
// https://drafts.fxtf.org/compositing-1/#ltblendmodegt
|
|
214
|
+
parseProductionRules([...doc.querySelectorAll('pre:not(.idl)')]
|
|
215
|
+
.filter(el => !el.closest(informativeSelector))
|
|
216
|
+
.filter(el => el.querySelector('dfn'))
|
|
217
|
+
.map(el => el.textContent));
|
|
218
|
+
|
|
219
|
+
// Complete with non-terminal value spaces defined in `dt` tags, as in:
|
|
220
|
+
// https://drafts.csswg.org/css-shapes-1/#funcdef-inset
|
|
221
|
+
// https://drafts.csswg.org/css-transforms/#funcdef-transform-matrix
|
|
222
|
+
parseProductionRules([...doc.querySelectorAll('dt > dfn.css, dt > span.prod > dfn.css')]
|
|
223
|
+
.filter(el => !el.closest(informativeSelector))
|
|
224
|
+
.filter(el => el.parentNode.textContent.match(/\s?=\s/))
|
|
225
|
+
.map(el => el.parentNode.textContent));
|
|
226
|
+
|
|
227
|
+
// Complete with function values defined in `dt` tags where definition and
|
|
228
|
+
// value are mixed together, as in:
|
|
229
|
+
// https://drafts.csswg.org/css-overflow-4/#funcdef-text-overflow-fade
|
|
230
|
+
parseProductionRules([...doc.querySelectorAll('dt > dfn.css')]
|
|
231
|
+
.filter(el => !el.closest(informativeSelector))
|
|
232
|
+
.filter(el => el.parentNode.textContent.trim().match(/^[a-zA-Z_][a-zA-Z0-9_\-]+\([^\)]*\)$/))
|
|
233
|
+
.map(el => {
|
|
234
|
+
let fn = el.parentNode.textContent.trim()
|
|
235
|
+
.match(/^([a-zA-Z_][a-zA-Z0-9_\-]+)\([^\)]*\)$/)[1];
|
|
236
|
+
return fn + '() = ' + el.parentNode.textContent;
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
// Complete with non-terminal value spaces defined in `.definition`
|
|
241
|
+
// paragraphs, as in:
|
|
242
|
+
// https://svgwg.org/svg2-draft/painting.html#DataTypeDasharray
|
|
243
|
+
parseProductionRules([...doc.querySelectorAll('.definition > dfn')]
|
|
244
|
+
.filter(el => !el.closest(informativeSelector))
|
|
245
|
+
.filter(el => el.parentNode.textContent.match(/\s?=\s/))
|
|
246
|
+
.map(el => el.parentNode.textContent));
|
|
247
|
+
|
|
248
|
+
// Complete with non-terminal value spaces defined in simple paragraphs,
|
|
249
|
+
// as in:
|
|
250
|
+
// https://drafts.csswg.org/css-animations-2/#typedef-single-animation-composition
|
|
251
|
+
// https://drafts.csswg.org/css-transitions/#single-transition-property
|
|
252
|
+
parseProductionRules([...doc.querySelectorAll('p > dfn, div.prod > dfn')]
|
|
253
|
+
.filter(el => !el.closest(informativeSelector))
|
|
254
|
+
.filter(el => el.parentNode.textContent.trim().match(/^<.*>\s?=\s/))
|
|
255
|
+
.map(el => el.parentNode.textContent));
|
|
256
|
+
|
|
257
|
+
// Complete with non-terminal value spaces defined in `dt` tags with
|
|
258
|
+
// production rules (or prose) in `dd` tags, as in:
|
|
259
|
+
// https://drafts.csswg.org/css-fonts/#absolute-size-value
|
|
260
|
+
// https://drafts.csswg.org/css-content/#typedef-content-content-list
|
|
261
|
+
[...doc.querySelectorAll('dt > dfn, dt > var')]
|
|
262
|
+
.filter(el => !el.closest(informativeSelector))
|
|
263
|
+
.filter(el => el.textContent.trim().match(/^<.*>$/))
|
|
264
|
+
.filter(el => {
|
|
265
|
+
let link = el.querySelector('a[href]');
|
|
266
|
+
if (!link) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
let href = (link ? link.getAttribute('href') : null);
|
|
270
|
+
return (href === '#' + el.getAttribute('id'));
|
|
271
|
+
})
|
|
272
|
+
.map(el => {
|
|
273
|
+
let dd = el.parentNode;
|
|
274
|
+
while (dd && (dd.nodeName !== 'DD')) {
|
|
275
|
+
dd = dd.nextSibling;
|
|
276
|
+
}
|
|
277
|
+
if (!dd) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
let code = dd.querySelector('p > code, pre.prod');
|
|
281
|
+
if (code) {
|
|
282
|
+
return {
|
|
283
|
+
name: el.textContent.trim(),
|
|
284
|
+
value: code.textContent.trim().replace(/\s+/g, ' ')
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Remove notes, details sections that link to tests, and subsections
|
|
289
|
+
// that go too much into details
|
|
290
|
+
dd = dd.cloneNode(true);
|
|
291
|
+
[...dd.children].forEach(c => {
|
|
292
|
+
if (c.tagName === 'DETAILS' ||
|
|
293
|
+
c.tagName === 'DL' ||
|
|
294
|
+
c.classList.contains('note')) {
|
|
295
|
+
c.remove();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
name: el.textContent.trim(),
|
|
300
|
+
prose: dd.textContent.trim().replace(/\s+/g, ' ')
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
.filter(space => !!space)
|
|
305
|
+
.forEach(space => {
|
|
306
|
+
if (!(space.name in res)) {
|
|
307
|
+
res[space.name] = {};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!res[space.name].prose && space.prose) {
|
|
311
|
+
res[space.name].prose = space.prose;
|
|
312
|
+
}
|
|
313
|
+
if (!res[space.name].value && space.value) {
|
|
314
|
+
res[space.name].value = space.value;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return res;
|
|
319
|
+
}
|