Skip to content

Commit 5a0c620

Browse files
authored
[CSS consolidation] Add extended to store list of extension URLs (#1904)
This adds a new `extended` key to entries in the consolidated CSS that lists URLs of specs that extend the base definition. For properties, the URLs target the actual extended definition. For functions and types, the URLs merely target the spec without any specific fragment, because there is no actual definition and we don't know where the extension appears at the consolidation phase. Note this also prepares the consolidation to deal with extended functions and types. As opposed to extensions of properties where new values are added to the syntax, function/type extensions re-define the entire syntax. Reffy does not support extraction of function/type extensions in itself yet, so that part of the update is not going to be used for now.
1 parent dba3382 commit 5a0c620

File tree

3 files changed

+120
-17
lines changed

3 files changed

+120
-17
lines changed

schemas/postprocessing/css.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
"type": "string"
1010
},
1111
"minItems": 1
12+
},
13+
14+
"extended": {
15+
"type": "array",
16+
"items": {
17+
"$ref": "../common.json#/$defs/url"
18+
}
1219
}
1320
},
1421

@@ -25,6 +32,7 @@
2532
"properties": {
2633
"name": { "type": "string", "pattern": "^@" },
2734
"href": { "$ref": "../common.json#/$defs/url" },
35+
"extended": { "$ref": "#/$defs/extended" },
2836
"syntax": { "$ref": "../common.json#/$defs/cssValue" },
2937
"prose": { "type": "string" },
3038
"descriptors": {
@@ -54,6 +62,7 @@
5462
"name": { "type": "string", "pattern": "^.*()$" },
5563
"for": { "$ref": "#/$defs/scopes" },
5664
"href": { "$ref": "../common.json#/$defs/url" },
65+
"extended": { "$ref": "#/$defs/extended" },
5766
"prose": { "type": "string" },
5867
"syntax": { "$ref": "../common.json#/$defs/cssValue" }
5968
}
@@ -68,6 +77,7 @@
6877
"properties": {
6978
"name": { "$ref": "../common.json#/$defs/cssPropertyName" },
7079
"href": { "$ref": "../common.json#/$defs/url" },
80+
"extended": { "$ref": "#/$defs/extended" },
7181
"syntax": { "$ref": "../common.json#/$defs/cssValue" },
7282
"legacyAliasOf": { "$ref": "../common.json#/$defs/cssPropertyName" },
7383
"styleDeclaration": {
@@ -87,6 +97,7 @@
8797
"properties": {
8898
"name": { "$ref": "../common.json#/$defs/cssPropertyName" },
8999
"href": { "$ref": "../common.json#/$defs/url" },
100+
"extended": { "$ref": "#/$defs/extended" },
90101
"prose": { "type": "string" },
91102
"syntax": { "$ref": "../common.json#/$defs/cssValue" }
92103
}
@@ -102,6 +113,7 @@
102113
"name": { "type": "string", "pattern": "^[a-zA-Z0-9-\\+\\(\\)\\[\\]\\{\\}]+$" },
103114
"for": { "$ref": "#/$defs/scopes" },
104115
"href": { "$ref": "../common.json#/$defs/url" },
116+
"extended": { "$ref": "#/$defs/extended" },
105117
"prose": { "type": "string" },
106118
"syntax": { "$ref": "../common.json#/$defs/cssValue" }
107119
}

src/postprocessing/cssmerge.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ export default {
111111

112112
// The job is "almost" done but we now need to de-duplicate entries.
113113
// Duplicated entries exist when:
114-
// - A property is defined in one spec and extended in other specs. We'll
115-
// consolidate the entries (and syntaxes) to get back to a single entry.
114+
// - A property, function or type is defined in one spec and extended in
115+
// other specs. We'll consolidate the entries (and syntaxes) to get back to
116+
// a single entry.
116117
// - An at-rule is defined in one spec. Additional descriptors are defined
117118
// in other specs. We'll consolidate the entries similarly.
118119
// - A descriptor is defined in one level of a spec series, and re-defined
@@ -179,13 +180,22 @@ export default {
179180
// Identify the base definition for each feature, using the definition
180181
// (that has some known syntax) in the most recent level. Move that base
181182
// definition to the beginning of the array and get rid of other base
182-
// definitions.
183-
// (Note: the code chooses one definition if duplicates of base
184-
// definitions in unrelated specs still exist)
183+
// definitions. Notes:
184+
// - For properties, an extension is an entry with a `newValues` key.
185+
// - For functions and types, an extension is an entry without an `href`
186+
// key (the spec that extends the base type does not contain any `<dfn>`)
187+
// - The code chooses one definition if duplicates of base definitions in
188+
// unrelated specs still exist.
185189
for (const [name, dfns] of Object.entries(featureDfns)) {
186-
let actualDfns = dfns.filter(dfn => dfn.syntax);
190+
let actualDfns = dfns.filter(dfn => dfn.href && dfn.syntax);
187191
if (actualDfns.length === 0) {
188-
actualDfns = dfns.filter(dfn => !dfn.newValues);
192+
actualDfns = dfns.filter(dfn => dfn.href && !dfn.newValues);
193+
}
194+
if (actualDfns.length === 0) {
195+
// No base definition, not a real type, let's discard it
196+
// (problem should be captured through tests on individual extracts)
197+
delete featureDfns[name];
198+
continue;
189199
}
190200
const best = actualDfns.reduce((dfn1, dfn2) => {
191201
if (dfn1.spec.series.shortname !== dfn2.spec.series.shortname) {
@@ -199,6 +209,7 @@ export default {
199209
return dfn1;
200210
}
201211
});
212+
best.extended = [];
202213
featureDfns[name] = [best].concat(
203214
dfns.filter(dfn => !actualDfns.includes(dfn))
204215
);
@@ -239,6 +250,18 @@ export default {
239250
continue;
240251
}
241252
baseDfn.syntax += ' | ' + dfn.newValues;
253+
baseDfn.extended.push(dfn.href);
254+
}
255+
else if (dfn.syntax) {
256+
// Extensions of functions and types are *re-definitions* in
257+
// practice, new syntax overrides the base one. There should be
258+
// only one such extension in unrelated specs, the code assumes
259+
// that some sort of curation already took place, and picks up
260+
// a winner randomly.
261+
// Note: we don't have any `href` info for functions/types
262+
// extensions, so we'll just use the URL of the crawled spec.
263+
baseDfn.syntax = dfn.syntax;
264+
baseDfn.extended = [dfn.spec.crawled ?? dfn.spec.url];
242265
}
243266
if (baseDfn.descriptors && dfn.descriptors?.length > 0) {
244267
baseDfn.descriptors.push(...dfn.descriptors.filter(desc =>

test/merge-css.js

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const descriptorBase = {
5454
type: 'discrete'
5555
};
5656

57-
const descriptorExtended = Object.assign({}, descriptorBase, {
57+
const descriptorExtension = Object.assign({}, descriptorBase, {
5858
href: 'https://drafts.csswg.org/css-stuff-2/#descdef-descriptor',
5959
value: 'extended'
6060
});
@@ -77,6 +77,7 @@ const property1 = {
7777

7878
const propertyLegacy = {
7979
name: 'good-old-overlay',
80+
href: 'https://compat.spec.whatwg.org/#good-old-overlay',
8081
legacyAliasOf: 'overlay'
8182
};
8283

@@ -93,6 +94,12 @@ const type1 = {
9394
value: 'repeat | space | round | no-repeat'
9495
};
9596

97+
const type1Extension = {
98+
name: '<repetition>',
99+
type: 'type',
100+
value: 'bis repetita'
101+
};
102+
96103
const functionVar = {
97104
name: 'var()',
98105
href: 'https://drafts.csswg.org/css-variables-2/#funcdef-var',
@@ -113,14 +120,17 @@ const functionEnv = {
113120
* the outputs to the inputs directly. This conversion function takes
114121
* some object or value and converts it to ease comparisons.
115122
*/
116-
function conv(entry) {
123+
function conv(entry, parentKey) {
117124
const res = {};
118125
if (typeof entry !== 'object') {
119126
return entry;
120127
}
128+
if (entry.href && !entry.extended && parentKey !== 'descriptors') {
129+
entry.extended = [];
130+
}
121131
for (const [key, value] of Object.entries(entry)) {
122132
if (Array.isArray(value)) {
123-
res[key] = value.map(conv);
133+
res[key] = value.map(v => conv(v, key));
124134
}
125135
else if (key === 'value') {
126136
res.syntax = value;
@@ -309,7 +319,8 @@ describe('CSS extracts consolidation', function () {
309319
properties: [
310320
Object.assign({}, property1, {
311321
value: null,
312-
newValues: 'train'
322+
newValues: 'train',
323+
href: 'https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou'
313324
})
314325
]
315326
})
@@ -318,7 +329,8 @@ describe('CSS extracts consolidation', function () {
318329
const result = await cssmerge.run({ results });
319330
assert.deepEqual(result.properties, [
320331
Object.assign({}, conv(property1), {
321-
syntax: 'none | auto | train'
332+
syntax: 'none | auto | train',
333+
extended: ['https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou']
322334
})
323335
]);
324336
});
@@ -344,7 +356,8 @@ describe('CSS extracts consolidation', function () {
344356
properties: [
345357
Object.assign({}, property1, {
346358
value: null,
347-
newValues: 'train'
359+
newValues: 'train',
360+
href: 'https://drafts.csswg.org/css-otherstuff-1/#tchou-tchou'
348361
})
349362
]
350363
})
@@ -357,7 +370,8 @@ describe('CSS extracts consolidation', function () {
357370
properties: [
358371
Object.assign({}, property1, {
359372
value: null,
360-
newValues: 'train'
373+
newValues: 'train',
374+
href: 'https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou'
361375
})
362376
]
363377
})
@@ -366,7 +380,8 @@ describe('CSS extracts consolidation', function () {
366380
const result = await cssmerge.run({ results });
367381
assert.deepEqual(result.properties, [
368382
Object.assign({}, conv(property1), {
369-
syntax: 'none | auto | train'
383+
syntax: 'none | auto | train',
384+
extended: ['https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou']
370385
})
371386
]);
372387
});
@@ -432,7 +447,7 @@ describe('CSS extracts consolidation', function () {
432447
css: Object.assign({}, emptyExtract, {
433448
atrules: [
434449
Object.assign({}, atrule2, {
435-
descriptors: [descriptorExtended]
450+
descriptors: [descriptorExtension]
436451
})
437452
]
438453
})
@@ -442,7 +457,7 @@ describe('CSS extracts consolidation', function () {
442457
assert.deepEqual(result.atrules, [
443458
conv(Object.assign({}, atrule2, {
444459
syntax: '@media foo',
445-
descriptors: [descriptorExtended]
460+
descriptors: [descriptorExtension]
446461
}))
447462
]);
448463
});
@@ -630,4 +645,57 @@ describe('CSS extracts consolidation', function () {
630645
]
631646
})));
632647
});
648+
649+
it('merges extended types', async () => {
650+
const results = structuredClone([
651+
{
652+
shortname: 'css-stuff-1',
653+
series: { shortname: 'css-stuff' },
654+
seriesVersion: '1',
655+
crawled: 'https://drafts.csswg.org/css-stuff-1/',
656+
css: Object.assign({}, emptyExtract, {
657+
values: [
658+
Object.assign({}, type1)
659+
]
660+
})
661+
},
662+
{
663+
shortname: 'css-otherstuff-1',
664+
series: { shortname: 'css-otherstuff' },
665+
seriesVersion: '1',
666+
crawled: 'https://drafts.csswg.org/css-otherstuff-1/',
667+
css: Object.assign({}, emptyExtract, {
668+
values: [
669+
Object.assign({}, type1Extension)
670+
]
671+
})
672+
},
673+
]);
674+
const result = await cssmerge.run({ results });
675+
assert.deepEqual(result, conv(Object.assign({}, emptyMerged, {
676+
types: [
677+
Object.assign({}, conv(type1), {
678+
syntax: type1Extension.value,
679+
extended: ['https://drafts.csswg.org/css-otherstuff-1/']
680+
})
681+
]
682+
})));
683+
});
684+
685+
it('discards type extensions without a base definition', async () => {
686+
const results = structuredClone([
687+
{
688+
shortname: 'css-stuff-1',
689+
series: { shortname: 'css-stuff' },
690+
seriesVersion: '1',
691+
css: Object.assign({}, emptyExtract, {
692+
values: [
693+
Object.assign({}, type1Extension)
694+
]
695+
})
696+
}
697+
]);
698+
const result = await cssmerge.run({ results });
699+
assert.deepEqual(result, conv(emptyMerged));
700+
});
633701
});

0 commit comments

Comments
 (0)