@nextcommerce/campaigns-os 1.52.0 → 1.53.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 +48 -0
- package/agents/claude/CLAUDE.md +2 -0
- package/agents/codex/AGENTS.md +2 -0
- package/agents/copilot/copilot-instructions.md +2 -0
- package/agents/cursor/campaigns-os.mdc +2 -0
- package/compatibility.json +1 -1
- package/contracts/effects.v1.json +201 -12
- package/contracts/release-ledger.json +327 -0
- package/contracts/supported-surface.json +2 -2
- package/docs/brand-theme-bridge.md +2 -0
- package/docs/build-packet.md +56 -15
- package/docs/campaign-build-brief.md +48 -17
- package/docs/effects.md +8 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +2 -0
- package/docs/progress-snapshots.md +2 -4
- package/docs/qa-and-test-orders.md +2 -0
- package/docs/readback.md +19 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -3
- package/skills/next-campaigns-os/SKILL.md +10 -19
- package/skills/next-campaigns-os/references/session-intake.md +10 -6
- package/skills/next-campaigns-os-setup/SKILL.md +4 -3
- package/skills/next-campaigns-polish/SKILL.md +7 -5
- package/skills/next-campaigns-qa/SKILL.md +5 -3
- package/skills.json +10 -10
- package/src/brand-theme.mjs +21 -20
- package/src/brief-summary.mjs +275 -0
- package/src/build-brief.mjs +167 -15
- package/src/cli.mjs +160 -112
- package/src/contrast.mjs +649 -0
- package/src/design-source-publication.mjs +110 -3
- package/src/doctor/checks.mjs +129 -14
- package/src/doctor/next-step.mjs +15 -6
- package/src/input-currency.mjs +394 -0
- package/src/input-refresh.mjs +708 -0
- package/src/invocation.mjs +3 -1
- package/src/polish-browser.mjs +503 -201
- package/src/polish-deadline.mjs +7 -0
- package/src/polish-node.mjs +326 -46
- package/src/polish-readability.mjs +828 -0
- package/src/progress-node.mjs +15 -3
- package/src/qa-browser.mjs +131 -84
- package/src/qa-node.mjs +14 -3
- package/src/qc-accept.mjs +44 -8
- package/src/qc-check-registry.mjs +15 -7
- package/src/qc-results.mjs +326 -34
- package/src/readback.mjs +58 -10
- package/src/stage-ledger.mjs +235 -44
- package/src/stage-record.mjs +119 -26
- package/src/tooling-setup.mjs +1 -1
package/src/contrast.mjs
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
// The shared contrast helper: colour parsing and conversion, WCAG 2.x
|
|
2
|
+
// luminance and ratio, the large-text and requirement rules, and the
|
|
3
|
+
// measurement of one text element and one document. Pages receive it as
|
|
4
|
+
// source text (Function.prototype.toString), so the factory references
|
|
5
|
+
// nothing outside itself except standard built-ins, and reaches the DOM only
|
|
6
|
+
// through the el / win / doc arguments it is given. It only reads the page.
|
|
7
|
+
export function contrastToolkit() {
|
|
8
|
+
// Chromium's computed-colour serializations, exactly as Chromium writes
|
|
9
|
+
// them, and nothing else: rgb(r, g, b) and rgba(r, g, b, a) with ", "
|
|
10
|
+
// separators; color() in eight predefined spaces, lab, lch, oklab and oklch
|
|
11
|
+
// with three single-space-separated components and an optional " / alpha".
|
|
12
|
+
// Numbers carry no "+", no bare "." and no unit or percent.
|
|
13
|
+
const NUMBER = "-?\\d+(?:\\.\\d+)?(?:e[+-]\\d+)?";
|
|
14
|
+
const MODERN_ARG = `(${NUMBER}|none)`;
|
|
15
|
+
const LEGACY_RGB = new RegExp(`^rgb(a?)\\((${NUMBER}), (${NUMBER}), (${NUMBER})(?:, (${NUMBER}))?\\)$`);
|
|
16
|
+
const MODERN = new RegExp(`^(?:color\\((srgb|srgb-linear|display-p3|a98-rgb|prophoto-rgb|rec2020|xyz-d50|xyz-d65) |(lab|lch|oklab|oklch)\\()${MODERN_ARG} ${MODERN_ARG} ${MODERN_ARG}(?: \\/ ${MODERN_ARG})?\\)$`);
|
|
17
|
+
|
|
18
|
+
// CSS Color 4 conversion matrices and transfer functions.
|
|
19
|
+
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
20
|
+
[12831 / 3959, -329 / 214, -1974 / 3959],
|
|
21
|
+
[-851781 / 878810, 1648619 / 878810, 36519 / 878810],
|
|
22
|
+
[705 / 12673, -2585 / 12673, 705 / 667],
|
|
23
|
+
];
|
|
24
|
+
const LINEAR_P3_TO_XYZ_D65 = [
|
|
25
|
+
[608311 / 1250200, 189793 / 714400, 198249 / 1000160],
|
|
26
|
+
[35783 / 156275, 247089 / 357200, 198249 / 2500400],
|
|
27
|
+
[0, 32229 / 714400, 5220557 / 5000800],
|
|
28
|
+
];
|
|
29
|
+
const LINEAR_A98_TO_XYZ_D65 = [
|
|
30
|
+
[573536 / 994567, 263643 / 1420810, 187206 / 994567],
|
|
31
|
+
[591459 / 1989134, 6239551 / 9945670, 374412 / 4972835],
|
|
32
|
+
[53769 / 1989134, 351524 / 4972835, 4929758 / 4972835],
|
|
33
|
+
];
|
|
34
|
+
const LINEAR_PROPHOTO_TO_XYZ_D50 = [
|
|
35
|
+
[0.7977666449006423, 0.1351812974005331, 0.0313477341283922],
|
|
36
|
+
[0.2880748288194013, 0.7118352342418731, 0.0000899369387256],
|
|
37
|
+
[0, 0, 0.8251046025104602],
|
|
38
|
+
];
|
|
39
|
+
const LINEAR_REC2020_TO_XYZ_D65 = [
|
|
40
|
+
[63426534 / 99577255, 20160776 / 139408157, 47086771 / 278816314],
|
|
41
|
+
[26158966 / 99577255, 472592308 / 697040785, 8267143 / 139408157],
|
|
42
|
+
[0, 19567812 / 697040785, 295819943 / 278816314],
|
|
43
|
+
];
|
|
44
|
+
const XYZ_D50_TO_D65 = [
|
|
45
|
+
[0.955473421488075, -0.02309845494876471, 0.06325924320057072],
|
|
46
|
+
[-0.0283697093338637, 1.0099953980813041, 0.021041441191917323],
|
|
47
|
+
[0.012314014864481998, -0.020507649298898964, 1.330365926242124],
|
|
48
|
+
];
|
|
49
|
+
const OKLAB_TO_LMS = [
|
|
50
|
+
[1, 0.3963377773761749, 0.2158037573099136],
|
|
51
|
+
[1, -0.1055613458156586, -0.0638541728258133],
|
|
52
|
+
[1, -0.0894841775298119, -1.2914855480194092],
|
|
53
|
+
];
|
|
54
|
+
const LMS_TO_XYZ_D65 = [
|
|
55
|
+
[1.2268798758459243, -0.5578149944602171, 0.2813910456659647],
|
|
56
|
+
[-0.0405757452148008, 1.112286803280317, -0.0717110580655164],
|
|
57
|
+
[-0.0763729366746601, -0.4214933324022432, 1.5869240198367816],
|
|
58
|
+
];
|
|
59
|
+
const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
60
|
+
const REC2020_ALPHA = 1.09929682680944;
|
|
61
|
+
const REC2020_BETA = 0.018053968510807;
|
|
62
|
+
|
|
63
|
+
// Math.pow, **, exp, log, sin and cos are implementation-approximated and
|
|
64
|
+
// differ in the last bits between Chromium and Node, so a page measurement
|
|
65
|
+
// would not re-derive exactly elsewhere. Every value here comes from + - * /
|
|
66
|
+
// alone (exactly rounded in every engine), with fixed-length series.
|
|
67
|
+
const LN2 = 0.6931471805599453;
|
|
68
|
+
const ln = (x) => {
|
|
69
|
+
let exponent = 0;
|
|
70
|
+
let m = x;
|
|
71
|
+
while (m > Math.SQRT2) { m /= 2; exponent += 1; }
|
|
72
|
+
while (m < Math.SQRT1_2) { m *= 2; exponent -= 1; }
|
|
73
|
+
const s = (m - 1) / (m + 1);
|
|
74
|
+
let term = s;
|
|
75
|
+
let sum = 0;
|
|
76
|
+
for (let k = 1; k <= 41; k += 2) { sum += term / k; term *= s * s; }
|
|
77
|
+
return 2 * sum + exponent * LN2;
|
|
78
|
+
};
|
|
79
|
+
const exp = (y) => {
|
|
80
|
+
const k = Math.round(y / LN2);
|
|
81
|
+
const r = y - k * LN2;
|
|
82
|
+
let term = 1;
|
|
83
|
+
let sum = 1;
|
|
84
|
+
for (let n = 1; n <= 20; n += 1) { term *= r / n; sum += term; }
|
|
85
|
+
for (let i = 0; i < Math.abs(k); i += 1) sum = k < 0 ? sum / 2 : sum * 2;
|
|
86
|
+
return sum;
|
|
87
|
+
};
|
|
88
|
+
const pow = (x, y) => (x === 0 || !Number.isFinite(x) ? x : exp(y * ln(x)));
|
|
89
|
+
const cube = (x) => x * x * x;
|
|
90
|
+
// [cos, sin] of a hue in degrees.
|
|
91
|
+
const cosSin = (degrees) => {
|
|
92
|
+
const turn = ((degrees % 360) + 360) % 360;
|
|
93
|
+
const quadrant = Math.round(turn / 90);
|
|
94
|
+
const x = (turn - quadrant * 90) * (Math.PI / 180);
|
|
95
|
+
let c = 1;
|
|
96
|
+
let s = x;
|
|
97
|
+
let tc = 1;
|
|
98
|
+
let ts = x;
|
|
99
|
+
for (let n = 1; n <= 10; n += 1) {
|
|
100
|
+
tc *= -(x * x) / ((2 * n - 1) * (2 * n));
|
|
101
|
+
ts *= -(x * x) / ((2 * n) * (2 * n + 1));
|
|
102
|
+
c += tc;
|
|
103
|
+
s += ts;
|
|
104
|
+
}
|
|
105
|
+
return [[c, s], [-s, c], [-c, -s], [s, -c], [c, s]][quadrant];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const multiply = (matrix, vector) => matrix.map((row) => row[0] * vector[0] + row[1] * vector[1] + row[2] * vector[2]);
|
|
109
|
+
const signed = (c, magnitude) => (c < 0 ? -magnitude : magnitude);
|
|
110
|
+
const srgbToLinear = (c) => (Math.abs(c) <= 0.04045 ? c / 12.92 : signed(c, pow((Math.abs(c) + 0.055) / 1.055, 2.4)));
|
|
111
|
+
const linearToSrgb = (c) => (Math.abs(c) > 0.0031308 ? signed(c, 1.055 * pow(Math.abs(c), 1 / 2.4) - 0.055) : 12.92 * c);
|
|
112
|
+
const polarToCartesian = ([l, chroma, hue]) => {
|
|
113
|
+
const [cos, sin] = cosSin(hue);
|
|
114
|
+
return [l, chroma * cos, chroma * sin];
|
|
115
|
+
};
|
|
116
|
+
const labToXyzD50 = ([l, a, b]) => {
|
|
117
|
+
const kappa = 24389 / 27;
|
|
118
|
+
const epsilon = 216 / 24389;
|
|
119
|
+
const fy = (l + 16) / 116;
|
|
120
|
+
const fx = a / 500 + fy;
|
|
121
|
+
const fz = fy - b / 200;
|
|
122
|
+
const xyz = [cube(fx) > epsilon ? cube(fx) : (116 * fx - 16) / kappa, l > kappa * epsilon ? cube(fy) : l / kappa, cube(fz) > epsilon ? cube(fz) : (116 * fz - 16) / kappa];
|
|
123
|
+
return xyz.map((value, i) => value * D50_WHITE[i]);
|
|
124
|
+
};
|
|
125
|
+
const TO_XYZ_D65 = {
|
|
126
|
+
"display-p3": (c) => multiply(LINEAR_P3_TO_XYZ_D65, c.map(srgbToLinear)),
|
|
127
|
+
"a98-rgb": (c) => multiply(LINEAR_A98_TO_XYZ_D65, c.map((v) => signed(v, pow(Math.abs(v), 563 / 256)))),
|
|
128
|
+
"prophoto-rgb": (c) => multiply(XYZ_D50_TO_D65, multiply(LINEAR_PROPHOTO_TO_XYZ_D50, c.map((v) => (Math.abs(v) <= 16 / 512 ? v / 16 : signed(v, pow(Math.abs(v), 1.8)))))),
|
|
129
|
+
rec2020: (c) => multiply(LINEAR_REC2020_TO_XYZ_D65, c.map((v) => (Math.abs(v) < REC2020_BETA * 4.5 ? v / 4.5 : signed(v, pow((Math.abs(v) + REC2020_ALPHA - 1) / REC2020_ALPHA, 1 / 0.45))))),
|
|
130
|
+
"xyz-d50": (c) => multiply(XYZ_D50_TO_D65, c),
|
|
131
|
+
"xyz-d65": (c) => c,
|
|
132
|
+
lab: (c) => multiply(XYZ_D50_TO_D65, labToXyzD50(c)),
|
|
133
|
+
lch: (c) => multiply(XYZ_D50_TO_D65, labToXyzD50(polarToCartesian(c))),
|
|
134
|
+
oklab: (c) => multiply(LMS_TO_XYZ_D65, multiply(OKLAB_TO_LMS, c).map(cube)),
|
|
135
|
+
oklch: (c) => multiply(LMS_TO_XYZ_D65, multiply(OKLAB_TO_LMS, polarToCartesian(c)).map(cube)),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function parseComputedColor(str) {
|
|
139
|
+
const text = typeof str === "string" ? str : "";
|
|
140
|
+
const component = (part) => (part === undefined ? 1 : part === "none" ? 0 : Number(part));
|
|
141
|
+
// rgba() carries an alpha and rgb() does not.
|
|
142
|
+
const legacy = LEGACY_RGB.exec(text);
|
|
143
|
+
if (legacy && (legacy[1] === "a") === (legacy[5] !== undefined)) return { space: "rgb", coords: [Number(legacy[2]), Number(legacy[3]), Number(legacy[4])], alpha: component(legacy[5]) };
|
|
144
|
+
const modern = MODERN.exec(text);
|
|
145
|
+
if (modern) return { space: modern[1] || modern[2], coords: [component(modern[3]), component(modern[4]), component(modern[5])], alpha: component(modern[6]) };
|
|
146
|
+
return { unparseable: str };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toSrgb(color) {
|
|
150
|
+
const { space, coords, alpha } = color;
|
|
151
|
+
const encoded = space === "rgb" ? coords.map((c) => c / 255)
|
|
152
|
+
: space === "srgb" ? coords
|
|
153
|
+
: space === "srgb-linear" ? coords.map(linearToSrgb)
|
|
154
|
+
: multiply(XYZ_D65_TO_LINEAR_SRGB, TO_XYZ_D65[space](coords)).map(linearToSrgb);
|
|
155
|
+
const rgb = encoded.map((c) => Math.min(1, Math.max(0, c)));
|
|
156
|
+
return { rgb, alpha, gamut_clipped: rgb.some((c, i) => c !== encoded[i]) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function relativeLuminance([r, g, b]) {
|
|
160
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function contrastRatio(La, Lb) {
|
|
164
|
+
return (Math.max(La, Lb) + 0.05) / (Math.min(La, Lb) + 0.05);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Source-over in gamma-encoded sRGB, as Chromium paints.
|
|
168
|
+
function compositeOver(top, bottom) {
|
|
169
|
+
const alpha = top.alpha + bottom.alpha * (1 - top.alpha);
|
|
170
|
+
if (alpha === 0) return { rgb: [0, 0, 0], alpha: 0 };
|
|
171
|
+
return { rgb: top.rgb.map((c, i) => (c * top.alpha + bottom.rgb[i] * bottom.alpha * (1 - top.alpha)) / alpha), alpha };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isLargeText({ fontSizePx, fontWeight }) {
|
|
175
|
+
return fontSizePx >= 24 || (fontSizePx >= 18.66 && fontWeight >= 700);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function requiredRatio(isLarge) {
|
|
179
|
+
return isLarge ? 3 : 4.5;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function meetsRequirement(ratio, required) {
|
|
183
|
+
return ratio >= required;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function displayRatio(ratio) {
|
|
187
|
+
return Math.round(ratio * 100) / 100;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// bg_layers_raw runs from the element outwards and ends at the opaque
|
|
191
|
+
// layer (or the default canvas). Without an opaque bottom there is no
|
|
192
|
+
// painted background to compare against, so the colour fields are null.
|
|
193
|
+
function deriveElementMeasurement({ fg_raw, fill_raw, bg_layers_raw, font_size_px, font_weight }) {
|
|
194
|
+
const large = isLargeText({ fontSizePx: font_size_px, fontWeight: font_weight });
|
|
195
|
+
const sized = { size_class: large ? "large" : "normal", required: requiredRatio(large) };
|
|
196
|
+
const color = parseComputedColor(fg_raw);
|
|
197
|
+
const fill = parseComputedColor(fill_raw);
|
|
198
|
+
const layers = (Array.isArray(bg_layers_raw) ? bg_layers_raw : []).map(parseComputedColor);
|
|
199
|
+
if ([color, fill, ...layers].some((parsed) => "unparseable" in parsed)) {
|
|
200
|
+
return { fg_srgb: null, bg_srgb: null, gamut_clipped: null, ratio: null, ...sized, review_reason: "unparseable_color" };
|
|
201
|
+
}
|
|
202
|
+
const foreground = toSrgb(JSON.stringify(fill) !== JSON.stringify(color) ? fill : color);
|
|
203
|
+
const backgrounds = layers.map(toSrgb);
|
|
204
|
+
const gamut_clipped = [foreground, ...backgrounds].some((converted) => converted.gamut_clipped);
|
|
205
|
+
const background = backgrounds.reduceRight((under, layer) => compositeOver(layer, under), { rgb: [0, 0, 0], alpha: 0 });
|
|
206
|
+
if (background.alpha < 1) return { fg_srgb: null, bg_srgb: null, gamut_clipped, ratio: null, ...sized, review_reason: null };
|
|
207
|
+
const fg_srgb = compositeOver(foreground, background).rgb;
|
|
208
|
+
const ratio = contrastRatio(relativeLuminance(fg_srgb), relativeLuminance(background.rgb));
|
|
209
|
+
return { fg_srgb, bg_srgb: background.rgb, gamut_clipped, ratio, ...sized, review_reason: null };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Element measurement. Review reasons are indexed by trigger number; the
|
|
213
|
+
// lowest-numbered trigger that holds is the element's review_reason.
|
|
214
|
+
const REVIEW_REASONS = [null, "background_gradient", "background_image", "pseudo_element_background", "overlapping_layer", "opacity", "filter", "blend_mode", "text_fill_background", "unparseable_color", "canvas_unknown", "disabled_state_uncertain", "mask"];
|
|
215
|
+
const CONTROL_SELECTOR = "button, input, select, textarea, a[href], [role=\"button\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [data-next-action], [data-next-upsell-action]";
|
|
216
|
+
const DISABLED_SELECTOR = ":disabled, [aria-disabled=\"true\"]";
|
|
217
|
+
const LOADING_ATTRIBUTES = ["data-next-loading", "data-next-sdk-loading"];
|
|
218
|
+
const STATE_SELECTORS = [
|
|
219
|
+
["selected", "[data-next-selected=\"true\"], .next-selected, [aria-checked=\"true\"]"],
|
|
220
|
+
["active", ".next-active, .next-in-cart, [aria-pressed=\"true\"]"],
|
|
221
|
+
];
|
|
222
|
+
const ROLE_SELECTOR_ATTRIBUTES = [
|
|
223
|
+
"data-next-action", "data-next-upsell-action", "os-checkout-payment", "type",
|
|
224
|
+
"data-next-bundle-card", "data-next-selector-card", "data-next-package-id", "data-next-bundle-id",
|
|
225
|
+
"data-next-toggle-card", "data-next-bump", "data-next-package-toggle",
|
|
226
|
+
"data-next-display", "data-next-bundle-display", "data-next-checkout-field",
|
|
227
|
+
];
|
|
228
|
+
const REPLACED_ELEMENTS = new Set(["img", "video", "canvas", "svg", "picture", "iframe", "object", "embed"]);
|
|
229
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
230
|
+
// What an SVG drawing paints as layers of its own: shapes, images, <use>,
|
|
231
|
+
// foreign objects and nested drawings.
|
|
232
|
+
const SVG_LAYERS = new Set(["rect", "circle", "ellipse", "line", "polyline", "polygon", "path", "image", "use", "foreignObject", "svg"]);
|
|
233
|
+
const SDK_LOADER_SRC = /campaign-cart(?:@[^"']*)?\/dist\/loader\.js/i;
|
|
234
|
+
const DEFAULT_CANVAS = "rgb(255, 255, 255)";
|
|
235
|
+
const COLOUR_FIELDS = { fg_raw: null, fill_raw: null, bg_layers_raw: null, fg_srgb: null, bg_srgb: null, gamut_clipped: null, ratio: null };
|
|
236
|
+
|
|
237
|
+
const isSet = (value, none = "none") => value !== "" && value !== none;
|
|
238
|
+
const transparent = (value) => parseComputedColor(value).alpha === 0;
|
|
239
|
+
const paintsBackground = (style) => isSet(style.getPropertyValue("background-image")) || !transparent(style.getPropertyValue("background-color"));
|
|
240
|
+
const intersects = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
241
|
+
const isVisible = (el) => {
|
|
242
|
+
const rect = el.getBoundingClientRect();
|
|
243
|
+
return el.checkVisibility({ visibilityProperty: true }) && rect.width > 0 && rect.height > 0;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// The flat tree, as the page renders it: an element slotted into an open
|
|
247
|
+
// shadow root sits in its slot, and the top element of a shadow tree sits
|
|
248
|
+
// in the root's host. Closed roots hide their slots, so an element slotted
|
|
249
|
+
// into one steps to its light-DOM parent.
|
|
250
|
+
const shadowRootOf = (el) => (el.parentNode && el.parentNode.nodeType === 11 && el.parentNode.host ? el.parentNode : null);
|
|
251
|
+
const flatParent = (el) => el.assignedSlot || el.parentElement || (shadowRootOf(el) ? shadowRootOf(el).host : null);
|
|
252
|
+
const flatClosest = (el, selector) => {
|
|
253
|
+
for (let at = el; at && at.nodeType === 1; at = flatParent(at)) if (at.matches(selector)) return at;
|
|
254
|
+
return null;
|
|
255
|
+
};
|
|
256
|
+
const flatContains = (ancestor, el) => {
|
|
257
|
+
for (let at = el; at; at = flatParent(at)) if (at === ancestor) return true;
|
|
258
|
+
return false;
|
|
259
|
+
};
|
|
260
|
+
// Every element at or under `node` (an element, a document or a shadow
|
|
261
|
+
// root), each open shadow root inside it included: the node's own tree in
|
|
262
|
+
// document order, then each shadow tree in the order its host was reached.
|
|
263
|
+
const flatSubtree = (node) => {
|
|
264
|
+
const found = node.nodeType === 1 ? [node, ...node.querySelectorAll("*")] : Array.from(node.querySelectorAll("*"));
|
|
265
|
+
for (let at = 0; at < found.length; at += 1) {
|
|
266
|
+
if (found[at].shadowRoot) found.push(...found[at].shadowRoot.querySelectorAll("*"));
|
|
267
|
+
}
|
|
268
|
+
return found;
|
|
269
|
+
};
|
|
270
|
+
// Inside an SVG drawing, CSS backgrounds are painted only by the outermost
|
|
271
|
+
// <svg> and by a foreignObject; its other elements paint with fill and
|
|
272
|
+
// stroke.
|
|
273
|
+
const isSvg = (el) => el.namespaceURI === SVG_NS;
|
|
274
|
+
const paintsCssBackground = (el) => !isSvg(el) || el.localName === "foreignObject" || (el.localName === "svg" && !(flatParent(el) && isSvg(flatParent(el))));
|
|
275
|
+
// The outermost <svg> of the drawing an element sits in, or null.
|
|
276
|
+
const drawingOf = (el) => {
|
|
277
|
+
let at = el;
|
|
278
|
+
while (at && !isSvg(at)) at = flatParent(at);
|
|
279
|
+
let drawing = null;
|
|
280
|
+
for (; at && isSvg(at); at = flatParent(at)) if (at.localName === "svg") drawing = at;
|
|
281
|
+
return drawing;
|
|
282
|
+
};
|
|
283
|
+
// The frame element a window is shown in, when the document around it is
|
|
284
|
+
// readable (same-origin); otherwise null.
|
|
285
|
+
const frameOf = (view) => {
|
|
286
|
+
try {
|
|
287
|
+
return (view && view.frameElement) || null;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
// The next element out from `at` as the page paints it, with that
|
|
293
|
+
// element's window: its flat-tree parent, or, from the root of a
|
|
294
|
+
// same-origin frame's document, the frame element in the document around
|
|
295
|
+
// it.
|
|
296
|
+
const outward = (at, view) => {
|
|
297
|
+
const up = flatParent(at);
|
|
298
|
+
if (up) return [up, view];
|
|
299
|
+
const frame = at.ownerDocument && at === at.ownerDocument.documentElement ? frameOf(view) : null;
|
|
300
|
+
return frame ? [frame, frame.ownerDocument.defaultView] : [null, view];
|
|
301
|
+
};
|
|
302
|
+
// Where an element's own text renders: the flat-tree parents of its
|
|
303
|
+
// non-blank text nodes. That is the element itself, except for the host of
|
|
304
|
+
// an open shadow root, whose own text renders only through the slot that
|
|
305
|
+
// takes it in. Such text inherits that slot's style and is painted by the
|
|
306
|
+
// shadow tree around the slot; text no slot takes in, or that its slot
|
|
307
|
+
// does not show, is not rendered.
|
|
308
|
+
const textParents = (el, win) => {
|
|
309
|
+
const own = Array.from(el.childNodes).filter((node) => node.nodeType === 3 && /\S/.test(node.nodeValue));
|
|
310
|
+
if (!el.shadowRoot) return own.length ? [el] : [];
|
|
311
|
+
const range = el.ownerDocument.createRange();
|
|
312
|
+
const shown = own.filter((node) => {
|
|
313
|
+
if (!node.assignedSlot || win.getComputedStyle(node.assignedSlot).getPropertyValue("visibility") !== "visible") return false;
|
|
314
|
+
range.selectNodeContents(node);
|
|
315
|
+
return range.getClientRects().length > 0;
|
|
316
|
+
});
|
|
317
|
+
return [...new Set(shown.map((node) => node.assignedSlot))];
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Overlap candidates are read once per document, open shadow roots
|
|
321
|
+
// included.
|
|
322
|
+
const overlapCandidates = new WeakMap();
|
|
323
|
+
function overlapCandidatesOf(doc, win) {
|
|
324
|
+
if (!overlapCandidates.has(doc)) {
|
|
325
|
+
const body = doc.body;
|
|
326
|
+
const elements = body ? flatSubtree(body).slice(1) : [];
|
|
327
|
+
const candidates = elements.filter((node) => {
|
|
328
|
+
if (!isVisible(node)) return false;
|
|
329
|
+
if (REPLACED_ELEMENTS.has(node.localName)) return true;
|
|
330
|
+
const style = win.getComputedStyle(node);
|
|
331
|
+
return ["absolute", "fixed", "sticky"].includes(style.getPropertyValue("position")) && paintsBackground(style);
|
|
332
|
+
});
|
|
333
|
+
overlapCandidates.set(doc, candidates.map((node) => ({ node, rect: node.getBoundingClientRect() })));
|
|
334
|
+
}
|
|
335
|
+
return overlapCandidates.get(doc);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// The element's place in the page: up to 8 flat-tree steps, outwards
|
|
339
|
+
// through each slot, each shadow host and each same-origin frame. ">>>"
|
|
340
|
+
// marks a step into another tree: a slot to the element slotted into it, a
|
|
341
|
+
// host to the top of its shadow tree, a frame to its document. The
|
|
342
|
+
// :nth-of-type index counts the element's siblings in its own tree.
|
|
343
|
+
function selectorPath(el) {
|
|
344
|
+
let path = "";
|
|
345
|
+
let joiner = ">";
|
|
346
|
+
let steps = 0;
|
|
347
|
+
for (let at = el; at && at.nodeType === 1 && steps < 8; steps += 1) {
|
|
348
|
+
const tag = at.localName;
|
|
349
|
+
const attributes = ROLE_SELECTOR_ATTRIBUTES.filter((name) => at.hasAttribute(name)).map((name) => `[${name}=${JSON.stringify(at.getAttribute(name))}]`).join("");
|
|
350
|
+
const siblings = at.parentElement || shadowRootOf(at);
|
|
351
|
+
const nth = siblings && tag !== "body" ? `:nth-of-type(${Array.from(siblings.children).filter((sibling) => sibling.localName === tag).indexOf(at) + 1})` : "";
|
|
352
|
+
path = `${tag}${attributes}${nth}${path ? `${joiner}${path}` : ""}`;
|
|
353
|
+
const up = flatParent(at);
|
|
354
|
+
const view = at.ownerDocument && at === at.ownerDocument.documentElement ? at.ownerDocument.defaultView : null;
|
|
355
|
+
joiner = up && up === at.parentElement ? ">" : ">>>";
|
|
356
|
+
at = up || (view && view.frameElement) || null;
|
|
357
|
+
}
|
|
358
|
+
return path;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Expanded: inside the content an aria-expanded="true" control names
|
|
362
|
+
// through aria-controls, resolved in the control's own document or shadow
|
|
363
|
+
// root.
|
|
364
|
+
function stateOf(el) {
|
|
365
|
+
for (const [state, selector] of STATE_SELECTORS) if (flatClosest(el, selector)) return state;
|
|
366
|
+
if (flatClosest(el, "details[open]")) return "expanded";
|
|
367
|
+
for (let at = el; at; at = flatParent(at)) {
|
|
368
|
+
const root = at.id ? at.getRootNode() : null;
|
|
369
|
+
if (!root || root.getElementById(at.id) !== at) continue;
|
|
370
|
+
if (Array.from(root.querySelectorAll("[aria-expanded=\"true\"][aria-controls]")).some((control) => control.getAttribute("aria-controls").split(/\s+/).includes(at.id))) return "expanded";
|
|
371
|
+
}
|
|
372
|
+
return "default";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function isTextBearing(el, win) {
|
|
376
|
+
if (!isVisible(el)) return false;
|
|
377
|
+
if (el.localName === "input") return (el.type === "submit" || el.type === "button") && el.value !== "";
|
|
378
|
+
return textParents(el, win).length > 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Whether an element's ::first-line or ::first-letter changes the colour,
|
|
382
|
+
// text fill or opacity of its text, or paints a background of its own.
|
|
383
|
+
// Read once per element.
|
|
384
|
+
const firstParts = new WeakMap();
|
|
385
|
+
const restylesFirstPart = (at, view, own) => {
|
|
386
|
+
if (!firstParts.has(at)) {
|
|
387
|
+
firstParts.set(at, ["::first-line", "::first-letter"].some((pseudo) => {
|
|
388
|
+
const part = view.getComputedStyle(at, pseudo);
|
|
389
|
+
const differs = (name) => part.getPropertyValue(name) !== own.getPropertyValue(name);
|
|
390
|
+
return differs("color") || differs("-webkit-text-fill-color")
|
|
391
|
+
|| (paintsBackground(part) && (differs("background-color") || differs("background-image")))
|
|
392
|
+
|| (Number.parseFloat(part.getPropertyValue("opacity")) < 1 && differs("opacity"));
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
return firstParts.get(at);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// A drawing's visible layers (SVG_LAYERS), read once per drawing.
|
|
399
|
+
const drawingLayers = new WeakMap();
|
|
400
|
+
const drawingLayersOf = (drawing) => {
|
|
401
|
+
if (!drawingLayers.has(drawing)) {
|
|
402
|
+
const layers = flatSubtree(drawing).slice(1).filter((node) => isSvg(node) && SVG_LAYERS.has(node.localName) && isVisible(node));
|
|
403
|
+
drawingLayers.set(drawing, layers.map((node) => ({ node, rect: node.getBoundingClientRect() })));
|
|
404
|
+
}
|
|
405
|
+
return drawingLayers.get(drawing);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// The text's style, its control and state, and the paint walk start where
|
|
409
|
+
// its text renders (textParents). The element's identity, its rendered
|
|
410
|
+
// flag and its box are its own. A shadow host's text can render through
|
|
411
|
+
// more than one slot (manual slot assignment): each slot's text is read
|
|
412
|
+
// through that slot, and the first slot's reading is recorded. When another
|
|
413
|
+
// slot's text sits in another control or state, or reads other colours,
|
|
414
|
+
// font or backgrounds, the host's box holds text painted by a subtree
|
|
415
|
+
// outside the recorded slot's ancestry: review trigger 4, never one slot's
|
|
416
|
+
// measurement standing for all of it. Every slot's triggers count.
|
|
417
|
+
//
|
|
418
|
+
// Text in a same-origin frame's document is painted by the frame element
|
|
419
|
+
// and everything around it too: the paint walk continues from the
|
|
420
|
+
// document's root to the frame element in the document around it, paint
|
|
421
|
+
// effects up to the top root, backgrounds through a transparent frame
|
|
422
|
+
// document until an opaque layer, and overlap candidates in each
|
|
423
|
+
// document the text box is shown in. A frame document whose root
|
|
424
|
+
// color-scheme differs from its frame element's paints its own canvas,
|
|
425
|
+
// which is not read: trigger 10.
|
|
426
|
+
//
|
|
427
|
+
// SVG text is painted with its fill, not with CSS color: a fill that is a
|
|
428
|
+
// plain colour is the text fill (fill_raw); a fill that is not (a gradient
|
|
429
|
+
// or pattern, none, a context paint), a stroke or a paint order is trigger
|
|
430
|
+
// 9, and a fill opacity below 1 is trigger 5. A shape, image, foreign
|
|
431
|
+
// object or nested drawing of the same drawing whose box meets the text's
|
|
432
|
+
// is trigger 4.
|
|
433
|
+
//
|
|
434
|
+
// ::first-line and ::first-letter paint the first line or letter with
|
|
435
|
+
// their own style, inherited by the text of descendants on that line: on
|
|
436
|
+
// the text or on any element around it in its document, one whose colour,
|
|
437
|
+
// text fill or opacity differs from its element's, or that paints a
|
|
438
|
+
// background of its own, is trigger 3.
|
|
439
|
+
//
|
|
440
|
+
// A placeholder (measurePlaceholder) is read with its ::placeholder
|
|
441
|
+
// colour, text fill, font size and weight, over the field's backgrounds.
|
|
442
|
+
// A ::placeholder that paints a background of its own is trigger 3, and
|
|
443
|
+
// its opacity, filter, blend mode and mask count like the field's.
|
|
444
|
+
function measureText(el, win, placeholder) {
|
|
445
|
+
const origins = placeholder ? [el] : textParents(el, win);
|
|
446
|
+
const [origin = el] = origins;
|
|
447
|
+
const textStyleOf = (at) => (placeholder ? win.getComputedStyle(at, "::placeholder") : win.getComputedStyle(at));
|
|
448
|
+
const style = textStyleOf(origin);
|
|
449
|
+
const font_size_px = Number.parseFloat(style.getPropertyValue("font-size"));
|
|
450
|
+
const font_weight = Number(style.getPropertyValue("font-weight"));
|
|
451
|
+
const large = isLargeText({ fontSizePx: font_size_px, fontWeight: font_weight });
|
|
452
|
+
const control = flatClosest(origin, CONTROL_SELECTOR);
|
|
453
|
+
const state = stateOf(origin);
|
|
454
|
+
const mixed = origins.slice(1).some((other) => flatClosest(other, CONTROL_SELECTOR) !== control || stateOf(other) !== state);
|
|
455
|
+
const disabled = el.matches(DISABLED_SELECTOR) || (!mixed && Boolean(control && control.matches(DISABLED_SELECTOR)));
|
|
456
|
+
const control_loading = !mixed && Boolean(control) && LOADING_ATTRIBUTES.some((name) => control.hasAttribute(name) && control.getAttribute(name) !== "false");
|
|
457
|
+
const rendered = placeholder ? isVisible(el) && el.value === "" && /\S/.test(el.placeholder || "") : isTextBearing(el, win);
|
|
458
|
+
const head = { selector_path: selectorPath(el), state, disabled, rendered, font_size_px, font_weight, size_class: large ? "large" : "normal" };
|
|
459
|
+
if (disabled || control_loading) return { ...head, ...COLOUR_FIELDS, required: requiredRatio(large), review_reason: null, control_loading };
|
|
460
|
+
|
|
461
|
+
const triggers = new Set();
|
|
462
|
+
const paintEffects = (layer) => {
|
|
463
|
+
if (Number.parseFloat(layer.getPropertyValue("opacity")) < 1) triggers.add(5);
|
|
464
|
+
if (isSet(layer.getPropertyValue("filter")) || isSet(layer.getPropertyValue("backdrop-filter"))) triggers.add(6);
|
|
465
|
+
if (isSet(layer.getPropertyValue("mix-blend-mode"), "normal")) triggers.add(7);
|
|
466
|
+
if (isSet(layer.getPropertyValue("mask-image")) || isSet(layer.getPropertyValue("-webkit-mask-image"))) triggers.add(12);
|
|
467
|
+
};
|
|
468
|
+
const schemeOf = (view, at) => view.getComputedStyle(at).getPropertyValue("color-scheme");
|
|
469
|
+
// A scheme list that includes light is used as light (the page is read
|
|
470
|
+
// with a light colour-scheme preference); normal is light too.
|
|
471
|
+
const light = (scheme) => scheme === "normal" || scheme.trim().split(/\s+/).includes("light");
|
|
472
|
+
const sameScheme = (a, b) => (light(a) ? light(b) : a === b);
|
|
473
|
+
// One slot's (or the element's own) reading, adding the triggers on its
|
|
474
|
+
// way out.
|
|
475
|
+
const readThrough = (start) => {
|
|
476
|
+
const text = start === origin ? style : textStyleOf(start);
|
|
477
|
+
const fg_raw = text.getPropertyValue("color");
|
|
478
|
+
let fill_raw = text.getPropertyValue("-webkit-text-fill-color");
|
|
479
|
+
if (isSvg(start)) {
|
|
480
|
+
const fill = text.getPropertyValue("fill");
|
|
481
|
+
if ("unparseable" in parseComputedColor(fill)) triggers.add(9);
|
|
482
|
+
else fill_raw = fill;
|
|
483
|
+
if (isSet(text.getPropertyValue("stroke")) || isSet(text.getPropertyValue("paint-order"), "normal")) triggers.add(9);
|
|
484
|
+
if (Number.parseFloat(text.getPropertyValue("fill-opacity")) < 1) triggers.add(5);
|
|
485
|
+
}
|
|
486
|
+
if (placeholder) {
|
|
487
|
+
paintEffects(text);
|
|
488
|
+
if (paintsBackground(text)) triggers.add(3);
|
|
489
|
+
}
|
|
490
|
+
const fillTransparent = transparent(fill_raw);
|
|
491
|
+
const bg_layers_raw = [];
|
|
492
|
+
let opaque = false;
|
|
493
|
+
let unread = false;
|
|
494
|
+
let view = win;
|
|
495
|
+
let root = el.ownerDocument.documentElement;
|
|
496
|
+
for (let at = start; at && at.nodeType === 1;) {
|
|
497
|
+
const layer = at === start && !placeholder ? text : view.getComputedStyle(at);
|
|
498
|
+
paintEffects(layer);
|
|
499
|
+
if (view === win && !isSvg(at) && restylesFirstPart(at, view, layer)) triggers.add(3);
|
|
500
|
+
if (!opaque && !unread && paintsCssBackground(at)) {
|
|
501
|
+
const image = layer.getPropertyValue("background-image");
|
|
502
|
+
if (image.includes("-gradient(")) triggers.add(1);
|
|
503
|
+
else if (isSet(image)) triggers.add(2);
|
|
504
|
+
if (["::before", "::after"].some((pseudo) => {
|
|
505
|
+
const generated = view.getComputedStyle(at, pseudo);
|
|
506
|
+
return !["none", "normal"].includes(generated.getPropertyValue("content")) && paintsBackground(generated);
|
|
507
|
+
})) triggers.add(3);
|
|
508
|
+
if (fillTransparent && [layer.getPropertyValue("background-clip"), layer.getPropertyValue("-webkit-background-clip")].some((clip) => /\btext\b/.test(clip))) triggers.add(8);
|
|
509
|
+
const color = layer.getPropertyValue("background-color");
|
|
510
|
+
bg_layers_raw.push(color);
|
|
511
|
+
opaque = parseComputedColor(color).alpha >= 1;
|
|
512
|
+
}
|
|
513
|
+
const [next, nextView] = outward(at, view);
|
|
514
|
+
if (next && nextView !== view) {
|
|
515
|
+
if (!opaque && !unread && !sameScheme(schemeOf(view, root), schemeOf(nextView, next))) {
|
|
516
|
+
triggers.add(10);
|
|
517
|
+
unread = true;
|
|
518
|
+
}
|
|
519
|
+
root = next.ownerDocument.documentElement;
|
|
520
|
+
}
|
|
521
|
+
at = next;
|
|
522
|
+
view = nextView;
|
|
523
|
+
}
|
|
524
|
+
if (!opaque && !unread) {
|
|
525
|
+
if (light(schemeOf(view, root))) bg_layers_raw.push(DEFAULT_CANVAS);
|
|
526
|
+
else triggers.add(10);
|
|
527
|
+
}
|
|
528
|
+
return { fg_raw, fill_raw, bg_layers_raw, font_size_px: Number.parseFloat(text.getPropertyValue("font-size")), font_weight: Number(text.getPropertyValue("font-weight")) };
|
|
529
|
+
};
|
|
530
|
+
const { fg_raw, fill_raw, bg_layers_raw } = readThrough(origin);
|
|
531
|
+
const reading = JSON.stringify({ fg_raw, fill_raw, bg_layers_raw, font_size_px, font_weight });
|
|
532
|
+
if (mixed) triggers.add(4);
|
|
533
|
+
for (const other of origins.slice(1)) if (JSON.stringify(readThrough(other)) !== reading) triggers.add(4);
|
|
534
|
+
|
|
535
|
+
// Overlap in the element's own document, then in each document around
|
|
536
|
+
// its frames, with the text box moved into that document's viewport.
|
|
537
|
+
let box = el.getBoundingClientRect();
|
|
538
|
+
let inner = origin;
|
|
539
|
+
let view = win;
|
|
540
|
+
for (let doc = el.ownerDocument; doc;) {
|
|
541
|
+
if (overlapCandidatesOf(doc, view).some(({ node, rect }) => intersects(rect, box) && !flatContains(node, inner) && !flatContains(inner, node))) triggers.add(4);
|
|
542
|
+
const frame = frameOf(view);
|
|
543
|
+
if (!frame) break;
|
|
544
|
+
const outer = frame.ownerDocument.defaultView;
|
|
545
|
+
const frameBox = frame.getBoundingClientRect();
|
|
546
|
+
const frameStyle = outer.getComputedStyle(frame);
|
|
547
|
+
const dx = frameBox.left + frame.clientLeft + (Number.parseFloat(frameStyle.getPropertyValue("padding-left")) || 0);
|
|
548
|
+
const dy = frameBox.top + frame.clientTop + (Number.parseFloat(frameStyle.getPropertyValue("padding-top")) || 0);
|
|
549
|
+
box = { left: box.left + dx, right: box.right + dx, top: box.top + dy, bottom: box.bottom + dy };
|
|
550
|
+
inner = frame;
|
|
551
|
+
view = outer;
|
|
552
|
+
doc = frame.ownerDocument;
|
|
553
|
+
}
|
|
554
|
+
const drawing = drawingOf(origin);
|
|
555
|
+
if (drawing) {
|
|
556
|
+
const textBox = el.getBoundingClientRect();
|
|
557
|
+
if (drawingLayersOf(drawing).some(({ node, rect }) => intersects(rect, textBox) && !flatContains(node, origin) && !flatContains(origin, node))) triggers.add(4);
|
|
558
|
+
}
|
|
559
|
+
if (el.matches(".next-disabled") || Boolean(control && control.matches(".next-disabled"))) triggers.add(11);
|
|
560
|
+
|
|
561
|
+
const derived = deriveElementMeasurement({ fg_raw, fill_raw, bg_layers_raw, font_size_px, font_weight });
|
|
562
|
+
if (derived.review_reason) triggers.add(9);
|
|
563
|
+
return {
|
|
564
|
+
...head,
|
|
565
|
+
fg_raw,
|
|
566
|
+
fill_raw,
|
|
567
|
+
bg_layers_raw,
|
|
568
|
+
fg_srgb: derived.fg_srgb,
|
|
569
|
+
bg_srgb: derived.bg_srgb,
|
|
570
|
+
gamut_clipped: derived.gamut_clipped,
|
|
571
|
+
ratio: derived.ratio,
|
|
572
|
+
required: derived.required,
|
|
573
|
+
review_reason: triggers.size ? REVIEW_REASONS[Math.min(...triggers)] : null,
|
|
574
|
+
control_loading,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function measureTextElement(el, win) {
|
|
579
|
+
return measureText(el, win, false);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// The placeholder of a text field, measured like its text would be.
|
|
583
|
+
function measurePlaceholder(field, win) {
|
|
584
|
+
return measureText(field, win, true);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// First match: SDK readiness, then stylesheets, then fonts. The SDK and
|
|
588
|
+
// stylesheet checks read every tree of the document: its own elements and
|
|
589
|
+
// each open shadow root inside it. Chromium gives a stylesheet that failed
|
|
590
|
+
// to load an empty sheet, so a sheet is also incomplete when its resource
|
|
591
|
+
// entry reports an HTTP error status. Each @import, in a <style> or in a
|
|
592
|
+
// linked or imported sheet, is a stylesheet too: incomplete while its rule
|
|
593
|
+
// has no sheet. A sheet whose rules cannot be read (cross-origin) has
|
|
594
|
+
// loaded, and its imports are not seen.
|
|
595
|
+
function documentMeasurability(doc) {
|
|
596
|
+
const body = doc.body;
|
|
597
|
+
const trees = [doc, ...flatSubtree(doc).filter((node) => node.shadowRoot).map((node) => node.shadowRoot)];
|
|
598
|
+
const everywhere = (selector) => trees.flatMap((tree) => Array.from(tree.querySelectorAll(selector)));
|
|
599
|
+
const declared = Boolean(body && body.hasAttribute("data-next-sdk-loading"))
|
|
600
|
+
|| everywhere("script[src]").some((script) => SDK_LOADER_SRC.test(script.getAttribute("src")));
|
|
601
|
+
const ready = Boolean(body && body.getAttribute("data-next-sdk-loading") === "false") || doc.documentElement.classList.contains("next-display-ready");
|
|
602
|
+
if (declared && !ready) return { measurable: false, reason: "sdk_not_ready" };
|
|
603
|
+
const timing = doc.defaultView ? doc.defaultView.performance : null;
|
|
604
|
+
const failed = (url) => Boolean(timing) && Boolean(url) && timing.getEntriesByName(url).some((entry) => entry.responseStatus >= 400);
|
|
605
|
+
const seen = new Set();
|
|
606
|
+
const importsIncomplete = (sheet) => {
|
|
607
|
+
if (!sheet || seen.has(sheet)) return false;
|
|
608
|
+
seen.add(sheet);
|
|
609
|
+
let rules;
|
|
610
|
+
try {
|
|
611
|
+
rules = Array.from(sheet.cssRules || []);
|
|
612
|
+
} catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return rules.some((rule) => rule.type === 3 && (!rule.styleSheet || failed(rule.styleSheet.href) || importsIncomplete(rule.styleSheet)));
|
|
616
|
+
};
|
|
617
|
+
const linkIncomplete = (link) => !link.sheet || failed(link.href) || importsIncomplete(link.sheet);
|
|
618
|
+
if (everywhere("link[rel=stylesheet]").some(linkIncomplete) || everywhere("style").some((style) => importsIncomplete(style.sheet))) return { measurable: false, reason: "styles_incomplete" };
|
|
619
|
+
if (doc.fonts.status !== "loaded") return { measurable: false, reason: "fonts_pending" };
|
|
620
|
+
return { measurable: true };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
version: "contrast/v1",
|
|
625
|
+
parseComputedColor,
|
|
626
|
+
toSrgb,
|
|
627
|
+
relativeLuminance,
|
|
628
|
+
contrastRatio,
|
|
629
|
+
compositeOver,
|
|
630
|
+
isLargeText,
|
|
631
|
+
requiredRatio,
|
|
632
|
+
meetsRequirement,
|
|
633
|
+
displayRatio,
|
|
634
|
+
deriveElementMeasurement,
|
|
635
|
+
isTextBearing,
|
|
636
|
+
measureTextElement,
|
|
637
|
+
measurePlaceholder,
|
|
638
|
+
documentMeasurability,
|
|
639
|
+
// The flat-tree walk the measurement uses, for page scripts that read
|
|
640
|
+
// the same trees.
|
|
641
|
+
shadowRootOf,
|
|
642
|
+
flatParent,
|
|
643
|
+
flatClosest,
|
|
644
|
+
flatContains,
|
|
645
|
+
flatSubtree,
|
|
646
|
+
isVisible,
|
|
647
|
+
selectorPath,
|
|
648
|
+
};
|
|
649
|
+
}
|