@variance-authority/core 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.
Files changed (260) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/dist/artifact.d.ts +47 -0
  5. package/dist/artifact.js +2 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/attribute/because.d.ts +43 -0
  8. package/dist/attribute/because.js +64 -0
  9. package/dist/attribute/because.js.map +1 -0
  10. package/dist/attribute/boundary.d.ts +191 -0
  11. package/dist/attribute/boundary.js +328 -0
  12. package/dist/attribute/boundary.js.map +1 -0
  13. package/dist/attribute/call-site.d.ts +96 -0
  14. package/dist/attribute/call-site.js +183 -0
  15. package/dist/attribute/call-site.js.map +1 -0
  16. package/dist/attribute/component-hash.d.ts +188 -0
  17. package/dist/attribute/component-hash.js +312 -0
  18. package/dist/attribute/component-hash.js.map +1 -0
  19. package/dist/attribute/composition-fixture.d.ts +18 -0
  20. package/dist/attribute/composition-fixture.js +53 -0
  21. package/dist/attribute/composition-fixture.js.map +1 -0
  22. package/dist/attribute/composition.d.ts +182 -0
  23. package/dist/attribute/composition.js +169 -0
  24. package/dist/attribute/composition.js.map +1 -0
  25. package/dist/attribute/control.d.ts +43 -0
  26. package/dist/attribute/control.js +54 -0
  27. package/dist/attribute/control.js.map +1 -0
  28. package/dist/attribute/coverage.d.ts +99 -0
  29. package/dist/attribute/coverage.js +86 -0
  30. package/dist/attribute/coverage.js.map +1 -0
  31. package/dist/attribute/divergence.d.ts +102 -0
  32. package/dist/attribute/divergence.js +152 -0
  33. package/dist/attribute/divergence.js.map +1 -0
  34. package/dist/attribute/index.d.ts +43 -0
  35. package/dist/attribute/index.js +29 -0
  36. package/dist/attribute/index.js.map +1 -0
  37. package/dist/attribute/instability.d.ts +89 -0
  38. package/dist/attribute/instability.js +183 -0
  39. package/dist/attribute/instability.js.map +1 -0
  40. package/dist/attribute/instances.d.ts +145 -0
  41. package/dist/attribute/instances.js +140 -0
  42. package/dist/attribute/instances.js.map +1 -0
  43. package/dist/attribute/locate.d.ts +22 -0
  44. package/dist/attribute/locate.js +100 -0
  45. package/dist/attribute/locate.js.map +1 -0
  46. package/dist/attribute/mask.d.ts +156 -0
  47. package/dist/attribute/mask.js +201 -0
  48. package/dist/attribute/mask.js.map +1 -0
  49. package/dist/attribute/movement.d.ts +211 -0
  50. package/dist/attribute/movement.js +181 -0
  51. package/dist/attribute/movement.js.map +1 -0
  52. package/dist/attribute/region.d.ts +188 -0
  53. package/dist/attribute/region.js +144 -0
  54. package/dist/attribute/region.js.map +1 -0
  55. package/dist/attribute/source-map.d.ts +98 -0
  56. package/dist/attribute/source-map.js +264 -0
  57. package/dist/attribute/source-map.js.map +1 -0
  58. package/dist/attribute/source.d.ts +77 -0
  59. package/dist/attribute/source.js +95 -0
  60. package/dist/attribute/source.js.map +1 -0
  61. package/dist/attribute/stack.d.ts +80 -0
  62. package/dist/attribute/stack.js +108 -0
  63. package/dist/attribute/stack.js.map +1 -0
  64. package/dist/compare/band.d.ts +85 -0
  65. package/dist/compare/band.js +70 -0
  66. package/dist/compare/band.js.map +1 -0
  67. package/dist/compare/cascade.d.ts +45 -0
  68. package/dist/compare/cascade.js +71 -0
  69. package/dist/compare/cascade.js.map +1 -0
  70. package/dist/compare/derive.d.ts +78 -0
  71. package/dist/compare/derive.js +39 -0
  72. package/dist/compare/derive.js.map +1 -0
  73. package/dist/compare/diff/attribution.d.ts +23 -0
  74. package/dist/compare/diff/attribution.js +244 -0
  75. package/dist/compare/diff/attribution.js.map +1 -0
  76. package/dist/compare/diff/compare-nodes.d.ts +15 -0
  77. package/dist/compare/diff/compare-nodes.js +227 -0
  78. package/dist/compare/diff/compare-nodes.js.map +1 -0
  79. package/dist/compare/diff/components.d.ts +20 -0
  80. package/dist/compare/diff/components.js +76 -0
  81. package/dist/compare/diff/components.js.map +1 -0
  82. package/dist/compare/diff/delta.d.ts +165 -0
  83. package/dist/compare/diff/delta.js +12 -0
  84. package/dist/compare/diff/delta.js.map +1 -0
  85. package/dist/compare/diff/index.d.ts +69 -0
  86. package/dist/compare/diff/index.js +101 -0
  87. package/dist/compare/diff/index.js.map +1 -0
  88. package/dist/compare/diff/match.d.ts +37 -0
  89. package/dist/compare/diff/match.js +167 -0
  90. package/dist/compare/diff/match.js.map +1 -0
  91. package/dist/compare/explain.d.ts +23 -0
  92. package/dist/compare/explain.js +190 -0
  93. package/dist/compare/explain.js.map +1 -0
  94. package/dist/compare/holding-diff.d.ts +25 -0
  95. package/dist/compare/holding-diff.js +74 -0
  96. package/dist/compare/holding-diff.js.map +1 -0
  97. package/dist/compare/impact.d.ts +56 -0
  98. package/dist/compare/impact.js +124 -0
  99. package/dist/compare/impact.js.map +1 -0
  100. package/dist/compare/index.d.ts +30 -0
  101. package/dist/compare/index.js +22 -0
  102. package/dist/compare/index.js.map +1 -0
  103. package/dist/compare/instance.d.ts +30 -0
  104. package/dist/compare/instance.js +145 -0
  105. package/dist/compare/instance.js.map +1 -0
  106. package/dist/compare/observability.d.ts +49 -0
  107. package/dist/compare/observability.js +57 -0
  108. package/dist/compare/observability.js.map +1 -0
  109. package/dist/compare/parting.d.ts +179 -0
  110. package/dist/compare/parting.js +244 -0
  111. package/dist/compare/parting.js.map +1 -0
  112. package/dist/compare/slice.d.ts +74 -0
  113. package/dist/compare/slice.js +79 -0
  114. package/dist/compare/slice.js.map +1 -0
  115. package/dist/compare/value.d.ts +60 -0
  116. package/dist/compare/value.js +111 -0
  117. package/dist/compare/value.js.map +1 -0
  118. package/dist/format/accessibility.d.ts +27 -0
  119. package/dist/format/accessibility.js +17 -0
  120. package/dist/format/accessibility.js.map +1 -0
  121. package/dist/format/canonical.d.ts +38 -0
  122. package/dist/format/canonical.js +65 -0
  123. package/dist/format/canonical.js.map +1 -0
  124. package/dist/format/capture.d.ts +212 -0
  125. package/dist/format/capture.js +2 -0
  126. package/dist/format/capture.js.map +1 -0
  127. package/dist/format/document.d.ts +279 -0
  128. package/dist/format/document.js +65 -0
  129. package/dist/format/document.js.map +1 -0
  130. package/dist/format/environment.d.ts +120 -0
  131. package/dist/format/environment.js +53 -0
  132. package/dist/format/environment.js.map +1 -0
  133. package/dist/format/hash.d.ts +45 -0
  134. package/dist/format/hash.js +41 -0
  135. package/dist/format/hash.js.map +1 -0
  136. package/dist/format/holding.d.ts +134 -0
  137. package/dist/format/holding.js +2 -0
  138. package/dist/format/holding.js.map +1 -0
  139. package/dist/format/index.d.ts +39 -0
  140. package/dist/format/index.js +25 -0
  141. package/dist/format/index.js.map +1 -0
  142. package/dist/format/intervention.d.ts +87 -0
  143. package/dist/format/intervention.js +11 -0
  144. package/dist/format/intervention.js.map +1 -0
  145. package/dist/format/profile.d.ts +59 -0
  146. package/dist/format/profile.js +66 -0
  147. package/dist/format/profile.js.map +1 -0
  148. package/dist/format/provenance.d.ts +179 -0
  149. package/dist/format/provenance.js +222 -0
  150. package/dist/format/provenance.js.map +1 -0
  151. package/dist/format/sha256.d.ts +34 -0
  152. package/dist/format/sha256.js +152 -0
  153. package/dist/format/sha256.js.map +1 -0
  154. package/dist/format/snapshot.d.ts +263 -0
  155. package/dist/format/snapshot.js +2 -0
  156. package/dist/format/snapshot.js.map +1 -0
  157. package/dist/format/stabilize.d.ts +190 -0
  158. package/dist/format/stabilize.js +333 -0
  159. package/dist/format/stabilize.js.map +1 -0
  160. package/dist/format/tier.d.ts +19 -0
  161. package/dist/format/tier.js +11 -0
  162. package/dist/format/tier.js.map +1 -0
  163. package/dist/format/value.d.ts +115 -0
  164. package/dist/format/value.js +182 -0
  165. package/dist/format/value.js.map +1 -0
  166. package/dist/format/wiring.d.ts +115 -0
  167. package/dist/format/wiring.js +60 -0
  168. package/dist/format/wiring.js.map +1 -0
  169. package/dist/index.d.ts +35 -0
  170. package/dist/index.js +35 -0
  171. package/dist/index.js.map +1 -0
  172. package/dist/judge/docket.d.ts +85 -0
  173. package/dist/judge/docket.js +162 -0
  174. package/dist/judge/docket.js.map +1 -0
  175. package/dist/judge/fingerprint.d.ts +53 -0
  176. package/dist/judge/fingerprint.js +144 -0
  177. package/dist/judge/fingerprint.js.map +1 -0
  178. package/dist/judge/ignore.d.ts +206 -0
  179. package/dist/judge/ignore.js +237 -0
  180. package/dist/judge/ignore.js.map +1 -0
  181. package/dist/judge/index.d.ts +44 -0
  182. package/dist/judge/index.js +34 -0
  183. package/dist/judge/index.js.map +1 -0
  184. package/dist/judge/inspect.d.ts +148 -0
  185. package/dist/judge/inspect.js +208 -0
  186. package/dist/judge/inspect.js.map +1 -0
  187. package/dist/judge/intent.d.ts +146 -0
  188. package/dist/judge/intent.js +150 -0
  189. package/dist/judge/intent.js.map +1 -0
  190. package/dist/judge/locale.d.ts +111 -0
  191. package/dist/judge/locale.js +247 -0
  192. package/dist/judge/locale.js.map +1 -0
  193. package/dist/judge/scope.d.ts +41 -0
  194. package/dist/judge/scope.js +47 -0
  195. package/dist/judge/scope.js.map +1 -0
  196. package/dist/judge/sensitivity.d.ts +202 -0
  197. package/dist/judge/sensitivity.js +152 -0
  198. package/dist/judge/sensitivity.js.map +1 -0
  199. package/dist/judge/trail.d.ts +122 -0
  200. package/dist/judge/trail.js +96 -0
  201. package/dist/judge/trail.js.map +1 -0
  202. package/dist/judge/verdict.d.ts +39 -0
  203. package/dist/judge/verdict.js +32 -0
  204. package/dist/judge/verdict.js.map +1 -0
  205. package/dist/plan/index.d.ts +12 -0
  206. package/dist/plan/index.js +11 -0
  207. package/dist/plan/index.js.map +1 -0
  208. package/dist/plan/tool.d.ts +85 -0
  209. package/dist/plan/tool.js +87 -0
  210. package/dist/plan/tool.js.map +1 -0
  211. package/dist/relate/graph.d.ts +190 -0
  212. package/dist/relate/graph.js +234 -0
  213. package/dist/relate/graph.js.map +1 -0
  214. package/dist/relate/index.d.ts +16 -0
  215. package/dist/relate/index.js +16 -0
  216. package/dist/relate/index.js.map +1 -0
  217. package/dist/relate/merkle.d.ts +110 -0
  218. package/dist/relate/merkle.js +246 -0
  219. package/dist/relate/merkle.js.map +1 -0
  220. package/dist/relate/reach.d.ts +65 -0
  221. package/dist/relate/reach.js +111 -0
  222. package/dist/relate/reach.js.map +1 -0
  223. package/dist/relate/records.d.ts +129 -0
  224. package/dist/relate/records.js +112 -0
  225. package/dist/relate/records.js.map +1 -0
  226. package/dist/rules/index.d.ts +19 -0
  227. package/dist/rules/index.js +17 -0
  228. package/dist/rules/index.js.map +1 -0
  229. package/dist/rules/normalize/alias.d.ts +50 -0
  230. package/dist/rules/normalize/alias.js +111 -0
  231. package/dist/rules/normalize/alias.js.map +1 -0
  232. package/dist/rules/normalize/cascade.d.ts +85 -0
  233. package/dist/rules/normalize/cascade.js +324 -0
  234. package/dist/rules/normalize/cascade.js.map +1 -0
  235. package/dist/rules/normalize/color.d.ts +30 -0
  236. package/dist/rules/normalize/color.js +185 -0
  237. package/dist/rules/normalize/color.js.map +1 -0
  238. package/dist/rules/normalize/fixture.d.ts +83 -0
  239. package/dist/rules/normalize/fixture.js +74 -0
  240. package/dist/rules/normalize/fixture.js.map +1 -0
  241. package/dist/rules/normalize/index.d.ts +39 -0
  242. package/dist/rules/normalize/index.js +268 -0
  243. package/dist/rules/normalize/index.js.map +1 -0
  244. package/dist/rules/normalize/project.d.ts +42 -0
  245. package/dist/rules/normalize/project.js +83 -0
  246. package/dist/rules/normalize/project.js.map +1 -0
  247. package/dist/rules/normalize/shorthand.d.ts +38 -0
  248. package/dist/rules/normalize/shorthand.js +388 -0
  249. package/dist/rules/normalize/shorthand.js.map +1 -0
  250. package/dist/rules/normalize/value.d.ts +23 -0
  251. package/dist/rules/normalize/value.js +162 -0
  252. package/dist/rules/normalize/value.js.map +1 -0
  253. package/dist/rules/normalize/wrapper.d.ts +16 -0
  254. package/dist/rules/normalize/wrapper.js +137 -0
  255. package/dist/rules/normalize/wrapper.js.map +1 -0
  256. package/dist/rules/ruleset.d.ts +93 -0
  257. package/dist/rules/ruleset.js +175 -0
  258. package/dist/rules/ruleset.js.map +1 -0
  259. package/mark.svg +30 -0
  260. package/package.json +69 -0
@@ -0,0 +1,162 @@
1
+ import { canonicalizeColor, isColorProperty } from './color.js';
2
+ /**
3
+ * Declaration-value canonicalization.
4
+ *
5
+ * Every value entering a snapshot passes through here. The job is to erase every
6
+ * way CSS lets an author write the same thing differently — spacing, casing,
7
+ * absolute unit choice, float formatting — so that reformatting a stylesheet is
8
+ * not a mass-invalidation event.
9
+ *
10
+ * The discipline is one-directional: collapse only differences that provably
11
+ * cannot reach a pixel. Anything that *might* render differently is left alone,
12
+ * because an over-eager equivalence here produces a false `unchanged`, and the
13
+ * cost of the opposite mistake is only a review that did not need to happen.
14
+ */
15
+ /** Absolute lengths, in CSS px. Fixed by spec, so conversion is lossless. */
16
+ const ABSOLUTE_LENGTHS = {
17
+ px: 1,
18
+ pt: 96 / 72,
19
+ pc: 16,
20
+ in: 96,
21
+ cm: 96 / 2.54,
22
+ mm: 96 / 25.4,
23
+ q: 96 / 101.6,
24
+ };
25
+ /** Angles, in degrees. */
26
+ const ANGLES = {
27
+ deg: 1,
28
+ grad: 0.9,
29
+ rad: 180 / Math.PI,
30
+ turn: 360,
31
+ };
32
+ /**
33
+ * Units left symbolic because they cannot be resolved without a layout engine.
34
+ *
35
+ * This is the `declared-only` boundary made concrete (ADR-0002): under a profile
36
+ * without computed style, `padding: 1rem` stays `1rem`. That is honest — it is
37
+ * genuinely all that tier knows — and it is why JSDOM decides the token band
38
+ * `declared-only` rather than `full`. Resolving these by assuming a root font
39
+ * size would fabricate precision the tier does not have.
40
+ */
41
+ export const RELATIVE_UNITS = [
42
+ 'em', 'rem', 'ex', 'ch', 'cap', 'ic', 'lh', 'rlh',
43
+ 'vw', 'vh', 'vmin', 'vmax', 'vi', 'vb',
44
+ 'svw', 'svh', 'lvw', 'lvh', 'dvw', 'dvh', '%',
45
+ ];
46
+ /** Decimal places kept for a length. Absorbs float noise, keeps sub-pixel intent. */
47
+ const LENGTH_PRECISION = 4;
48
+ export function canonicalizeValue(property, value) {
49
+ if (isColorProperty(property))
50
+ return canonicalizeColor(value);
51
+ return canonicalizeTokens(value);
52
+ }
53
+ /**
54
+ * Rewrite a value token by token.
55
+ *
56
+ * Quoted strings and `url()` payloads are passed through untouched: a font
57
+ * family name and a resource path are opaque data, and normalizing their case
58
+ * would change what they refer to.
59
+ */
60
+ export function canonicalizeTokens(value) {
61
+ const out = [];
62
+ let index = 0;
63
+ const input = value.trim();
64
+ while (index < input.length) {
65
+ const char = input[index];
66
+ if (/\s/.test(char)) {
67
+ if (out.length > 0 && out[out.length - 1] !== ' ')
68
+ out.push(' ');
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (char === '"' || char === "'") {
73
+ const end = closingQuote(input, index);
74
+ out.push(input.slice(index, end + 1));
75
+ index = end + 1;
76
+ continue;
77
+ }
78
+ if (input.slice(index).toLowerCase().startsWith('url(')) {
79
+ const end = matchingParen(input, index + 3);
80
+ out.push(`url${input.slice(index + 3, end + 1)}`);
81
+ index = end + 1;
82
+ continue;
83
+ }
84
+ const numeric = /^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?([a-z%]*)/i.exec(input.slice(index));
85
+ if (numeric && numeric[0].length > 0) {
86
+ out.push(canonicalizeDimension(numeric[0]));
87
+ index += numeric[0].length;
88
+ continue;
89
+ }
90
+ const identifier = /^[a-z_-][\w-]*/i.exec(input.slice(index));
91
+ if (identifier) {
92
+ // Unquoted identifiers are CSS keywords and function names, both
93
+ // case-insensitive to the parser and therefore safe to fold.
94
+ out.push(identifier[0].toLowerCase());
95
+ index += identifier[0].length;
96
+ continue;
97
+ }
98
+ // Punctuation: `,` `(` `)` `/` `*`. Drop whitespace before a comma so
99
+ // `a , b` and `a, b` converge.
100
+ if (char === ',' && out[out.length - 1] === ' ')
101
+ out.pop();
102
+ out.push(char);
103
+ index += 1;
104
+ }
105
+ return out.join('').trim();
106
+ }
107
+ /** Canonicalize one `<number><unit>` token. */
108
+ export function canonicalizeDimension(token) {
109
+ const match = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i.exec(token);
110
+ if (!match)
111
+ return token.toLowerCase();
112
+ const magnitude = Number.parseFloat(match[1]);
113
+ const unit = match[2].toLowerCase();
114
+ if (!Number.isFinite(magnitude))
115
+ return token.toLowerCase();
116
+ // A zero length is unitless per spec, and `0`, `0px`, `0em` are the same
117
+ // declaration written three ways.
118
+ if (magnitude === 0 && (unit === '' || unit in ABSOLUTE_LENGTHS || isRelativeUnit(unit))) {
119
+ return '0';
120
+ }
121
+ const lengthFactor = ABSOLUTE_LENGTHS[unit];
122
+ if (lengthFactor !== undefined)
123
+ return `${round(magnitude * lengthFactor)}px`;
124
+ const angleFactor = ANGLES[unit];
125
+ if (angleFactor !== undefined)
126
+ return `${round(magnitude * angleFactor)}deg`;
127
+ return `${round(magnitude)}${unit}`;
128
+ }
129
+ export function isRelativeUnit(unit) {
130
+ return RELATIVE_UNITS.includes(unit.toLowerCase());
131
+ }
132
+ function round(value) {
133
+ const factor = 10 ** LENGTH_PRECISION;
134
+ const rounded = Math.round(value * factor) / factor;
135
+ return Object.is(rounded, -0) ? 0 : rounded;
136
+ }
137
+ function closingQuote(input, start) {
138
+ const quote = input[start];
139
+ for (let i = start + 1; i < input.length; i += 1) {
140
+ if (input[i] === '\\') {
141
+ i += 1;
142
+ continue;
143
+ }
144
+ if (input[i] === quote)
145
+ return i;
146
+ }
147
+ return input.length - 1;
148
+ }
149
+ function matchingParen(input, openIndex) {
150
+ let depth = 0;
151
+ for (let i = openIndex; i < input.length; i += 1) {
152
+ if (input[i] === '(')
153
+ depth += 1;
154
+ else if (input[i] === ')') {
155
+ depth -= 1;
156
+ if (depth === 0)
157
+ return i;
158
+ }
159
+ }
160
+ return input.length - 1;
161
+ }
162
+ //# sourceMappingURL=value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value.js","sourceRoot":"","sources":["../../../src/rules/normalize/value.ts"],"names":[],"mappings":"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","sourcesContent":["import { canonicalizeColor, isColorProperty } from './color.js';\n\n/**\n * Declaration-value canonicalization.\n *\n * Every value entering a snapshot passes through here. The job is to erase every\n * way CSS lets an author write the same thing differently — spacing, casing,\n * absolute unit choice, float formatting — so that reformatting a stylesheet is\n * not a mass-invalidation event.\n *\n * The discipline is one-directional: collapse only differences that provably\n * cannot reach a pixel. Anything that *might* render differently is left alone,\n * because an over-eager equivalence here produces a false `unchanged`, and the\n * cost of the opposite mistake is only a review that did not need to happen.\n */\n\n/** Absolute lengths, in CSS px. Fixed by spec, so conversion is lossless. */\nconst ABSOLUTE_LENGTHS: Readonly<Record<string, number>> = {\n px: 1,\n pt: 96 / 72,\n pc: 16,\n in: 96,\n cm: 96 / 2.54,\n mm: 96 / 25.4,\n q: 96 / 101.6,\n};\n\n/** Angles, in degrees. */\nconst ANGLES: Readonly<Record<string, number>> = {\n deg: 1,\n grad: 0.9,\n rad: 180 / Math.PI,\n turn: 360,\n};\n\n/**\n * Units left symbolic because they cannot be resolved without a layout engine.\n *\n * This is the `declared-only` boundary made concrete (ADR-0002): under a profile\n * without computed style, `padding: 1rem` stays `1rem`. That is honest — it is\n * genuinely all that tier knows — and it is why JSDOM decides the token band\n * `declared-only` rather than `full`. Resolving these by assuming a root font\n * size would fabricate precision the tier does not have.\n */\nexport const RELATIVE_UNITS: readonly string[] = [\n 'em', 'rem', 'ex', 'ch', 'cap', 'ic', 'lh', 'rlh',\n 'vw', 'vh', 'vmin', 'vmax', 'vi', 'vb',\n 'svw', 'svh', 'lvw', 'lvh', 'dvw', 'dvh', '%',\n];\n\n/** Decimal places kept for a length. Absorbs float noise, keeps sub-pixel intent. */\nconst LENGTH_PRECISION = 4;\n\nexport function canonicalizeValue(property: string, value: string): string {\n if (isColorProperty(property)) return canonicalizeColor(value);\n return canonicalizeTokens(value);\n}\n\n/**\n * Rewrite a value token by token.\n *\n * Quoted strings and `url()` payloads are passed through untouched: a font\n * family name and a resource path are opaque data, and normalizing their case\n * would change what they refer to.\n */\nexport function canonicalizeTokens(value: string): string {\n const out: string[] = [];\n let index = 0;\n const input = value.trim();\n\n while (index < input.length) {\n const char = input[index]!;\n\n if (/\\s/.test(char)) {\n if (out.length > 0 && out[out.length - 1] !== ' ') out.push(' ');\n index += 1;\n continue;\n }\n\n if (char === '\"' || char === \"'\") {\n const end = closingQuote(input, index);\n out.push(input.slice(index, end + 1));\n index = end + 1;\n continue;\n }\n\n if (input.slice(index).toLowerCase().startsWith('url(')) {\n const end = matchingParen(input, index + 3);\n out.push(`url${input.slice(index + 3, end + 1)}`);\n index = end + 1;\n continue;\n }\n\n const numeric = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?([a-z%]*)/i.exec(input.slice(index));\n if (numeric && numeric[0].length > 0) {\n out.push(canonicalizeDimension(numeric[0]));\n index += numeric[0].length;\n continue;\n }\n\n const identifier = /^[a-z_-][\\w-]*/i.exec(input.slice(index));\n if (identifier) {\n // Unquoted identifiers are CSS keywords and function names, both\n // case-insensitive to the parser and therefore safe to fold.\n out.push(identifier[0].toLowerCase());\n index += identifier[0].length;\n continue;\n }\n\n // Punctuation: `,` `(` `)` `/` `*`. Drop whitespace before a comma so\n // `a , b` and `a, b` converge.\n if (char === ',' && out[out.length - 1] === ' ') out.pop();\n out.push(char);\n index += 1;\n }\n\n return out.join('').trim();\n}\n\n/** Canonicalize one `<number><unit>` token. */\nexport function canonicalizeDimension(token: string): string {\n const match = /^([+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?)([a-z%]*)$/i.exec(token);\n if (!match) return token.toLowerCase();\n\n const magnitude = Number.parseFloat(match[1]!);\n const unit = match[2]!.toLowerCase();\n\n if (!Number.isFinite(magnitude)) return token.toLowerCase();\n\n // A zero length is unitless per spec, and `0`, `0px`, `0em` are the same\n // declaration written three ways.\n if (magnitude === 0 && (unit === '' || unit in ABSOLUTE_LENGTHS || isRelativeUnit(unit))) {\n return '0';\n }\n\n const lengthFactor = ABSOLUTE_LENGTHS[unit];\n if (lengthFactor !== undefined) return `${round(magnitude * lengthFactor)}px`;\n\n const angleFactor = ANGLES[unit];\n if (angleFactor !== undefined) return `${round(magnitude * angleFactor)}deg`;\n\n return `${round(magnitude)}${unit}`;\n}\n\nexport function isRelativeUnit(unit: string): boolean {\n return RELATIVE_UNITS.includes(unit.toLowerCase());\n}\n\nfunction round(value: number): number {\n const factor = 10 ** LENGTH_PRECISION;\n const rounded = Math.round(value * factor) / factor;\n return Object.is(rounded, -0) ? 0 : rounded;\n}\n\nfunction closingQuote(input: string, start: number): number {\n const quote = input[start];\n for (let i = start + 1; i < input.length; i += 1) {\n if (input[i] === '\\\\') {\n i += 1;\n continue;\n }\n if (input[i] === quote) return i;\n }\n return input.length - 1;\n}\n\nfunction matchingParen(input: string, openIndex: number): number {\n let depth = 0;\n for (let i = openIndex; i < input.length; i += 1) {\n if (input[i] === '(') depth += 1;\n else if (input[i] === ')') {\n depth -= 1;\n if (depth === 0) return i;\n }\n }\n return input.length - 1;\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { RawNode } from '../../format/capture.js';
2
+ import type { SemanticNode } from '../../format/snapshot.js';
3
+ /**
4
+ * Whether a node exists only to hold its children.
5
+ *
6
+ * Conservative on purpose: this is the one normalization rule that *removes* a
7
+ * node, so a wrong answer here deletes evidence. Anything carrying a role, a
8
+ * name, an id, an admitted attribute, text, or a non-inherited style declaration
9
+ * is kept, whatever it looks like.
10
+ *
11
+ * Inherited properties are excluded from the test because a wrapper inheriting
12
+ * `color` from above passes that same value to its children either way — its
13
+ * presence changes nothing that renders.
14
+ */
15
+ export declare function isInertWrapper(raw: RawNode, node: SemanticNode, declaredBy: WeakMap<SemanticNode, ReadonlySet<string>>): boolean;
16
+ //# sourceMappingURL=wrapper.d.ts.map
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Whether a node exists only to hold its children.
3
+ *
4
+ * Conservative on purpose: this is the one normalization rule that *removes* a
5
+ * node, so a wrong answer here deletes evidence. Anything carrying a role, a
6
+ * name, an id, an admitted attribute, text, or a non-inherited style declaration
7
+ * is kept, whatever it looks like.
8
+ *
9
+ * Inherited properties are excluded from the test because a wrapper inheriting
10
+ * `color` from above passes that same value to its children either way — its
11
+ * presence changes nothing that renders.
12
+ */
13
+ export function isInertWrapper(raw, node, declaredBy) {
14
+ if (node.tag !== 'div' && node.tag !== 'span')
15
+ return false;
16
+ if (node.role !== undefined || node.name !== undefined || node.state !== undefined)
17
+ return false;
18
+ if (node.alias !== undefined || node.text !== undefined)
19
+ return false;
20
+ if (Object.keys(node.attributes).length > 0)
21
+ return false;
22
+ if (raw.shadowChildren !== undefined && raw.shadowChildren.length > 0)
23
+ return false;
24
+ // A wrapper the operator excluded is never inert, whatever it declares.
25
+ //
26
+ // The mark is not an attribute — `data-variance-ignore` is deliberately outside
27
+ // the allowlist so that adding it re-baselines nothing — so the check above
28
+ // cannot see it, and a bare marked `<div>` is exactly the shape this function
29
+ // deletes. Collapsing it drops the mark with it, `sitesIn` finds no site, and
30
+ // the exclusion silently evaporates: the operator reads their config, sees the
31
+ // rule, and the run compares the region anyway. An ignore that stops working
32
+ // without saying so is the same failure as one that absorbs too much, pointed
33
+ // the other way.
34
+ if (node.ignoredBy !== undefined && node.ignoredBy.length > 0)
35
+ return false;
36
+ // A wrapper that roots a component boundary is never inert either, and for the
37
+ // same reason one sentence up: the collapse would take the holding with it, and
38
+ // `function Summary() { return <span>{total}</span> }` is not an exotic shape —
39
+ // it is most of a component library. Losing the boundary there loses the only
40
+ // record of what that component was handed and what it retained, which is the
41
+ // evidence `compare/parting.ts` traces a difference back through.
42
+ //
43
+ // The cost is stated rather than hidden: a run that reads holdings keeps
44
+ // wrappers a run without them collapses, so the two produce different
45
+ // `structureHash`es for one page. That is why holding is opted into per run
46
+ // rather than defaulted on, and why both sides of a comparison must be read
47
+ // the same way — the same bargain `ignoredBy` makes above.
48
+ //
49
+ // FIXME: `wiring` is destroyed by this collapse and is not checked here. It is
50
+ // hashed into a band, so adding it would move an existing baseline for anyone
51
+ // already reading wiring, which is a change that needs its own changeset.
52
+ if (node.holding !== undefined)
53
+ return false;
54
+ const declaredHere = declaredBy.get(node) ?? EMPTY_PROPERTIES;
55
+ for (const [property, value] of Object.entries(node.style)) {
56
+ // Only what the wrapper *declared* is evidence about the wrapper.
57
+ //
58
+ // An inherited value passes through unchanged — the children receive it
59
+ // whether or not the wrapper is there. An engine-computed value is worse
60
+ // than uninformative: under a profile with computed style every one of the
61
+ // ~200 allowlisted properties arrives with a resolved value, including used
62
+ // values like `width: 1264px` that describe the *parent's* layout rather
63
+ // than anything the wrapper did. Testing those against an initial-value
64
+ // table meant `isInertDeclaration` returned false on the first unrecognized
65
+ // one and no wrapper anywhere collapsed under `chromium` — the same rule
66
+ // disabled by a different accident under `jsdom` in journal 0005, and
67
+ // invisible until the two profiles were scored against each other (P4).
68
+ //
69
+ // Conservatism is kept where it is evidence: an unrecognized property the
70
+ // wrapper *declared* still blocks the collapse.
71
+ if (!declaredHere.has(property))
72
+ continue;
73
+ if (!isInertDeclaration(property, value))
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ const EMPTY_PROPERTIES = new Set();
79
+ /**
80
+ * Initial values for the properties a bare `div`/`span` legitimately carries.
81
+ *
82
+ * Needed because a profile with computed style reports *every* property, initial
83
+ * ones included — so "declares no styling" cannot be tested by an empty map.
84
+ */
85
+ const INERT_VALUES = {
86
+ // `contents` generates no box at all, so a wrapper carrying it is inert by
87
+ // definition: its children already participate in the parent's layout.
88
+ display: ['block', 'inline', 'contents'],
89
+ position: ['static'],
90
+ 'box-sizing': ['content-box', 'border-box'],
91
+ 'overflow-x': ['visible'],
92
+ 'overflow-y': ['visible'],
93
+ opacity: ['1'],
94
+ visibility: ['visible'],
95
+ transform: ['none'],
96
+ filter: ['none'],
97
+ 'backdrop-filter': ['none'],
98
+ 'mix-blend-mode': ['normal'],
99
+ 'background-color': ['rgb(0 0 0 / 0)'],
100
+ 'background-image': ['none'],
101
+ 'z-index': ['auto'],
102
+ float: ['none'],
103
+ clear: ['none'],
104
+ width: ['auto'],
105
+ height: ['auto'],
106
+ 'min-width': ['0', 'auto'],
107
+ 'min-height': ['0', 'auto'],
108
+ 'max-width': ['none'],
109
+ 'max-height': ['none'],
110
+ 'aspect-ratio': ['auto'],
111
+ 'content-visibility': ['visible'],
112
+ 'object-fit': ['fill'],
113
+ 'table-layout': ['auto'],
114
+ };
115
+ const ZERO_PREFIXED = ['margin-', 'padding-', 'border-', 'outline-', 'inset', 'top', 'right', 'bottom', 'left'];
116
+ function isInertDeclaration(property, value) {
117
+ const allowed = INERT_VALUES[property];
118
+ if (allowed)
119
+ return allowed.includes(value);
120
+ if (property.startsWith('border-') && property.endsWith('-style'))
121
+ return value === 'none';
122
+ if (property.startsWith('border-') && property.endsWith('-color'))
123
+ return true;
124
+ if (property === 'outline-style')
125
+ return value === 'none';
126
+ if (property === 'outline-color')
127
+ return true;
128
+ if (property === 'box-shadow' || property === 'text-shadow')
129
+ return value === 'none';
130
+ if (ZERO_PREFIXED.some((prefix) => property.startsWith(prefix))) {
131
+ return value === '0' || value === 'auto';
132
+ }
133
+ // An unrecognized property on a wrapper is a reason to keep it. Silence is
134
+ // not evidence of inertness.
135
+ return false;
136
+ }
137
+ //# sourceMappingURL=wrapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../../src/rules/normalize/wrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RawNode } from '../../format/capture.js';\nimport type { SemanticNode } from '../../format/snapshot.js';\n\n/**\n * Whether a node exists only to hold its children.\n *\n * Conservative on purpose: this is the one normalization rule that *removes* a\n * node, so a wrong answer here deletes evidence. Anything carrying a role, a\n * name, an id, an admitted attribute, text, or a non-inherited style declaration\n * is kept, whatever it looks like.\n *\n * Inherited properties are excluded from the test because a wrapper inheriting\n * `color` from above passes that same value to its children either way — its\n * presence changes nothing that renders.\n */\nexport function isInertWrapper(\n raw: RawNode,\n node: SemanticNode,\n declaredBy: WeakMap<SemanticNode, ReadonlySet<string>>,\n): boolean {\n if (node.tag !== 'div' && node.tag !== 'span') return false;\n if (node.role !== undefined || node.name !== undefined || node.state !== undefined) return false;\n if (node.alias !== undefined || node.text !== undefined) return false;\n if (Object.keys(node.attributes).length > 0) return false;\n if (raw.shadowChildren !== undefined && raw.shadowChildren.length > 0) return false;\n\n // A wrapper the operator excluded is never inert, whatever it declares.\n //\n // The mark is not an attribute — `data-variance-ignore` is deliberately outside\n // the allowlist so that adding it re-baselines nothing — so the check above\n // cannot see it, and a bare marked `<div>` is exactly the shape this function\n // deletes. Collapsing it drops the mark with it, `sitesIn` finds no site, and\n // the exclusion silently evaporates: the operator reads their config, sees the\n // rule, and the run compares the region anyway. An ignore that stops working\n // without saying so is the same failure as one that absorbs too much, pointed\n // the other way.\n if (node.ignoredBy !== undefined && node.ignoredBy.length > 0) return false;\n\n // A wrapper that roots a component boundary is never inert either, and for the\n // same reason one sentence up: the collapse would take the holding with it, and\n // `function Summary() { return <span>{total}</span> }` is not an exotic shape —\n // it is most of a component library. Losing the boundary there loses the only\n // record of what that component was handed and what it retained, which is the\n // evidence `compare/parting.ts` traces a difference back through.\n //\n // The cost is stated rather than hidden: a run that reads holdings keeps\n // wrappers a run without them collapses, so the two produce different\n // `structureHash`es for one page. That is why holding is opted into per run\n // rather than defaulted on, and why both sides of a comparison must be read\n // the same way — the same bargain `ignoredBy` makes above.\n //\n // FIXME: `wiring` is destroyed by this collapse and is not checked here. It is\n // hashed into a band, so adding it would move an existing baseline for anyone\n // already reading wiring, which is a change that needs its own changeset.\n if (node.holding !== undefined) return false;\n\n const declaredHere = declaredBy.get(node) ?? EMPTY_PROPERTIES;\n\n for (const [property, value] of Object.entries(node.style)) {\n // Only what the wrapper *declared* is evidence about the wrapper.\n //\n // An inherited value passes through unchanged — the children receive it\n // whether or not the wrapper is there. An engine-computed value is worse\n // than uninformative: under a profile with computed style every one of the\n // ~200 allowlisted properties arrives with a resolved value, including used\n // values like `width: 1264px` that describe the *parent's* layout rather\n // than anything the wrapper did. Testing those against an initial-value\n // table meant `isInertDeclaration` returned false on the first unrecognized\n // one and no wrapper anywhere collapsed under `chromium` — the same rule\n // disabled by a different accident under `jsdom` in journal 0005, and\n // invisible until the two profiles were scored against each other (P4).\n //\n // Conservatism is kept where it is evidence: an unrecognized property the\n // wrapper *declared* still blocks the collapse.\n if (!declaredHere.has(property)) continue;\n if (!isInertDeclaration(property, value)) return false;\n }\n\n return true;\n}\n\nconst EMPTY_PROPERTIES: ReadonlySet<string> = new Set();\n\n/**\n * Initial values for the properties a bare `div`/`span` legitimately carries.\n *\n * Needed because a profile with computed style reports *every* property, initial\n * ones included — so \"declares no styling\" cannot be tested by an empty map.\n */\nconst INERT_VALUES: Readonly<Record<string, readonly string[]>> = {\n // `contents` generates no box at all, so a wrapper carrying it is inert by\n // definition: its children already participate in the parent's layout.\n display: ['block', 'inline', 'contents'],\n position: ['static'],\n 'box-sizing': ['content-box', 'border-box'],\n 'overflow-x': ['visible'],\n 'overflow-y': ['visible'],\n opacity: ['1'],\n visibility: ['visible'],\n transform: ['none'],\n filter: ['none'],\n 'backdrop-filter': ['none'],\n 'mix-blend-mode': ['normal'],\n 'background-color': ['rgb(0 0 0 / 0)'],\n 'background-image': ['none'],\n 'z-index': ['auto'],\n float: ['none'],\n clear: ['none'],\n width: ['auto'],\n height: ['auto'],\n 'min-width': ['0', 'auto'],\n 'min-height': ['0', 'auto'],\n 'max-width': ['none'],\n 'max-height': ['none'],\n 'aspect-ratio': ['auto'],\n 'content-visibility': ['visible'],\n 'object-fit': ['fill'],\n 'table-layout': ['auto'],\n};\n\nconst ZERO_PREFIXED = ['margin-', 'padding-', 'border-', 'outline-', 'inset', 'top', 'right', 'bottom', 'left'];\n\nfunction isInertDeclaration(property: string, value: string): boolean {\n const allowed = INERT_VALUES[property];\n if (allowed) return allowed.includes(value);\n\n if (property.startsWith('border-') && property.endsWith('-style')) return value === 'none';\n if (property.startsWith('border-') && property.endsWith('-color')) return true;\n if (property === 'outline-style') return value === 'none';\n if (property === 'outline-color') return true;\n if (property === 'box-shadow' || property === 'text-shadow') return value === 'none';\n\n if (ZERO_PREFIXED.some((prefix) => property.startsWith(prefix))) {\n return value === '0' || value === 'auto';\n }\n\n // An unrecognized property on a wrapper is a reason to keep it. Silence is\n // not evidence of inertness.\n return false;\n}\n"]}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The normalization ruleset and computed-style allowlist.
3
+ *
4
+ * Both versions are components of the environment key, so **changing anything in
5
+ * this file invalidates every baseline in every repository using it** (spec §7.3).
6
+ * That is correct and intended — a rule change is a render-input change — but it
7
+ * means edits here are releases, not tweaks. Bump the version in the same commit.
8
+ */
9
+ /** Bump on any change to normalization behavior. */
10
+ export declare const RULESET_VERSION = "r1";
11
+ /**
12
+ * Bump on any change to {@link STYLE_ALLOWLIST}.
13
+ *
14
+ * `a2` added `accent-color` and `-webkit-text-stroke-width`, both found by
15
+ * probing a real Chromium against the semantic snapshot: each moves hundreds of
16
+ * pixels and neither moved the render hash.
17
+ *
18
+ * `-webkit-text-stroke-color` was added alongside them and immediately removed.
19
+ * Its initial value is `currentcolor`, so it moves whenever `color` moves — which
20
+ * split a colour-token root in two, since the derived property resolves through
21
+ * no token and looked like an independent cause. Nothing measured it as a gap;
22
+ * it was added on the assumption that its sibling implied it. Properties whose
23
+ * computed value derives from another property need the derivation modelled
24
+ * before they can be admitted.
25
+ */
26
+ export declare const ALLOWLIST_VERSION = "a2";
27
+ /**
28
+ * Properties that enter a snapshot.
29
+ *
30
+ * The allowlist is the direct lever on the product's central tradeoff (spec
31
+ * §11.3): too narrow and real regressions pass unseen; too wide and every
32
+ * snapshot churns, reintroducing the fatigue that makes teams stop reading
33
+ * diffs. So the admission test is deliberately strict —
34
+ *
35
+ * > Can a change to this property alone alter what a user sees or how assistive
36
+ * > technology reports the page?
37
+ *
38
+ * Only longhands appear. Shorthands are expanded before matching, because
39
+ * `margin: 4px` and `margin-top: 4px; …` must produce identical snapshots or
40
+ * refactoring a stylesheet becomes a mass-invalidation event.
41
+ *
42
+ * Notable exclusions, each for a reason:
43
+ * - `transition-*` / `animation-*` — snapshots are taken at a declared settle
44
+ * point with animation disabled, so these describe a journey the snapshot does
45
+ * not contain. Including them would make every easing tweak a diff.
46
+ * - `cursor`, `user-select`, `will-change`, `contain` — invisible in both raster
47
+ * and the accessibility tree at rest.
48
+ * - `content` — resolved pseudo-element text is captured as text, not as a
49
+ * declaration, so that it diffs like the content it is.
50
+ */
51
+ export declare const STYLE_ALLOWLIST: readonly string[];
52
+ export declare function isAllowedProperty(property: string): boolean;
53
+ /**
54
+ * Custom properties are always admitted, whatever their name.
55
+ *
56
+ * They cannot be enumerated in advance — every project invents its own — and they
57
+ * are the mechanism design tokens travel through, which makes them the highest-
58
+ * value signal the system collects: a custom property resolves a *name* alongside
59
+ * a value, and that name is what lets one token edit collapse into one docket
60
+ * root with counted collateral rather than hundreds of separate colour diffs.
61
+ */
62
+ export declare function isCustomProperty(property: string): boolean;
63
+ /** Whether a property survives the allowlist projection (ADR-0003 step 4). */
64
+ export declare function admits(property: string): boolean;
65
+ /**
66
+ * Attributes kept on a semantic node.
67
+ *
68
+ * An allowlist rather than a denylist. Frameworks emit an open-ended supply of
69
+ * bookkeeping attributes — `data-reactroot`, `data-styled`, `data-testid`,
70
+ * `data-v-7f3a`, Storybook's own markers — and a denylist would need a new entry
71
+ * for each one, silently churning baselines until someone noticed. Anything not
72
+ * named here is dropped.
73
+ *
74
+ * `class` is absent by design (ADR-0003): a class name records *how* a style was
75
+ * applied, and the snapshot already records *what* was applied, resolved.
76
+ * `id` is absent because it has been replaced by a structural alias.
77
+ * `aria-*` is absent because those are resolved into `role`/`name`/`state`,
78
+ * which is what actually reaches a user.
79
+ */
80
+ export declare const ATTRIBUTE_ALLOWLIST: readonly string[];
81
+ export declare function admitsAttribute(name: string): boolean;
82
+ /**
83
+ * Attributes whose values are id references, rewritten to structural aliases.
84
+ *
85
+ * Aliasing rather than masking is what preserves the *relationship* while
86
+ * deleting the volatile *value* (ADR-0003): a `useId` renumbering becomes a
87
+ * no-op, while breaking a `label ↔ input` association stays a real structural
88
+ * change. Masking both to a constant cannot tell those apart.
89
+ */
90
+ export declare const ID_REFERENCE_ATTRIBUTES: readonly string[];
91
+ /** Attributes holding a *space-separated list* of id references, not just one. */
92
+ export declare const ID_REFERENCE_LIST_ATTRIBUTES: readonly string[];
93
+ //# sourceMappingURL=ruleset.d.ts.map
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The normalization ruleset and computed-style allowlist.
3
+ *
4
+ * Both versions are components of the environment key, so **changing anything in
5
+ * this file invalidates every baseline in every repository using it** (spec §7.3).
6
+ * That is correct and intended — a rule change is a render-input change — but it
7
+ * means edits here are releases, not tweaks. Bump the version in the same commit.
8
+ */
9
+ /** Bump on any change to normalization behavior. */
10
+ export const RULESET_VERSION = 'r1';
11
+ /**
12
+ * Bump on any change to {@link STYLE_ALLOWLIST}.
13
+ *
14
+ * `a2` added `accent-color` and `-webkit-text-stroke-width`, both found by
15
+ * probing a real Chromium against the semantic snapshot: each moves hundreds of
16
+ * pixels and neither moved the render hash.
17
+ *
18
+ * `-webkit-text-stroke-color` was added alongside them and immediately removed.
19
+ * Its initial value is `currentcolor`, so it moves whenever `color` moves — which
20
+ * split a colour-token root in two, since the derived property resolves through
21
+ * no token and looked like an independent cause. Nothing measured it as a gap;
22
+ * it was added on the assumption that its sibling implied it. Properties whose
23
+ * computed value derives from another property need the derivation modelled
24
+ * before they can be admitted.
25
+ */
26
+ export const ALLOWLIST_VERSION = 'a2';
27
+ /**
28
+ * Properties that enter a snapshot.
29
+ *
30
+ * The allowlist is the direct lever on the product's central tradeoff (spec
31
+ * §11.3): too narrow and real regressions pass unseen; too wide and every
32
+ * snapshot churns, reintroducing the fatigue that makes teams stop reading
33
+ * diffs. So the admission test is deliberately strict —
34
+ *
35
+ * > Can a change to this property alone alter what a user sees or how assistive
36
+ * > technology reports the page?
37
+ *
38
+ * Only longhands appear. Shorthands are expanded before matching, because
39
+ * `margin: 4px` and `margin-top: 4px; …` must produce identical snapshots or
40
+ * refactoring a stylesheet becomes a mass-invalidation event.
41
+ *
42
+ * Notable exclusions, each for a reason:
43
+ * - `transition-*` / `animation-*` — snapshots are taken at a declared settle
44
+ * point with animation disabled, so these describe a journey the snapshot does
45
+ * not contain. Including them would make every easing tweak a diff.
46
+ * - `cursor`, `user-select`, `will-change`, `contain` — invisible in both raster
47
+ * and the accessibility tree at rest.
48
+ * - `content` — resolved pseudo-element text is captured as text, not as a
49
+ * declaration, so that it diffs like the content it is.
50
+ */
51
+ export const STYLE_ALLOWLIST = [
52
+ // Box model and formatting context
53
+ 'display', 'position', 'top', 'right', 'bottom', 'left', 'float', 'clear',
54
+ 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
55
+ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
56
+ 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
57
+ 'box-sizing', 'overflow-x', 'overflow-y', 'z-index', 'aspect-ratio',
58
+ // Flex and grid
59
+ 'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',
60
+ 'justify-content', 'align-items', 'align-self', 'align-content', 'order',
61
+ 'grid-template-columns', 'grid-template-rows', 'grid-template-areas',
62
+ 'grid-auto-columns', 'grid-auto-rows', 'grid-auto-flow',
63
+ 'grid-column-start', 'grid-column-end', 'grid-row-start', 'grid-row-end',
64
+ 'row-gap', 'column-gap',
65
+ // Typography
66
+ 'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
67
+ 'font-stretch', 'line-height', 'letter-spacing', 'word-spacing',
68
+ 'text-align', 'text-indent', 'text-transform', 'text-overflow',
69
+ 'text-decoration-line', 'text-decoration-color', 'text-decoration-style',
70
+ 'text-decoration-thickness', 'white-space', 'word-break', 'overflow-wrap',
71
+ 'vertical-align', 'writing-mode', 'direction',
72
+ // Paint
73
+ 'color', 'opacity', 'background-color', 'background-image',
74
+ // Native-control painting. `accent-color` restyles checkboxes and radios,
75
+ // which is exactly the kind of property the rest of this list misses: it
76
+ // describes what the *engine* paints rather than what the author declares
77
+ // about a box. See the note below.
78
+ 'accent-color', '-webkit-text-stroke-width',
79
+ 'background-position', 'background-size', 'background-repeat',
80
+ 'background-clip', 'background-origin',
81
+ 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
82
+ 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
83
+ 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
84
+ 'border-top-left-radius', 'border-top-right-radius',
85
+ 'border-bottom-right-radius', 'border-bottom-left-radius',
86
+ 'outline-width', 'outline-style', 'outline-color', 'outline-offset',
87
+ 'box-shadow', 'text-shadow', 'filter', 'backdrop-filter', 'mix-blend-mode',
88
+ // Visibility and geometry transforms
89
+ 'visibility', 'content-visibility', 'transform', 'transform-origin',
90
+ 'object-fit', 'object-position',
91
+ // Tables
92
+ 'table-layout', 'border-collapse', 'border-spacing', 'caption-side',
93
+ ];
94
+ /**
95
+ * Known weakness, recorded because two additions do not fix it.
96
+ *
97
+ * The list above was assembled by asking what an author declares about a box,
98
+ * and it is good at that. It is poor at properties describing what the *engine*
99
+ * paints — native control appearance, text decoration painted outside the glyph
100
+ * box, platform accent colours. `accent-color` and `-webkit-text-stroke-width`
101
+ * were found in an afternoon of probing a real browser, which is weak evidence
102
+ * that they are the only two.
103
+ *
104
+ * The right response is a systematic audit against the CSS property index rather
105
+ * than more ad-hoc additions, and until that happens this list should be assumed
106
+ * to have holes of this shape. A missing property is a false `unchanged`, so the
107
+ * gap is in the direction that matters.
108
+ */
109
+ const ALLOWED = new Set(STYLE_ALLOWLIST);
110
+ export function isAllowedProperty(property) {
111
+ return ALLOWED.has(property);
112
+ }
113
+ /**
114
+ * Custom properties are always admitted, whatever their name.
115
+ *
116
+ * They cannot be enumerated in advance — every project invents its own — and they
117
+ * are the mechanism design tokens travel through, which makes them the highest-
118
+ * value signal the system collects: a custom property resolves a *name* alongside
119
+ * a value, and that name is what lets one token edit collapse into one docket
120
+ * root with counted collateral rather than hundreds of separate colour diffs.
121
+ */
122
+ export function isCustomProperty(property) {
123
+ return property.startsWith('--');
124
+ }
125
+ /** Whether a property survives the allowlist projection (ADR-0003 step 4). */
126
+ export function admits(property) {
127
+ return isCustomProperty(property) || isAllowedProperty(property);
128
+ }
129
+ /**
130
+ * Attributes kept on a semantic node.
131
+ *
132
+ * An allowlist rather than a denylist. Frameworks emit an open-ended supply of
133
+ * bookkeeping attributes — `data-reactroot`, `data-styled`, `data-testid`,
134
+ * `data-v-7f3a`, Storybook's own markers — and a denylist would need a new entry
135
+ * for each one, silently churning baselines until someone noticed. Anything not
136
+ * named here is dropped.
137
+ *
138
+ * `class` is absent by design (ADR-0003): a class name records *how* a style was
139
+ * applied, and the snapshot already records *what* was applied, resolved.
140
+ * `id` is absent because it has been replaced by a structural alias.
141
+ * `aria-*` is absent because those are resolved into `role`/`name`/`state`,
142
+ * which is what actually reaches a user.
143
+ */
144
+ export const ATTRIBUTE_ALLOWLIST = [
145
+ 'type', 'value', 'checked', 'disabled', 'readonly', 'required', 'multiple',
146
+ 'placeholder', 'name', 'href', 'target', 'rel', 'src', 'alt', 'title',
147
+ 'for', 'form', 'list', 'headers', 'colspan', 'rowspan', 'scope',
148
+ 'lang', 'dir', 'hidden', 'open', 'selected', 'download',
149
+ 'min', 'max', 'step', 'pattern', 'maxlength', 'minlength', 'autocomplete',
150
+ 'role', 'tabindex', 'draggable', 'contenteditable',
151
+ 'width', 'height', 'loading', 'decoding', 'srcset', 'sizes', 'poster', 'controls',
152
+ ];
153
+ const ATTRIBUTES = new Set(ATTRIBUTE_ALLOWLIST);
154
+ export function admitsAttribute(name) {
155
+ return ATTRIBUTES.has(name);
156
+ }
157
+ /**
158
+ * Attributes whose values are id references, rewritten to structural aliases.
159
+ *
160
+ * Aliasing rather than masking is what preserves the *relationship* while
161
+ * deleting the volatile *value* (ADR-0003): a `useId` renumbering becomes a
162
+ * no-op, while breaking a `label ↔ input` association stays a real structural
163
+ * change. Masking both to a constant cannot tell those apart.
164
+ */
165
+ export const ID_REFERENCE_ATTRIBUTES = [
166
+ 'id', 'for', 'form', 'list', 'headers',
167
+ 'aria-labelledby', 'aria-describedby', 'aria-controls', 'aria-owns',
168
+ 'aria-activedescendant', 'aria-details', 'aria-errormessage', 'aria-flowto',
169
+ ];
170
+ /** Attributes holding a *space-separated list* of id references, not just one. */
171
+ export const ID_REFERENCE_LIST_ATTRIBUTES = [
172
+ 'headers', 'aria-labelledby', 'aria-describedby', 'aria-controls',
173
+ 'aria-owns', 'aria-flowto',
174
+ ];
175
+ //# sourceMappingURL=ruleset.js.map