Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 18 additions & 9 deletions src/browserlib/extract-cssdfn.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,25 @@ export default function () {
}
if (matchingValues.length === 0) {
// Dangling production rule. That should never happen for properties,
// at-rules, descriptors and functions, since they should always be
// defined somewhere. That happens from time to time for types that are
// described in prose and that don't have a dfn. One could perhaps argue
// that these constructs ought to have a dfn too.
if (!res.warnings) {
res.warnings = []
// at-rules, descriptors: they should always be defined somewhere. That
// happens from time to time for functions and types that are defined
// in a spec and (temporarily) extended in another spec.
if (rule.name.match(/^<.*>$/)) {
const isFunction = !!rule.name.match(/\(\)/);
res.values.push({
name: isFunction ? rule.name.replace(/^<(.*)>$/, '$1') : rule.name,
type: isFunction ? 'function' : 'type',
value: rule.value
});
}
else {
if (!res.warnings) {
res.warnings = [];
}
const warning = Object.assign({ msg: 'Missing definition' }, rule);
warnings.push(warning);
rootDfns.push(warning);
}
const warning = Object.assign({ msg: 'Missing definition' }, rule);
warnings.push(warning);
rootDfns.push(warning);
}
}
}
Expand Down
32 changes: 25 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 Down Expand Up @@ -240,6 +250,14 @@ export default {
}
baseDfn.syntax += ' | ' + dfn.newValues;
}
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.
baseDfn.syntax = dfn.syntax;
}
if (baseDfn.descriptors && dfn.descriptors?.length > 0) {
baseDfn.descriptors.push(...dfn.descriptors.filter(desc =>
!hasNewerDescriptorDfn(desc, dfn, dfns)));
Expand Down
19 changes: 16 additions & 3 deletions test/extract-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -1010,16 +1010,29 @@ that spans multiple lines */
]
},

{
title: 'creates a definition when one is missing for a type',
html: `
<pre class="prod">&lt;my-type> = none | auto</pre>
`,
propertyName: 'values',
css: [{
name: '<my-type>',
type: 'type',
value: 'none | auto'
}]
},

{
title: 'issues a warning when a definition is missing',
html: `
<pre class="prod">&lt;my-type&gt; = none | auto
<pre class="prod">foo = bar</pre>
`,
propertyName: 'warnings',
css: [{
msg: 'Missing definition',
name: '<my-type>',
value: 'none | auto'
name: 'foo',
value: 'bar'
}]
},

Expand Down
63 changes: 60 additions & 3 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 Down Expand Up @@ -432,7 +439,7 @@ describe('CSS extracts consolidation', function () {
css: Object.assign({}, emptyExtract, {
atrules: [
Object.assign({}, atrule2, {
descriptors: [descriptorExtended]
descriptors: [descriptorExtension]
})
]
})
Expand All @@ -442,7 +449,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 +637,54 @@ describe('CSS extracts consolidation', function () {
]
})));
});

it('merges extended types', async () => {
const results = structuredClone([
{
shortname: 'css-stuff-1',
series: { shortname: 'css-stuff' },
seriesVersion: '1',
css: Object.assign({}, emptyExtract, {
values: [
Object.assign({}, type1)
]
})
},
{
shortname: 'css-otherstuff-1',
series: { shortname: 'css-otherstuff' },
seriesVersion: '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
})
]
})));
});

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));
});
});