Skip to content

Commit b65179a

Browse files
authored
[CSS post-processing] Merge scopes when possible (breaking) (#1858)
A CSS feature may be defined within a single dfn for multiple scopes. These features appeared in separate entries in the consolidated file. To ease indexing and lookup by feature name, this update further merges scopes. This does not fully turn feature names into identifiers, but only 8 features remain that use the same name but have different definitions for different scopes. Some (but not all) of them, we should be able to get rid of through spec fixes and possible improvements to the curation logic in Webref. See list and details at: w3c/webref#1519 (comment) This is a breaking change since it turns `for` keys into arrays in the consolidated file. Side note: the update also fixes an unrelated comment in the code.
1 parent ec2d2ad commit b65179a

File tree

3 files changed

+126
-9
lines changed

3 files changed

+126
-9
lines changed

schemas/postprocessing/css.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
"$schema": "http://json-schema.org/schema#",
33
"$id": "https://github.com/w3c/reffy/blob/main/schemas/postprocessing/css.json",
44

5+
"$defs": {
6+
"scopes": {
7+
"type": "array",
8+
"items": {
9+
"type": "string"
10+
},
11+
"minItems": 1
12+
}
13+
},
14+
515
"type": "object",
616
"additionalProperties": false,
717
"required": ["atrules", "functions", "properties", "selectors", "types"],
@@ -42,7 +52,7 @@
4252
"additionalProperties": false,
4353
"properties": {
4454
"name": { "type": "string", "pattern": "^<[^>]+>$|^.*()$" },
45-
"for": { "type": "string" },
55+
"for": { "$ref": "#/$defs/scopes" },
4656
"href": { "$ref": "../common.json#/$defs/url" },
4757
"type": { "type": "string", "enum": ["function"] },
4858
"prose": { "type": "string" },
@@ -91,7 +101,7 @@
91101
"additionalProperties": false,
92102
"properties": {
93103
"name": { "type": "string", "pattern": "^<[^>]+>$|^.*()$" },
94-
"for": { "type": "string" },
104+
"for": { "$ref": "#/$defs/scopes" },
95105
"href": { "$ref": "../common.json#/$defs/url" },
96106
"type": { "type": "string", "enum": ["type"] },
97107
"prose": { "type": "string" },

src/postprocessing/cssmerge.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* In CSS extracts, functions and types that are defined for another construct
1414
* appear under the `values` key of that construct entry. In the resulting
1515
* 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
16+
* and get a `for` key that contains the list of constructs that they are
1717
* defined for.
1818
*
1919
* CSS properties that are defined in one spec and extended in other specs get
@@ -178,8 +178,8 @@ export default {
178178
// (that has some known syntax) in the most recent level. Move that base
179179
// definition to the beginning of the array and get rid of other base
180180
// definitions.
181-
// (Note: the code throws an error if duplicates of base definitions in
182-
// unrelated specs still exist)
181+
// (Note: the code chooses one definition if duplicates of base
182+
// definitions in unrelated specs still exist)
183183
for (const [name, dfns] of Object.entries(featureDfns)) {
184184
let actualDfns = dfns.filter(dfn => dfn.value);
185185
if (actualDfns.length === 0) {
@@ -296,6 +296,32 @@ export default {
296296
}
297297
}
298298

299+
// The same feature may be defined for multiple scopes.
300+
// To ease indexing and lookup by feature name, let's merge the scopes
301+
// when possible, turning the `for` key into an array. This will not get
302+
// rid of all scoping duplicates, but should still make the feature name
303+
// unique for all but a handful of them.
304+
categorized[category] = categorized[category]
305+
.map((feature, idx, list) => {
306+
const first = list.find(f => f.href === feature.href);
307+
if (first === feature) {
308+
if (feature.for) {
309+
feature.for = [feature.for];
310+
}
311+
return feature;
312+
}
313+
// Not the first time we see this feature, let's merge scopes.
314+
// Both scopes should be defined as there is no way to author a
315+
// single dfn that defines a feature both as scoped and unscoped.
316+
if (!first.for || !feature.for) {
317+
throw new Error(`Feature ${feature.name} defined both as unscoped and scoped within the same dfn, see ${feature.href}`);
318+
}
319+
first.for.push(feature.for);
320+
first.for.sort();
321+
return null;
322+
})
323+
.filter(feature => !!feature);
324+
299325
// Let's sort lists before we return to ease human-readability and
300326
// avoid non-substantive diff
301327
for (const feature of categorized[category]) {
@@ -313,13 +339,14 @@ export default {
313339

314340

315341
/**
316-
* Return the identifier of a feature, taking scoping construct into account
342+
* Return the identifier of a feature, taking scoping construct(s) into account
317343
* when needed.
318344
*/
319345
function getFeatureId(feature) {
320346
let featureId = feature.name;
321347
if (feature.for) {
322-
featureId += ' for ' + feature.for;
348+
featureId += ' for ' +
349+
(Array.isArray(feature.for) ? feature.for.join(',') : feature.for);
323350
}
324351
return featureId;
325352
}

test/merge-css.js

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,12 @@ describe('CSS extracts consolidation', function () {
193193
assert.deepEqual(result, Object.assign({}, emptyMerged, {
194194
functions: [
195195
Object.assign({}, functionEnv, {
196-
for: '<track-repeat>'
196+
for: ['<track-repeat>']
197197
})
198198
],
199199
types: [
200200
Object.assign({}, type1, {
201-
for: '<track-repeat>'
201+
for: ['<track-repeat>']
202202
}),
203203
{
204204
name: '<track-repeat>',
@@ -504,4 +504,84 @@ describe('CSS extracts consolidation', function () {
504504
property1
505505
]);
506506
});
507+
508+
it('merges scopes when possible', async () => {
509+
// We'll have 3 definitions of the `env()` functions: one unscoped,
510+
// another one scoped to two types, and a third one scoped to yet another
511+
// type. Note syntaxes must differ otherwise merge will drop duplicates.
512+
const scopedFunctionEnv = Object.assign({}, functionEnv, {
513+
href: 'https://drafts.csswg.org/css-first-1/#funcdef-env',
514+
value: 'env(first)'
515+
});
516+
const otherScopedFunctionEnv = Object.assign({}, functionEnv, {
517+
href: 'https://drafts.csswg.org/css-second-1/#funcdef-env',
518+
value: 'env(second)'
519+
});
520+
const results = structuredClone([
521+
{
522+
shortname: 'css-stuff-1',
523+
series: { shortname: 'css-stuff' },
524+
seriesVersion: '1',
525+
css: Object.assign({}, emptyExtract, {
526+
values: [
527+
functionEnv,
528+
{
529+
name: '<track-repeat>',
530+
href: 'https://drafts.csswg.org/css-grid-2/#typedef-track-repeat',
531+
type: 'type',
532+
values: [
533+
scopedFunctionEnv
534+
]
535+
},
536+
{
537+
name: '<repeat-ad-libitum>',
538+
href: 'https://drafts.csswg.org/css-grid-2/#typedef-repeat-ad-libitum',
539+
type: 'type',
540+
values: [
541+
scopedFunctionEnv
542+
]
543+
},
544+
{
545+
name: '<another-repeat>',
546+
href: 'https://drafts.csswg.org/css-grid-2/#typedef-another-repeat',
547+
type: 'type',
548+
values: [
549+
otherScopedFunctionEnv
550+
]
551+
}
552+
]
553+
})
554+
}
555+
]);
556+
const result = await cssmerge.run({ results });
557+
console.log(JSON.stringify(result, null, 2));
558+
assert.deepEqual(result, Object.assign({}, emptyMerged, {
559+
functions: [
560+
functionEnv,
561+
Object.assign({}, otherScopedFunctionEnv, {
562+
for: ['<another-repeat>']
563+
}),
564+
Object.assign({}, scopedFunctionEnv, {
565+
for: ['<repeat-ad-libitum>', '<track-repeat>']
566+
}),
567+
],
568+
types: [
569+
{
570+
name: '<another-repeat>',
571+
href: 'https://drafts.csswg.org/css-grid-2/#typedef-another-repeat',
572+
type: 'type'
573+
},
574+
{
575+
name: '<repeat-ad-libitum>',
576+
href: 'https://drafts.csswg.org/css-grid-2/#typedef-repeat-ad-libitum',
577+
type: 'type'
578+
},
579+
{
580+
name: '<track-repeat>',
581+
href: 'https://drafts.csswg.org/css-grid-2/#typedef-track-repeat',
582+
type: 'type'
583+
}
584+
]
585+
}));
586+
});
507587
});

0 commit comments

Comments
 (0)