@shapeshift-labs/frontier-lang-css 0.1.5 → 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/dist/index.d.ts +19 -0
- package/dist/index.js +8 -4
- package/dist/postcss-parser-evidence.js +151 -0
- package/dist/semantic-merge.js +8 -1
- package/package.json +3 -2
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;
|
|
@@ -129,6 +145,8 @@ export interface CssSemanticRecord {
|
|
|
129
145
|
readonly scopedCascadeGraphHash?: string;
|
|
130
146
|
readonly sourceSpan: CssSourceSpan;
|
|
131
147
|
readonly sourceHash: string;
|
|
148
|
+
readonly parser?: 'postcss' | string;
|
|
149
|
+
readonly rawTextHash?: string;
|
|
132
150
|
readonly ruleHash?: string;
|
|
133
151
|
readonly atRuleHash?: string;
|
|
134
152
|
readonly proofGaps?: readonly CssSemanticProofGap[];
|
|
@@ -200,6 +218,7 @@ export interface CssSemanticSheet {
|
|
|
200
218
|
readonly sheetHash: string;
|
|
201
219
|
readonly summary: Readonly<Record<string, number>>;
|
|
202
220
|
readonly proofGaps: readonly CssSemanticProofGap[];
|
|
221
|
+
readonly parser: CssParserEvidence;
|
|
203
222
|
}
|
|
204
223
|
|
|
205
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
|
|
|
@@ -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 };
|
package/dist/semantic-merge.js
CHANGED
|
@@ -22,13 +22,14 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
22
22
|
};
|
|
23
23
|
const moduleChanges = cssModuleContractChanges(sheets, hash);
|
|
24
24
|
const proofConflicts = proofGapConflicts(id, sourcePath, changed, indexes);
|
|
25
|
+
const parserConflicts = parserErrorConflicts(id, sourcePath, sheets);
|
|
25
26
|
const overlapConflicts = [
|
|
26
27
|
...overlapDeclarationConflicts(id, sourcePath, changed.worker, changed.head),
|
|
27
28
|
...shorthandOverlapConflicts(id, sourcePath, changed.worker, changed.head)
|
|
28
29
|
];
|
|
29
30
|
const moduleConflicts = cssModuleContractConflicts(id, sourcePath, moduleChanges);
|
|
30
31
|
const sourceShapeConflicts = unsupportedSourceShapeConflicts(id, sourcePath, sheets, changed, hash);
|
|
31
|
-
const conflicts = [...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts];
|
|
32
|
+
const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts];
|
|
32
33
|
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts);
|
|
33
34
|
const mergedIndex = applyDeclarationChanges(applyDeclarationChanges(indexes.base, changed.head), changed.worker);
|
|
34
35
|
return merged(id, sourcePath, renderDeclarationIndex(mergedIndex), 'semantic-declaration-merge', hash, {
|
|
@@ -100,6 +101,12 @@ function proofGapConflicts(id, sourcePath, changed, indexes) {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
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
|
+
|
|
103
110
|
function overlapDeclarationConflicts(id, sourcePath, workerChanges, headChanges) {
|
|
104
111
|
const headByKey = new Map(headChanges.map((change) => [change.key, change]));
|
|
105
112
|
return workerChanges.flatMap((workerChange) => {
|
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"
|