sheetom 0.1.0-rc.0 → 0.1.0-rc.1

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
@@ -9,9 +9,7 @@ not implement the DOM, cascade, selector matching, layout, or computed styles.
9
9
 
10
10
  ## Install
11
11
 
12
- SheetOM is not published yet. The first registry release will be
13
- `0.1.0-rc.0` under the `next` dist-tag after the complete conformance gate
14
- passes. Once published:
12
+ Install the current release candidate from the `next` dist-tag:
15
13
 
16
14
  ```sh
17
15
  npm install sheetom@next
@@ -59,9 +57,10 @@ Constructed sheets follow browser replacement behavior and strip `@import`.
59
57
  `parseStyleSheet` creates a regular authoring sheet and preserves valid imports
60
58
  without loading them.
61
59
 
62
- ## Recovered values and safe serialization
60
+ ## Recovered values and reparsable serialization
63
61
 
64
- SheetOM distinguishes the browser-facing CSSOM text from safe output. Chromium
62
+ SheetOM distinguishes browser-facing CSSOM text from reparsable stylesheet
63
+ output. Chromium
65
64
  accepts some values using CSS Syntax end-of-input recovery while preserving the
66
65
  unclosed input in `getPropertyValue()` and `cssText`:
67
66
 
@@ -82,6 +81,19 @@ sheet.serialize();
82
81
  // reparsable CSS with the missing syntax repaired
83
82
  ```
84
83
 
84
+ The three text surfaces have distinct contracts:
85
+
86
+ | Surface | Contract |
87
+ | --- | --- |
88
+ | `rule.style.cssText` | Browser-compatible observable declaration text, including accepted recovery quirks. |
89
+ | `rule.cssText` | Browser-compatible observable text for one rule and its current descendants. |
90
+ | `sheet.serialize()` | Deterministic, reparsable stylesheet output with recoverable syntax confined to its declaration or rule. |
91
+
92
+ Reparsable serialization means that the output can be parsed again without one
93
+ declaration leaking into the next declaration or rule, while preserving the
94
+ valid semantic state owned by SheetOM. It does not sanitize URLs, enforce CSP,
95
+ block remote resources, or make valid hostile CSS safe to render.
96
+
85
97
  Invalid `setProperty` values and priorities are atomic no-ops, matching browser
86
98
  behavior. Opt into mutation diagnostics with `{ diagnostics: true }` and drain
87
99
  them using `takeDiagnostics()`.
@@ -0,0 +1,108 @@
1
+ {
2
+ "$schema": "../schemas/compatibility-report.schema.json",
3
+ "schemaVersion": 1,
4
+ "packageVersion": "0.1.0-rc.1",
5
+ "baseline": {
6
+ "wptCommit": "54f8f933629e7c010ae98a246729af01f8abcda5",
7
+ "lightningcss": "1.33.0",
8
+ "cssTree": "3.2.1",
9
+ "runtimes": {
10
+ "node": "v24.18.0",
11
+ "bun": "1.3.1",
12
+ "deno": "2.9.5"
13
+ },
14
+ "browsers": {
15
+ "chromium": "151.0.7922.34 (revision 1234)",
16
+ "firefox": "153.0 (revision 1538)",
17
+ "webkit": "26.5 (revision 2336)"
18
+ }
19
+ },
20
+ "summary": {
21
+ "passed": 5,
22
+ "divergences": 2,
23
+ "excluded": 0,
24
+ "unexplained": 0
25
+ },
26
+ "evidence": {
27
+ "operationFixtures": {
28
+ "passed": 7,
29
+ "total": 7,
30
+ "sha256": "b4ec45fce8ad92393966d1b538748f6b3256d7d51af2fb0f76b4539a3300c349",
31
+ "adapters": [
32
+ {
33
+ "adapter": "sheetom",
34
+ "passed": 7,
35
+ "total": 7
36
+ },
37
+ {
38
+ "adapter": "chromium",
39
+ "passed": 7,
40
+ "total": 7
41
+ },
42
+ {
43
+ "adapter": "firefox",
44
+ "passed": 7,
45
+ "total": 7
46
+ },
47
+ {
48
+ "adapter": "webkit",
49
+ "passed": 7,
50
+ "total": 7
51
+ }
52
+ ]
53
+ },
54
+ "nativeWpt": [
55
+ {
56
+ "engine": "chrome",
57
+ "version": "150.0.7871.128",
58
+ "sha256": "33aaf2418507bb1d335cd5350b21a86cee86cbe3059e9eb1b2001c9e2693c1f1",
59
+ "passed": 4,
60
+ "total": 4
61
+ },
62
+ {
63
+ "engine": "firefox",
64
+ "version": "153.0.3",
65
+ "sha256": "20ee01df1d83cc67a50e7949ff6a62c87c572c2fb0159ee5c2b15bc8e9a13a1f",
66
+ "passed": 4,
67
+ "total": 4
68
+ }
69
+ ]
70
+ },
71
+ "resolutions": [
72
+ {
73
+ "fixtureId": "declarations.basic-set-property",
74
+ "decision": "shared",
75
+ "rationale": "The observable declaration state is shared by the pinned browser engines."
76
+ },
77
+ {
78
+ "fixtureId": "declarations.malformed-padding",
79
+ "decision": "chromium-fallback",
80
+ "rationale": "CSS Syntax accepts the EOF-recovered value; Chromium preserves the unclosed observable spelling while Firefox materializes closing parentheses."
81
+ },
82
+ {
83
+ "fixtureId": "declarations.setproperty-null-undefined",
84
+ "decision": "shared",
85
+ "rationale": "The four adapted WPT subtests define the shared null and undefined conversion behavior."
86
+ },
87
+ {
88
+ "fixtureId": "declarations.required-arguments",
89
+ "decision": "shared",
90
+ "rationale": "Required WebIDL arguments throw TypeError before string conversion in the pinned browser engines."
91
+ },
92
+ {
93
+ "fixtureId": "declarations.set-property-multiple-declarations",
94
+ "decision": "shared",
95
+ "rationale": "A setProperty value is parsed independently and cannot escape into another declaration; invalid input leaves the previous declaration unchanged."
96
+ },
97
+ {
98
+ "fixtureId": "declarations.escaped-custom-property-name",
99
+ "decision": "shared",
100
+ "rationale": "Custom-property method arguments retain their logical names and cssText escapes those names only when serializing CSS syntax."
101
+ },
102
+ {
103
+ "fixtureId": "integration.weweb-authoring-tree",
104
+ "decision": "chromium-fallback",
105
+ "rationale": "The standard rule-tree operations and detachment identity are shared; the recovered padding spelling follows the existing Chromium fallback resolution."
106
+ }
107
+ ]
108
+ }
@@ -43,7 +43,23 @@
43
43
  "required": ["passed", "total"],
44
44
  "properties": {
45
45
  "passed": { "type": "integer", "minimum": 0 },
46
- "total": { "type": "integer", "minimum": 0 }
46
+ "total": { "type": "integer", "minimum": 0 },
47
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
48
+ "adapters": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["adapter", "passed", "total"],
54
+ "properties": {
55
+ "adapter": {
56
+ "enum": ["sheetom", "chromium", "firefox", "webkit"]
57
+ },
58
+ "passed": { "type": "integer", "minimum": 0 },
59
+ "total": { "type": "integer", "minimum": 0 }
60
+ }
61
+ }
62
+ }
47
63
  }
48
64
  },
49
65
  "nativeWpt": {
package/dist/index.cjs CHANGED
@@ -52,7 +52,7 @@ var DeclarationBlock = class {
52
52
  });
53
53
  };
54
54
  for (const declaration of declarations) {
55
- const name = declaration.name.startsWith("--") ? declaration.name : declaration.name.toLowerCase();
55
+ const name = this.#codec.normalizeName(declaration.name);
56
56
  const propertyValue = this.#codec.parseValue(name, declaration.value);
57
57
  if (!propertyValue) {
58
58
  sourceIndex += 1;
@@ -110,19 +110,19 @@ var DeclarationBlock = class {
110
110
  return this.#records[this.#codec.normalizeIndex(index)]?.name ?? "";
111
111
  }
112
112
  getPropertyValue(name) {
113
- const normalizedName = name.startsWith("--") ? name : name.toLowerCase();
113
+ const normalizedName = this.#codec.normalizeName(name);
114
114
  const shorthand = this.#shorthand(normalizedName, false);
115
115
  if (shorthand) return shorthand.value;
116
116
  return this.#records.find((record) => record.name === normalizedName)?.observableValue ?? "";
117
117
  }
118
118
  getPropertyPriority(name) {
119
- const normalizedName = name.startsWith("--") ? name : name.toLowerCase();
119
+ const normalizedName = this.#codec.normalizeName(name);
120
120
  const shorthand = this.#shorthand(normalizedName, false);
121
121
  if (shorthand) return shorthand.important ? "important" : "";
122
122
  return this.#records.find((record) => record.name === normalizedName)?.important ? "important" : "";
123
123
  }
124
124
  setProperty(name, value, priority) {
125
- const normalizedName = name.startsWith("--") ? name : name.toLowerCase();
125
+ const normalizedName = this.#codec.normalizeName(name);
126
126
  const normalizedPriority = priority.toLowerCase();
127
127
  if (normalizedPriority !== "" && normalizedPriority !== "important") {
128
128
  this.#reportDiagnostic("INVALID_PRIORITY", normalizedName, priority);
@@ -160,7 +160,7 @@ var DeclarationBlock = class {
160
160
  this.#commitRecord(normalizedName, parsed, important);
161
161
  }
162
162
  removeProperty(name) {
163
- const normalizedName = name.startsWith("--") ? name : name.toLowerCase();
163
+ const normalizedName = this.#codec.normalizeName(name);
164
164
  const longhands = this.#codec.shorthandLonghands(normalizedName);
165
165
  if (longhands) {
166
166
  const previousValue = this.getPropertyValue(normalizedName);
@@ -1110,6 +1110,109 @@ const chromiumSupportedProperties = /* @__PURE__ */ new Set([
1110
1110
  "z-index",
1111
1111
  "zoom"
1112
1112
  ]);
1113
+ const chromiumPropertyAliases = {
1114
+ "-webkit-align-content": "align-content",
1115
+ "-webkit-align-items": "align-items",
1116
+ "-webkit-align-self": "align-self",
1117
+ "-webkit-animation-delay": "animation-delay",
1118
+ "-webkit-animation-direction": "animation-direction",
1119
+ "-webkit-animation-duration": "animation-duration",
1120
+ "-webkit-animation-fill-mode": "animation-fill-mode",
1121
+ "-webkit-animation-iteration-count": "animation-iteration-count",
1122
+ "-webkit-animation-name": "animation-name",
1123
+ "-webkit-animation-play-state": "animation-play-state",
1124
+ "-webkit-animation-timing-function": "animation-timing-function",
1125
+ "-webkit-app-region": "app-region",
1126
+ "-webkit-appearance": "appearance",
1127
+ "-webkit-backface-visibility": "backface-visibility",
1128
+ "-webkit-background-clip": "background-clip",
1129
+ "-webkit-background-origin": "background-origin",
1130
+ "-webkit-background-size": "background-size",
1131
+ "-webkit-border-after-color": "border-block-end-color",
1132
+ "-webkit-border-after-style": "border-block-end-style",
1133
+ "-webkit-border-after-width": "border-block-end-width",
1134
+ "-webkit-border-before-color": "border-block-start-color",
1135
+ "-webkit-border-before-style": "border-block-start-style",
1136
+ "-webkit-border-before-width": "border-block-start-width",
1137
+ "-webkit-border-bottom-left-radius": "border-bottom-left-radius",
1138
+ "-webkit-border-bottom-right-radius": "border-bottom-right-radius",
1139
+ "-webkit-border-end-color": "border-inline-end-color",
1140
+ "-webkit-border-end-style": "border-inline-end-style",
1141
+ "-webkit-border-end-width": "border-inline-end-width",
1142
+ "-webkit-border-start-color": "border-inline-start-color",
1143
+ "-webkit-border-start-style": "border-inline-start-style",
1144
+ "-webkit-border-start-width": "border-inline-start-width",
1145
+ "-webkit-border-top-left-radius": "border-top-left-radius",
1146
+ "-webkit-border-top-right-radius": "border-top-right-radius",
1147
+ "-webkit-box-shadow": "box-shadow",
1148
+ "-webkit-box-sizing": "box-sizing",
1149
+ "-webkit-clip-path": "clip-path",
1150
+ "-webkit-column-break-after": "break-after",
1151
+ "-webkit-column-break-before": "break-before",
1152
+ "-webkit-column-break-inside": "break-inside",
1153
+ "-webkit-column-count": "column-count",
1154
+ "-webkit-column-gap": "column-gap",
1155
+ "-webkit-column-rule-color": "column-rule-color",
1156
+ "-webkit-column-rule-style": "column-rule-style",
1157
+ "-webkit-column-rule-width": "column-rule-width",
1158
+ "-webkit-column-span": "column-span",
1159
+ "-webkit-column-width": "column-width",
1160
+ "-webkit-filter": "filter",
1161
+ "-webkit-flex-basis": "flex-basis",
1162
+ "-webkit-flex-direction": "flex-direction",
1163
+ "-webkit-flex-grow": "flex-grow",
1164
+ "-webkit-flex-shrink": "flex-shrink",
1165
+ "-webkit-flex-wrap": "flex-wrap",
1166
+ "-webkit-font-feature-settings": "font-feature-settings",
1167
+ "-webkit-hyphenate-character": "hyphenate-character",
1168
+ "-webkit-justify-content": "justify-content",
1169
+ "-webkit-logical-height": "block-size",
1170
+ "-webkit-logical-width": "inline-size",
1171
+ "-webkit-margin-after": "margin-block-end",
1172
+ "-webkit-margin-before": "margin-block-start",
1173
+ "-webkit-margin-end": "margin-inline-end",
1174
+ "-webkit-margin-start": "margin-inline-start",
1175
+ "-webkit-mask-clip": "mask-clip",
1176
+ "-webkit-mask-composite": "mask-composite",
1177
+ "-webkit-mask-image": "mask-image",
1178
+ "-webkit-mask-origin": "mask-origin",
1179
+ "-webkit-mask-repeat": "mask-repeat",
1180
+ "-webkit-mask-size": "mask-size",
1181
+ "-webkit-max-logical-height": "max-block-size",
1182
+ "-webkit-max-logical-width": "max-inline-size",
1183
+ "-webkit-min-logical-height": "min-block-size",
1184
+ "-webkit-min-logical-width": "min-inline-size",
1185
+ "-webkit-opacity": "opacity",
1186
+ "-webkit-order": "order",
1187
+ "-webkit-padding-after": "padding-block-end",
1188
+ "-webkit-padding-before": "padding-block-start",
1189
+ "-webkit-padding-end": "padding-inline-end",
1190
+ "-webkit-padding-start": "padding-inline-start",
1191
+ "-webkit-perspective": "perspective",
1192
+ "-webkit-perspective-origin": "perspective-origin",
1193
+ "-webkit-print-color-adjust": "print-color-adjust",
1194
+ "-webkit-shape-image-threshold": "shape-image-threshold",
1195
+ "-webkit-shape-margin": "shape-margin",
1196
+ "-webkit-shape-outside": "shape-outside",
1197
+ "-webkit-text-emphasis-color": "text-emphasis-color",
1198
+ "-webkit-text-emphasis-position": "text-emphasis-position",
1199
+ "-webkit-text-emphasis-style": "text-emphasis-style",
1200
+ "-webkit-text-size-adjust": "text-size-adjust",
1201
+ "-webkit-transform": "transform",
1202
+ "-webkit-transform-origin": "transform-origin",
1203
+ "-webkit-transform-style": "transform-style",
1204
+ "-webkit-transition-delay": "transition-delay",
1205
+ "-webkit-transition-duration": "transition-duration",
1206
+ "-webkit-transition-property": "transition-property",
1207
+ "-webkit-transition-timing-function": "transition-timing-function",
1208
+ "-webkit-user-select": "user-select",
1209
+ "grid-column-gap": "column-gap",
1210
+ "grid-row-gap": "row-gap",
1211
+ "page-break-after": "break-after",
1212
+ "page-break-before": "break-before",
1213
+ "page-break-inside": "break-inside",
1214
+ "word-wrap": "overflow-wrap"
1215
+ };
1113
1216
  const chromiumShorthandLonghands = {
1114
1217
  "-webkit-align-content": ["align-content"],
1115
1218
  "-webkit-align-items": ["align-items"],
@@ -2016,6 +2119,18 @@ function analyzeSubstitutions(value) {
2016
2119
  valid: true
2017
2120
  };
2018
2121
  let found = value.type === "var" || value.type === "env";
2122
+ if (found) {
2123
+ const details = value.value;
2124
+ if (!isUnknownRecord(details)) return {
2125
+ found: true,
2126
+ valid: false
2127
+ };
2128
+ const fallback = details.fallback;
2129
+ if (Array.isArray(fallback) && fallback.some((item) => isToken(item, "semicolon") || isToken(item, "delim", "!"))) return {
2130
+ found: true,
2131
+ valid: false
2132
+ };
2133
+ }
2019
2134
  const cssFunction = functionValue(value);
2020
2135
  if (cssFunction?.name === "attr") {
2021
2136
  found = true;
@@ -2076,14 +2191,6 @@ function serializeIdentifier(value) {
2076
2191
  }
2077
2192
  return result;
2078
2193
  }
2079
- function containsPriorityDelimiter(value) {
2080
- let found = false;
2081
- css_tree.tokenize(value, (type, start, end) => {
2082
- if (css_tree.tokenNames[type] !== "delim-token") return;
2083
- if (value.slice(start, end) === "!") found = true;
2084
- });
2085
- return found;
2086
- }
2087
2194
  function canonicalizeLeadingDecimal(value) {
2088
2195
  const replacements = [];
2089
2196
  css_tree.tokenize(value, (type, start, end) => {
@@ -2109,32 +2216,80 @@ function canonicalizeLeadingDecimal(value) {
2109
2216
  }
2110
2217
  return result;
2111
2218
  }
2219
+ function containsTopLevelDeclarationBoundary(value) {
2220
+ let depth = 0;
2221
+ let found = false;
2222
+ css_tree.tokenize(value, (type, start, end) => {
2223
+ const tokenName = css_tree.tokenNames[type];
2224
+ if (tokenName === void 0) return;
2225
+ if ([
2226
+ "function-token",
2227
+ "(-token",
2228
+ "[-token",
2229
+ "{-token"
2230
+ ].includes(tokenName)) {
2231
+ depth += 1;
2232
+ return;
2233
+ }
2234
+ if ([
2235
+ ")-token",
2236
+ "]-token",
2237
+ "}-token"
2238
+ ].includes(tokenName)) {
2239
+ if (depth > 0) depth -= 1;
2240
+ return;
2241
+ }
2242
+ if (depth === 0 && (tokenName === "semicolon-token" || tokenName === "delim-token" && value.slice(start, end) === "!")) found = true;
2243
+ });
2244
+ return found;
2245
+ }
2246
+ function canonicalizeBrowserValue(name, value) {
2247
+ if (name === "-webkit-box-reflect" && [
2248
+ "above",
2249
+ "below",
2250
+ "left",
2251
+ "right"
2252
+ ].includes(value)) return `${value} 0px`;
2253
+ return value;
2254
+ }
2112
2255
  function parsePropertyValue(name, observableValue) {
2113
2256
  if (name === "--") return null;
2114
2257
  if (!name.startsWith("--") && !chromiumSupportedProperties.has(name)) return null;
2115
- if (containsPriorityDelimiter(observableValue)) return null;
2258
+ if (containsTopLevelDeclarationBoundary(observableValue)) return null;
2116
2259
  let declaration;
2260
+ let declarationCount = 0;
2117
2261
  try {
2118
2262
  const result = (0, lightningcss.transformStyleAttribute)({
2119
2263
  code: encoder$1.encode(`${name.startsWith("--") ? serializeIdentifier(name) : name}: ${observableValue}`),
2120
2264
  visitor: { Declaration(candidate) {
2265
+ declarationCount += 1;
2121
2266
  declaration = candidate;
2122
2267
  } }
2123
2268
  });
2124
- if (!isUnknownRecord(declaration)) return null;
2269
+ if (declarationCount !== 1 || !isUnknownRecord(declaration)) return null;
2125
2270
  if (!Object.hasOwn(declaration, "property")) return null;
2126
2271
  const pendingSubstitution = declaration.property === "unparsed";
2127
- if (pendingSubstitution) {
2128
- const analysis = analyzeSubstitutions(declaration);
2129
- if (!analysis.found || !analysis.valid) return null;
2130
- }
2272
+ const analysis = analyzeSubstitutions(declaration);
2273
+ if (!analysis.valid || pendingSubstitution && !analysis.found) return null;
2131
2274
  const serialized = decoder$1.decode(result.code);
2132
- const colonIndex = serialized.indexOf(":");
2133
- if (colonIndex === -1) return null;
2134
- const trimmedObservableValue = canonicalizeLeadingDecimal(observableValue.trim());
2275
+ const parsed = css_tree.parse(serialized, {
2276
+ context: "declarationList",
2277
+ positions: true
2278
+ });
2279
+ if (parsed.type !== "DeclarationList") return null;
2280
+ const serializedDeclarations = parsed.children.toArray();
2281
+ if (serializedDeclarations.length !== 1) return null;
2282
+ const serializedDeclaration = serializedDeclarations[0];
2283
+ if (serializedDeclaration?.type !== "Declaration") return null;
2284
+ if (serializedDeclaration.important) return null;
2285
+ if (css_tree.ident.decode(serializedDeclaration.property) !== name) return null;
2286
+ const valueLocation = serializedDeclaration.value.loc;
2287
+ const safeValue = valueLocation ? serialized.slice(valueLocation.start.offset, valueLocation.end.offset) : css_tree.generate(serializedDeclaration.value);
2288
+ const trimmedObservableValue = canonicalizeBrowserValue(name, canonicalizeLeadingDecimal(observableValue.trim()));
2289
+ const normalizedSafeValue = canonicalizeBrowserValue(name, safeValue.trim());
2135
2290
  return {
2136
2291
  observableValue: trimmedObservableValue === "0" && zeroLengthProperties.has(name) ? "0px" : trimmedObservableValue,
2137
- safeValue: serialized.slice(colonIndex + 1).trim(),
2292
+ safeValue: normalizedSafeValue,
2138
2293
  pendingSubstitution
2139
2294
  };
2140
2295
  } catch {
@@ -2142,7 +2297,7 @@ function parsePropertyValue(name, observableValue) {
2142
2297
  }
2143
2298
  }
2144
2299
  function parseAtruleDescriptorValue(atrule, name, observableValue) {
2145
- if (containsPriorityDelimiter(observableValue)) return null;
2300
+ if (containsTopLevelDeclarationBoundary(observableValue)) return null;
2146
2301
  try {
2147
2302
  if (css_tree.lexer.matchAtruleDescriptor(atrule, name, observableValue).error) return null;
2148
2303
  const prelude = atrule === "counter-style" ? " sheetom" : "";
@@ -2223,6 +2378,11 @@ function parseDeclarationValue(declaration, name, observableValue) {
2223
2378
  if (atrule) return parseAtruleDescriptorValue(atrule, name, observableValue);
2224
2379
  return parsePropertyValue(name, observableValue);
2225
2380
  }
2381
+ function normalizeDeclarationName(name) {
2382
+ if (name.startsWith("--")) return name;
2383
+ const lowerName = name.toLowerCase();
2384
+ return chromiumPropertyAliases[lowerName] ?? lowerName;
2385
+ }
2226
2386
  function namedPropertyToCSS(property) {
2227
2387
  if (property === "cssFloat") return "float";
2228
2388
  let cssName = property.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`);
@@ -2359,14 +2519,9 @@ var CSSRuleList = class {
2359
2519
  };
2360
2520
  function normalizeMediaText(value) {
2361
2521
  try {
2362
- const result = (0, lightningcss.transform)({
2363
- filename: "sheetom-media.css",
2364
- code: encoder.encode(`@media ${value} { .sheetom-probe { --sheetom: 0; } }`)
2365
- });
2366
- const serialized = decoder.decode(result.code);
2367
- const blockIndex = serialized.indexOf("{");
2368
- if (!serialized.startsWith("@media ") || blockIndex === -1) return null;
2369
- return serialized.slice(7, blockIndex).trim();
2522
+ const parsed = css_tree.parse(value, { context: "mediaQueryList" });
2523
+ if (parsed.type !== "MediaQueryList") return null;
2524
+ return css_tree.generate(parsed).replace(/,\s*/g, ", ").replace(/:\s*/g, ": ").replace(/\s*(<=|>=|=|<|>)\s*/g, " $1 ");
2370
2525
  } catch {
2371
2526
  return null;
2372
2527
  }
@@ -2644,6 +2799,7 @@ var CSSStyleDeclaration = class {
2644
2799
  this.parentRule = parentRule;
2645
2800
  lockOwnProperties(this, "parentRule");
2646
2801
  this.#block = new DeclarationBlock({
2802
+ normalizeName: normalizeDeclarationName,
2647
2803
  parseValue: (name, value) => parseDeclarationValue(this, name, value),
2648
2804
  shorthandLonghands: getShorthandLonghands,
2649
2805
  expandFourSide: expandStaticFourSide,
@@ -2698,7 +2854,7 @@ var CSSStyleDeclaration = class {
2698
2854
  for (const child of parsed.children) {
2699
2855
  if (child.type !== "Declaration") continue;
2700
2856
  declarations.push({
2701
- name: child.property,
2857
+ name: css_tree.ident.decode(child.property),
2702
2858
  value: css_tree.generate(child.value),
2703
2859
  important: child.important === true || child.important === "important"
2704
2860
  });
@@ -3443,9 +3599,18 @@ function createRuleFromNodeInternal(node, reportDiagnostic, preserveImports) {
3443
3599
  rule = new CSSContainerRule(prelude);
3444
3600
  break;
3445
3601
  case "layer":
3446
- if (!node.block) return new CSSGenericRule(0, css_tree.generate(node));
3602
+ if (!node.block) return new CSSGenericRule(0, `@layer ${prelude.split(",").map((part) => part.trim()).join(", ")};`);
3447
3603
  rule = new CSSLayerBlockRule(prelude);
3448
3604
  break;
3605
+ case "property": {
3606
+ if (!node.block || !prelude.startsWith("--")) return null;
3607
+ const descriptors = [];
3608
+ for (const child of node.block.children) {
3609
+ if (child.type !== "Declaration") continue;
3610
+ descriptors.push(`${child.property}: ${generateDescriptorInput(child.value)}${child.important ? " !important" : ""};`);
3611
+ }
3612
+ return new CSSGenericRule(genericRuleType(name), `@property ${prelude} {${descriptors.length === 0 ? "" : ` ${descriptors.join(" ")}`} }`);
3613
+ }
3449
3614
  case "scope": {
3450
3615
  const [start, end] = parseScopePrelude(prelude);
3451
3616
  rule = new CSSScopeRule(start, end);