sheetom 0.1.0-rc.8 → 0.1.0

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
@@ -64,6 +64,31 @@ Constructed sheets strip `@import` during replacement. `parseStyleSheet()` is a
64
64
  SheetOM extension for existing regular stylesheets and preserves valid imports
65
65
  without fetching them.
66
66
 
67
+ Compilers can apply all known mutations for one rule with one engine crossing
68
+ and still receive one acceptance result per operation:
69
+
70
+ ```ts
71
+ function applyCompilerDeclarations(rule: CSSStyleRule, dynamicWidth: string) {
72
+ const results = rule.style.applyMutations([
73
+ { kind: "set", property: "display", value: "grid" },
74
+ { kind: "set", property: "width", value: dynamicWidth },
75
+ { kind: "remove", property: "margin-left" },
76
+ ]);
77
+
78
+ for (const result of results) {
79
+ if (result.kind === "set" && !result.accepted) {
80
+ console.warn(result.diagnostic);
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ Operations run in order through the same validation and shorthand/longhand
87
+ state machine as individual calls. Group one batch per declaration block;
88
+ never combine values into declaration text. Batch results already include
89
+ rejection diagnostics, so high-volume callers can leave the sheet diagnostic
90
+ queue disabled unless they also need diagnostics from other APIs.
91
+
67
92
  ## Browser recovery, safe output
68
93
 
69
94
  Browsers accept some CSS using end-of-input recovery and expose the incomplete
@@ -94,6 +119,13 @@ sheet.serialize();
94
119
  | `style.cssText` | Browser-compatible declaration text, including measured recovery quirks. |
95
120
  | `rule.cssText` | Browser-compatible text for the current rule. |
96
121
  | `sheet.serialize()` | Deterministic, reparsable CSS for a complete stylesheet. |
122
+ | `sheet.serializeStrict()` | Exact-only output that rejects CSSOM states CSS text cannot represent faithfully. |
123
+
124
+ Pending-substitution shorthands survive later longhand edits in reparsable
125
+ output. If an imperative priority combination cannot be represented exactly,
126
+ `serialize()` emits a local best-effort result and an opt-in structured
127
+ diagnostic rather than blocking the whole stylesheet. Use `serializeStrict()`
128
+ when approximation is unacceptable.
97
129
 
98
130
  Invalid property values and priorities are atomic no-ops: the previous state
99
131
  survives unchanged. Static shorthands are stored as canonical longhands, so
package/dist/index.cjs CHANGED
@@ -28,9 +28,9 @@ let node_url = require("node:url");
28
28
  //#endregion
29
29
  //#region src/internal/engine-binding.ts
30
30
  const expectedEngineAbiIdentity = Object.freeze({
31
- abiVersion: 1,
32
- sheetomVersion: "0.1.0-rc.8",
33
- syntaxEngineSetSha256: "8337ff1bf7940a5772394a810eb2e3108299700e862fe20b54f01bce6b69c011"
31
+ abiVersion: 3,
32
+ sheetomVersion: "0.1.0",
33
+ syntaxEngineSetSha256: "31990c26ede4e17c3a8941027f92b2be96a167984d0ab6529e724c3d6126be50"
34
34
  });
35
35
  var SheetOMEngineBindingError = class extends Error {
36
36
  code;
@@ -114,16 +114,47 @@ function limitValue(value, name, fallback, maximum = maximumUnsignedLong) {
114
114
  throw new RangeError(`${name} must be an integer between 0 and ${maximum}`);
115
115
  }
116
116
  //#endregion
117
+ //#region src/serialization-error.ts
118
+ /** A reparsable stylesheet cannot faithfully represent the current CSSOM state. */
119
+ var SheetOMSerializationError = class extends Error {
120
+ code;
121
+ shorthand;
122
+ conflictingLonghands;
123
+ constructor(shorthand, conflictingLonghands, options) {
124
+ super(`SheetOM cannot serialize the pending ${shorthand} shorthand without changing its semantics.`, options);
125
+ this.name = "SheetOMSerializationError";
126
+ this.code = "UNREPRESENTABLE_PENDING_SHORTHAND";
127
+ this.shorthand = shorthand;
128
+ this.conflictingLonghands = Object.freeze([...conflictingLonghands]);
129
+ }
130
+ };
131
+ const unrepresentablePendingShorthandPattern = /^SHEETOM_UNREPRESENTABLE_PENDING_SHORTHAND: shorthand=([^;]+); conflicting=([a-z0-9,-]+)$/u;
132
+ function serializationErrorFrom(error) {
133
+ if (!(error instanceof Error)) return null;
134
+ const match = unrepresentablePendingShorthandPattern.exec(error.message);
135
+ if (!match) return null;
136
+ const shorthand = match[1];
137
+ const conflicts = match[2];
138
+ if (!shorthand || !conflicts) return null;
139
+ return new SheetOMSerializationError(shorthand, conflicts.split(","), { cause: error });
140
+ }
141
+ function rethrowSerializationError(error) {
142
+ const serializationError = serializationErrorFrom(error);
143
+ if (serializationError) throw serializationError;
144
+ }
145
+ //#endregion
117
146
  //#region src/internal/native-declaration-block.ts
118
147
  /** Thin JS ownership boundary around the Rust declaration state machine. */
119
148
  var NativeDeclarationBlock = class {
120
149
  #state;
121
150
  #reportDiagnostic;
151
+ #reportSerializationDiagnostic;
122
152
  #reservedNestingDepth;
123
153
  #observableCache;
124
154
  #serializationCache = /* @__PURE__ */ new Map();
125
- constructor(reportDiagnostic, context = "style", resourceBudget = defaultResourceBudget, reservedNestingDepth = () => 0) {
155
+ constructor(reportDiagnostic, reportSerializationDiagnostic, context = "style", resourceBudget = defaultResourceBudget, reservedNestingDepth = () => 0) {
126
156
  this.#reportDiagnostic = reportDiagnostic;
157
+ this.#reportSerializationDiagnostic = reportSerializationDiagnostic;
127
158
  this.#reservedNestingDepth = reservedNestingDepth;
128
159
  this.#state = engineBinding.createDeclarationState(context, ...nativeBudgetArguments(resourceBudget));
129
160
  }
@@ -155,9 +186,59 @@ var NativeDeclarationBlock = class {
155
186
  this.#invalidateSerialization();
156
187
  return;
157
188
  }
158
- const normalizedName = name.startsWith("--") ? name : name.toLowerCase();
159
- const [code, input] = outcome === "invalid-priority" ? ["INVALID_PRIORITY", priority] : outcome === "unsupported-shorthand" ? ["UNSUPPORTED_SHORTHAND_VALUE", value] : ["INVALID_PROPERTY_VALUE", value];
160
- this.#reportDiagnostic(code, normalizedName, input);
189
+ this.#diagnosticForOutcome(outcome, name, value, priority);
190
+ }
191
+ applyMutations(mutations) {
192
+ const kinds = [];
193
+ const properties = [];
194
+ const values = [];
195
+ const priorities = [];
196
+ for (const mutation of mutations) {
197
+ kinds.push(mutation.kind === "set" ? 0 : 1);
198
+ properties.push(mutation.property);
199
+ values.push(mutation.kind === "set" ? mutation.value : "");
200
+ priorities.push(mutation.kind === "set" ? mutation.priority : "");
201
+ }
202
+ let outcomes;
203
+ try {
204
+ outcomes = this.#state.applyMutations(kinds, properties, values, priorities, this.#reservedNestingDepth());
205
+ } catch (error) {
206
+ rethrowResourceBudgetError(error);
207
+ throw error;
208
+ }
209
+ if (outcomes.length !== mutations.length) throw new Error("SheetOM engine returned an incomplete declaration mutation result.");
210
+ const results = [];
211
+ let mutated = false;
212
+ for (let index = 0; index < outcomes.length; index += 1) {
213
+ const mutation = mutations[index];
214
+ const outcome = outcomes[index];
215
+ if (!mutation || outcome === void 0) throw new Error("SheetOM engine returned a mismatched declaration mutation result.");
216
+ if (mutation.kind === "remove") {
217
+ mutated = true;
218
+ results.push({
219
+ kind: "remove",
220
+ value: outcome
221
+ });
222
+ continue;
223
+ }
224
+ if (outcome === "applied") {
225
+ mutated = true;
226
+ results.push({
227
+ kind: "set",
228
+ accepted: true,
229
+ diagnostic: null
230
+ });
231
+ continue;
232
+ }
233
+ if (!isEngineMutationOutcome(outcome)) throw new Error("SheetOM engine returned an invalid declaration mutation outcome.");
234
+ results.push({
235
+ kind: "set",
236
+ accepted: false,
237
+ diagnostic: this.#diagnosticForOutcome(outcome, mutation.property, mutation.value, mutation.priority)
238
+ });
239
+ }
240
+ if (mutated) this.#invalidateSerialization();
241
+ return results;
161
242
  }
162
243
  removeProperty(name) {
163
244
  const removed = this.#state.removeProperty(name);
@@ -173,19 +254,63 @@ var NativeDeclarationBlock = class {
173
254
  }
174
255
  this.#invalidateSerialization();
175
256
  }
176
- serialize(safe, indent, separator) {
177
- const cacheKey = `${safe ? "1" : "0"}\0${indent}\0${separator}`;
257
+ serialize(safe, indent, separator, strict = false) {
258
+ const cacheKey = `${safe ? "1" : "0"}${strict ? "1" : "0"}\0${indent}\0${separator}`;
178
259
  const cached = this.#serializationCache.get(cacheKey);
179
- if (cached !== void 0) return cached;
180
- const serialized = this.#state.serializeFormatted(safe, indent, separator);
181
- this.#serializationCache.set(cacheKey, serialized);
182
- return serialized;
260
+ if (cached !== void 0) {
261
+ if (typeof cached === "string") return cached;
262
+ this.#reportSerializationIssues(cached.issues);
263
+ return cached.serialized;
264
+ }
265
+ let result;
266
+ try {
267
+ if (strict) result = this.#state.serializeFormatted(safe, indent, separator);
268
+ else result = parseResilientSerializationResult(this.#state.serializeFormattedResilient(safe, indent, separator));
269
+ } catch (error) {
270
+ rethrowResourceBudgetError(error);
271
+ rethrowSerializationError(error);
272
+ throw error;
273
+ }
274
+ this.#serializationCache.set(cacheKey, result);
275
+ if (typeof result === "string") return result;
276
+ this.#reportSerializationIssues(result.issues);
277
+ return result.serialized;
183
278
  }
184
279
  #invalidateSerialization() {
185
280
  this.#observableCache = void 0;
186
281
  this.#serializationCache.clear();
187
282
  }
283
+ #diagnosticForOutcome(outcome, name, value, priority) {
284
+ const normalizedName = name.startsWith("--") ? name : name.toLowerCase();
285
+ const [code, input] = outcome === "invalid-priority" ? ["INVALID_PRIORITY", priority] : outcome === "unsupported-shorthand" ? ["UNSUPPORTED_SHORTHAND_VALUE", value] : ["INVALID_PROPERTY_VALUE", value];
286
+ return this.#reportDiagnostic(code, normalizedName, input);
287
+ }
288
+ #reportSerializationIssues(issues) {
289
+ for (const issue of issues) this.#reportSerializationDiagnostic(issue.shorthand, issue.conflictingLonghands);
290
+ }
188
291
  };
292
+ function parseResilientSerializationResult(output) {
293
+ const serialized = output[0];
294
+ if (serialized === void 0 || output.length % 2 === 0) throw new Error("SheetOM engine returned an invalid resilient serialization result.");
295
+ if (output.length === 1) return serialized;
296
+ const issues = [];
297
+ for (let index = 1; index < output.length; index += 2) {
298
+ const shorthand = output[index];
299
+ const conflicts = output[index + 1];
300
+ if (!shorthand || !conflicts) throw new Error("SheetOM engine returned an incomplete resilient serialization issue.");
301
+ issues.push({
302
+ shorthand,
303
+ conflictingLonghands: Object.freeze(conflicts.split(","))
304
+ });
305
+ }
306
+ return {
307
+ serialized,
308
+ issues
309
+ };
310
+ }
311
+ function isEngineMutationOutcome(value) {
312
+ return value === "invalid-name" || value === "invalid-priority" || value === "invalid-value" || value === "unsupported-shorthand";
313
+ }
189
314
  function toUnsignedLong$1(value) {
190
315
  const numeric = Number(value);
191
316
  if (!Number.isFinite(numeric) || numeric === 0) return 0;
@@ -377,13 +502,20 @@ var Serializer = class {
377
502
  this.#describe = describe;
378
503
  }
379
504
  serialize(node) {
505
+ return this.serializeMany([node]);
506
+ }
507
+ serializeMany(nodes) {
380
508
  const chunks = [];
381
- const pending = [{
382
- kind: "node",
383
- node,
384
- depth: 0,
385
- child: false
386
- }];
509
+ const pending = [];
510
+ for (let index = nodes.length - 1; index >= 0; index -= 1) {
511
+ const node = nodes[index];
512
+ if (node) pending.push({
513
+ kind: "node",
514
+ node,
515
+ depth: 0,
516
+ child: false
517
+ });
518
+ }
387
519
  while (pending.length > 0) {
388
520
  const frame = pending.pop();
389
521
  if (!frame) continue;
@@ -497,7 +629,8 @@ const ruleTree = new RuleTree((rule, parentRule, parentStyleSheet) => {
497
629
  parentStyleSheet
498
630
  });
499
631
  }, (rule) => rule.parentStyleSheet);
500
- const safeSerializer = new Serializer(describeRuleSafe);
632
+ const safeSerializer = new Serializer((rule) => describeRuleSafe(rule, false));
633
+ const strictSerializer = new Serializer((rule) => describeRuleSafe(rule, true));
501
634
  let activeConstructionResourceBudget = defaultResourceBudget;
502
635
  function constructWithResourceBudget(factory, resourceBudget) {
503
636
  const previous = activeConstructionResourceBudget;
@@ -1150,19 +1283,32 @@ var CSSStyleDeclaration = class {
1150
1283
  this.parentRule = parentRule;
1151
1284
  lockOwnProperties(this, "parentRule");
1152
1285
  const reportDeclarationDiagnostic = (code, property, input) => {
1153
- const priority = code === "INVALID_PRIORITY";
1154
- const unsupportedShorthand = code === "UNSUPPORTED_SHORTHAND_VALUE";
1155
- (ruleDiagnostics.get(this.parentRule) ?? ignoreDiagnostic)({
1286
+ const diagnostic = {
1156
1287
  code,
1157
1288
  severity: "warning",
1158
1289
  operation: "setProperty",
1159
- message: priority ? `The mutation was ignored because ${input} is not a valid priority.` : unsupportedShorthand ? `The value was ignored because the shorthand codec for ${property} cannot expand it.` : `The value was ignored because it is invalid for ${property}.`,
1290
+ message: code === "INVALID_PRIORITY" ? `The mutation was ignored because ${input} is not a valid priority.` : code === "UNSUPPORTED_SHORTHAND_VALUE" ? `The value was ignored because the shorthand codec for ${property} cannot expand it.` : `The value was ignored because it is invalid for ${property}.`,
1160
1291
  property,
1161
1292
  input,
1162
1293
  location: null
1163
- });
1294
+ };
1295
+ (ruleDiagnostics.get(this.parentRule) ?? ignoreDiagnostic)(diagnostic);
1296
+ return diagnostic;
1297
+ };
1298
+ const reportSerializationDiagnostic = (shorthand, conflictingLonghands) => {
1299
+ const diagnostic = {
1300
+ code: "UNREPRESENTABLE_PENDING_SHORTHAND",
1301
+ severity: "warning",
1302
+ operation: "serialize",
1303
+ message: `The pending ${shorthand} shorthand required a best-effort recovery during serialization.`,
1304
+ property: shorthand,
1305
+ input: "",
1306
+ location: null,
1307
+ conflictingLonghands: Object.freeze([...conflictingLonghands])
1308
+ };
1309
+ (ruleDiagnostics.get(this.parentRule) ?? ignoreDiagnostic)(diagnostic);
1164
1310
  };
1165
- this.#block = new NativeDeclarationBlock(reportDeclarationDiagnostic, parentRule instanceof CSSFontFaceRule ? "font-face" : parentRule instanceof CSSFunctionDeclarations ? "function" : "style", ruleResourceBudgets.get(parentRule) ?? defaultResourceBudget, () => declarationSerializationDepth(parentRule));
1311
+ this.#block = new NativeDeclarationBlock(reportDeclarationDiagnostic, reportSerializationDiagnostic, parentRule instanceof CSSFontFaceRule ? "font-face" : parentRule instanceof CSSFunctionDeclarations ? "function" : "style", ruleResourceBudgets.get(parentRule) ?? defaultResourceBudget, () => declarationSerializationDepth(parentRule));
1166
1312
  return new Proxy(this, {
1167
1313
  get(target, property) {
1168
1314
  if (typeof property === "string" && arrayIndexPattern.test(property)) return target.item(Number(property)) || void 0;
@@ -1207,13 +1353,41 @@ var CSSStyleDeclaration = class {
1207
1353
  const stringPriority = priority === null ? "" : `${priority}`;
1208
1354
  this.#block.setProperty(stringName, stringValue, stringPriority);
1209
1355
  }
1356
+ /** Apply ordered declaration mutations through one engine boundary crossing. */
1357
+ applyMutations(mutations) {
1358
+ requireArguments(arguments.length, 1, "CSSStyleDeclaration", "applyMutations");
1359
+ if (!Array.isArray(mutations)) throw new TypeError("CSSStyleDeclaration.applyMutations requires an array.");
1360
+ const normalized = [];
1361
+ for (let index = 0; index < mutations.length; index += 1) {
1362
+ const mutation = mutations[index];
1363
+ if (typeof mutation !== "object" || mutation === null) throw new TypeError(`Declaration mutation at index ${index} must be an object.`);
1364
+ if (mutation.kind !== "set" && mutation.kind !== "remove") throw new TypeError(`Declaration mutation at index ${index} has an invalid kind.`);
1365
+ if (typeof mutation.property !== "string") throw new TypeError(`Declaration mutation at index ${index} requires a string property.`);
1366
+ if (mutation.kind === "remove") {
1367
+ normalized.push({
1368
+ kind: "remove",
1369
+ property: mutation.property
1370
+ });
1371
+ continue;
1372
+ }
1373
+ if (typeof mutation.value !== "string" && mutation.value !== null) throw new TypeError(`Declaration mutation at index ${index} requires a string or null value.`);
1374
+ if (mutation.priority !== void 0 && mutation.priority !== null && typeof mutation.priority !== "string") throw new TypeError(`Declaration mutation at index ${index} requires a string priority.`);
1375
+ normalized.push({
1376
+ kind: "set",
1377
+ property: mutation.property,
1378
+ value: mutation.value ?? "",
1379
+ priority: mutation.priority ?? ""
1380
+ });
1381
+ }
1382
+ return this.#block.applyMutations(normalized);
1383
+ }
1210
1384
  removeProperty(name) {
1211
1385
  requireArguments(arguments.length, 1, "CSSStyleDeclaration", "removeProperty");
1212
1386
  return this.#block.removeProperty(`${name}`);
1213
1387
  }
1214
1388
  /** @internal */
1215
- serializeSafe(indent) {
1216
- return this.#block.serialize(true, indent, "\n");
1389
+ serializeSafe(indent, strict = false) {
1390
+ return this.#block.serialize(true, indent, "\n", strict);
1217
1391
  }
1218
1392
  };
1219
1393
  /** A live `@font-face` descriptor rule. */
@@ -1702,26 +1876,23 @@ function replaceGroupingRules(group, rules) {
1702
1876
  function attachRuleTree(rule, parentRule, parentStyleSheet) {
1703
1877
  ruleTree.attach(rule, parentRule, parentStyleSheet);
1704
1878
  }
1705
- function serializeRuleSafe(rule) {
1706
- return safeSerializer.serialize(rule);
1707
- }
1708
- function describeRuleSafe(rule) {
1879
+ function describeRuleSafe(rule, strict) {
1709
1880
  if (rule instanceof CSSStyleRule) return {
1710
1881
  kind: "block",
1711
1882
  header: rule.selectorText,
1712
- declarations: rule.style.serializeSafe(" "),
1883
+ declarations: rule.style.serializeSafe(" ", strict),
1713
1884
  children: ruleTree.children(rule)
1714
1885
  };
1715
1886
  if (rule instanceof CSSNestedDeclarations || rule instanceof CSSFunctionDeclarations) return {
1716
1887
  kind: "declarations",
1717
- declarations: rule.style.serializeSafe("")
1888
+ declarations: rule.style.serializeSafe("", strict)
1718
1889
  };
1719
1890
  if (rule instanceof CSSFontFaceRule || rule instanceof CSSMarginRule || rule instanceof CSSPositionTryRule || rule instanceof CSSKeyframeRule) {
1720
1891
  const blockIndex = rule.cssText.indexOf("{");
1721
1892
  return {
1722
1893
  kind: "block",
1723
1894
  header: rule.cssText.slice(0, blockIndex).trimEnd(),
1724
- declarations: rule.style.serializeSafe(" ")
1895
+ declarations: rule.style.serializeSafe(" ", strict)
1725
1896
  };
1726
1897
  }
1727
1898
  if (rule instanceof CSSKeyframesRule) return {
@@ -1732,7 +1903,7 @@ function describeRuleSafe(rule) {
1732
1903
  if (rule instanceof CSSPageRule) return {
1733
1904
  kind: "block",
1734
1905
  header: `@page${rule.selectorText === "" ? "" : ` ${rule.selectorText}`}`,
1735
- declarations: rule.style.serializeSafe(" "),
1906
+ declarations: rule.style.serializeSafe(" ", strict),
1736
1907
  children: ruleTree.children(rule)
1737
1908
  };
1738
1909
  if (!(rule instanceof CSSGroupingRule)) return {
@@ -2127,9 +2298,13 @@ var CSSStyleSheet = class {
2127
2298
  }
2128
2299
  /** Serialize current state as reparsable CSS without mutating live objects. */
2129
2300
  serialize() {
2130
- return this.#rules.map(serializeRuleSafe).join("");
2301
+ return safeSerializer.serializeMany(this.#rules);
2302
+ }
2303
+ /** Serialize current state only when its CSS semantics can be preserved exactly. */
2304
+ serializeStrict() {
2305
+ return strictSerializer.serializeMany(this.#rules);
2131
2306
  }
2132
- /** Drain opt-in mutation diagnostics without affecting CSSOM behavior. */
2307
+ /** Drain opt-in mutation and serialization diagnostics without affecting CSSOM behavior. */
2133
2308
  takeDiagnostics() {
2134
2309
  if (!this.#diagnostics) return [];
2135
2310
  return this.#diagnostics.splice(0);
@@ -2180,6 +2355,7 @@ exports.CSSStyleSheet = CSSStyleSheet;
2180
2355
  exports.CSSSupportsRule = CSSSupportsRule;
2181
2356
  exports.CSSViewTransitionRule = CSSViewTransitionRule;
2182
2357
  exports.MediaList = MediaList;
2358
+ exports.SheetOMSerializationError = SheetOMSerializationError;
2183
2359
  exports.parseStyleSheet = parseStyleSheet;
2184
2360
 
2185
2361
  //# sourceMappingURL=index.cjs.map