Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions schemas/postprocessing/css.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
"type": "string"
},
"minItems": 1
},

"extended": {
"type": "array",
"items": {
"$ref": "../common.json#/$defs/url"
}
}
},

Expand All @@ -25,6 +32,7 @@
"properties": {
"name": { "type": "string", "pattern": "^@" },
"href": { "$ref": "../common.json#/$defs/url" },
"extended": { "$ref": "#/$defs/extended" },
"syntax": { "$ref": "../common.json#/$defs/cssValue" },
"prose": { "type": "string" },
"descriptors": {
Expand Down Expand Up @@ -54,6 +62,7 @@
"name": { "type": "string", "pattern": "^.*()$" },
"for": { "$ref": "#/$defs/scopes" },
"href": { "$ref": "../common.json#/$defs/url" },
"extended": { "$ref": "#/$defs/extended" },
"prose": { "type": "string" },
"syntax": { "$ref": "../common.json#/$defs/cssValue" }
}
Expand All @@ -68,6 +77,7 @@
"properties": {
"name": { "$ref": "../common.json#/$defs/cssPropertyName" },
"href": { "$ref": "../common.json#/$defs/url" },
"extended": { "$ref": "#/$defs/extended" },
"syntax": { "$ref": "../common.json#/$defs/cssValue" },
"legacyAliasOf": { "$ref": "../common.json#/$defs/cssPropertyName" },
"styleDeclaration": {
Expand All @@ -87,6 +97,7 @@
"properties": {
"name": { "$ref": "../common.json#/$defs/cssPropertyName" },
"href": { "$ref": "../common.json#/$defs/url" },
"extended": { "$ref": "#/$defs/extended" },
"prose": { "type": "string" },
"syntax": { "$ref": "../common.json#/$defs/cssValue" }
}
Expand All @@ -102,6 +113,7 @@
"name": { "type": "string", "pattern": "^[a-zA-Z0-9-\\+\\(\\)\\[\\]\\{\\}]+$" },
"for": { "$ref": "#/$defs/scopes" },
"href": { "$ref": "../common.json#/$defs/url" },
"extended": { "$ref": "#/$defs/extended" },
"prose": { "type": "string" },
"syntax": { "$ref": "../common.json#/$defs/cssValue" }
}
Expand Down
37 changes: 30 additions & 7 deletions src/postprocessing/cssmerge.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ export default {

// The job is "almost" done but we now need to de-duplicate entries.
// Duplicated entries exist when:
// - A property is defined in one spec and extended in other specs. We'll
// consolidate the entries (and syntaxes) to get back to a single entry.
// - A property, function or type is defined in one spec and extended in
// other specs. We'll consolidate the entries (and syntaxes) to get back to
// a single entry.
// - An at-rule is defined in one spec. Additional descriptors are defined
// in other specs. We'll consolidate the entries similarly.
// - A descriptor is defined in one level of a spec series, and re-defined
Expand Down Expand Up @@ -179,13 +180,22 @@ export default {
// Identify the base definition for each feature, using the definition
// (that has some known syntax) in the most recent level. Move that base
// definition to the beginning of the array and get rid of other base
// definitions.
// (Note: the code chooses one definition if duplicates of base
// definitions in unrelated specs still exist)
// definitions. Notes:
// - For properties, an extension is an entry with a `newValues` key.
// - For functions and types, an extension is an entry without an `href`
// key (the spec that extends the base type does not contain any `<dfn>`)
// - The code chooses one definition if duplicates of base definitions in
// unrelated specs still exist.
for (const [name, dfns] of Object.entries(featureDfns)) {
let actualDfns = dfns.filter(dfn => dfn.syntax);
let actualDfns = dfns.filter(dfn => dfn.href && dfn.syntax);
if (actualDfns.length === 0) {
actualDfns = dfns.filter(dfn => !dfn.newValues);
actualDfns = dfns.filter(dfn => dfn.href && !dfn.newValues);
}
if (actualDfns.length === 0) {
// No base definition, not a real type, let's discard it
// (problem should be captured through tests on individual extracts)
delete featureDfns[name];
continue;
}
const best = actualDfns.reduce((dfn1, dfn2) => {
if (dfn1.spec.series.shortname !== dfn2.spec.series.shortname) {
Expand All @@ -199,6 +209,7 @@ export default {
return dfn1;
}
});
best.extended = [];
featureDfns[name] = [best].concat(
dfns.filter(dfn => !actualDfns.includes(dfn))
);
Expand Down Expand Up @@ -239,6 +250,18 @@ export default {
continue;
}
baseDfn.syntax += ' | ' + dfn.newValues;
baseDfn.extended.push(dfn.href);
}
else if (dfn.syntax) {
// Extensions of functions and types are *re-definitions* in
// practice, new syntax overrides the base one. There should be
// only one such extension in unrelated specs, the code assumes
// that some sort of curation already took place, and picks up
// a winner randomly.
// Note: we don't have any `href` info for functions/types
// extensions, so we'll just use the URL of the crawled spec.
baseDfn.syntax = dfn.syntax;
baseDfn.extended = [dfn.spec.crawled ?? dfn.spec.url];
}
if (baseDfn.descriptors && dfn.descriptors?.length > 0) {
baseDfn.descriptors.push(...dfn.descriptors.filter(desc =>
Expand Down
88 changes: 78 additions & 10 deletions test/merge-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const descriptorBase = {
type: 'discrete'
};

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

const propertyLegacy = {
name: 'good-old-overlay',
href: 'https://compat.spec.whatwg.org/#good-old-overlay',
legacyAliasOf: 'overlay'
};

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

const type1Extension = {
name: '<repetition>',
type: 'type',
value: 'bis repetita'
};

const functionVar = {
name: 'var()',
href: 'https://drafts.csswg.org/css-variables-2/#funcdef-var',
Expand All @@ -113,14 +120,17 @@ const functionEnv = {
* the outputs to the inputs directly. This conversion function takes
* some object or value and converts it to ease comparisons.
*/
function conv(entry) {
function conv(entry, parentKey) {
const res = {};
if (typeof entry !== 'object') {
return entry;
}
if (entry.href && !entry.extended && parentKey !== 'descriptors') {
entry.extended = [];
}
for (const [key, value] of Object.entries(entry)) {
if (Array.isArray(value)) {
res[key] = value.map(conv);
res[key] = value.map(v => conv(v, key));
}
else if (key === 'value') {
res.syntax = value;
Expand Down Expand Up @@ -309,7 +319,8 @@ describe('CSS extracts consolidation', function () {
properties: [
Object.assign({}, property1, {
value: null,
newValues: 'train'
newValues: 'train',
href: 'https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou'
})
]
})
Expand All @@ -318,7 +329,8 @@ describe('CSS extracts consolidation', function () {
const result = await cssmerge.run({ results });
assert.deepEqual(result.properties, [
Object.assign({}, conv(property1), {
syntax: 'none | auto | train'
syntax: 'none | auto | train',
extended: ['https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou']
})
]);
});
Expand All @@ -344,7 +356,8 @@ describe('CSS extracts consolidation', function () {
properties: [
Object.assign({}, property1, {
value: null,
newValues: 'train'
newValues: 'train',
href: 'https://drafts.csswg.org/css-otherstuff-1/#tchou-tchou'
})
]
})
Expand All @@ -357,7 +370,8 @@ describe('CSS extracts consolidation', function () {
properties: [
Object.assign({}, property1, {
value: null,
newValues: 'train'
newValues: 'train',
href: 'https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou'
})
]
})
Expand All @@ -366,7 +380,8 @@ describe('CSS extracts consolidation', function () {
const result = await cssmerge.run({ results });
assert.deepEqual(result.properties, [
Object.assign({}, conv(property1), {
syntax: 'none | auto | train'
syntax: 'none | auto | train',
extended: ['https://drafts.csswg.org/css-otherstuff-2/#tchou-tchou']
})
]);
});
Expand Down Expand Up @@ -432,7 +447,7 @@ describe('CSS extracts consolidation', function () {
css: Object.assign({}, emptyExtract, {
atrules: [
Object.assign({}, atrule2, {
descriptors: [descriptorExtended]
descriptors: [descriptorExtension]
})
]
})
Expand All @@ -442,7 +457,7 @@ describe('CSS extracts consolidation', function () {
assert.deepEqual(result.atrules, [
conv(Object.assign({}, atrule2, {
syntax: '@media foo',
descriptors: [descriptorExtended]
descriptors: [descriptorExtension]
}))
]);
});
Expand Down Expand Up @@ -630,4 +645,57 @@ describe('CSS extracts consolidation', function () {
]
})));
});

it('merges extended types', async () => {
const results = structuredClone([
{
shortname: 'css-stuff-1',
series: { shortname: 'css-stuff' },
seriesVersion: '1',
crawled: 'https://drafts.csswg.org/css-stuff-1/',
css: Object.assign({}, emptyExtract, {
values: [
Object.assign({}, type1)
]
})
},
{
shortname: 'css-otherstuff-1',
series: { shortname: 'css-otherstuff' },
seriesVersion: '1',
crawled: 'https://drafts.csswg.org/css-otherstuff-1/',
css: Object.assign({}, emptyExtract, {
values: [
Object.assign({}, type1Extension)
]
})
},
]);
const result = await cssmerge.run({ results });
assert.deepEqual(result, conv(Object.assign({}, emptyMerged, {
types: [
Object.assign({}, conv(type1), {
syntax: type1Extension.value,
extended: ['https://drafts.csswg.org/css-otherstuff-1/']
})
]
})));
});

it('discards type extensions without a base definition', async () => {
const results = structuredClone([
{
shortname: 'css-stuff-1',
series: { shortname: 'css-stuff' },
seriesVersion: '1',
css: Object.assign({}, emptyExtract, {
values: [
Object.assign({}, type1Extension)
]
})
}
]);
const result = await cssmerge.run({ results });
assert.deepEqual(result, conv(emptyMerged));
});
});