@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.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- 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
|