sheetom 0.1.0-rc.1 → 0.1.0-rc.2

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 CHANGED
@@ -96,7 +96,10 @@ block remote resources, or make valid hostile CSS safe to render.
96
96
 
97
97
  Invalid `setProperty` values and priorities are atomic no-ops, matching browser
98
98
  behavior. Opt into mutation diagnostics with `{ diagnostics: true }` and drain
99
- them using `takeDiagnostics()`.
99
+ them using `takeDiagnostics()`. `SheetOMDiagnostic.code` is a stable string
100
+ union. Diagnostics retain the complete rejected input until drained; when
101
+ processing untrusted or very large input, callers should leave diagnostics off
102
+ or drain them promptly to bound memory use.
100
103
 
101
104
  ## API scope
102
105
 
@@ -153,8 +156,13 @@ npm run pack:test
153
156
 
154
157
  `npm test` runs unit, deterministic fuzz, and local Chromium projects. Use
155
158
  `npm run test:browser:matrix` on a machine with all Playwright dependencies to
156
- run Chromium, Firefox, and WebKit. `npm run docs:build` generates the TypeDoc
157
- reference under `site/api`.
159
+ run Chromium, Firefox, and WebKit. The browser matrix also runs a deterministic,
160
+ grammar-oriented declaration differential; any minimized mismatch must become
161
+ an immutable Operation Fixture with an explicit Compatibility Resolution.
162
+ `npm run docs:build` generates the TypeDoc reference under `site/api`. The benchmark gates both a 10,000-rule stress case
163
+ and a publisher-shaped reference workload with one shared stylesheet, 20 page
164
+ stylesheets, nested layers/media, distributed mutations, and two idempotent
165
+ serialization passes.
158
166
 
159
167
  Only the latest published `0.x` minor and its active prereleases receive fixes
160
168
  before 1.0. During `0.x`, observable compatibility corrections and interface
@@ -0,0 +1,142 @@
1
+ {
2
+ "$schema": "../schemas/compatibility-report.schema.json",
3
+ "schemaVersion": 2,
4
+ "packageVersion": "0.1.0-rc.2",
5
+ "baseline": {
6
+ "wptCommit": "54f8f933629e7c010ae98a246729af01f8abcda5",
7
+ "syntaxEngineSet": {
8
+ "lightningcss": "1.33.0",
9
+ "cssTree": "3.2.1",
10
+ "cssstyle": "6.2.0",
11
+ "cssTokenizer": "4.0.0"
12
+ },
13
+ "runtimes": {
14
+ "node": "v24.18.0",
15
+ "bun": "1.3.1",
16
+ "deno": "2.9.5"
17
+ },
18
+ "browsers": {
19
+ "chromium": "151.0.7922.34 (revision 1234)",
20
+ "firefox": "153.0 (revision 1538)",
21
+ "webkit": "26.5 (revision 2336)"
22
+ }
23
+ },
24
+ "summary": {
25
+ "passed": 6,
26
+ "divergences": 7,
27
+ "excluded": 0,
28
+ "unexplained": 0
29
+ },
30
+ "evidence": {
31
+ "operationFixtures": {
32
+ "passed": 13,
33
+ "total": 13,
34
+ "sha256": "7909f90571b8d6595d18b006bb4f2c5337cca9d413084558e913e760be45a3aa",
35
+ "adapters": [
36
+ {
37
+ "adapter": "sheetom",
38
+ "passed": 13,
39
+ "total": 13
40
+ },
41
+ {
42
+ "adapter": "chromium",
43
+ "passed": 13,
44
+ "total": 13
45
+ },
46
+ {
47
+ "adapter": "firefox",
48
+ "passed": 13,
49
+ "total": 13
50
+ },
51
+ {
52
+ "adapter": "webkit",
53
+ "passed": 13,
54
+ "total": 13
55
+ }
56
+ ]
57
+ },
58
+ "nativeWpt": [
59
+ {
60
+ "engine": "chrome",
61
+ "version": "150.0.7871.128",
62
+ "sha256": "3f66a5f324bb9023cdd4b168e780640700449f22f28ca378eca91504b7d68e3f",
63
+ "passed": 4,
64
+ "total": 4
65
+ },
66
+ {
67
+ "engine": "firefox",
68
+ "version": "153.0.3",
69
+ "sha256": "e5f88e3431f602dc29fd8dbd79667bef33930ee7f87ee6d2069f17126421a230",
70
+ "passed": 4,
71
+ "total": 4
72
+ }
73
+ ]
74
+ },
75
+ "resolutions": [
76
+ {
77
+ "fixtureId": "declarations.basic-set-property",
78
+ "decision": "shared",
79
+ "rationale": "The observable declaration state is shared by the pinned browser engines."
80
+ },
81
+ {
82
+ "fixtureId": "declarations.malformed-padding",
83
+ "decision": "chromium-fallback",
84
+ "rationale": "CSS Syntax accepts the EOF-recovered value; Chromium preserves the unclosed observable spelling while Firefox materializes closing parentheses."
85
+ },
86
+ {
87
+ "fixtureId": "declarations.modern-color-canonicalization",
88
+ "decision": "shared",
89
+ "rationale": "The pinned browser engines expose modern rgb syntax through the shared legacy rgba serialization in CSSStyleDeclaration."
90
+ },
91
+ {
92
+ "fixtureId": "declarations.typed-eof-recovery",
93
+ "decision": "chromium-fallback",
94
+ "rationale": "Typed property values expose the Chromium/WebKit canonical serialization after CSS Syntax EOF recovery; Firefox retains quotes around the recovered font-family string."
95
+ },
96
+ {
97
+ "fixtureId": "declarations.quoted-font-family",
98
+ "decision": "chromium-fallback",
99
+ "rationale": "Chromium and WebKit remove unnecessary quotes around a simple font-family while Firefox retains them; SheetOM follows the measured Chromium fallback."
100
+ },
101
+ {
102
+ "fixtureId": "declarations.custom-property-eof-recovery",
103
+ "decision": "chromium-fallback",
104
+ "rationale": "Chromium retains unclosed custom-property token structures, removes EOF comments, and replaces an escaped EOF with U+FFFD."
105
+ },
106
+ {
107
+ "fixtureId": "declarations.custom-property-priority-replacement",
108
+ "decision": "chromium-fallback",
109
+ "rationale": "Chromium and Firefox replace the existing custom-property priority when setProperty is called without a priority, while WebKit retains !important; SheetOM follows the measured Chromium fallback."
110
+ },
111
+ {
112
+ "fixtureId": "declarations.pending-substitution-eof-recovery",
113
+ "decision": "chromium-fallback",
114
+ "rationale": "Chromium retains the unclosed pending-substitution spelling while applying lexical EOF recovery such as comment removal."
115
+ },
116
+ {
117
+ "fixtureId": "declarations.setproperty-null-undefined",
118
+ "decision": "shared",
119
+ "rationale": "The four adapted WPT subtests define the shared null and undefined conversion behavior."
120
+ },
121
+ {
122
+ "fixtureId": "declarations.required-arguments",
123
+ "decision": "shared",
124
+ "rationale": "Required WebIDL arguments throw TypeError before string conversion in the pinned browser engines."
125
+ },
126
+ {
127
+ "fixtureId": "declarations.set-property-multiple-declarations",
128
+ "decision": "shared",
129
+ "rationale": "A setProperty value is parsed independently and cannot escape into another declaration; invalid input leaves the previous declaration unchanged."
130
+ },
131
+ {
132
+ "fixtureId": "declarations.escaped-custom-property-name",
133
+ "decision": "shared",
134
+ "rationale": "Custom-property method arguments retain their logical names and cssText escapes those names only when serializing CSS syntax."
135
+ },
136
+ {
137
+ "fixtureId": "integration.weweb-authoring-tree",
138
+ "decision": "chromium-fallback",
139
+ "rationale": "The standard rule-tree operations and detachment identity are shared; the recovered padding spelling follows the existing Chromium fallback resolution."
140
+ }
141
+ ]
142
+ }
@@ -7,16 +7,27 @@
7
7
  "required": ["schemaVersion", "packageVersion", "baseline", "summary", "evidence", "resolutions"],
8
8
  "properties": {
9
9
  "$schema": { "type": "string" },
10
- "schemaVersion": { "const": 1 },
10
+ "schemaVersion": { "enum": [1, 2] },
11
11
  "packageVersion": { "type": "string" },
12
12
  "baseline": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
15
- "required": ["wptCommit", "lightningcss", "cssTree", "runtimes", "browsers"],
15
+ "required": ["wptCommit", "runtimes", "browsers"],
16
16
  "properties": {
17
17
  "wptCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
18
18
  "lightningcss": { "type": "string" },
19
19
  "cssTree": { "type": "string" },
20
+ "syntaxEngineSet": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["lightningcss", "cssTree", "cssstyle", "cssTokenizer"],
24
+ "properties": {
25
+ "lightningcss": { "type": "string" },
26
+ "cssTree": { "type": "string" },
27
+ "cssstyle": { "type": "string" },
28
+ "cssTokenizer": { "type": "string" }
29
+ }
30
+ },
20
31
  "runtimes": { "type": "object" },
21
32
  "browsers": { "type": "object" }
22
33
  }
@@ -80,5 +91,36 @@
80
91
  }
81
92
  },
82
93
  "resolutions": { "type": "array" }
83
- }
94
+ },
95
+ "allOf": [
96
+ {
97
+ "if": { "properties": { "schemaVersion": { "const": 1 } } },
98
+ "then": {
99
+ "properties": {
100
+ "baseline": {
101
+ "type": "object",
102
+ "required": ["lightningcss", "cssTree"],
103
+ "properties": {
104
+ "lightningcss": { "type": "string" },
105
+ "cssTree": { "type": "string" }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ {
112
+ "if": { "properties": { "schemaVersion": { "const": 2 } } },
113
+ "then": {
114
+ "properties": {
115
+ "baseline": {
116
+ "type": "object",
117
+ "required": ["syntaxEngineSet"],
118
+ "properties": {
119
+ "syntaxEngineSet": { "type": "object" }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ ]
84
126
  }
package/dist/index.cjs CHANGED
@@ -1975,6 +1975,87 @@ function expandStaticShorthand(name, parsed, important) {
1975
1975
  return records;
1976
1976
  }
1977
1977
  //#endregion
1978
+ //#region src/internal/observable-value-codec.ts
1979
+ const closingToken = /* @__PURE__ */ new Map([
1980
+ [_csstools_css_tokenizer.TokenType.Function, {
1981
+ token: _csstools_css_tokenizer.TokenType.CloseParen,
1982
+ text: ")"
1983
+ }],
1984
+ [_csstools_css_tokenizer.TokenType.OpenParen, {
1985
+ token: _csstools_css_tokenizer.TokenType.CloseParen,
1986
+ text: ")"
1987
+ }],
1988
+ [_csstools_css_tokenizer.TokenType.OpenSquare, {
1989
+ token: _csstools_css_tokenizer.TokenType.CloseSquare,
1990
+ text: "]"
1991
+ }],
1992
+ [_csstools_css_tokenizer.TokenType.OpenCurly, {
1993
+ token: _csstools_css_tokenizer.TokenType.CloseCurly,
1994
+ text: "}"
1995
+ }]
1996
+ ]);
1997
+ function escapedString(value, quote) {
1998
+ return value.replace(/\\/g, "\\\\").replace(new RegExp(quote, "g"), `\\${quote}`);
1999
+ }
2000
+ function retainedToken(token) {
2001
+ if (token[0] === _csstools_css_tokenizer.TokenType.Comment || token[0] === _csstools_css_tokenizer.TokenType.EOF) return "";
2002
+ const details = token[4];
2003
+ if (token[0] === _csstools_css_tokenizer.TokenType.Ident && token[1].endsWith("\\") && details && "value" in details && `${details.value}`.endsWith("�")) return `${token[1].slice(0, -1)}�`;
2004
+ if (token[0] === _csstools_css_tokenizer.TokenType.URL && token[1].endsWith("\\") && details && "value" in details) return `url(${details.value})`;
2005
+ if (token[0] === _csstools_css_tokenizer.TokenType.String && token[1].endsWith("\\") && details && "value" in details) {
2006
+ const quote = token[1][0] ?? "\"";
2007
+ return `${quote}${escapedString(`${details.value}`, quote)}${quote}`;
2008
+ }
2009
+ return token[1];
2010
+ }
2011
+ function recoverTokenText(input) {
2012
+ let parseError = false;
2013
+ const tokens = (0, _csstools_css_tokenizer.tokenize)({ css: input }, { onParseError: () => {
2014
+ parseError = true;
2015
+ } });
2016
+ const stack = [];
2017
+ let retained = "";
2018
+ let closed = "";
2019
+ let removedComment = false;
2020
+ for (const token of tokens) {
2021
+ if (token[0] === _csstools_css_tokenizer.TokenType.EOF) continue;
2022
+ if (token[0] === _csstools_css_tokenizer.TokenType.Comment) {
2023
+ removedComment = true;
2024
+ continue;
2025
+ }
2026
+ const text = retainedToken(token);
2027
+ retained += text;
2028
+ closed += text;
2029
+ const closing = closingToken.get(token[0]);
2030
+ if (closing) {
2031
+ stack.push(closing);
2032
+ continue;
2033
+ }
2034
+ if (stack.at(-1)?.token === token[0]) stack.pop();
2035
+ if (token[0] === _csstools_css_tokenizer.TokenType.URL && !text.endsWith(")")) closed += ")";
2036
+ if (token[0] !== _csstools_css_tokenizer.TokenType.String) continue;
2037
+ const quote = token[1][0];
2038
+ if (quote && !text.endsWith(quote)) closed += quote;
2039
+ }
2040
+ for (let index = stack.length - 1; index >= 0; index -= 1) closed += stack[index]?.text ?? "";
2041
+ return {
2042
+ closed: closed.trim(),
2043
+ recovered: parseError || removedComment || stack.length > 0,
2044
+ retained: retained.trim()
2045
+ };
2046
+ }
2047
+ /** Produces browser-facing text without changing acceptance or reparsable output. */
2048
+ function serializeObservableValue({ name, input, safeValue, category }) {
2049
+ const recovered = recoverTokenText(input.trim());
2050
+ if (category !== "typed") return recovered.retained;
2051
+ if (name === "font-family") return safeValue;
2052
+ const oracle = new cssstyle.CSSStyleDeclaration();
2053
+ oracle.setProperty(name, recovered.closed);
2054
+ const canonicalValue = oracle.getPropertyValue(name);
2055
+ if (canonicalValue !== "") return canonicalValue;
2056
+ return recovered.recovered ? recovered.closed : input.trim();
2057
+ }
2058
+ //#endregion
1978
2059
  //#region src/internal/value-gate.ts
1979
2060
  const encoder$1 = new TextEncoder();
1980
2061
  const decoder$1 = new TextDecoder();
@@ -2285,7 +2366,14 @@ function parsePropertyValue(name, observableValue) {
2285
2366
  if (css_tree.ident.decode(serializedDeclaration.property) !== name) return null;
2286
2367
  const valueLocation = serializedDeclaration.value.loc;
2287
2368
  const safeValue = valueLocation ? serialized.slice(valueLocation.start.offset, valueLocation.end.offset) : css_tree.generate(serializedDeclaration.value);
2288
- const trimmedObservableValue = canonicalizeBrowserValue(name, canonicalizeLeadingDecimal(observableValue.trim()));
2369
+ const observableCategory = name.startsWith("--") ? "custom" : pendingSubstitution ? "pending-substitution" : "typed";
2370
+ const serializedObservableValue = serializeObservableValue({
2371
+ name,
2372
+ input: observableValue,
2373
+ safeValue: safeValue.trim(),
2374
+ category: observableCategory
2375
+ });
2376
+ const trimmedObservableValue = canonicalizeBrowserValue(name, observableCategory === "typed" ? canonicalizeLeadingDecimal(serializedObservableValue) : serializedObservableValue);
2289
2377
  const normalizedSafeValue = canonicalizeBrowserValue(name, safeValue.trim());
2290
2378
  return {
2291
2379
  observableValue: trimmedObservableValue === "0" && zeroLengthProperties.has(name) ? "0px" : trimmedObservableValue,
@@ -2541,6 +2629,28 @@ function normalizeConditionalPrelude(name, value) {
2541
2629
  return value;
2542
2630
  }
2543
2631
  }
2632
+ function parseContainerPrelude(value) {
2633
+ try {
2634
+ const parsed = css_tree.parse(`@container ${value} {}`);
2635
+ const rule = parsed.type === "StyleSheet" ? parsed.children.first : null;
2636
+ if (rule?.type !== "Atrule" || rule.name.toLowerCase() !== "container") return null;
2637
+ if (rule.prelude?.type !== "AtrulePrelude") return null;
2638
+ const children = rule.prelude.children.toArray();
2639
+ const first = children[0];
2640
+ const name = first?.type === "Identifier" ? first.name : "";
2641
+ const queryNodes = name === "" ? children : children.slice(1);
2642
+ if (queryNodes.length === 0) return null;
2643
+ const query = queryNodes.map((node) => css_tree.generate(node)).join(" ").replace(/:\s*/g, ": ").replace(/\s*(<=|>=|=|<|>)\s*/g, " $1 ");
2644
+ if (query === "") return null;
2645
+ return {
2646
+ conditionText: name === "" ? query : `${name} ${query}`,
2647
+ name,
2648
+ query
2649
+ };
2650
+ } catch {
2651
+ return null;
2652
+ }
2653
+ }
2544
2654
  function splitMediaQueries(value) {
2545
2655
  const queries = [];
2546
2656
  let start = 0;
@@ -2697,11 +2807,11 @@ var CSSContainerRule = class extends CSSConditionRule {
2697
2807
  containerName;
2698
2808
  containerQuery;
2699
2809
  constructor(conditionText) {
2700
- const normalizedCondition = normalizeConditionalPrelude("container", conditionText);
2810
+ const parsed = parseContainerPrelude(conditionText);
2811
+ const normalizedCondition = parsed?.conditionText ?? conditionText.trim();
2701
2812
  super(0, normalizedCondition);
2702
- const openingParenthesis = normalizedCondition.indexOf("(");
2703
- this.containerName = openingParenthesis === -1 ? "" : normalizedCondition.slice(0, openingParenthesis).trim();
2704
- this.containerQuery = openingParenthesis === -1 ? normalizedCondition : normalizedCondition.slice(openingParenthesis).trim();
2813
+ this.containerName = parsed?.name ?? "";
2814
+ this.containerQuery = parsed?.query ?? normalizedCondition;
2705
2815
  lockOwnProperties(this, "containerName", "containerQuery");
2706
2816
  }
2707
2817
  get cssText() {