@shapeshift-labs/frontier-lang-css 0.1.15 → 0.1.17
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/README.md +3 -3
- package/dist/index.d.ts +3 -2
- package/dist/semantic-merge-duplicate-cascade.js +43 -0
- package/dist/semantic-merge-shorthand-evidence.js +38 -0
- package/dist/semantic-merge-shorthand.js +133 -1
- package/dist/semantic-merge.js +25 -31
- package/dist/shorthand-expansion.d.ts +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -232,11 +232,11 @@ const merge = safeMergeCssSource({
|
|
|
232
232
|
});
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
-
`sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, `var()` fallback references, animation/keyframe links, font-face links, URL asset references, cascade keys, statement-form at-rules, block-form runtime at-rules, CSS Modules exports, ICSS edges, scoped cascade graph proof hashes, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent declaration edits by cascade key, including existing scoped `@media` / `@supports` / `@container` / `@layer` declarations when a scoped cascade graph proof hash is supplied, carries source-bound dependency graph evidence into the merge result, blocks dependency-affecting declaration edits unless an exact source-bound dependency graph proof is supplied, blocks selector-target rebases unless the host supplies a source-bound selector target graph hash and the parser proves selector specificity is invariant, blocks parallel edits whose CSS shorthand expansions overlap a longhand or sub-shorthand, preserves unchanged statement-form at-rules such as `@layer reset, components;`, preserves unchanged block-form runtime at-rules such as `@keyframes` and `@font-face`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts. Cascade-sensitive source-shape changes still block by default, but a host can attach a `css-source-bound-cascade-runtime-proof` / `css-cascade-runtime-proof` that is bound to the exact source path, reason, side, shape key, and base/worker/head/output source hashes to admit that specific change.
|
|
235
|
+
`sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, `var()` fallback references, animation/keyframe links, font-face links, URL asset references, cascade keys, statement-form at-rules, block-form runtime at-rules, CSS Modules exports, ICSS edges, scoped cascade graph proof hashes, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent declaration edits by cascade key, including existing scoped `@media` / `@supports` / `@container` / `@layer` declarations when a scoped cascade graph proof hash is supplied, carries deterministic box-shorthand expansion evidence for supported margin/padding/inset/gap/scroll/border-edge families, carries source-bound dependency graph evidence into the merge result, blocks duplicate ordered occurrences of the same cascade key until ordered cascade evidence exists, blocks dependency-affecting declaration edits unless an exact source-bound dependency graph proof is supplied, blocks selector-target rebases unless the host supplies a source-bound selector target graph hash and the parser proves selector specificity is invariant, blocks parallel edits whose CSS shorthand expansions overlap a longhand or sub-shorthand, preserves unchanged statement-form at-rules such as `@layer reset, components;`, preserves unchanged block-form runtime at-rules such as `@keyframes` and `@font-face`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts. Cascade-sensitive source-shape changes still block by default, but a host can attach a `css-source-bound-cascade-runtime-proof` / `css-cascade-runtime-proof` that is bound to the exact source path, reason, side, shape key, and base/worker/head/output source hashes to admit that specific change.
|
|
236
236
|
|
|
237
237
|
## Support Boundary
|
|
238
238
|
|
|
239
239
|
- Ready evidence: style rules, selectors, specificity, declarations, source-bound dependency graph hashes for custom properties, `var()` fallbacks, animations/keyframes, font faces, and URL assets, statement-form at-rules, block-form runtime at-rules, CSS Modules local exports, generated class-name map coverage, JS/TS use-site graph hashes, composition graph hashes, ICSS graph hashes, scoped cascade graph hashes, source-bound cascade runtime proofs, source spans, stable hashes.
|
|
240
|
-
- Safe merge: independent declarations with non-overlapping cascade keys, non-overlapping shorthand expansion sets, and no changed dependency graph surface; unchanged statement-form at-rules and unchanged block-form runtime at-rules preserved in canonical output; existing scoped declaration edits when scoped cascade graph proof is supplied; dependency-affecting declaration edits only when an exact source-bound dependency graph proof is supplied; selector-target rebases only when a matching selector target graph hash is supplied and before/after selector specificity is identical; source-shape/cascade-sensitive edits only when an exact source-bound cascade runtime proof is supplied; explicit CSS Modules export additions/deletions when generated class-name and JS/TS use-site graph proof is supplied; composition edits when composition graph proof is supplied; ICSS edits when ICSS graph proof is supplied. Output is a canonical CSS render and not a byte/trivia-preserving claim.
|
|
241
|
-
- Review-only gaps: incomplete generated class-name maps, unproved dependency graph changes, unproved CSS Modules use-site graphs, unproved composition or ICSS graphs, shorthand value expansion/equivalence
|
|
240
|
+
- Safe merge: independent declarations with non-overlapping cascade keys, no duplicate ordered occurrences for any changed cascade key, deterministic box-shorthand expansion evidence for supported shorthand families, non-overlapping shorthand expansion sets, and no changed dependency graph surface; unchanged statement-form at-rules and unchanged block-form runtime at-rules preserved in canonical output; existing scoped declaration edits when scoped cascade graph proof is supplied; dependency-affecting declaration edits only when an exact source-bound dependency graph proof is supplied; selector-target rebases only when a matching selector target graph hash is supplied and before/after selector specificity is identical; source-shape/cascade-sensitive edits only when an exact source-bound cascade runtime proof is supplied; explicit CSS Modules export additions/deletions when generated class-name and JS/TS use-site graph proof is supplied; composition edits when composition graph proof is supplied; ICSS edits when ICSS graph proof is supplied. Output is a canonical CSS render and not a byte/trivia-preserving claim.
|
|
241
|
+
- Review-only gaps: duplicate ordered occurrences of the same cascade key, incomplete generated class-name maps, unproved dependency graph changes, unproved CSS Modules use-site graphs, unproved composition or ICSS graphs, ambiguous shorthand value expansion/equivalence such as `font`, layered `background`, and runtime-substituted `var()` / `env()` shorthand values, statement-form at-rule order/condition changes, one-sided or structurally changed scoped cascade under `@media` / `@supports` / `@container` / `@layer`, changed runtime blocks such as `@keyframes`, `@font-face`, `@page`, and `@property`, browser layout and render equivalence.
|
|
242
242
|
- Claims: dependency graph evidence is an admission/review signal only unless an exact source-bound dependency graph proof admits the changed dependency surface; `autoMergeClaim`, `semanticEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false. `browserCascadeEquivalenceClaim` is only true on a safe-merge result when a source-bound cascade runtime proof admits the specific cascade-sensitive source-shape change.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel'; import type { CssCascadeRuntimeProof, CssCascadeRuntimeProofRecord } from './cascade-runtime-proof.js'; import type { CssDependencyGraphEvidence, CssDependencyGraphProof, CssDependencyGraphProofRecord } from './dependency-graph.js'; export type { CssCascadeRuntimeProof, CssCascadeRuntimeProofRecord } from './cascade-runtime-proof.js'; export type { CssDependencyGraphChange, CssDependencyGraphEvidence, CssDependencyGraphProof, CssDependencyGraphProofRecord } from './dependency-graph.js';
|
|
1
|
+
import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel'; import type { CssCascadeRuntimeProof, CssCascadeRuntimeProofRecord } from './cascade-runtime-proof.js'; import type { CssDependencyGraphEvidence, CssDependencyGraphProof, CssDependencyGraphProofRecord } from './dependency-graph.js'; import type { CssSafeMergeShorthandExpansionEvidence, CssShorthandExpansionEvidence } from './shorthand-expansion.js'; export type { CssCascadeRuntimeProof, CssCascadeRuntimeProofRecord } from './cascade-runtime-proof.js'; export type { CssDependencyGraphChange, CssDependencyGraphEvidence, CssDependencyGraphProof, CssDependencyGraphProofRecord } from './dependency-graph.js'; export type { CssSafeMergeChangedShorthandExpansion, CssSafeMergeShorthandExpansionEvidence, CssSafeMergeShorthandExpansionSideEvidence, CssShorthandExpansionEvidence, CssShorthandLonghandExpansion } from './shorthand-expansion.js';
|
|
2
2
|
|
|
3
3
|
export interface CssProjectionOptions {
|
|
4
4
|
readonly banner?: string;
|
|
@@ -111,6 +111,7 @@ export interface CssSemanticDeclaration {
|
|
|
111
111
|
readonly ordinal: number;
|
|
112
112
|
readonly cascadeKey: string;
|
|
113
113
|
readonly declarationHash: string;
|
|
114
|
+
readonly shorthandExpansion?: CssShorthandExpansionEvidence;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export interface CssSemanticRecord {
|
|
@@ -241,7 +242,7 @@ export interface CssSafeMergeResult {
|
|
|
241
242
|
readonly baseSheetHash?: string; readonly workerSheetHash?: string; readonly headSheetHash?: string;
|
|
242
243
|
readonly workerChangedDeclarations?: number; readonly headChangedDeclarations?: number;
|
|
243
244
|
readonly workerChangedCssModuleContracts?: number; readonly headChangedCssModuleContracts?: number;
|
|
244
|
-
readonly parserEvidence?: CssSafeMergeParserEvidence; readonly selectorTargetEvidence?: CssSafeMergeSelectorTargetEvidence; readonly dependencyGraphEvidence?: CssDependencyGraphEvidence;
|
|
245
|
+
readonly parserEvidence?: CssSafeMergeParserEvidence; readonly shorthandExpansionEvidence?: CssSafeMergeShorthandExpansionEvidence; readonly selectorTargetEvidence?: CssSafeMergeSelectorTargetEvidence; readonly dependencyGraphEvidence?: CssDependencyGraphEvidence;
|
|
245
246
|
readonly cascadeRuntimeProofs?: readonly CssCascadeRuntimeProofRecord[];
|
|
246
247
|
readonly dependencyGraphProofs?: readonly CssDependencyGraphProofRecord[];
|
|
247
248
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function duplicateCascadeKeyConflictsForIndexes(id, sourcePath, indexes) {
|
|
2
|
+
return Object.entries(indexes).flatMap(([side, index]) => (index.duplicateCascadeKeyGroups ?? [])
|
|
3
|
+
.map((group) => conflict(id, sourcePath, side, group)));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function conflict(id, sourcePath, side, group) {
|
|
7
|
+
return {
|
|
8
|
+
code: 'css-duplicate-cascade-key-blocked',
|
|
9
|
+
gateId: 'css-semantic-merge',
|
|
10
|
+
sourcePath,
|
|
11
|
+
details: {
|
|
12
|
+
reasonCode: 'css-duplicate-cascade-key-order-unproved',
|
|
13
|
+
conflictKey: `css#${id}#css-duplicate-cascade-key-order-unproved#${side}#${group.cascadeKey}`,
|
|
14
|
+
side,
|
|
15
|
+
cascadeKey: group.cascadeKey,
|
|
16
|
+
count: group.count,
|
|
17
|
+
declarations: group.entries.map(duplicateCascadeDeclarationDetails),
|
|
18
|
+
proofGap: {
|
|
19
|
+
code: 'css-duplicate-cascade-key-order-unproved',
|
|
20
|
+
status: 'not-claimed',
|
|
21
|
+
summary: 'Duplicate CSS cascade keys require ordered cascade occurrence evidence before semantic merge admission.',
|
|
22
|
+
failClosed: true,
|
|
23
|
+
semanticEquivalenceClaim: false,
|
|
24
|
+
browserCascadeEquivalenceClaim: false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function duplicateCascadeDeclarationDetails(entry) {
|
|
31
|
+
return {
|
|
32
|
+
ruleKey: entry.ruleKey,
|
|
33
|
+
selectors: entry.selectors,
|
|
34
|
+
scopes: entry.scopes,
|
|
35
|
+
property: entry.property,
|
|
36
|
+
value: entry.value,
|
|
37
|
+
important: entry.important,
|
|
38
|
+
declarationOrdinal: entry.declarationOrdinal,
|
|
39
|
+
declarationHash: entry.declarationHash
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { duplicateCascadeKeyConflictsForIndexes };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function mergeShorthandExpansionEvidence(indexes, changed) {
|
|
2
|
+
const sides = Object.fromEntries(Object.entries(indexes).map(([side, index]) => [side, shorthandExpansionSideEvidence(index)]));
|
|
3
|
+
const changedEntries = [...changed.worker, ...changed.head].flatMap((change) => [change.before, change.after].filter(Boolean));
|
|
4
|
+
const changedShorthands = changedEntries.filter((entry, index, entries) => entry.shorthandExpansion && entries.findIndex((candidate) => candidate.key === entry.key && candidate.declarationHash === entry.declarationHash) === index);
|
|
5
|
+
const expanded = changedShorthands.filter((entry) => entry.shorthandExpansion.status === 'expanded');
|
|
6
|
+
const unsupported = changedShorthands.filter((entry) => entry.shorthandExpansion.status !== 'expanded');
|
|
7
|
+
return {
|
|
8
|
+
kind: 'frontier.lang.cssSafeMergeShorthandExpansionEvidence',
|
|
9
|
+
version: 1,
|
|
10
|
+
shorthandDeclarationCount: Object.values(sides).reduce((sum, side) => sum + side.shorthandDeclarationCount, 0),
|
|
11
|
+
changedShorthandCount: changedShorthands.length,
|
|
12
|
+
expandedChangedShorthandCount: expanded.length,
|
|
13
|
+
unsupportedChangedShorthandCount: unsupported.length,
|
|
14
|
+
deterministicExpansionClaim: changedShorthands.length > 0 && unsupported.length === 0,
|
|
15
|
+
sides,
|
|
16
|
+
changedShorthands: changedShorthands.map((entry) => ({
|
|
17
|
+
cascadeKey: entry.key,
|
|
18
|
+
property: entry.property,
|
|
19
|
+
value: entry.value,
|
|
20
|
+
expansion: entry.shorthandExpansion
|
|
21
|
+
}))
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shorthandExpansionSideEvidence(index) {
|
|
26
|
+
const expansions = [...index.declarations.values()].filter((entry) => entry.shorthandExpansion).map((entry) => entry.shorthandExpansion);
|
|
27
|
+
return {
|
|
28
|
+
shorthandDeclarationCount: expansions.length,
|
|
29
|
+
expandedShorthandDeclarations: expansions.filter((entry) => entry.status === 'expanded').length,
|
|
30
|
+
unsupportedShorthandDeclarations: expansions.filter((entry) => entry.status !== 'expanded').length,
|
|
31
|
+
supportedProperties: unique(expansions.filter((entry) => entry.status === 'expanded').map((entry) => entry.property)),
|
|
32
|
+
unsupportedProperties: unique(expansions.filter((entry) => entry.status !== 'expanded').map((entry) => entry.property))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
37
|
+
|
|
38
|
+
export { mergeShorthandExpansionEvidence };
|
|
@@ -6,6 +6,38 @@ function shorthandGroupForProperty(property) {
|
|
|
6
6
|
return undefined;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function deterministicShorthandExpansion(property, value, hash) {
|
|
10
|
+
const normalized = normalizeProperty(property);
|
|
11
|
+
if (!normalized || !ShorthandExpansionKeys.has(normalized)) return undefined;
|
|
12
|
+
const definition = DeterministicShorthandExpansions.get(normalized);
|
|
13
|
+
const unsupported = (reasonCode) => ({
|
|
14
|
+
kind: 'frontier.lang.cssShorthandExpansionEvidence',
|
|
15
|
+
version: 1,
|
|
16
|
+
property: normalized,
|
|
17
|
+
value: String(value ?? ''),
|
|
18
|
+
status: 'unsupported',
|
|
19
|
+
deterministic: false,
|
|
20
|
+
reasonCode,
|
|
21
|
+
expansionHash: hash?.({ kind: 'frontier.lang.css.shorthandExpansion.v1', property: normalized, value: String(value ?? ''), status: 'unsupported', reasonCode })
|
|
22
|
+
});
|
|
23
|
+
if (!definition) return unsupported('css-shorthand-expansion-unsupported');
|
|
24
|
+
const tokens = cssValueTokens(value);
|
|
25
|
+
if (!tokens.length || tokens.length > definition.maxTokens) return unsupported('css-shorthand-expansion-unsupported-value-shape');
|
|
26
|
+
if (tokens.some(isRuntimeSubstitutionToken)) return unsupported('css-shorthand-expansion-runtime-substitution');
|
|
27
|
+
const longhands = expandTokens(definition.properties, tokens);
|
|
28
|
+
return {
|
|
29
|
+
kind: 'frontier.lang.cssShorthandExpansionEvidence',
|
|
30
|
+
version: 1,
|
|
31
|
+
property: normalized,
|
|
32
|
+
value: String(value ?? ''),
|
|
33
|
+
group: normalized,
|
|
34
|
+
status: 'expanded',
|
|
35
|
+
deterministic: true,
|
|
36
|
+
longhands,
|
|
37
|
+
expansionHash: hash?.({ kind: 'frontier.lang.css.shorthandExpansion.v1', property: normalized, value: String(value ?? ''), longhands })
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
function declarationsOverlapByCssProperty(leftProperty, rightProperty) {
|
|
10
42
|
const left = normalizeProperty(leftProperty);
|
|
11
43
|
const right = normalizeProperty(rightProperty);
|
|
@@ -28,6 +60,80 @@ function normalizeProperty(property) {
|
|
|
28
60
|
return String(property ?? '').trim().toLowerCase();
|
|
29
61
|
}
|
|
30
62
|
|
|
63
|
+
function cssValueTokens(value) {
|
|
64
|
+
const text = String(value ?? '').trim();
|
|
65
|
+
if (!text) return [];
|
|
66
|
+
const tokens = [];
|
|
67
|
+
let token = '';
|
|
68
|
+
let depth = 0;
|
|
69
|
+
let quote = '';
|
|
70
|
+
let escaped = false;
|
|
71
|
+
for (const char of text) {
|
|
72
|
+
if (escaped) {
|
|
73
|
+
token += char;
|
|
74
|
+
escaped = false;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === '\\') {
|
|
78
|
+
token += char;
|
|
79
|
+
escaped = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (quote) {
|
|
83
|
+
token += char;
|
|
84
|
+
if (char === quote) quote = '';
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '"' || char === "'") {
|
|
88
|
+
token += char;
|
|
89
|
+
quote = char;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '(') {
|
|
93
|
+
token += char;
|
|
94
|
+
depth += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === ')') {
|
|
98
|
+
token += char;
|
|
99
|
+
depth = Math.max(0, depth - 1);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (/\s/.test(char) && depth === 0) {
|
|
103
|
+
if (token) tokens.push(token);
|
|
104
|
+
token = '';
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
token += char;
|
|
108
|
+
}
|
|
109
|
+
if (token) tokens.push(token);
|
|
110
|
+
return tokens;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isRuntimeSubstitutionToken(token) {
|
|
114
|
+
return /\b(?:var|env)\s*\(/i.test(String(token ?? ''));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function expandTokens(properties, tokens) {
|
|
118
|
+
if (properties.length === 4) {
|
|
119
|
+
const [top, right = top, bottom = top, left = right] = tokens;
|
|
120
|
+
return [
|
|
121
|
+
{ property: properties[0], value: top },
|
|
122
|
+
{ property: properties[1], value: right },
|
|
123
|
+
{ property: properties[2], value: bottom },
|
|
124
|
+
{ property: properties[3], value: left }
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
if (properties.length === 2) {
|
|
128
|
+
const [first, second = first] = tokens;
|
|
129
|
+
return [
|
|
130
|
+
{ property: properties[0], value: first },
|
|
131
|
+
{ property: properties[1], value: second }
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
return properties.map((longhand, index) => ({ property: longhand, value: tokens[index] ?? tokens[0] }));
|
|
135
|
+
}
|
|
136
|
+
|
|
31
137
|
function allPropertyExcluded(property) {
|
|
32
138
|
return property.startsWith('--') || property === 'direction' || property === 'unicode-bidi';
|
|
33
139
|
}
|
|
@@ -132,6 +238,32 @@ const ShorthandExpansionKeys = new Map(Object.entries({
|
|
|
132
238
|
transition: ['transition-behavior', 'transition-delay', 'transition-duration', 'transition-property', 'transition-timing-function']
|
|
133
239
|
}).map(([property, keys]) => [property, new Set(keys)]));
|
|
134
240
|
|
|
241
|
+
const DeterministicShorthandExpansions = new Map(Object.entries({
|
|
242
|
+
margin: box4('margin', ['top', 'right', 'bottom', 'left']),
|
|
243
|
+
padding: box4('padding', ['top', 'right', 'bottom', 'left']),
|
|
244
|
+
inset: { properties: ['top', 'right', 'bottom', 'left'], maxTokens: 4 },
|
|
245
|
+
'margin-block': { properties: ['margin-block-start', 'margin-block-end'], maxTokens: 2 },
|
|
246
|
+
'margin-inline': { properties: ['margin-inline-start', 'margin-inline-end'], maxTokens: 2 },
|
|
247
|
+
'padding-block': { properties: ['padding-block-start', 'padding-block-end'], maxTokens: 2 },
|
|
248
|
+
'padding-inline': { properties: ['padding-inline-start', 'padding-inline-end'], maxTokens: 2 },
|
|
249
|
+
'inset-block': { properties: ['inset-block-start', 'inset-block-end'], maxTokens: 2 },
|
|
250
|
+
'inset-inline': { properties: ['inset-inline-start', 'inset-inline-end'], maxTokens: 2 },
|
|
251
|
+
gap: { properties: ['row-gap', 'column-gap'], maxTokens: 2 },
|
|
252
|
+
'scroll-margin': box4('scroll-margin', ['top', 'right', 'bottom', 'left']),
|
|
253
|
+
'scroll-padding': box4('scroll-padding', ['top', 'right', 'bottom', 'left']),
|
|
254
|
+
'scroll-margin-block': { properties: ['scroll-margin-block-start', 'scroll-margin-block-end'], maxTokens: 2 },
|
|
255
|
+
'scroll-margin-inline': { properties: ['scroll-margin-inline-start', 'scroll-margin-inline-end'], maxTokens: 2 },
|
|
256
|
+
'scroll-padding-block': { properties: ['scroll-padding-block-start', 'scroll-padding-block-end'], maxTokens: 2 },
|
|
257
|
+
'scroll-padding-inline': { properties: ['scroll-padding-inline-start', 'scroll-padding-inline-end'], maxTokens: 2 },
|
|
258
|
+
'border-width': box4('border', ['top-width', 'right-width', 'bottom-width', 'left-width']),
|
|
259
|
+
'border-style': box4('border', ['top-style', 'right-style', 'bottom-style', 'left-style']),
|
|
260
|
+
'border-color': box4('border', ['top-color', 'right-color', 'bottom-color', 'left-color'])
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
function box4(prefix, suffixes) {
|
|
264
|
+
return { properties: suffixes.map((suffix) => `${prefix}-${suffix}`), maxTokens: 4 };
|
|
265
|
+
}
|
|
266
|
+
|
|
135
267
|
function edgeKeys(prefix, attributes) {
|
|
136
268
|
return ['top', 'right', 'bottom', 'left'].flatMap((edge) => attributes.map((attribute) => `${prefix}-${edge}-${attribute}`));
|
|
137
269
|
}
|
|
@@ -144,4 +276,4 @@ function propertyTuple(prefix, attributes) {
|
|
|
144
276
|
return attributes.map((attribute) => `${prefix}-${attribute}`);
|
|
145
277
|
}
|
|
146
278
|
|
|
147
|
-
export { declarationsOverlapByCssProperty, shorthandGroupForProperty };
|
|
279
|
+
export { declarationsOverlapByCssProperty, deterministicShorthandExpansion, shorthandGroupForProperty };
|
package/dist/semantic-merge.js
CHANGED
|
@@ -3,7 +3,9 @@ import { admitCascadeRuntimeProofs } from './semantic-merge-cascade-runtime.js';
|
|
|
3
3
|
import { admitCssDependencyGraphProofs, mergeCssDependencyGraphEvidence } from './dependency-graph.js';
|
|
4
4
|
import { mergeSelectorTargetEvidence, planSelectorTargetRebase } from './semantic-merge-selector-targets.js';
|
|
5
5
|
import { applyAtRuleBlockChanges, atRuleBlockEntry, atRuleBlockOverlapConflicts, atRuleOccurrenceKey, changedAtRuleBlocks, renderAtRuleBlock, renderAtRuleStatement } from './semantic-merge-at-rules.js';
|
|
6
|
-
import { declarationsOverlapByCssProperty, shorthandGroupForProperty } from './semantic-merge-shorthand.js';
|
|
6
|
+
import { declarationsOverlapByCssProperty, deterministicShorthandExpansion, shorthandGroupForProperty } from './semantic-merge-shorthand.js';
|
|
7
|
+
import { mergeShorthandExpansionEvidence } from './semantic-merge-shorthand-evidence.js';
|
|
8
|
+
import { duplicateCascadeKeyConflictsForIndexes } from './semantic-merge-duplicate-cascade.js';
|
|
7
9
|
|
|
8
10
|
function safeMergeCssSource(input = {}, context = {}) {
|
|
9
11
|
const parseSheet = context.parseCssSemanticSheet;
|
|
@@ -20,7 +22,7 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
20
22
|
worker: parseSheet(worker, sheetOptions(input, 'worker', sourcePath)),
|
|
21
23
|
head: parseSheet(head, sheetOptions(input, 'head', sourcePath))
|
|
22
24
|
};
|
|
23
|
-
const indexes = Object.fromEntries(Object.entries(sheets).map(([name, sheet]) => [name, declarationIndex(sheet)]));
|
|
25
|
+
const indexes = Object.fromEntries(Object.entries(sheets).map(([name, sheet]) => [name, declarationIndex(sheet, hash)]));
|
|
24
26
|
const changed = {
|
|
25
27
|
worker: changedDeclarations(indexes.base, indexes.worker, 'worker'),
|
|
26
28
|
head: changedDeclarations(indexes.base, indexes.head, 'head')
|
|
@@ -32,6 +34,7 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
32
34
|
const moduleChanges = cssModuleContractChanges(sheets, hash);
|
|
33
35
|
const proofConflicts = proofGapConflicts(id, sourcePath, changed, indexes);
|
|
34
36
|
const parserConflicts = parserErrorConflicts(id, sourcePath, sheets);
|
|
37
|
+
const duplicateCascadeKeyConflicts = duplicateCascadeKeyConflictsForIndexes(id, sourcePath, indexes);
|
|
35
38
|
const overlapConflicts = [
|
|
36
39
|
...overlapDeclarationConflicts(id, sourcePath, changed.worker, changed.head),
|
|
37
40
|
...shorthandOverlapConflicts(id, sourcePath, changed.worker, changed.head),
|
|
@@ -39,6 +42,7 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
39
42
|
];
|
|
40
43
|
const moduleConflicts = cssModuleContractConflicts(id, sourcePath, moduleChanges);
|
|
41
44
|
const parserEvidence = mergeParserEvidence(sheets);
|
|
45
|
+
const shorthandExpansionEvidence = mergeShorthandExpansionEvidence(indexes, changed);
|
|
42
46
|
const dependencyGraphEvidence = mergeCssDependencyGraphEvidence(sheets, changed);
|
|
43
47
|
const selectorTargetPlan = planSelectorTargetRebase(id, sourcePath, mergeSelectorTargetEvidence(sheets, changed), changed, input);
|
|
44
48
|
const shapeChanges = unsupportedSourceShapeChanges(sheets, changed, hash);
|
|
@@ -53,8 +57,8 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
53
57
|
hash
|
|
54
58
|
});
|
|
55
59
|
const dependencyGraphAdmission = admitCssDependencyGraphProofs({ id, sourcePath, input, dependencyGraphEvidence, binding: { base, worker, head, output: mergedSourceText }, hash });
|
|
56
|
-
const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...cascadeRuntimeAdmission.conflicts, ...dependencyGraphAdmission.conflicts, ...selectorTargetPlan.conflicts];
|
|
57
|
-
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts, { parserEvidence, dependencyGraphEvidence, selectorTargetEvidence: selectorTargetPlan.evidence, cascadeRuntimeProofs: cascadeRuntimeAdmission.proofs, dependencyGraphProofs: dependencyGraphAdmission.proofs });
|
|
60
|
+
const conflicts = [...parserConflicts, ...duplicateCascadeKeyConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...cascadeRuntimeAdmission.conflicts, ...dependencyGraphAdmission.conflicts, ...selectorTargetPlan.conflicts];
|
|
61
|
+
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts, { parserEvidence, shorthandExpansionEvidence, dependencyGraphEvidence, selectorTargetEvidence: selectorTargetPlan.evidence, cascadeRuntimeProofs: cascadeRuntimeAdmission.proofs, dependencyGraphProofs: dependencyGraphAdmission.proofs });
|
|
58
62
|
return merged(id, sourcePath, mergedSourceText, 'semantic-declaration-merge', hash, {
|
|
59
63
|
baseSheetHash: sheets.base.sheetHash,
|
|
60
64
|
workerSheetHash: sheets.worker.sheetHash,
|
|
@@ -64,6 +68,7 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
64
68
|
workerChangedCssModuleContracts: moduleChanges.worker.length,
|
|
65
69
|
headChangedCssModuleContracts: moduleChanges.head.length,
|
|
66
70
|
parserEvidence,
|
|
71
|
+
shorthandExpansionEvidence,
|
|
67
72
|
dependencyGraphEvidence,
|
|
68
73
|
selectorTargetEvidence: selectorTargetPlan.evidence,
|
|
69
74
|
cascadeRuntimeProofs: cascadeRuntimeAdmission.proofs,
|
|
@@ -72,28 +77,21 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
72
77
|
});
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
function declarationIndex(sheet) {
|
|
80
|
+
function declarationIndex(sheet, hash) {
|
|
76
81
|
const declarations = new Map();
|
|
77
82
|
const order = [];
|
|
78
83
|
const statements = [];
|
|
79
84
|
const atRuleBlocks = new Map();
|
|
80
85
|
const atRuleBlockOrder = [];
|
|
81
86
|
const atRuleOccurrences = new Map();
|
|
87
|
+
const declarationOccurrences = new Map();
|
|
82
88
|
for (const record of sheet.records) {
|
|
83
89
|
const block = atRuleBlockEntry(record, atRuleOccurrenceKey(record, atRuleOccurrences));
|
|
84
90
|
if (block) {
|
|
85
91
|
atRuleBlocks.set(block.key, block);
|
|
86
92
|
atRuleBlockOrder.push(block.key);
|
|
87
93
|
}
|
|
88
|
-
if (record.kind === 'at-rule-statement') {
|
|
89
|
-
statements.push({
|
|
90
|
-
key: record.atRuleHash,
|
|
91
|
-
scopes: record.scopes ?? [],
|
|
92
|
-
statementText: record.statementText,
|
|
93
|
-
atRuleName: record.atRuleName,
|
|
94
|
-
conditionText: record.conditionText
|
|
95
|
-
});
|
|
96
|
-
}
|
|
94
|
+
if (record.kind === 'at-rule-statement') statements.push({ key: record.atRuleHash, scopes: record.scopes ?? [], statementText: record.statementText, atRuleName: record.atRuleName, conditionText: record.conditionText });
|
|
97
95
|
if (record.kind !== 'rule') continue;
|
|
98
96
|
const ruleKey = ruleIdentityKey(record);
|
|
99
97
|
for (const declaration of record.declarations ?? []) {
|
|
@@ -105,15 +103,21 @@ function declarationIndex(sheet) {
|
|
|
105
103
|
property: declaration.property,
|
|
106
104
|
value: declaration.value,
|
|
107
105
|
important: declaration.important,
|
|
106
|
+
declarationOrdinal: declaration.ordinal,
|
|
108
107
|
declarationHash: declaration.declarationHash,
|
|
108
|
+
shorthandExpansion: deterministicShorthandExpansion(declaration.property, declaration.value, hash),
|
|
109
109
|
selectorTargetGraphHash: record.selectorTargetGraphHash,
|
|
110
110
|
proofGaps: proofGapsForDeclaration(record, declaration)
|
|
111
111
|
};
|
|
112
|
+
declarationOccurrences.set(entry.key, [...(declarationOccurrences.get(entry.key) ?? []), entry]);
|
|
112
113
|
declarations.set(entry.key, entry);
|
|
113
114
|
order.push(entry.key);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
+
const duplicateCascadeKeyGroups = [...declarationOccurrences.entries()]
|
|
118
|
+
.filter(([, entries]) => entries.length > 1)
|
|
119
|
+
.map(([cascadeKey, entries]) => ({ cascadeKey, count: entries.length, entries }));
|
|
120
|
+
return { declarations, order: unique(order), statements, atRuleBlocks, atRuleBlockOrder: unique(atRuleBlockOrder), duplicateCascadeKeyGroups };
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
function changedDeclarations(baseIndex, currentIndex, side) {
|
|
@@ -134,6 +138,8 @@ function proofGapConflicts(id, sourcePath, changed, indexes) {
|
|
|
134
138
|
.filter((gap) => !canAdmitProofGap(gap, entry, changed, indexes))
|
|
135
139
|
.map((gap) => conflict(id, sourcePath, 'css-proof-gap-blocked', gap.code, {
|
|
136
140
|
cascadeKey: key,
|
|
141
|
+
property: entry.property,
|
|
142
|
+
shorthandExpansion: entry.shorthandExpansion,
|
|
137
143
|
proofGap: gap
|
|
138
144
|
}));
|
|
139
145
|
});
|
|
@@ -202,7 +208,7 @@ function shorthandOverlapConflicts(id, sourcePath, workerChanges, headChanges) {
|
|
|
202
208
|
function canAdmitProofGap(gap, entry, changed, indexes) {
|
|
203
209
|
if (gap.code !== 'css-shorthand-expansion-unproved' || !entry) return false;
|
|
204
210
|
const group = shorthandGroupForProperty(entry.property);
|
|
205
|
-
if (!group || hasRelatedExistingDeclaration(entry, indexes)) return false;
|
|
211
|
+
if (!group || entry.shorthandExpansion?.status !== 'expanded' || hasRelatedExistingDeclaration(entry, indexes)) return false;
|
|
206
212
|
const oppositeChanges = [...changed.worker, ...changed.head].filter((change) => change.key !== entry.key);
|
|
207
213
|
return !oppositeChanges.some((change) => declarationsOverlapByShorthandGroup(entry, change.after ?? change.before));
|
|
208
214
|
}
|
|
@@ -239,9 +245,7 @@ function renderDeclarationIndex(index) {
|
|
|
239
245
|
const chunks = [];
|
|
240
246
|
for (const statement of index.statements ?? []) renderAtRuleStatement(chunks, statement);
|
|
241
247
|
for (const key of index.atRuleBlockOrder ?? []) renderAtRuleBlock(chunks, index.atRuleBlocks.get(key));
|
|
242
|
-
for (const declarations of groups.values())
|
|
243
|
-
renderDeclarationGroup(chunks, declarations);
|
|
244
|
-
}
|
|
248
|
+
for (const declarations of groups.values()) renderDeclarationGroup(chunks, declarations);
|
|
245
249
|
return `${chunks.join('\n').trimEnd()}\n`;
|
|
246
250
|
}
|
|
247
251
|
|
|
@@ -263,21 +267,11 @@ function renderDeclarationGroup(chunks, declarations) {
|
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
function merged(id, sourcePath, sourceText, operation, hash, extra = {}) {
|
|
266
|
-
return result(id, sourcePath, 'merged', {
|
|
267
|
-
operation,
|
|
268
|
-
mergedSourceText: sourceText,
|
|
269
|
-
mergedSourceHash: hash?.(sourceText),
|
|
270
|
-
conflicts: [],
|
|
271
|
-
...extra
|
|
272
|
-
});
|
|
270
|
+
return result(id, sourcePath, 'merged', { operation, mergedSourceText: sourceText, mergedSourceHash: hash?.(sourceText), conflicts: [], ...extra });
|
|
273
271
|
}
|
|
274
272
|
|
|
275
273
|
function blocked(id, sourcePath, reasonCode, conflicts = [], extra = {}) {
|
|
276
|
-
return result(id, sourcePath, 'blocked', {
|
|
277
|
-
operation: 'blocked',
|
|
278
|
-
conflicts: conflicts.length ? conflicts : [conflict(id, sourcePath, reasonCode, reasonCode)],
|
|
279
|
-
...extra
|
|
280
|
-
});
|
|
274
|
+
return result(id, sourcePath, 'blocked', { operation: 'blocked', conflicts: conflicts.length ? conflicts : [conflict(id, sourcePath, reasonCode, reasonCode)], ...extra });
|
|
281
275
|
}
|
|
282
276
|
|
|
283
277
|
function result(id, sourcePath, status, body) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface CssShorthandLonghandExpansion {
|
|
2
|
+
readonly property: string;
|
|
3
|
+
readonly value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CssShorthandExpansionEvidence {
|
|
7
|
+
readonly kind: 'frontier.lang.cssShorthandExpansionEvidence';
|
|
8
|
+
readonly version: 1;
|
|
9
|
+
readonly property: string;
|
|
10
|
+
readonly value: string;
|
|
11
|
+
readonly group?: string;
|
|
12
|
+
readonly status: 'expanded' | 'unsupported' | string;
|
|
13
|
+
readonly deterministic: boolean;
|
|
14
|
+
readonly reasonCode?: string;
|
|
15
|
+
readonly longhands?: readonly CssShorthandLonghandExpansion[];
|
|
16
|
+
readonly expansionHash?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CssSafeMergeShorthandExpansionEvidence {
|
|
20
|
+
readonly kind: 'frontier.lang.cssSafeMergeShorthandExpansionEvidence';
|
|
21
|
+
readonly version: 1;
|
|
22
|
+
readonly shorthandDeclarationCount: number;
|
|
23
|
+
readonly changedShorthandCount: number;
|
|
24
|
+
readonly expandedChangedShorthandCount: number;
|
|
25
|
+
readonly unsupportedChangedShorthandCount: number;
|
|
26
|
+
readonly deterministicExpansionClaim: boolean;
|
|
27
|
+
readonly sides: Readonly<Record<string, CssSafeMergeShorthandExpansionSideEvidence>>;
|
|
28
|
+
readonly changedShorthands: readonly CssSafeMergeChangedShorthandExpansion[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CssSafeMergeShorthandExpansionSideEvidence {
|
|
32
|
+
readonly shorthandDeclarationCount: number;
|
|
33
|
+
readonly expandedShorthandDeclarations: number;
|
|
34
|
+
readonly unsupportedShorthandDeclarations: number;
|
|
35
|
+
readonly supportedProperties: readonly string[];
|
|
36
|
+
readonly unsupportedProperties: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CssSafeMergeChangedShorthandExpansion {
|
|
40
|
+
readonly cascadeKey: string;
|
|
41
|
+
readonly property: string;
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly expansion: CssShorthandExpansionEvidence;
|
|
44
|
+
}
|
package/package.json
CHANGED