domotion-svg 0.13.3 → 0.14.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/dist/animation/animator.js +32 -5
- package/dist/capture/emoji.d.ts +37 -0
- package/dist/capture/emoji.js +41 -21
- package/dist/capture/script/dotted-circle-detect.js +9 -2
- package/dist/capture/script/emoji-detect.js +51 -5
- package/dist/capture/script/index.js +321 -2
- package/dist/capture/script/utils.js +12 -6
- package/dist/capture/script/walker/form-controls.d.ts +9 -0
- package/dist/capture/script/walker/form-controls.js +28 -0
- package/dist/capture/script/walker/pseudo-inject.js +89 -3
- package/dist/capture/script/walker/text-segments.d.ts +1 -0
- package/dist/capture/script/walker/text-segments.js +76 -19
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +43 -1
- package/dist/cli/animate.d.ts +30 -0
- package/dist/cli/animate.js +86 -2
- package/dist/cli/capture.js +18 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +19 -1
- package/dist/cli/term.d.ts +16 -0
- package/dist/cli/term.js +192 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/render/borders.d.ts +46 -1
- package/dist/render/borders.js +78 -5
- package/dist/render/colors.js +154 -16
- package/dist/render/conic-raster.js +6 -0
- package/dist/render/device-chrome.d.ts +61 -0
- package/dist/render/device-chrome.js +187 -0
- package/dist/render/element-tree-to-svg.d.ts +15 -0
- package/dist/render/element-tree-to-svg.js +191 -33
- package/dist/render/embedded-font-builder.js +13 -0
- package/dist/render/form-controls.d.ts +28 -0
- package/dist/render/form-controls.js +321 -116
- package/dist/render/gradients.d.ts +6 -3
- package/dist/render/gradients.js +25 -20
- package/dist/render/harfbuzz-shaper.d.ts +67 -0
- package/dist/render/harfbuzz-shaper.js +151 -0
- package/dist/render/index.d.ts +1 -0
- package/dist/render/index.js +3 -0
- package/dist/render/text-to-path.d.ts +27 -0
- package/dist/render/text-to-path.js +602 -34
- package/dist/render/vertical-text.js +7 -4
- package/dist/scroll/composer.js +2 -0
- package/dist/scroll/executor.js +17 -2
- package/dist/scroll/pattern.d.ts +8 -1
- package/dist/scroll/pattern.js +30 -2
- package/dist/terminal/cast.d.ts +53 -0
- package/dist/terminal/cast.js +83 -0
- package/dist/terminal/emulator.d.ts +64 -0
- package/dist/terminal/emulator.js +122 -0
- package/dist/terminal/incremental.d.ts +77 -0
- package/dist/terminal/incremental.js +382 -0
- package/dist/terminal/index.d.ts +93 -0
- package/dist/terminal/index.js +142 -0
- package/dist/terminal/pty.d.ts +74 -0
- package/dist/terminal/pty.js +102 -0
- package/dist/terminal/render.d.ts +65 -0
- package/dist/terminal/render.js +156 -0
- package/dist/terminal/theme.d.ts +43 -0
- package/dist/terminal/theme.js +90 -0
- package/package.json +8 -1
- package/schemas/animate-config.schema.json +90 -0
|
@@ -273,6 +273,11 @@ export function generateAnimatedSvg(config) {
|
|
|
273
273
|
const enterStartPct = entersViaOverlap
|
|
274
274
|
? pct(timeOffset - prevTransDur, totalDuration)
|
|
275
275
|
: startPct;
|
|
276
|
+
// DM-1207: the last frame holds solid to 100% (no loop cross-dissolve)
|
|
277
|
+
// unless `loopFade` is set — same rule the crossfade/cut path applies via
|
|
278
|
+
// DM-1148 (see emitCrossfadeOrCutFrame). For the slide paths (push-left /
|
|
279
|
+
// scroll) this means: slide in, then hold (no slide-out / fade-out).
|
|
280
|
+
const holdLastFrame = i === frames.length - 1 && config.loopFade !== true;
|
|
276
281
|
if (transType === "push-left") {
|
|
277
282
|
// Push: slide in from right, slide out to left
|
|
278
283
|
frameGroups.push(` <g class="f f-${i}"><clipPath id="fc-${i}"><rect width="${width}" height="${height}" /></clipPath><g clip-path="url(#fc-${i})" class="fp fp-${i}">\n${frame.svgContent}\n </g></g>`);
|
|
@@ -282,7 +287,7 @@ export function generateAnimatedSvg(config) {
|
|
|
282
287
|
// Window is [enterStartPct .. transEndPct] (when the slide has fully
|
|
283
288
|
// exited the viewBox); 0.01% pad on each side keeps the snap inside the
|
|
284
289
|
// existing opacity:0 bookend.
|
|
285
|
-
keyframes.push(slideKeyframes(i, "X", width, entersViaPush, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec));
|
|
290
|
+
keyframes.push(slideKeyframes(i, "X", width, entersViaPush, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec, holdLastFrame));
|
|
286
291
|
}
|
|
287
292
|
else if (transType === "scroll") {
|
|
288
293
|
// DM-609: `scroll` now means real geometric scroll between two frames
|
|
@@ -294,7 +299,7 @@ export function generateAnimatedSvg(config) {
|
|
|
294
299
|
// `fd-${i}` display animation). (`entersViaScroll` is already computed in
|
|
295
300
|
// the outer scope above — same value, no need to redeclare/shadow it.)
|
|
296
301
|
frameGroups.push(` <g class="f f-${i}"><clipPath id="fc-${i}"><rect width="${width}" height="${height}" /></clipPath><g clip-path="url(#fc-${i})" class="fp fp-${i}">\n${frame.svgContent}\n </g></g>`);
|
|
297
|
-
keyframes.push(slideKeyframes(i, "Y", height, entersViaScroll, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec));
|
|
302
|
+
keyframes.push(slideKeyframes(i, "Y", height, entersViaScroll, enterStartPct, startPct, holdEndPct, transEndPct, enterStartPct, transEndPct, totalSec, holdLastFrame));
|
|
298
303
|
}
|
|
299
304
|
else if (transType === "magic-move" && frame.magicMove != null) {
|
|
300
305
|
// DM-898: magic-move. Frame i holds [start..holdEnd] then HARD-CUTS out;
|
|
@@ -686,17 +691,39 @@ function buildDisplayKeyframes(name, visibleStartPct, visibleEndPct) {
|
|
|
686
691
|
* exits to `-size`. 0.1% pads on each bookend keep the snap inside the
|
|
687
692
|
* opacity:0 frame. Emits the fp/fv/fd keyframes + the `.f-`/`.fp-` rules.
|
|
688
693
|
*/
|
|
689
|
-
function slideKeyframes(i, axis, size, entersSliding, enterStartPct, startPct, holdEndPct, transEndPct, visStart, visEnd, totalSec
|
|
694
|
+
function slideKeyframes(i, axis, size, entersSliding, enterStartPct, startPct, holdEndPct, transEndPct, visStart, visEnd, totalSec,
|
|
695
|
+
/** DM-1207: the last frame, when the loop must NOT cross-dissolve, slides in
|
|
696
|
+
* and then HOLDS solid (transform 0, opacity 1, visible) to 100% — no
|
|
697
|
+
* slide-out / fade-out — and the loop hard-cuts back to frame 0. Mirrors the
|
|
698
|
+
* crossfade/cut path's DM-1148 holdToEnd. Without it the slide-out keyframes
|
|
699
|
+
* ramp the last frame to opacity 0 across its whole hold, washing it out. */
|
|
700
|
+
holdToEnd = false) {
|
|
701
|
+
const enterBound = padBefore(parseFloat(enterStartPct), KEYFRAME_EPSILON.slide, 2);
|
|
702
|
+
if (holdToEnd) {
|
|
703
|
+
return `
|
|
704
|
+
@keyframes fp-${i} {
|
|
705
|
+
0%, ${enterBound}% { transform: translate${axis}(${entersSliding ? size : 0}px); }
|
|
706
|
+
${startPct} { transform: translate${axis}(0); }
|
|
707
|
+
100% { transform: translate${axis}(0); }
|
|
708
|
+
}
|
|
709
|
+
@keyframes fv-${i} {
|
|
710
|
+
0%, ${enterBound}% { opacity: 0; }
|
|
711
|
+
${enterStartPct} { opacity: 1; }
|
|
712
|
+
100% { opacity: 1; }
|
|
713
|
+
}${buildDisplayKeyframes(`fd-${i}`, visStart, "100")}
|
|
714
|
+
.f-${i} { animation: fv-${i} ${totalSec.toFixed(2)}s infinite, fd-${i} ${totalSec.toFixed(2)}s infinite step-end; }
|
|
715
|
+
.fp-${i} { animation: fp-${i} ${totalSec.toFixed(2)}s infinite; }`;
|
|
716
|
+
}
|
|
690
717
|
return `
|
|
691
718
|
@keyframes fp-${i} {
|
|
692
|
-
0%, ${
|
|
719
|
+
0%, ${enterBound}% { transform: translate${axis}(${entersSliding ? size : 0}px); }
|
|
693
720
|
${startPct} { transform: translate${axis}(0); }
|
|
694
721
|
${holdEndPct} { transform: translate${axis}(0); }
|
|
695
722
|
${transEndPct} { transform: translate${axis}(-${size}px); }
|
|
696
723
|
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { transform: translate${axis}(-${size}px); }
|
|
697
724
|
}
|
|
698
725
|
@keyframes fv-${i} {
|
|
699
|
-
0%, ${
|
|
726
|
+
0%, ${enterBound}% { opacity: 0; }
|
|
700
727
|
${enterStartPct} { opacity: 1; }
|
|
701
728
|
${transEndPct} { opacity: 1; }
|
|
702
729
|
${padAfter(parseFloat(transEndPct), KEYFRAME_EPSILON.slide, 2)}%, 100% { opacity: 0; }
|
package/dist/capture/emoji.d.ts
CHANGED
|
@@ -17,6 +17,43 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { Page } from "@playwright/test";
|
|
19
19
|
import type { CapturedElement } from "./types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Snap an Apple Color Emoji sbix bitmap to the SQUARE box Chrome actually
|
|
22
|
+
* paints it in.
|
|
23
|
+
*
|
|
24
|
+
* Chrome paints a color-emoji glyph as a square whose side equals the glyph
|
|
25
|
+
* ADVANCE — the captured `Range.getBoundingClientRect()` width, minus any
|
|
26
|
+
* letter-spacing Chrome appends to the right of the advance. Two facts make
|
|
27
|
+
* the advance (not the font size) the correct side:
|
|
28
|
+
* - At small font sizes Chrome enforces a minimum emoji advance that exceeds
|
|
29
|
+
* the font size — e.g. a 20px advance at font-size 16 (~1.25×). Sizing the
|
|
30
|
+
* overlay to the font size painted the emoji ~20% too small (DM-1198: "our
|
|
31
|
+
* emojis seem a lot smaller").
|
|
32
|
+
* - The sbix PNG is a full square bitmap (the emoji fills a square em), so
|
|
33
|
+
* drawing it into an advance × advance box reproduces both full-bleed emoji
|
|
34
|
+
* and ones with transparent margins (e.g. 📈) without distortion.
|
|
35
|
+
*
|
|
36
|
+
* Geometry:
|
|
37
|
+
* - Horizontal: the bitmap sits flush at the advance's left (`rect.x`); any
|
|
38
|
+
* letter-spacing pads to the RIGHT, so no horizontal shift is applied
|
|
39
|
+
* (DM-919 — the original DM-381 centering pass wrongly shifted it right
|
|
40
|
+
* whenever letter-spacing > 0).
|
|
41
|
+
* - Vertical: the square is centered in the captured rect's line box, which
|
|
42
|
+
* matches Chrome within ~1px across sizes (DM-438 — a 20×17 rect extends
|
|
43
|
+
* upward to a 20×20 square; DM-801 — a 56×63 rect with 8px letter-spacing
|
|
44
|
+
* snaps to 48×48).
|
|
45
|
+
*/
|
|
46
|
+
export declare function emojiSquareRect(rect: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}, letterSpacing: number): {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
20
57
|
/**
|
|
21
58
|
* For every text segment CAPTURE_SCRIPT flagged with a rasterRect (contains a
|
|
22
59
|
* color-bitmap glyph like U+2713 ✓ or an emoji), ask Playwright for a
|
package/dist/capture/emoji.js
CHANGED
|
@@ -97,6 +97,42 @@ function extractEmojiBitmap(codepoint, paintedWidthPx) {
|
|
|
97
97
|
_sbixCache.set(cacheKey, result);
|
|
98
98
|
return result;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Snap an Apple Color Emoji sbix bitmap to the SQUARE box Chrome actually
|
|
102
|
+
* paints it in.
|
|
103
|
+
*
|
|
104
|
+
* Chrome paints a color-emoji glyph as a square whose side equals the glyph
|
|
105
|
+
* ADVANCE — the captured `Range.getBoundingClientRect()` width, minus any
|
|
106
|
+
* letter-spacing Chrome appends to the right of the advance. Two facts make
|
|
107
|
+
* the advance (not the font size) the correct side:
|
|
108
|
+
* - At small font sizes Chrome enforces a minimum emoji advance that exceeds
|
|
109
|
+
* the font size — e.g. a 20px advance at font-size 16 (~1.25×). Sizing the
|
|
110
|
+
* overlay to the font size painted the emoji ~20% too small (DM-1198: "our
|
|
111
|
+
* emojis seem a lot smaller").
|
|
112
|
+
* - The sbix PNG is a full square bitmap (the emoji fills a square em), so
|
|
113
|
+
* drawing it into an advance × advance box reproduces both full-bleed emoji
|
|
114
|
+
* and ones with transparent margins (e.g. 📈) without distortion.
|
|
115
|
+
*
|
|
116
|
+
* Geometry:
|
|
117
|
+
* - Horizontal: the bitmap sits flush at the advance's left (`rect.x`); any
|
|
118
|
+
* letter-spacing pads to the RIGHT, so no horizontal shift is applied
|
|
119
|
+
* (DM-919 — the original DM-381 centering pass wrongly shifted it right
|
|
120
|
+
* whenever letter-spacing > 0).
|
|
121
|
+
* - Vertical: the square is centered in the captured rect's line box, which
|
|
122
|
+
* matches Chrome within ~1px across sizes (DM-438 — a 20×17 rect extends
|
|
123
|
+
* upward to a 20×20 square; DM-801 — a 56×63 rect with 8px letter-spacing
|
|
124
|
+
* snaps to 48×48).
|
|
125
|
+
*/
|
|
126
|
+
export function emojiSquareRect(rect, letterSpacing) {
|
|
127
|
+
const ls = Math.max(0, letterSpacing) || 0;
|
|
128
|
+
const side = Math.max(1, rect.width - ls);
|
|
129
|
+
return {
|
|
130
|
+
x: rect.x,
|
|
131
|
+
y: rect.y + (rect.height - side) / 2,
|
|
132
|
+
width: side,
|
|
133
|
+
height: side,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
100
136
|
/**
|
|
101
137
|
* For every text segment CAPTURE_SCRIPT flagged with a rasterRect (contains a
|
|
102
138
|
* color-bitmap glyph like U+2713 ✓ or an emoji), ask Playwright for a
|
|
@@ -178,28 +214,12 @@ export async function rasterizeBitmapGlyphs(page, tree, viewport) {
|
|
|
178
214
|
// isn't carried on the segment (only the SVG path needs it,
|
|
179
215
|
// not the existing screenshot path which already round-trips
|
|
180
216
|
// a rectangular PNG).
|
|
181
|
-
const elFs = parseFloat(el.styles.fontSize ?? "") || 0;
|
|
182
|
-
const fs = seg.fontSize ?? (elFs > 0 ? elFs : Math.max(g.rect.width, g.rect.height));
|
|
183
|
-
// DM-919: Chrome's per-emoji paint origin = advance start
|
|
184
|
-
// (rect.x) when there's NO letter-spacing — the bitmap
|
|
185
|
-
// sits flush at the left of the advance. When letter-
|
|
186
|
-
// spacing > 0, Chrome ADDS the letter-spacing to the
|
|
187
|
-
// advance, which captures as a wider rect — the bitmap
|
|
188
|
-
// is still at rect.x (the spacing pads to the right).
|
|
189
|
-
// The original DM-381 centering pass mis-handled this:
|
|
190
|
-
// it shifted the bitmap by `(rect.width - fs) / 2`,
|
|
191
|
-
// moving the emoji right whenever letter-spacing >0.
|
|
192
|
-
// Pull out the captured letter-spacing and subtract it
|
|
193
|
-
// from rect.width FIRST, then center the bitmap inside
|
|
194
|
-
// the REAL advance — handles both the centered emoji-
|
|
195
|
-
// alone case (where rect.w ≈ fs) and the letter-spaced
|
|
196
|
-
// case (where rect.w = fs + letter-spacing).
|
|
197
217
|
const ls = parseFloat(el.styles.letterSpacing ?? "") || 0;
|
|
198
|
-
const
|
|
199
|
-
g.rect.x
|
|
200
|
-
g.rect.y
|
|
201
|
-
g.rect.width =
|
|
202
|
-
g.rect.height =
|
|
218
|
+
const sq = emojiSquareRect(g.rect, ls);
|
|
219
|
+
g.rect.x = sq.x;
|
|
220
|
+
g.rect.y = sq.y;
|
|
221
|
+
g.rect.width = sq.width;
|
|
222
|
+
g.rect.height = sq.height;
|
|
203
223
|
continue;
|
|
204
224
|
}
|
|
205
225
|
}
|
|
@@ -57,13 +57,20 @@ export const createDottedCircleDetect = () => {
|
|
|
57
57
|
}
|
|
58
58
|
return { cnt, w: cnt > 0 ? (maxx - minx + 1) : 0 };
|
|
59
59
|
};
|
|
60
|
-
// Does Chrome auto-insert a U+25CC before this lone mark in
|
|
60
|
+
// Does Chrome auto-insert a U+25CC before this lone mark/cluster-letter in
|
|
61
|
+
// `font`? Probes category M (combining marks), category Lo (some Brahmic
|
|
62
|
+
// cluster-initial LETTERS — e.g. Soyombo U+11A84) AND category Lm (modifier
|
|
63
|
+
// LETTERS the Universal Shaping Engine also circles when orphaned — e.g. Kirat
|
|
64
|
+
// Rai U+16D6B/6C, length / vowel modifiers that paint "◌ □" when stranded).
|
|
65
|
+
// The ink heuristic below is the real gate (a normal letter renders WITHOUT a
|
|
66
|
+
// circle, so bare ≠ comb → false), so including Lo / Lm only widens what's
|
|
67
|
+
// probed, never forces a false positive.
|
|
61
68
|
const markGetsDottedCircle = (cp, ch, font) => {
|
|
62
69
|
if (cp < 0x0900)
|
|
63
70
|
return false;
|
|
64
71
|
if (font == null || font === '')
|
|
65
72
|
return false;
|
|
66
|
-
if (!/\p{M}/u.test(ch))
|
|
73
|
+
if (!/\p{M}|\p{Lo}|\p{Lm}/u.test(ch))
|
|
67
74
|
return false;
|
|
68
75
|
const key = cp + '|' + font;
|
|
69
76
|
const hit = _cache.get(key);
|
|
@@ -30,11 +30,20 @@ export const createEmojiDetect = () => {
|
|
|
30
30
|
// painting as a dropped/empty path-mode glyph: ✅ ✊ ✋. Unconditional like
|
|
31
31
|
// the ✨ ❌ ➡ family above — emoji presentation wins over the cascade.
|
|
32
32
|
0x2705, 0x270A, 0x270B,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
]);
|
|
34
|
+
// DM-1165: the Miscellaneous Symbols and Arrows (U+2B??) code points with
|
|
35
|
+
// default emoji presentation — ⬅⬆⬇ (2B05-07), ⬛⬜ (2B1B/1C), ⭐ (2B50),
|
|
36
|
+
// ⭕ (2B55). DM-728 added these to `rasterCps` unconditionally for the ⭐ in
|
|
37
|
+
// `20-deep-font-palette.html`, but Chrome's choice is actually CASCADE-
|
|
38
|
+
// DEPENDENT: when the element's font stack reaches a monochrome symbol/math
|
|
39
|
+
// font that covers them first, Chrome paints text, not color. Verified via
|
|
40
|
+
// `CSS.getPlatformFontsForNode` on the 2B00 fixture (cells lead with "Apple
|
|
41
|
+
// Symbols"): 2B05→Apple Symbols, 2B1B & 2B50→STIX Two Math — all MONOCHROME,
|
|
42
|
+
// so the unconditional raster was stamping blue emoji / a yellow star over
|
|
43
|
+
// Chrome's black arrows / hollow star. Probe per-element via `isColorGlyph`
|
|
44
|
+
// in `needsRaster` instead (color → raster, monochrome → path). A Set so the
|
|
45
|
+
// membership test in `needsRaster` is O(1).
|
|
46
|
+
const emojiPresentation2B = new Set([
|
|
38
47
|
0x2B05, 0x2B06, 0x2B07, 0x2B1B, 0x2B1C, 0x2B50, 0x2B55,
|
|
39
48
|
]);
|
|
40
49
|
// Codepoints in the U+2600-26FF Misc Symbols block with EmojiPresentation=Yes
|
|
@@ -200,6 +209,43 @@ export const createEmojiDetect = () => {
|
|
|
200
209
|
// cascade DOES reach the color font still rasters correctly.
|
|
201
210
|
if (cp >= 0x1F700 && cp <= 0x1F77F)
|
|
202
211
|
return isColorGlyph(cp, font);
|
|
212
|
+
// DM-1168: the two Emoji_Presentation=Yes code points in the Enclosed CJK
|
|
213
|
+
// Letters and Months block (U+3200-32FF) — ㊗ U+3297 CIRCLED IDEOGRAPH
|
|
214
|
+
// CONGRATULATION and ㊙ U+3299 CIRCLED IDEOGRAPH SECRET. Chrome paints them
|
|
215
|
+
// as Apple Color Emoji by default (the fixture cells show the red circled
|
|
216
|
+
// ideographs). But several macOS text fonts (Hiragino, Arial Unicode) also
|
|
217
|
+
// cover them with a MONOCHROME glyph, so a `lang=ja` cascade that reaches
|
|
218
|
+
// Hiragino first paints text, not color. Probe Chrome's actual per-element
|
|
219
|
+
// choice via the canvas (color → raster, monochrome → path), exactly like
|
|
220
|
+
// the DM-1025 emojiPresentation26 / DM-1125 alchemical branches.
|
|
221
|
+
if (cp === 0x3297 || cp === 0x3299)
|
|
222
|
+
return isColorGlyph(cp, font);
|
|
223
|
+
// DM-1173: 〽 U+303D PART ALTERNATION MARK (CJK Symbols and Punctuation,
|
|
224
|
+
// U+3000-303F). Emoji=Yes but text-default presentation, so Chrome paints
|
|
225
|
+
// the color glyph only when the cascade reaches Apple Color Emoji and no
|
|
226
|
+
// text font covers it first (many do — Hiragino, M+ 1p, Shippori Mincho).
|
|
227
|
+
// The fixture cell paints the orange color mark, so probe per-element font
|
|
228
|
+
// (color → raster, monochrome → path) like the branches above.
|
|
229
|
+
if (cp === 0x303D)
|
|
230
|
+
return isColorGlyph(cp, font);
|
|
231
|
+
// DM-1165: the U+2B?? emoji-presentation symbols (arrows ⬅⬆⬇, squares ⬛⬜,
|
|
232
|
+
// ⭐, ⭕). Cascade-dependent — Chrome paints text when the stack reaches a
|
|
233
|
+
// monochrome symbol/math font first (Apple Symbols / STIX Two Math), color
|
|
234
|
+
// otherwise. See `emojiPresentation2B` above.
|
|
235
|
+
if (emojiPresentation2B.has(cp))
|
|
236
|
+
return isColorGlyph(cp, font);
|
|
237
|
+
// DM-1167: the ONLY two codepoints in the Misc Symbols & Pictographs block
|
|
238
|
+
// (U+1F300-1F5FF) that a macOS text font also covers monochrome are
|
|
239
|
+
// 🌐 U+1F310 (GLOBE WITH MERIDIANS) and 🎤 U+1F3A4 (MICROPHONE) — Apple
|
|
240
|
+
// Symbols carries both. When the element's cascade leads with Apple Symbols
|
|
241
|
+
// (the html-test `.f1` cells do; CSS.getPlatformFontsForNode confirms),
|
|
242
|
+
// Chrome paints the MONOCHROME path glyph, not the color emoji. Probe
|
|
243
|
+
// Chrome's actual per-element choice (color → raster, monochrome → path)
|
|
244
|
+
// exactly like the DM-1125 Alchemical / DM-1168 ㊗㊙ branches, instead of
|
|
245
|
+
// unconditionally stamping the Apple Color Emoji bitmap over Chrome's text
|
|
246
|
+
// glyph. The common Apple-Color-Emoji-first cell still rasters (probe → true).
|
|
247
|
+
if (cp === 0x1F310 || cp === 0x1F3A4)
|
|
248
|
+
return isColorGlyph(cp, font);
|
|
203
249
|
// Main emoji blocks: Misc Symbols & Pictographs, Emoticons, Transport &
|
|
204
250
|
// Map, Alchemical, Supplemental Symbols & Pictographs, Pictographs
|
|
205
251
|
// Extended-A, Symbols & Pictographs Extended-B.
|
|
@@ -383,8 +383,31 @@ export const captureScript = (args) => {
|
|
|
383
383
|
if (tag === 'select' && (child.tagName.toLowerCase() === 'option' || child.tagName.toLowerCase() === 'optgroup'))
|
|
384
384
|
continue;
|
|
385
385
|
const c = capture(child);
|
|
386
|
-
if (c)
|
|
386
|
+
if (c) {
|
|
387
|
+
// DM-1177: splice a captured `scroll-marker-group` in as a real sibling
|
|
388
|
+
// of its scroller — before the scroller for `scroll-marker-group: before`,
|
|
389
|
+
// after it for `after`. As a sibling it sits OUTSIDE the scroller's
|
|
390
|
+
// overflow clip (Chrome paints the group outside the scrollport) and the
|
|
391
|
+
// normal paint-order pass places it correctly.
|
|
392
|
+
const _grp = c.scrollMarkerGroup;
|
|
393
|
+
const _before = c._scrollMarkerGroupBefore;
|
|
394
|
+
delete c.scrollMarkerGroup;
|
|
395
|
+
delete c._scrollMarkerGroupBefore;
|
|
396
|
+
// DM-1234: `::scroll-button(<dir>)` paging arrows captured as replica
|
|
397
|
+
// siblings; Chrome paints them OUTSIDE the scroller's overflow clip and
|
|
398
|
+
// ABOVE its content (the fixture's `z-index:1`), so emit them AFTER the
|
|
399
|
+
// scroller (and after the marker group) as last-painted siblings.
|
|
400
|
+
const _btns = c.scrollButtons;
|
|
401
|
+
delete c.scrollButtons;
|
|
402
|
+
if (_grp && _before)
|
|
403
|
+
children.push(_grp);
|
|
387
404
|
children.push(c);
|
|
405
|
+
if (_grp && !_before)
|
|
406
|
+
children.push(_grp);
|
|
407
|
+
if (_btns)
|
|
408
|
+
for (let _bi = 0; _bi < _btns.length; _bi++)
|
|
409
|
+
children.push(_btns[_bi]);
|
|
410
|
+
}
|
|
388
411
|
}
|
|
389
412
|
const _animId = el.dataset != null ? el.dataset.domotionAnim : undefined;
|
|
390
413
|
// DM-900: author-supplied magic-move pairing key (`data-magic-key`). When
|
|
@@ -809,8 +832,298 @@ export const captureScript = (args) => {
|
|
|
809
832
|
// .imageReplacement, and on the sprite-icon path .styles.backgroundImage /
|
|
810
833
|
// .text / .textSegments). See walker/replaced-elements.ts.
|
|
811
834
|
handleReplacedElement(el, cs, tag, rect, _captured, bordersOnlyCell);
|
|
835
|
+
// DM-1177: CSS `scroll-marker-group` (Chrome 135+). Capture the synthesized
|
|
836
|
+
// dot/pill marker-group box as a replica subtree (see _captureScrollMarkerGroup).
|
|
837
|
+
// Stash it (plus its before/after placement) on the node so the PARENT's
|
|
838
|
+
// children loop can splice it in as a real sibling — emitting it inside the
|
|
839
|
+
// scroller's own render fights the renderer's paint-order / overflow-clip
|
|
840
|
+
// machinery, so it must be a first-class tree node next to the scroller.
|
|
841
|
+
if (!bordersOnlyCell) {
|
|
842
|
+
var _smg = _captureScrollMarkerGroup(el, cs, rect);
|
|
843
|
+
if (_smg) {
|
|
844
|
+
_captured.scrollMarkerGroup = _smg.node;
|
|
845
|
+
_captured._scrollMarkerGroupBefore = _smg.before;
|
|
846
|
+
}
|
|
847
|
+
var _sbtns = _captureScrollButtons(el, cs, rect);
|
|
848
|
+
if (_sbtns)
|
|
849
|
+
_captured.scrollButtons = _sbtns;
|
|
850
|
+
}
|
|
812
851
|
return _captured;
|
|
813
852
|
};
|
|
853
|
+
// DM-1177: A scroll container with `scroll-marker-group: after | before`
|
|
854
|
+
// synthesizes an anonymous marker-group box, and each scrollable child whose
|
|
855
|
+
// `::scroll-marker` has non-`none` content becomes a dot/pill flex item inside
|
|
856
|
+
// it. The generated boxes have NO DOM node (un-measurable). To reproduce
|
|
857
|
+
// Chrome's paint faithfully without reimplementing the marker-group flex
|
|
858
|
+
// layout, build a hidden REPLICA from the resolved `::scroll-marker-group` /
|
|
859
|
+
// `::scroll-marker` computed styles — `:target-current` is already baked into
|
|
860
|
+
// the active marker's computed style by the engine — position it where Chrome
|
|
861
|
+
// paints the real group (after → below the scroller, before → above it, full
|
|
862
|
+
// scroller width), and walk it with the normal `capture()` so each marker is a
|
|
863
|
+
// styled box (with centered text for pill labels). Chrome lays out the replica
|
|
864
|
+
// identically to the real group, so the measured rects ARE Chrome's geometry.
|
|
865
|
+
function _captureScrollMarkerGroup(el, cs, rect) {
|
|
866
|
+
var smg = cs.scrollMarkerGroup != null && cs.scrollMarkerGroup !== ''
|
|
867
|
+
? cs.scrollMarkerGroup
|
|
868
|
+
: (cs.getPropertyValue ? cs.getPropertyValue('scroll-marker-group') : '');
|
|
869
|
+
if (!smg || smg.indexOf('none') === 0)
|
|
870
|
+
return undefined;
|
|
871
|
+
var position = smg.indexOf('before') === 0 ? 'before'
|
|
872
|
+
: (smg.indexOf('after') === 0 ? 'after' : null);
|
|
873
|
+
if (!position)
|
|
874
|
+
return undefined;
|
|
875
|
+
// One marker per child whose ::scroll-marker has real content.
|
|
876
|
+
var items = [];
|
|
877
|
+
for (var i = 0; i < el.children.length; i++) {
|
|
878
|
+
var child = el.children[i];
|
|
879
|
+
var mcs = window.getComputedStyle(child, '::scroll-marker');
|
|
880
|
+
var content = mcs.content;
|
|
881
|
+
if (!content || content === 'none' || content === 'normal')
|
|
882
|
+
continue;
|
|
883
|
+
items.push({ mcs: mcs, content: content });
|
|
884
|
+
}
|
|
885
|
+
if (items.length === 0)
|
|
886
|
+
return undefined;
|
|
887
|
+
var gcs = window.getComputedStyle(el, '::scroll-marker-group');
|
|
888
|
+
var doc = el.ownerDocument;
|
|
889
|
+
var container = doc.createElement('div');
|
|
890
|
+
var groupWidth = rect.width; // scroller border-box width
|
|
891
|
+
container.style.boxSizing = 'border-box';
|
|
892
|
+
container.style.position = 'absolute';
|
|
893
|
+
container.style.margin = '0';
|
|
894
|
+
container.style.display = gcs.display && gcs.display !== 'inline' ? gcs.display : 'flex';
|
|
895
|
+
container.style.justifyContent = gcs.justifyContent || 'center';
|
|
896
|
+
container.style.alignItems = gcs.alignItems && gcs.alignItems !== 'normal' ? gcs.alignItems : 'center';
|
|
897
|
+
if (gcs.gap && gcs.gap !== 'normal')
|
|
898
|
+
container.style.gap = gcs.gap;
|
|
899
|
+
container.style.padding = gcs.padding || '0';
|
|
900
|
+
container.style.background = gcs.backgroundColor || 'transparent';
|
|
901
|
+
container.style.borderRadius = gcs.borderRadius || '0';
|
|
902
|
+
container.style.width = groupWidth + 'px';
|
|
903
|
+
container.style.left = '-99999px';
|
|
904
|
+
container.style.top = '0px';
|
|
905
|
+
for (var j = 0; j < items.length; j++) {
|
|
906
|
+
var mc = items[j].mcs;
|
|
907
|
+
var m = doc.createElement('div');
|
|
908
|
+
var txt = items[j].content;
|
|
909
|
+
if (txt === '""' || txt === "''")
|
|
910
|
+
txt = '';
|
|
911
|
+
else if (txt.length >= 2 && ((txt[0] === '"' && txt[txt.length - 1] === '"') || (txt[0] === "'" && txt[txt.length - 1] === "'")))
|
|
912
|
+
txt = txt.slice(1, -1);
|
|
913
|
+
else
|
|
914
|
+
txt = '';
|
|
915
|
+
m.textContent = txt;
|
|
916
|
+
m.style.boxSizing = mc.boxSizing || 'content-box';
|
|
917
|
+
m.style.flex = '0 0 auto';
|
|
918
|
+
// Empty content ⇒ a sized dot (author set explicit width/height). Non-empty
|
|
919
|
+
// ⇒ a content-sized pill (width/height auto from text + padding).
|
|
920
|
+
if (txt === '') {
|
|
921
|
+
m.style.width = mc.width;
|
|
922
|
+
m.style.height = mc.height;
|
|
923
|
+
}
|
|
924
|
+
m.style.borderRadius = mc.borderRadius;
|
|
925
|
+
m.style.background = mc.backgroundColor;
|
|
926
|
+
m.style.color = mc.color;
|
|
927
|
+
// Horizontal margin spaces the markers along the row; vertical margin does
|
|
928
|
+
// NOT expand Chrome's generated group box (measured), so zero it out so the
|
|
929
|
+
// replica's measured height matches the real group.
|
|
930
|
+
m.style.marginTop = '0';
|
|
931
|
+
m.style.marginBottom = '0';
|
|
932
|
+
m.style.marginLeft = mc.marginLeft || '0';
|
|
933
|
+
m.style.marginRight = mc.marginRight || '0';
|
|
934
|
+
m.style.padding = mc.padding;
|
|
935
|
+
m.style.fontSize = mc.fontSize;
|
|
936
|
+
m.style.fontWeight = mc.fontWeight;
|
|
937
|
+
m.style.fontFamily = mc.fontFamily;
|
|
938
|
+
m.style.lineHeight = mc.lineHeight;
|
|
939
|
+
m.style.display = mc.display && mc.display !== 'inline' ? mc.display : 'inline-block';
|
|
940
|
+
if (mc.transform && mc.transform !== 'none')
|
|
941
|
+
m.style.transform = mc.transform;
|
|
942
|
+
if (mc.transformOrigin)
|
|
943
|
+
m.style.transformOrigin = mc.transformOrigin;
|
|
944
|
+
var bw = parseFloat(mc.borderTopWidth || '0') || 0;
|
|
945
|
+
if (bw > 0) {
|
|
946
|
+
m.style.borderStyle = mc.borderTopStyle;
|
|
947
|
+
m.style.borderWidth = mc.borderTopWidth;
|
|
948
|
+
m.style.borderColor = mc.borderTopColor;
|
|
949
|
+
}
|
|
950
|
+
container.appendChild(m);
|
|
951
|
+
}
|
|
952
|
+
doc.body.appendChild(container);
|
|
953
|
+
var gh = container.getBoundingClientRect().height;
|
|
954
|
+
// Chrome paints the marker group with its MARKERS flush against the scroller
|
|
955
|
+
// edge — the group's outer padding overlaps the scroller's own padding band
|
|
956
|
+
// rather than adding a separate gap (measured: the visible group below an
|
|
957
|
+
// `after` scroller is ~group-height-minus-top-padding tall, with the dots
|
|
958
|
+
// sitting right at the scroller's bottom edge). Reproduce that by sliding the
|
|
959
|
+
// replica so the marker row's content edge meets the scroller edge: for
|
|
960
|
+
// `after`, content-top (= containerTop + paddingTop) lands at rect.bottom;
|
|
961
|
+
// for `before`, content-bottom (= containerTop + gh - paddingBottom) lands at
|
|
962
|
+
// rect.top.
|
|
963
|
+
var padTop = parseFloat(gcs.paddingTop || '0') || 0;
|
|
964
|
+
var padBottom = parseFloat(gcs.paddingBottom || '0') || 0;
|
|
965
|
+
var targetTop = position === 'after' ? (rect.bottom - padTop) : (rect.top - gh + padBottom);
|
|
966
|
+
container.style.left = (rect.left + window.scrollX) + 'px';
|
|
967
|
+
container.style.top = (targetTop + window.scrollY) + 'px';
|
|
968
|
+
var node = capture(container);
|
|
969
|
+
doc.body.removeChild(container);
|
|
970
|
+
if (!node)
|
|
971
|
+
return undefined;
|
|
972
|
+
return { node: node, before: position === 'before' };
|
|
973
|
+
}
|
|
974
|
+
// DM-1234: CSS `::scroll-button(<dir>)` paging arrows (Chrome 135+). Like the
|
|
975
|
+
// marker-group these are generated boxes with NO DOM node, and — crucially —
|
|
976
|
+
// Chrome lays them out against the INITIAL CONTAINING BLOCK (the viewport),
|
|
977
|
+
// NOT the scroller's `position:relative` ancestor: `top:50%` resolves to 50%
|
|
978
|
+
// of the viewport height and `left`/`right` to insets from the viewport edges
|
|
979
|
+
// (verified by probe-and-match against Chrome's painted output across two
|
|
980
|
+
// viewport heights — the button center tracked 50%·viewportHeight while the
|
|
981
|
+
// scroller stayed put). `getComputedStyle(el, '::scroll-button(left)')` can't
|
|
982
|
+
// disambiguate the parameterized pseudo — it returns ONE merged style (the
|
|
983
|
+
// box props are shared, but `content` is the cascade-last value and both
|
|
984
|
+
// insets are reported) — so the per-side `content` + the `:disabled`
|
|
985
|
+
// declarations are read from the author stylesheet (CSSOM). Geometry is then
|
|
986
|
+
// resolved the same trick the marker-group uses: an absolutely-positioned
|
|
987
|
+
// replica appended to <body> ALSO takes the ICB as its containing block, so
|
|
988
|
+
// `top:50%`/`left`/`right`/`transform` land exactly where Chrome paints the
|
|
989
|
+
// real button — capture() measures that, so the rect IS Chrome's geometry.
|
|
990
|
+
// Enabled/disabled comes from the captured scroll offset vs the scroll range.
|
|
991
|
+
function _scrollButtonAuthorRules(el) {
|
|
992
|
+
// Per-direction author declarations + the merged `:disabled` declarations,
|
|
993
|
+
// gathered from every stylesheet rule whose `::scroll-button(<dir>)`
|
|
994
|
+
// selector matches `el`. `*` (universal direction) is folded in as a base.
|
|
995
|
+
var sides = {};
|
|
996
|
+
var disabled = {};
|
|
997
|
+
var star = {};
|
|
998
|
+
var sheets = el.ownerDocument.styleSheets;
|
|
999
|
+
for (var s = 0; s < sheets.length; s++) {
|
|
1000
|
+
var rules;
|
|
1001
|
+
try {
|
|
1002
|
+
rules = sheets[s].cssRules;
|
|
1003
|
+
}
|
|
1004
|
+
catch (e) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (!rules)
|
|
1008
|
+
continue;
|
|
1009
|
+
for (var r = 0; r < rules.length; r++) {
|
|
1010
|
+
var rule = rules[r];
|
|
1011
|
+
var sel = rule.selectorText;
|
|
1012
|
+
if (!sel)
|
|
1013
|
+
continue;
|
|
1014
|
+
var at = sel.indexOf('::scroll-button(');
|
|
1015
|
+
if (at < 0)
|
|
1016
|
+
continue;
|
|
1017
|
+
var close = sel.indexOf(')', at);
|
|
1018
|
+
if (close < 0)
|
|
1019
|
+
continue;
|
|
1020
|
+
var dir = sel.slice(at + 16, close).trim();
|
|
1021
|
+
var base = sel.slice(0, at).trim();
|
|
1022
|
+
var matches = false;
|
|
1023
|
+
try {
|
|
1024
|
+
matches = base === '' || el.matches(base);
|
|
1025
|
+
}
|
|
1026
|
+
catch (e) {
|
|
1027
|
+
matches = false;
|
|
1028
|
+
}
|
|
1029
|
+
if (!matches)
|
|
1030
|
+
continue;
|
|
1031
|
+
var isDisabled = sel.slice(close + 1).indexOf(':disabled') >= 0;
|
|
1032
|
+
var bucket = isDisabled ? disabled : (dir === '*' ? star : (sides[dir] || (sides[dir] = {})));
|
|
1033
|
+
var decl = rule.style;
|
|
1034
|
+
for (var d = 0; d < decl.length; d++)
|
|
1035
|
+
bucket[decl[d]] = decl.getPropertyValue(decl[d]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// Fold the universal `*` declarations in as a lower-priority base per side.
|
|
1039
|
+
for (var k in sides) {
|
|
1040
|
+
if (!Object.prototype.hasOwnProperty.call(sides, k))
|
|
1041
|
+
continue;
|
|
1042
|
+
var merged = {};
|
|
1043
|
+
for (var sp in star)
|
|
1044
|
+
if (Object.prototype.hasOwnProperty.call(star, sp))
|
|
1045
|
+
merged[sp] = star[sp];
|
|
1046
|
+
for (var op in sides[k])
|
|
1047
|
+
if (Object.prototype.hasOwnProperty.call(sides[k], op))
|
|
1048
|
+
merged[op] = sides[k][op];
|
|
1049
|
+
sides[k] = merged;
|
|
1050
|
+
}
|
|
1051
|
+
return { sides: sides, disabled: disabled };
|
|
1052
|
+
}
|
|
1053
|
+
function _captureScrollButtons(el, cs, rect) {
|
|
1054
|
+
// Cheap gate: only elements that actually generate a scroll-button get the
|
|
1055
|
+
// CSSOM scan. A non-`none` `content` on the merged pseudo means buttons exist.
|
|
1056
|
+
var probe = window.getComputedStyle(el, '::scroll-button(left)').content;
|
|
1057
|
+
if (!probe || probe === 'none' || probe === 'normal') {
|
|
1058
|
+
probe = window.getComputedStyle(el, '::scroll-button(right)').content;
|
|
1059
|
+
if (!probe || probe === 'none' || probe === 'normal')
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
var rules = _scrollButtonAuthorRules(el);
|
|
1063
|
+
var doc = el.ownerDocument;
|
|
1064
|
+
var maxX = el.scrollWidth - el.clientWidth;
|
|
1065
|
+
var maxY = el.scrollHeight - el.clientHeight;
|
|
1066
|
+
var nodes = [];
|
|
1067
|
+
for (var dir in rules.sides) {
|
|
1068
|
+
if (!Object.prototype.hasOwnProperty.call(rules.sides, dir))
|
|
1069
|
+
continue;
|
|
1070
|
+
var decls = rules.sides[dir];
|
|
1071
|
+
var isDisabled = false;
|
|
1072
|
+
if (dir === 'left' || dir === 'inline-start')
|
|
1073
|
+
isDisabled = el.scrollLeft <= 0;
|
|
1074
|
+
else if (dir === 'right' || dir === 'inline-end')
|
|
1075
|
+
isDisabled = el.scrollLeft >= maxX - 1;
|
|
1076
|
+
else if (dir === 'up' || dir === 'block-start')
|
|
1077
|
+
isDisabled = el.scrollTop <= 0;
|
|
1078
|
+
else if (dir === 'down' || dir === 'block-end')
|
|
1079
|
+
isDisabled = el.scrollTop >= maxY - 1;
|
|
1080
|
+
var btn = doc.createElement('div');
|
|
1081
|
+
var content = '';
|
|
1082
|
+
for (var p in decls) {
|
|
1083
|
+
if (!Object.prototype.hasOwnProperty.call(decls, p))
|
|
1084
|
+
continue;
|
|
1085
|
+
if (p === 'content') {
|
|
1086
|
+
content = decls[p];
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
btn.style.setProperty(p, decls[p]);
|
|
1091
|
+
}
|
|
1092
|
+
catch (e) { /* unsupported prop */ }
|
|
1093
|
+
}
|
|
1094
|
+
if (isDisabled) {
|
|
1095
|
+
for (var dp in rules.disabled) {
|
|
1096
|
+
if (!Object.prototype.hasOwnProperty.call(rules.disabled, dp))
|
|
1097
|
+
continue;
|
|
1098
|
+
try {
|
|
1099
|
+
btn.style.setProperty(dp, rules.disabled[dp]);
|
|
1100
|
+
}
|
|
1101
|
+
catch (e) { /* unsupported */ }
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// Match the real button's containing block (ICB) by living on <body> as an
|
|
1105
|
+
// absolutely-positioned box; the author's left/right/top/transform then
|
|
1106
|
+
// resolve against the viewport exactly as Chrome resolves them.
|
|
1107
|
+
if (!btn.style.position || btn.style.position === 'static')
|
|
1108
|
+
btn.style.position = 'absolute';
|
|
1109
|
+
btn.style.margin = '0';
|
|
1110
|
+
// Only a quoted string `content` becomes a text glyph. DM-1248: a `url()` /
|
|
1111
|
+
// counter() / image content value is NOT text — set no glyph rather than
|
|
1112
|
+
// rendering the literal CSS string (e.g. `url("x.png")`) as garbage text.
|
|
1113
|
+
// (Faithful image-content rendering is a tracked TODO in DM-1248.)
|
|
1114
|
+
var txt = '';
|
|
1115
|
+
if (content.length >= 2 && ((content[0] === '"' && content[content.length - 1] === '"') || (content[0] === "'" && content[content.length - 1] === "'"))) {
|
|
1116
|
+
txt = content.slice(1, -1);
|
|
1117
|
+
}
|
|
1118
|
+
btn.textContent = txt;
|
|
1119
|
+
doc.body.appendChild(btn);
|
|
1120
|
+
var node = capture(btn);
|
|
1121
|
+
doc.body.removeChild(btn);
|
|
1122
|
+
if (node)
|
|
1123
|
+
nodes.push(node);
|
|
1124
|
+
}
|
|
1125
|
+
return nodes.length ? nodes : undefined;
|
|
1126
|
+
}
|
|
814
1127
|
const root = document.querySelector(sel);
|
|
815
1128
|
if (!root)
|
|
816
1129
|
return { tree: [], warnings: [] };
|
|
@@ -1145,7 +1458,13 @@ export const captureScript = (args) => {
|
|
|
1145
1458
|
try {
|
|
1146
1459
|
var _isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
1147
1460
|
result[0].styles.rootColorScheme = _isDark ? 'dark' : 'light';
|
|
1148
|
-
|
|
1461
|
+
var _docCs = window.getComputedStyle(document.documentElement);
|
|
1462
|
+
result[0].styles.rootBgComputed = _docCs.backgroundColor;
|
|
1463
|
+
// DM-1244: <html>'s overflow decides whether <body>'s overflow propagates
|
|
1464
|
+
// to the viewport (it only does when <html> is `overflow: visible`). The
|
|
1465
|
+
// renderer needs it to know whether to apply <body>'s own overflow clip.
|
|
1466
|
+
result[0].styles.rootOverflowX = _docCs.overflowX;
|
|
1467
|
+
result[0].styles.rootOverflowY = _docCs.overflowY;
|
|
1149
1468
|
}
|
|
1150
1469
|
catch (_e) { /* no-op — never block capture on this */ }
|
|
1151
1470
|
}
|