@shapeshift-labs/frontier-lang-css 0.1.4 → 0.1.6
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 +4 -4
- package/dist/index.d.ts +20 -0
- package/dist/index.js +54 -6
- package/dist/postcss-parser-evidence.js +151 -0
- package/dist/semantic-merge-css-modules.js +23 -2
- package/dist/semantic-merge.js +35 -5
- package/package.json +3 -2
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, cascade keys, 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, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts.
|
|
235
|
+
`sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, cascade keys, statement-form 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, preserves unchanged statement-form at-rules such as `@layer reset, components;`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts.
|
|
236
236
|
|
|
237
237
|
## Support Boundary
|
|
238
238
|
|
|
239
|
-
- Ready evidence: style rules, selectors, specificity, declarations, custom properties, 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 spans, stable hashes.
|
|
240
|
-
- Safe merge: independent declarations with non-overlapping cascade keys; existing scoped declaration edits when scoped cascade graph 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 CSS Modules use-site graphs, unproved composition or ICSS graphs, shorthands without longhand expansion,
|
|
239
|
+
- Ready evidence: style rules, selectors, specificity, declarations, custom properties, statement-form 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 spans, stable hashes.
|
|
240
|
+
- Safe merge: independent declarations with non-overlapping cascade keys; unchanged statement-form at-rules preserved in canonical output; existing scoped declaration edits when scoped cascade graph 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 CSS Modules use-site graphs, unproved composition or ICSS graphs, shorthands without longhand expansion, statement-form at-rule order/condition changes, one-sided or structurally changed scoped cascade under `@media` / `@supports` / `@container` / `@layer`, `@keyframes`, `@font-face`, `@page`, browser layout and render equivalence.
|
|
242
242
|
- Claims: `autoMergeClaim`, `semanticEquivalenceClaim`, `browserCascadeEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false.
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,20 @@ export interface CssSourceSpan {
|
|
|
36
36
|
readonly endColumn: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface CssParserDiagnostic {
|
|
40
|
+
readonly reason?: string;
|
|
41
|
+
readonly line?: number;
|
|
42
|
+
readonly column?: number;
|
|
43
|
+
readonly input?: string;
|
|
44
|
+
readonly [key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CssParserEvidence {
|
|
48
|
+
readonly name: 'postcss' | string;
|
|
49
|
+
readonly sourceCodeLocationInfo: boolean;
|
|
50
|
+
readonly parseErrors: readonly CssParserDiagnostic[];
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export interface CssSourceRef {
|
|
40
54
|
readonly semanticNodeId: string;
|
|
41
55
|
readonly semanticNodeKind?: string;
|
|
@@ -109,6 +123,8 @@ export interface CssSemanticDeclaration {
|
|
|
109
123
|
readonly value: string;
|
|
110
124
|
readonly important: boolean;
|
|
111
125
|
readonly valueHash: string;
|
|
126
|
+
readonly sourceSpan?: CssSourceSpan;
|
|
127
|
+
readonly rawTextHash?: string;
|
|
112
128
|
readonly ordinal: number;
|
|
113
129
|
readonly cascadeKey: string;
|
|
114
130
|
readonly declarationHash: string;
|
|
@@ -125,9 +141,12 @@ export interface CssSemanticRecord {
|
|
|
125
141
|
readonly atRuleName?: string;
|
|
126
142
|
readonly conditionText?: string;
|
|
127
143
|
readonly scopeKey?: string;
|
|
144
|
+
readonly statementText?: string;
|
|
128
145
|
readonly scopedCascadeGraphHash?: string;
|
|
129
146
|
readonly sourceSpan: CssSourceSpan;
|
|
130
147
|
readonly sourceHash: string;
|
|
148
|
+
readonly parser?: 'postcss' | string;
|
|
149
|
+
readonly rawTextHash?: string;
|
|
131
150
|
readonly ruleHash?: string;
|
|
132
151
|
readonly atRuleHash?: string;
|
|
133
152
|
readonly proofGaps?: readonly CssSemanticProofGap[];
|
|
@@ -199,6 +218,7 @@ export interface CssSemanticSheet {
|
|
|
199
218
|
readonly sheetHash: string;
|
|
200
219
|
readonly summary: Readonly<Record<string, number>>;
|
|
201
220
|
readonly proofGaps: readonly CssSemanticProofGap[];
|
|
221
|
+
readonly parser: CssParserEvidence;
|
|
202
222
|
}
|
|
203
223
|
|
|
204
224
|
export interface CssSemanticMergeEvidence {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import { createCssModuleEvidence } from './css-modules.js';
|
|
3
|
+
import { parsePostcssSemanticRecords } from './postcss-parser-evidence.js';
|
|
3
4
|
import { safeMergeCssSource as safeMergeCssSourceImpl } from './semantic-merge.js';
|
|
4
5
|
|
|
5
6
|
const ShorthandProperties = new Set(['all', 'animation', 'background', 'border', 'border-block', 'border-color', 'border-image', 'border-inline', 'border-radius', 'border-style', 'border-width', 'columns', 'flex', 'font', 'gap', 'grid', 'grid-area', 'grid-column', 'grid-row', 'inset', 'list-style', 'margin', 'offset', 'outline', 'overflow', 'padding', 'place-content', 'place-items', 'place-self', 'text-decoration', 'transition']);
|
|
@@ -57,11 +58,12 @@ export function emitCssWithSourceMap(document, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export function parseCssSemanticSheet(sourceText, options = {}) {
|
|
60
|
-
const lineStarts = computeLineStarts(sourceText);
|
|
61
61
|
const sourceHash = options.sourceHash ?? hashSemanticValue({ kind: 'frontier.lang.css.source.v1', sourceText });
|
|
62
|
-
const
|
|
62
|
+
const parsed = parsePostcssSemanticRecords(sourceText, sourceHash, options);
|
|
63
|
+
const records = parsed.records;
|
|
63
64
|
const cssModules = createCssModuleEvidence(records, options, sourceHash);
|
|
64
65
|
const proofGaps = [
|
|
66
|
+
...parsed.proofGaps,
|
|
65
67
|
...records.flatMap((record) => record.proofGaps ?? []),
|
|
66
68
|
...(cssModules?.proofGaps ?? [])
|
|
67
69
|
];
|
|
@@ -81,9 +83,11 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
|
|
|
81
83
|
cssModuleCompositions: cssModules?.compositions.length ?? 0,
|
|
82
84
|
icssImports: cssModules?.icssImports.length ?? 0,
|
|
83
85
|
icssExports: cssModules?.icssExports.length ?? 0,
|
|
84
|
-
proofGaps: proofGaps.length
|
|
86
|
+
proofGaps: proofGaps.length,
|
|
87
|
+
parseErrors: parsed.parser.parseErrors.length
|
|
85
88
|
},
|
|
86
|
-
proofGaps
|
|
89
|
+
proofGaps,
|
|
90
|
+
parser: parsed.parser
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -114,6 +118,7 @@ export function safeMergeCssSource(input = {}) {
|
|
|
114
118
|
|
|
115
119
|
function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash, options) {
|
|
116
120
|
const records = [];
|
|
121
|
+
const blockRanges = [];
|
|
117
122
|
let index = start;
|
|
118
123
|
while (index < end) {
|
|
119
124
|
const open = sourceText.indexOf('{', index);
|
|
@@ -121,6 +126,7 @@ function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash,
|
|
|
121
126
|
const close = matchingBrace(sourceText, open, end);
|
|
122
127
|
if (close < 0) break;
|
|
123
128
|
const preludeStart = previousBoundary(sourceText, index, open);
|
|
129
|
+
blockRanges.push([preludeStart, close + 1]);
|
|
124
130
|
const prelude = sourceText.slice(preludeStart, open).replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
125
131
|
const body = sourceText.slice(open + 1, close);
|
|
126
132
|
if (prelude.startsWith('@')) {
|
|
@@ -132,7 +138,8 @@ function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash,
|
|
|
132
138
|
}
|
|
133
139
|
index = close + 1;
|
|
134
140
|
}
|
|
135
|
-
|
|
141
|
+
records.push(...parseAtRuleStatements(sourceText, start, end, scopes, lineStarts, sourceHash, options, blockRanges));
|
|
142
|
+
return records.sort((left, right) => left.sourceSpan.startOffset - right.sourceSpan.startOffset);
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes, options) {
|
|
@@ -184,6 +191,47 @@ function parseAtRule(prelude, start, end, lineStarts, sourceHash, scopes, option
|
|
|
184
191
|
});
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
function parseAtRuleStatements(sourceText, start, end, scopes, lineStarts, sourceHash, options, blockRanges) {
|
|
195
|
+
const records = [];
|
|
196
|
+
let index = start;
|
|
197
|
+
while (index < end) {
|
|
198
|
+
const semicolon = sourceText.indexOf(';', index);
|
|
199
|
+
if (semicolon < 0 || semicolon >= end) break;
|
|
200
|
+
const range = blockRanges.find(([rangeStart, rangeEnd]) => semicolon >= rangeStart && semicolon < rangeEnd);
|
|
201
|
+
if (range) {
|
|
202
|
+
index = range[1];
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const statementStart = previousBoundary(sourceText, index, semicolon);
|
|
206
|
+
const statementText = sourceText.slice(statementStart, semicolon + 1).replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
207
|
+
if (statementText.startsWith('@') && !statementText.includes('{')) records.push(parseAtRuleStatement(statementText, statementStart, semicolon + 1, lineStarts, sourceHash, scopes, options));
|
|
208
|
+
index = semicolon + 1;
|
|
209
|
+
}
|
|
210
|
+
return records;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseAtRuleStatement(statementText, start, end, lineStarts, sourceHash, scopes, options) {
|
|
214
|
+
const body = statementText.replace(/;$/, '').trim();
|
|
215
|
+
const match = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(body);
|
|
216
|
+
const atRuleName = match?.[1]?.toLowerCase() ?? 'unknown';
|
|
217
|
+
const conditionText = match?.[2]?.trim() ?? '';
|
|
218
|
+
const proofGaps = [];
|
|
219
|
+
if (atRuleName === 'layer') proofGaps.push(proofGap('css-layer-order-statement-unsupported', 'CSS @layer statement order requires cascade order evidence.'));
|
|
220
|
+
else proofGaps.push(proofGap(`css-${atRuleName}-statement-equivalence-unproved`, `CSS @${atRuleName} statement semantics require host evidence.`));
|
|
221
|
+
return compactRecord({
|
|
222
|
+
kind: 'at-rule-statement',
|
|
223
|
+
atRuleName,
|
|
224
|
+
conditionText,
|
|
225
|
+
statementText,
|
|
226
|
+
scopes,
|
|
227
|
+
scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
|
|
228
|
+
sourceSpan: sourceSpan(start, end, lineStarts),
|
|
229
|
+
sourceHash,
|
|
230
|
+
atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRuleStatement.v1', atRuleName, conditionText, scopes, statementText }),
|
|
231
|
+
proofGaps
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
187
235
|
function parseDeclarations(body) {
|
|
188
236
|
return body
|
|
189
237
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
@@ -256,7 +304,7 @@ function positionAt(offset, lineStarts) {
|
|
|
256
304
|
|
|
257
305
|
function sourceRef(node, extra = {}) { return { semanticNodeId: node.id, semanticNodeKind: node.kind, semanticNodeName: node.name, ...extra }; }
|
|
258
306
|
function proofGap(code, summary) { return { code, status: 'not-claimed', summary, failClosed: true, semanticEquivalenceClaim: false }; }
|
|
259
|
-
function hashableCssRecord(record) { return { kind: record.kind, selectors: record.selectors, specificity: record.specificity, scopes: record.scopes, atRuleName: record.atRuleName, conditionText: record.conditionText, declarations: record.declarations?.map((item) => ({ property: item.property, value: item.value, important: item.important })), proofGaps: record.proofGaps?.map((gap) => gap.code) }; }
|
|
307
|
+
function hashableCssRecord(record) { return { kind: record.kind, selectors: record.selectors, specificity: record.specificity, scopes: record.scopes, atRuleName: record.atRuleName, conditionText: record.conditionText, statementText: record.statementText, declarations: record.declarations?.map((item) => ({ property: item.property, value: item.value, important: item.important })), proofGaps: record.proofGaps?.map((gap) => gap.code) }; }
|
|
260
308
|
function computeLineStarts(text) { const starts = [0]; for (let index = 0; index < text.length; index += 1) if (text[index] === '\n') starts.push(index + 1); return starts; }
|
|
261
309
|
function cssIdentifier(value) { return String(value ?? 'unknown').replace(/[^A-Za-z0-9_-]/g, '-').replace(/^-+/, '') || 'unknown'; }
|
|
262
310
|
function cssString(value) { return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import postcss from 'postcss';
|
|
3
|
+
|
|
4
|
+
const ShorthandProperties = new Set(['all', 'animation', 'background', 'border', 'border-block', 'border-color', 'border-image', 'border-inline', 'border-radius', 'border-style', 'border-width', 'columns', 'flex', 'font', 'gap', 'grid', 'grid-area', 'grid-column', 'grid-row', 'inset', 'list-style', 'margin', 'offset', 'outline', 'overflow', 'padding', 'place-content', 'place-items', 'place-self', 'text-decoration', 'transition']);
|
|
5
|
+
const RuntimeAtRules = new Set(['keyframes', 'font-face', 'page', 'property']);
|
|
6
|
+
const ScopeAtRules = new Set(['media', 'supports', 'container', 'layer', 'scope']);
|
|
7
|
+
|
|
8
|
+
function parsePostcssSemanticRecords(sourceText, sourceHash, options) {
|
|
9
|
+
try {
|
|
10
|
+
const root = postcss.parse(sourceText, { from: options.sourcePath });
|
|
11
|
+
const records = postcssContainerRecords(root.nodes ?? [], [], sourceHash, options);
|
|
12
|
+
return { records, proofGaps: [], parser: { name: 'postcss', sourceCodeLocationInfo: true, parseErrors: [] } };
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const reason = error?.reason ?? error?.message ?? 'CSS parser failed';
|
|
15
|
+
return {
|
|
16
|
+
records: [],
|
|
17
|
+
proofGaps: [proofGap('css-parser-error', `CSS parser rejected source: ${reason}`)],
|
|
18
|
+
parser: {
|
|
19
|
+
name: 'postcss',
|
|
20
|
+
sourceCodeLocationInfo: true,
|
|
21
|
+
parseErrors: [compactRecord({ reason, line: error?.line, column: error?.column, input: error?.input?.file })]
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function postcssContainerRecords(nodes, scopes, sourceHash, options) {
|
|
28
|
+
const records = [];
|
|
29
|
+
for (const node of nodes) {
|
|
30
|
+
if (node.type === 'rule') records.push(postcssRuleRecord(node, scopes, sourceHash, options));
|
|
31
|
+
else if (node.type === 'atrule') {
|
|
32
|
+
records.push(postcssAtRuleRecord(node, scopes, sourceHash, options));
|
|
33
|
+
if (ScopeAtRules.has(String(node.name).toLowerCase()) && node.nodes?.length) {
|
|
34
|
+
records.push(...postcssContainerRecords(node.nodes, [...scopes, postcssAtRuleScopeKey(node)], sourceHash, options));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return records.sort((left, right) => left.sourceSpan.startOffset - right.sourceSpan.startOffset);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function postcssRuleRecord(node, scopes, sourceHash, options) {
|
|
42
|
+
const selectors = String(node.selector ?? '').split(',').map((selector) => selector.trim()).filter(Boolean);
|
|
43
|
+
const declarations = (node.nodes ?? []).filter((child) => child.type === 'decl').map(postcssDeclaration);
|
|
44
|
+
const nestedChildren = (node.nodes ?? []).filter((child) => child.type !== 'decl' && child.type !== 'comment');
|
|
45
|
+
const proofGaps = [
|
|
46
|
+
...declarations.filter((declaration) => ShorthandProperties.has(declaration.property)).map((declaration) => proofGap('css-shorthand-expansion-unproved', `CSS shorthand ${declaration.property} needs longhand expansion evidence.`)),
|
|
47
|
+
...scopes.length && !options.scopedCascadeGraphHash ? [proofGap('css-scoped-cascade-equivalence-unproved', 'Scoped cascade equivalence requires browser/style evidence.')] : [],
|
|
48
|
+
...nestedChildren.length ? [proofGap('css-nesting-semantic-unproved', 'CSS nested rule semantics require nesting expansion evidence.')] : []
|
|
49
|
+
];
|
|
50
|
+
return compactRecord({
|
|
51
|
+
kind: 'rule',
|
|
52
|
+
selectors,
|
|
53
|
+
selectorHash: hashSemanticValue({ kind: 'frontier.lang.css.selectors.v2.postcss', selectors }),
|
|
54
|
+
specificity: selectors.map(selectorSpecificity),
|
|
55
|
+
scopes,
|
|
56
|
+
declarations: declarations.map((declaration, ordinal) => ({
|
|
57
|
+
...declaration,
|
|
58
|
+
ordinal,
|
|
59
|
+
cascadeKey: [...scopes, selectors.join(','), declaration.property].join('::'),
|
|
60
|
+
declarationHash: hashSemanticValue({ kind: 'frontier.lang.css.declaration.v2.postcss', scopes, selectors, property: declaration.property, rawProperty: declaration.rawProperty, value: declaration.value, important: declaration.important })
|
|
61
|
+
})),
|
|
62
|
+
customProperties: declarations.filter((declaration) => declaration.property.startsWith('--')).map((declaration) => declaration.property),
|
|
63
|
+
scopedCascadeGraphHash: scopes.length ? options.scopedCascadeGraphHash : undefined,
|
|
64
|
+
sourceSpan: sourceSpanFromPostcss(node.source, options.sourcePath),
|
|
65
|
+
sourceHash,
|
|
66
|
+
rawTextHash: hashSemanticValue({ kind: 'frontier.lang.css.rawRuleText.v1', text: node.toString() }),
|
|
67
|
+
ruleHash: hashSemanticValue({ kind: 'frontier.lang.css.rule.v2.postcss', selectors, scopes, declarations, nestedChildren: nestedChildren.map((child) => child.type) }),
|
|
68
|
+
parser: 'postcss',
|
|
69
|
+
proofGaps: proofGaps.length ? proofGaps : undefined
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function postcssDeclaration(node) {
|
|
74
|
+
const property = String(node.prop ?? '').toLowerCase();
|
|
75
|
+
const value = String(node.value ?? '').trim();
|
|
76
|
+
return {
|
|
77
|
+
property,
|
|
78
|
+
rawProperty: node.raws?.prop?.raw ?? node.prop,
|
|
79
|
+
value,
|
|
80
|
+
important: node.important === true,
|
|
81
|
+
valueHash: hashSemanticValue({ kind: 'frontier.lang.css.value.v2.postcss', value }),
|
|
82
|
+
sourceSpan: sourceSpanFromPostcss(node.source, node.source?.input?.file),
|
|
83
|
+
rawTextHash: hashSemanticValue({ kind: 'frontier.lang.css.rawDeclarationText.v1', text: node.toString() })
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function postcssAtRuleRecord(node, scopes, sourceHash, options) {
|
|
88
|
+
const atRuleName = String(node.name ?? 'unknown').toLowerCase();
|
|
89
|
+
const conditionText = String(node.params ?? '').trim();
|
|
90
|
+
const rawText = rawPostcssText(node);
|
|
91
|
+
const proofGaps = [];
|
|
92
|
+
if (RuntimeAtRules.has(atRuleName)) proofGaps.push(proofGap(`css-${atRuleName}-runtime-equivalence-unproved`, `CSS @${atRuleName} semantics require browser evidence.`));
|
|
93
|
+
if (ScopeAtRules.has(atRuleName) && !options.scopedCascadeGraphHash) proofGaps.push(proofGap(`css-${atRuleName}-cascade-scope-unproved`, `CSS @${atRuleName} scoped cascade requires condition evaluation evidence.`));
|
|
94
|
+
if (!node.nodes?.length && atRuleName === 'layer') proofGaps.push(proofGap('css-layer-order-statement-unsupported', 'CSS @layer statement order requires cascade order evidence.'));
|
|
95
|
+
else if (!node.nodes?.length) proofGaps.push(proofGap(`css-${atRuleName}-statement-equivalence-unproved`, `CSS @${atRuleName} statement semantics require host evidence.`));
|
|
96
|
+
const kind = node.nodes?.length ? 'at-rule' : 'at-rule-statement';
|
|
97
|
+
return compactRecord({
|
|
98
|
+
kind,
|
|
99
|
+
atRuleName,
|
|
100
|
+
conditionText,
|
|
101
|
+
statementText: kind === 'at-rule-statement' ? rawText : undefined,
|
|
102
|
+
scopeKey: postcssAtRuleScopeKey(node),
|
|
103
|
+
scopes,
|
|
104
|
+
scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
|
|
105
|
+
sourceSpan: sourceSpanFromPostcss(node.source, options.sourcePath),
|
|
106
|
+
sourceHash,
|
|
107
|
+
rawTextHash: hashSemanticValue({ kind: 'frontier.lang.css.rawAtRuleText.v1', text: rawText }),
|
|
108
|
+
atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRule.v2.postcss', atRuleName, conditionText, scopes, statementText: kind === 'at-rule-statement' ? rawText : undefined }),
|
|
109
|
+
parser: 'postcss',
|
|
110
|
+
proofGaps: proofGaps.length ? proofGaps : undefined
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function postcssAtRuleScopeKey(node) {
|
|
115
|
+
return `@${String(node.name ?? 'unknown').toLowerCase()} ${String(node.params ?? '').trim()}`.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sourceSpanFromPostcss(source, fallbackPath) {
|
|
119
|
+
const start = source?.start;
|
|
120
|
+
const end = source?.end;
|
|
121
|
+
if (!start || !end) return undefined;
|
|
122
|
+
return compactRecord({
|
|
123
|
+
path: fallbackPath,
|
|
124
|
+
startOffset: start.offset,
|
|
125
|
+
endOffset: end.offset,
|
|
126
|
+
startLine: start.line,
|
|
127
|
+
startColumn: start.column,
|
|
128
|
+
endLine: end.line,
|
|
129
|
+
endColumn: end.column
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rawPostcssText(node) {
|
|
134
|
+
const css = node.source?.input?.css;
|
|
135
|
+
const start = node.source?.start?.offset;
|
|
136
|
+
const end = node.source?.end?.offset;
|
|
137
|
+
return typeof css === 'string' && Number.isFinite(start) && Number.isFinite(end) ? css.slice(start, end) : node.toString();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function selectorSpecificity(selector) {
|
|
141
|
+
const withoutStrings = selector.replace(/"[^"]*"|'[^']*'/g, '');
|
|
142
|
+
const ids = (withoutStrings.match(/#[\w-]+/g) ?? []).length;
|
|
143
|
+
const classes = (withoutStrings.match(/\.[\w-]+|\[[^\]]+\]|:(?!:)[\w-]+(?:\([^)]*\))?/g) ?? []).length;
|
|
144
|
+
const elements = (withoutStrings.replace(/#[\w-]+|\.[\w-]+|\[[^\]]+\]|:{1,2}[\w-]+(?:\([^)]*\))?/g, ' ').match(/\b[A-Za-z][\w-]*\b/g) ?? []).length;
|
|
145
|
+
return [ids, classes, elements];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function proofGap(code, summary) { return { code, status: 'not-claimed', summary, failClosed: true, semanticEquivalenceClaim: false }; }
|
|
149
|
+
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
|
|
150
|
+
|
|
151
|
+
export { parsePostcssSemanticRecords };
|
|
@@ -150,7 +150,7 @@ function unsupportedSourceShapeChanges(baseSheet, currentSheet, declarationChang
|
|
|
150
150
|
if (changedDeclarationRuleKeys.has(before?.ruleKey) || changedDeclarationRuleKeys.has(after?.ruleKey)) return [];
|
|
151
151
|
return [{
|
|
152
152
|
side,
|
|
153
|
-
reasonCode:
|
|
153
|
+
reasonCode: sourceShapeChangeReason(before, after),
|
|
154
154
|
shapeKey: key,
|
|
155
155
|
before: sourceShapeDetails(before),
|
|
156
156
|
after: sourceShapeDetails(after)
|
|
@@ -181,6 +181,19 @@ function sourceShapeIndex(sheet, hash) {
|
|
|
181
181
|
atRuleName: record.atRuleName,
|
|
182
182
|
conditionText: record.conditionText,
|
|
183
183
|
representedByDeclarations: false,
|
|
184
|
+
unsupportedReasonCode: atRuleUnsupportedReasonCode(record),
|
|
185
|
+
hash: record.atRuleHash
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (record.kind === 'at-rule-statement') {
|
|
189
|
+
const shapeKey = `at-rule-statement:${[...(record.scopes ?? []), record.atRuleName, record.conditionText].join('::')}`;
|
|
190
|
+
result.set(shapeKey, {
|
|
191
|
+
kind: 'at-rule-statement',
|
|
192
|
+
atRuleName: record.atRuleName,
|
|
193
|
+
conditionText: record.conditionText,
|
|
194
|
+
statementText: record.statementText,
|
|
195
|
+
representedByDeclarations: false,
|
|
196
|
+
unsupportedReasonCode: atRuleStatementUnsupportedReasonCode(record),
|
|
184
197
|
hash: record.atRuleHash
|
|
185
198
|
});
|
|
186
199
|
}
|
|
@@ -195,7 +208,15 @@ function conflict(id, sourcePath, code, reasonCode, details = {}) {
|
|
|
195
208
|
|
|
196
209
|
function sameContractChange(left, right) { return (left.after?.hash ?? '') === (right.after?.hash ?? '') && left.kind === right.kind; }
|
|
197
210
|
function contractChangeDetails(change) { return { kind: change.kind, contractKind: (change.after ?? change.before)?.contractKind, name: (change.after ?? change.before)?.name, hash: change.after?.hash }; }
|
|
198
|
-
function sourceShapeDetails(shape) { return shape ? { kind: shape.kind, selectors: shape.selectors, atRuleName: shape.atRuleName, conditionText: shape.conditionText, representedByDeclarations: shape.representedByDeclarations } : undefined; }
|
|
211
|
+
function sourceShapeDetails(shape) { return shape ? { kind: shape.kind, selectors: shape.selectors, atRuleName: shape.atRuleName, conditionText: shape.conditionText, statementText: shape.statementText, representedByDeclarations: shape.representedByDeclarations } : undefined; }
|
|
212
|
+
function sourceShapeChangeReason(before, after) {
|
|
213
|
+
if (!before && after?.kind === 'at-rule') return 'css-atrule-new-scope-unsupported';
|
|
214
|
+
if (before?.kind === 'at-rule' || after?.kind === 'at-rule') return after?.unsupportedReasonCode ?? before?.unsupportedReasonCode ?? 'css-atrule-condition-edit-unsupported';
|
|
215
|
+
if (before?.kind === 'at-rule-statement' || after?.kind === 'at-rule-statement') return after?.unsupportedReasonCode ?? before?.unsupportedReasonCode ?? 'css-atrule-statement-unsupported';
|
|
216
|
+
return 'css-source-shape-unsupported';
|
|
217
|
+
}
|
|
218
|
+
function atRuleUnsupportedReasonCode(record) { return record.atRuleName === 'layer' ? 'css-layer-name-edit-unsupported' : 'css-atrule-condition-edit-unsupported'; }
|
|
219
|
+
function atRuleStatementUnsupportedReasonCode(record) { return record.atRuleName === 'layer' ? 'css-layer-order-statement-unsupported' : 'css-atrule-statement-unsupported'; }
|
|
199
220
|
function ruleIdentityKey(record) { return [...(record.scopes ?? []), record.selectors.join(',')].join('::'); }
|
|
200
221
|
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
201
222
|
function uniqueProofGaps(values) {
|
package/dist/semantic-merge.js
CHANGED
|
@@ -10,8 +10,6 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
10
10
|
const head = input.headSourceText ?? base;
|
|
11
11
|
if (typeof base !== 'string' || typeof worker !== 'string' || typeof head !== 'string') return blocked(id, sourcePath, 'css-source-text-missing');
|
|
12
12
|
if (worker === head) return merged(id, sourcePath, worker, 'worker-head-identical', hash);
|
|
13
|
-
if (worker === base) return merged(id, sourcePath, head, 'worker-unchanged', hash);
|
|
14
|
-
if (head === base) return merged(id, sourcePath, worker, 'head-unchanged', hash);
|
|
15
13
|
const sheets = {
|
|
16
14
|
base: parseSheet(base, sheetOptions(input, 'base', sourcePath)),
|
|
17
15
|
worker: parseSheet(worker, sheetOptions(input, 'worker', sourcePath)),
|
|
@@ -24,13 +22,14 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
24
22
|
};
|
|
25
23
|
const moduleChanges = cssModuleContractChanges(sheets, hash);
|
|
26
24
|
const proofConflicts = proofGapConflicts(id, sourcePath, changed, indexes);
|
|
25
|
+
const parserConflicts = parserErrorConflicts(id, sourcePath, sheets);
|
|
27
26
|
const overlapConflicts = [
|
|
28
27
|
...overlapDeclarationConflicts(id, sourcePath, changed.worker, changed.head),
|
|
29
28
|
...shorthandOverlapConflicts(id, sourcePath, changed.worker, changed.head)
|
|
30
29
|
];
|
|
31
30
|
const moduleConflicts = cssModuleContractConflicts(id, sourcePath, moduleChanges);
|
|
32
31
|
const sourceShapeConflicts = unsupportedSourceShapeConflicts(id, sourcePath, sheets, changed, hash);
|
|
33
|
-
const conflicts = [...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts];
|
|
32
|
+
const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts];
|
|
34
33
|
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts);
|
|
35
34
|
const mergedIndex = applyDeclarationChanges(applyDeclarationChanges(indexes.base, changed.head), changed.worker);
|
|
36
35
|
return merged(id, sourcePath, renderDeclarationIndex(mergedIndex), 'semantic-declaration-merge', hash, {
|
|
@@ -47,7 +46,17 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
47
46
|
function declarationIndex(sheet) {
|
|
48
47
|
const declarations = new Map();
|
|
49
48
|
const order = [];
|
|
49
|
+
const statements = [];
|
|
50
50
|
for (const record of sheet.records) {
|
|
51
|
+
if (record.kind === 'at-rule-statement') {
|
|
52
|
+
statements.push({
|
|
53
|
+
key: record.atRuleHash,
|
|
54
|
+
scopes: record.scopes ?? [],
|
|
55
|
+
statementText: record.statementText,
|
|
56
|
+
atRuleName: record.atRuleName,
|
|
57
|
+
conditionText: record.conditionText
|
|
58
|
+
});
|
|
59
|
+
}
|
|
51
60
|
if (record.kind !== 'rule') continue;
|
|
52
61
|
const ruleKey = ruleIdentityKey(record);
|
|
53
62
|
for (const declaration of record.declarations ?? []) {
|
|
@@ -66,7 +75,7 @@ function declarationIndex(sheet) {
|
|
|
66
75
|
order.push(entry.key);
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
|
-
return { declarations, order: unique(order) };
|
|
78
|
+
return { declarations, order: unique(order), statements };
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
function changedDeclarations(baseIndex, currentIndex, side) {
|
|
@@ -92,6 +101,12 @@ function proofGapConflicts(id, sourcePath, changed, indexes) {
|
|
|
92
101
|
});
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
function parserErrorConflicts(id, sourcePath, sheets) {
|
|
105
|
+
return Object.entries(sheets).flatMap(([side, sheet]) => (sheet.proofGaps ?? [])
|
|
106
|
+
.filter((gap) => gap.code === 'css-parser-error')
|
|
107
|
+
.map((gap) => conflict(id, sourcePath, 'css-parser-error-blocked', gap.code, { side, proofGap: gap })));
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
function overlapDeclarationConflicts(id, sourcePath, workerChanges, headChanges) {
|
|
96
111
|
const headByKey = new Map(headChanges.map((change) => [change.key, change]));
|
|
97
112
|
return workerChanges.flatMap((workerChange) => {
|
|
@@ -143,7 +158,7 @@ function applyDeclarationChanges(index, changes) {
|
|
|
143
158
|
if (!order.includes(change.key)) order.push(change.key);
|
|
144
159
|
}
|
|
145
160
|
}
|
|
146
|
-
return { declarations, order: order.filter((key) => declarations.has(key)) };
|
|
161
|
+
return { declarations, order: order.filter((key) => declarations.has(key)), statements: index.statements ?? [] };
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
function renderDeclarationIndex(index) {
|
|
@@ -154,12 +169,27 @@ function renderDeclarationIndex(index) {
|
|
|
154
169
|
groups.set(declaration.ruleKey, [...(groups.get(declaration.ruleKey) ?? []), declaration]);
|
|
155
170
|
}
|
|
156
171
|
const chunks = [];
|
|
172
|
+
for (const statement of index.statements ?? []) renderAtRuleStatement(chunks, statement);
|
|
157
173
|
for (const declarations of groups.values()) {
|
|
158
174
|
renderDeclarationGroup(chunks, declarations);
|
|
159
175
|
}
|
|
160
176
|
return `${chunks.join('\n').trimEnd()}\n`;
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
function renderAtRuleStatement(chunks, statement) {
|
|
180
|
+
let indent = 0;
|
|
181
|
+
for (const scope of statement.scopes ?? []) {
|
|
182
|
+
chunks.push(`${spaces(indent)}${scope} {`);
|
|
183
|
+
indent += 2;
|
|
184
|
+
}
|
|
185
|
+
chunks.push(`${spaces(indent)}${statement.statementText}`);
|
|
186
|
+
for (let index = (statement.scopes ?? []).length - 1; index >= 0; index -= 1) {
|
|
187
|
+
indent -= 2;
|
|
188
|
+
chunks.push(`${spaces(indent)}}`);
|
|
189
|
+
}
|
|
190
|
+
chunks.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
163
193
|
function renderDeclarationGroup(chunks, declarations) {
|
|
164
194
|
const first = declarations[0];
|
|
165
195
|
let indent = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shapeshift-labs/frontier-lang-css",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "CSS semantic merge evidence and projection adapter for Frontier Lang semantic source documents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@shapeshift-labs/frontier-lang-kernel": "0.3.12"
|
|
56
|
+
"@shapeshift-labs/frontier-lang-kernel": "0.3.12",
|
|
57
|
+
"postcss": "^8.5.15"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"typescript": "^5.9.3"
|