@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 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 records = parseCssBlocks(sourceText, 0, sourceText.length, [], lineStarts, sourceHash, options);
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 };
@@ -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.5",
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"