domotion-svg 0.23.0 → 0.24.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/capture/backdrop-isolation.d.ts +14 -0
- package/dist/capture/backdrop-isolation.js +49 -0
- package/dist/capture/emoji.js +115 -1
- package/dist/capture/script/emoji-detect.js +22 -1
- package/dist/capture/script/index.js +36 -2
- package/dist/capture/script/walker/form-controls.d.ts +1 -19
- package/dist/capture/script/walker/form-controls.js +34 -3
- package/dist/capture/script/walker/fragmentation.js +60 -0
- package/dist/capture/script/walker/lists-counters.d.ts +3 -1
- package/dist/capture/script/walker/lists-counters.js +2 -1
- package/dist/capture/script/walker/pseudo-content.d.ts +2 -0
- package/dist/capture/script/walker/pseudo-content.js +53 -35
- package/dist/capture/script/walker/pseudo-inject.js +4 -2
- package/dist/capture/script/walker/text-segments.d.ts +2 -0
- package/dist/capture/script/walker/text-segments.js +2 -0
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +46 -3
- package/dist/render/element-tree-to-svg.js +73 -55
- package/dist/render/font-resolution.js +7 -1
- package/dist/render/form-controls.js +34 -10
- package/dist/render/gradient-defs.js +113 -32
- package/dist/render/gradients.js +35 -27
- package/dist/render/harfbuzz-shaper.js +4 -4
- package/dist/render/helper-acquire.js +1 -1
- package/dist/render/list-marker-geometry.d.ts +24 -0
- package/dist/render/list-marker-geometry.js +57 -0
- package/dist/render/script-segmentation.js +19 -9
- package/dist/render/text-to-path.d.ts +5 -3
- package/dist/render/text-to-path.js +113 -40
- package/dist/render/text.js +2 -0
- package/dist/render/vertical-text.js +17 -20
- package/package.json +2 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SnapshotNode {
|
|
2
|
+
backendNodeId: number;
|
|
3
|
+
parentIndex: number;
|
|
4
|
+
attributes: string[];
|
|
5
|
+
bounds?: [number, number, number, number];
|
|
6
|
+
paintOrder?: number;
|
|
7
|
+
layoutOrder?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface IsolationPlan {
|
|
10
|
+
targetBackendNodeId: number;
|
|
11
|
+
hideBackendNodeIds: number[];
|
|
12
|
+
}
|
|
13
|
+
/** Pure CDP snapshot planner; DOM mutation and restoration stay in emoji.ts. */
|
|
14
|
+
export declare function planBackdropIsolation(nodes: SnapshotNode[], token: string): IsolationPlan | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function overlaps(a, b) {
|
|
2
|
+
return a[0] < b[0] + b[2] && a[0] + a[2] > b[0]
|
|
3
|
+
&& a[1] < b[1] + b[3] && a[1] + a[3] > b[1];
|
|
4
|
+
}
|
|
5
|
+
function hasToken(node, token) {
|
|
6
|
+
for (let i = 0; i + 1 < node.attributes.length; i += 2) {
|
|
7
|
+
if (node.attributes[i] === "data-domotion-backdrop-raster" && node.attributes[i + 1] === token)
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
function isAncestor(nodes, ancestor, child) {
|
|
13
|
+
for (let i = child; i >= 0; i = nodes[i]?.parentIndex ?? -1)
|
|
14
|
+
if (i === ancestor)
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
/** Pure CDP snapshot planner; DOM mutation and restoration stay in emoji.ts. */
|
|
19
|
+
export function planBackdropIsolation(nodes, token) {
|
|
20
|
+
const target = nodes.findIndex((node) => hasToken(node, token));
|
|
21
|
+
if (target < 0)
|
|
22
|
+
return null;
|
|
23
|
+
const targetNode = nodes[target];
|
|
24
|
+
if (targetNode.bounds == null || targetNode.paintOrder == null)
|
|
25
|
+
return null;
|
|
26
|
+
const hide = [];
|
|
27
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
28
|
+
const node = nodes[i];
|
|
29
|
+
if (i === target || node.bounds == null || node.paintOrder == null)
|
|
30
|
+
continue;
|
|
31
|
+
const paintsLater = node.paintOrder > targetNode.paintOrder
|
|
32
|
+
|| (node.paintOrder === targetNode.paintOrder
|
|
33
|
+
&& node.layoutOrder != null && targetNode.layoutOrder != null
|
|
34
|
+
&& node.layoutOrder > targetNode.layoutOrder);
|
|
35
|
+
if (!paintsLater || !overlaps(node.bounds, targetNode.bounds))
|
|
36
|
+
continue;
|
|
37
|
+
if (isAncestor(nodes, i, target) || isAncestor(nodes, target, i))
|
|
38
|
+
continue;
|
|
39
|
+
// If an already-selected later ancestor hides this node, do not resolve and
|
|
40
|
+
// mutate the descendant too. Snapshot order is parent-before-child.
|
|
41
|
+
if (hide.some((id) => {
|
|
42
|
+
const ancestor = nodes.findIndex((n) => n.backendNodeId === id);
|
|
43
|
+
return ancestor >= 0 && isAncestor(nodes, ancestor, i);
|
|
44
|
+
}))
|
|
45
|
+
continue;
|
|
46
|
+
hide.push(node.backendNodeId);
|
|
47
|
+
}
|
|
48
|
+
return { targetBackendNodeId: targetNode.backendNodeId, hideBackendNodeIds: hide };
|
|
49
|
+
}
|
package/dist/capture/emoji.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as fontkit from "fontkit";
|
|
|
20
20
|
import sharp from "sharp";
|
|
21
21
|
import { clipRectForScreenshot } from "./clip-rect.js";
|
|
22
22
|
import { forEachElement } from "../tree-ops/for-each-element.js";
|
|
23
|
+
import { planBackdropIsolation } from "./backdrop-isolation.js";
|
|
23
24
|
const APPLE_COLOR_EMOJI_PATH = "/System/Library/Fonts/Apple Color Emoji.ttc";
|
|
24
25
|
let _aceFont = null;
|
|
25
26
|
let _aceFontLoaded = false;
|
|
@@ -382,7 +383,119 @@ async function calibrateSbixOverlays(page, viewport, jobs) {
|
|
|
382
383
|
catch { /* leave this overlay as stamped */ }
|
|
383
384
|
}
|
|
384
385
|
}
|
|
386
|
+
async function rasterizeBackdropFilters(page, tree, viewport) {
|
|
387
|
+
const targets = [];
|
|
388
|
+
forEachElement(tree, (el) => { if (el.backdropFilterRaster?.token != null)
|
|
389
|
+
targets.push(el.backdropFilterRaster); });
|
|
390
|
+
if (targets.length === 0)
|
|
391
|
+
return;
|
|
392
|
+
let cdp;
|
|
393
|
+
try {
|
|
394
|
+
cdp = await page.context().newCDPSession(page);
|
|
395
|
+
const snap = await cdp.send("DOMSnapshot.captureSnapshot", {
|
|
396
|
+
computedStyles: [], includePaintOrder: true, includeDOMRects: true,
|
|
397
|
+
});
|
|
398
|
+
const doc = snap.documents?.[0];
|
|
399
|
+
const strings = snap.strings ?? [];
|
|
400
|
+
const paintByNode = new Map();
|
|
401
|
+
for (let i = 0; i < (doc?.layout?.nodeIndex?.length ?? 0); i++) {
|
|
402
|
+
const nodeIndex = doc.layout.nodeIndex[i];
|
|
403
|
+
const bounds = doc.layout.bounds[i];
|
|
404
|
+
const paintOrder = doc.layout.paintOrders?.[i];
|
|
405
|
+
if (paintOrder != null && !paintByNode.has(nodeIndex))
|
|
406
|
+
paintByNode.set(nodeIndex, { bounds, paintOrder, layoutOrder: i });
|
|
407
|
+
}
|
|
408
|
+
const attributesByNode = new Map();
|
|
409
|
+
const rareAttributes = doc?.nodes?.attributes;
|
|
410
|
+
if (Array.isArray(rareAttributes)) {
|
|
411
|
+
for (let i = 0; i < rareAttributes.length; i++)
|
|
412
|
+
attributesByNode.set(i, rareAttributes[i]);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
for (let i = 0; i < (rareAttributes?.index?.length ?? 0); i++) {
|
|
416
|
+
attributesByNode.set(rareAttributes.index[i], rareAttributes.value[i]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const nodes = (doc?.nodes?.backendNodeId ?? []).map((backendNodeId, i) => {
|
|
420
|
+
const attrIndexes = attributesByNode.get(i) ?? [];
|
|
421
|
+
const attributes = attrIndexes.map((idx) => strings[idx]);
|
|
422
|
+
const layout = paintByNode.get(i);
|
|
423
|
+
return {
|
|
424
|
+
backendNodeId,
|
|
425
|
+
parentIndex: doc.nodes.parentIndex?.[i] ?? -1,
|
|
426
|
+
attributes,
|
|
427
|
+
bounds: layout?.bounds,
|
|
428
|
+
paintOrder: layout?.paintOrder,
|
|
429
|
+
layoutOrder: layout?.layoutOrder,
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
for (const target of targets) {
|
|
433
|
+
const plan = planBackdropIsolation(nodes, target.token);
|
|
434
|
+
const restores = [];
|
|
435
|
+
try {
|
|
436
|
+
if (plan != null) {
|
|
437
|
+
for (const backendNodeId of plan.hideBackendNodeIds) {
|
|
438
|
+
try {
|
|
439
|
+
const resolved = await cdp.send("DOM.resolveNode", { backendNodeId });
|
|
440
|
+
const objectId = resolved.object?.objectId;
|
|
441
|
+
if (objectId == null)
|
|
442
|
+
continue;
|
|
443
|
+
const changed = await cdp.send("Runtime.callFunctionOn", {
|
|
444
|
+
objectId,
|
|
445
|
+
functionDeclaration: "function(){const v=this.style.getPropertyValue('visibility');const p=this.style.getPropertyPriority('visibility');this.style.setProperty('visibility','hidden','important');return {v,p};}",
|
|
446
|
+
returnByValue: true,
|
|
447
|
+
});
|
|
448
|
+
restores.push({ objectId, value: changed.result?.value?.v ?? "", priority: changed.result?.value?.p ?? "" });
|
|
449
|
+
}
|
|
450
|
+
catch { /* conservative fallback: leave this node painted */ }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const clip = clipRectForScreenshot(target, viewport);
|
|
454
|
+
const buf = await page.screenshot({ clip, omitBackground: true, type: "png" });
|
|
455
|
+
target.dataUri = `data:image/png;base64,${Buffer.from(buf).toString("base64")}`;
|
|
456
|
+
target.x = clip.x - viewport.x;
|
|
457
|
+
target.y = clip.y - viewport.y;
|
|
458
|
+
target.width = clip.width;
|
|
459
|
+
target.height = clip.height;
|
|
460
|
+
}
|
|
461
|
+
catch { /* retain vector fallback if screenshot itself fails */ }
|
|
462
|
+
finally {
|
|
463
|
+
for (let i = restores.length - 1; i >= 0; i--) {
|
|
464
|
+
const restore = restores[i];
|
|
465
|
+
try {
|
|
466
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
467
|
+
objectId: restore.objectId,
|
|
468
|
+
functionDeclaration: "function(v,p){if(v==='')this.style.removeProperty('visibility');else this.style.setProperty('visibility',v,p);}",
|
|
469
|
+
arguments: [{ value: restore.value }, { value: restore.priority }],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch { /* page teardown */ }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// DOMSnapshot is Chromium-only and mapping can fail for pseudo/fragments.
|
|
479
|
+
// Fall back to the original full-page crop for every unresolved target.
|
|
480
|
+
for (const target of targets) {
|
|
481
|
+
try {
|
|
482
|
+
const clip = clipRectForScreenshot(target, viewport);
|
|
483
|
+
const buf = await page.screenshot({ clip, omitBackground: true, type: "png" });
|
|
484
|
+
target.dataUri = `data:image/png;base64,${Buffer.from(buf).toString("base64")}`;
|
|
485
|
+
}
|
|
486
|
+
catch { /* leave dataUri absent */ }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
await cdp?.detach().catch(() => undefined);
|
|
491
|
+
await page.evaluate(() => {
|
|
492
|
+
for (const el of document.querySelectorAll("[data-domotion-backdrop-raster]"))
|
|
493
|
+
el.removeAttribute("data-domotion-backdrop-raster");
|
|
494
|
+
}).catch(() => undefined);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
385
497
|
export async function rasterizeBitmapGlyphs(page, tree, viewport) {
|
|
498
|
+
await rasterizeBackdropFilters(page, tree, viewport);
|
|
386
499
|
// Two kinds of candidates share the pipeline:
|
|
387
500
|
// - Segment-level rasterRect (SK-1058): the whole pseudo text is a color-
|
|
388
501
|
// bitmap run; renderer emits one <image> and skips the text path.
|
|
@@ -403,9 +516,10 @@ export async function rasterizeBitmapGlyphs(page, tree, viewport) {
|
|
|
403
516
|
if (el.nativeControlRaster != null) {
|
|
404
517
|
const nr = el.nativeControlRaster;
|
|
405
518
|
candidates.push({
|
|
406
|
-
rect:
|
|
519
|
+
rect: nr,
|
|
407
520
|
key: `native-control|${el.tag}|${el.styles.inputType ?? ''}|${nr.x}|${nr.y}|${nr.width}x${nr.height}`,
|
|
408
521
|
setDataUri: (uri) => { nr.dataUri = uri; },
|
|
522
|
+
snapRectToClip: true,
|
|
409
523
|
});
|
|
410
524
|
}
|
|
411
525
|
// Element-level raster (SK-1108): textarea content region, too
|
|
@@ -338,6 +338,23 @@ export const createEmojiDetect = () => {
|
|
|
338
338
|
// emoji over Chrome's text glyph.
|
|
339
339
|
if (emojiPresentation26.has(cp))
|
|
340
340
|
return isColorGlyph(cp, font);
|
|
341
|
+
// DM-2167: Emoji_Presentation is not confined to the 2600 block or the
|
|
342
|
+
// supplementary emoji planes. Misc Technical contains default-emoji
|
|
343
|
+
// characters such as WATCH, HOURGLASS and the media-control buttons
|
|
344
|
+
// (U+231A/U+231B/U+23E9..U+23F3). Linux Chromium routes these through
|
|
345
|
+
// Noto Color Emoji, but the old hand-partitioned ranges left them on the
|
|
346
|
+
// outline path, where a CBDT-only face has no vector glyph and vanished.
|
|
347
|
+
// Keep this cascade-sensitive: a leading monochrome face must still win.
|
|
348
|
+
if (cp >= 0x2300 && cp <= 0x23FF
|
|
349
|
+
&& RE_EMOJI_PRESENTATION.test(String.fromCodePoint(cp)))
|
|
350
|
+
return isColorGlyph(cp, font);
|
|
351
|
+
// The Mahjong Tiles and Playing Cards blocks are another gap below the
|
|
352
|
+
// historical U+1F300 supplementary-plane floor. Chromium paints MAHJONG
|
|
353
|
+
// TILE RED DRAGON and PLAYING CARD BLACK JOKER from the color font when
|
|
354
|
+
// that is the resolved cascade face; keep text-font coverage authoritative.
|
|
355
|
+
if (cp >= 0x1F000 && cp <= 0x1F0FF
|
|
356
|
+
&& RE_EMOJI_PRESENTATION.test(String.fromCodePoint(cp)))
|
|
357
|
+
return isColorGlyph(cp, font);
|
|
341
358
|
// U+FE0F (Variation Selector-16) after a base emoji codepoint requests
|
|
342
359
|
// emoji presentation — Chrome paints the colorful glyph instead of the
|
|
343
360
|
// text-mode path glyph. DM-278.
|
|
@@ -425,8 +442,12 @@ export const createEmojiDetect = () => {
|
|
|
425
442
|
// ⭐, ⭕). Cascade-dependent — Chrome paints text when the stack reaches a
|
|
426
443
|
// monochrome symbol/math font first (Apple Symbols / STIX Two Math), color
|
|
427
444
|
// otherwise. See `emojiPresentation2B` above.
|
|
445
|
+
// Some color-font glyphs in this set are intentionally achromatic (the
|
|
446
|
+
// black/white square pair). A saturation-only probe mistakes those bitmap
|
|
447
|
+
// glyphs for text. Color fonts ignore CSS fill while monochrome symbol
|
|
448
|
+
// fonts inherit it, so fill invariance supplies the missing discriminator.
|
|
428
449
|
if (emojiPresentation2B.has(cp))
|
|
429
|
-
return isColorGlyph(cp, font);
|
|
450
|
+
return isColorGlyph(cp, font) || ignoresFillColor(cp, font);
|
|
430
451
|
// DM-1167: the ONLY two codepoints in the Misc Symbols & Pictographs block
|
|
431
452
|
// (U+1F300-1F5FF) that a macOS text font also covers monochrome are
|
|
432
453
|
// 🌐 U+1F310 (GLOBE WITH MERIDIANS) and 🎤 U+1F3A4 (MICROPHONE) — Apple
|
|
@@ -49,6 +49,7 @@ const captureDocumentTree = (args) => {
|
|
|
49
49
|
// `--cross-origin-frames` value, passed in as `args.cof`). null in the
|
|
50
50
|
// default (Phase 1) configuration — only same-origin frames recurse then.
|
|
51
51
|
const _crossOriginAllow = parseCrossOriginAllowlist(args.cof);
|
|
52
|
+
let _backdropRasterSeq = 0;
|
|
52
53
|
// Wire up per-concern helpers. Each factory closes over its own state and
|
|
53
54
|
// returns the handles captureInner / the orchestration tail call. Renamed
|
|
54
55
|
// (e.g. `warnings: _warnings`) to keep captureInner's existing references
|
|
@@ -79,7 +80,7 @@ const captureDocumentTree = (args) => {
|
|
|
79
80
|
// against a recursed iframe's own document (`_runCounterStylePrewalk(doc)`).
|
|
80
81
|
const _runCounterStylePrewalk = createCounterStylePrewalk({ counterStyles: _counterStyles });
|
|
81
82
|
const { resolveCounterStyle, resolveCounterValue, isCustomCounterStyle } = createCounterStyleResolver({ counterStyles: _counterStyles });
|
|
82
|
-
const { captureListsCounters } = createListsCountersHandler({ normColor, resolveCounterStyle, isCustomCounterStyle });
|
|
83
|
+
const { captureListsCounters } = createListsCountersHandler({ normColor, resolveCounterStyle, isCustomCounterStyle, measureFontMetrics: _measureFontMetrics });
|
|
83
84
|
const { handleReplacedElement } = createReplacedElementsHandler({ vp });
|
|
84
85
|
const { discoverMasks, discoverClipPaths, discoverFilters, maskDefs: _maskDefs, maskRasters: _maskRasters, clipPathDefs: _clipPathDefs, filterDefs: _filterDefs } = createMasksClipsHandler({ vp, warn });
|
|
85
86
|
const { captureFormControls } = createFormControlsHandler({ normColor, resolvePseudo: _resolvePseudo });
|
|
@@ -768,7 +769,40 @@ const captureDocumentTree = (args) => {
|
|
|
768
769
|
|| tag === 'button' || tag === 'progress' || tag === 'meter';
|
|
769
770
|
if (!nativeTag || cs.appearance === 'none' || rect.width <= 0 || rect.height <= 0)
|
|
770
771
|
return undefined;
|
|
771
|
-
|
|
772
|
+
// Author outlines and validation-state focus rings paint outside the
|
|
773
|
+
// host border box even though the themed control itself is native.
|
|
774
|
+
// Blink's outline visual overflow is width + positive offset; include
|
|
775
|
+
// that surface in the same snapshot instead of clipping it at `rect`.
|
|
776
|
+
const outlineWidth = parseFloat(cs.outlineWidth) || 0;
|
|
777
|
+
const outlineOffset = parseFloat(cs.outlineOffset) || 0;
|
|
778
|
+
const expand = cs.outlineStyle !== 'none' && cs.outlineStyle !== 'hidden'
|
|
779
|
+
? Math.max(0, outlineWidth + outlineOffset)
|
|
780
|
+
: 0;
|
|
781
|
+
// Skia AA coverage may extend one device-independent pixel past the
|
|
782
|
+
// layout border box (notably the lower edge of rounded author borders
|
|
783
|
+
// on otherwise native inputs). Preserve that visual-overflow fringe;
|
|
784
|
+
// the screenshot and emitted <image> use this same rect, so no scaling
|
|
785
|
+
// or fixture geometry is introduced.
|
|
786
|
+
const paintOverflow = 1;
|
|
787
|
+
const rasterExpand = expand + paintOverflow;
|
|
788
|
+
return {
|
|
789
|
+
x: rect.left - vp.x - rasterExpand,
|
|
790
|
+
y: rect.top - vp.y - rasterExpand,
|
|
791
|
+
width: rect.width + rasterExpand * 2,
|
|
792
|
+
height: rect.height + rasterExpand * 2,
|
|
793
|
+
};
|
|
794
|
+
})(),
|
|
795
|
+
// DM-2171: backdrop-filter samples already-painted content behind this
|
|
796
|
+
// element through a distinct Blink effect node. An img-rendered SVG has
|
|
797
|
+
// no equivalent input surface, so preserve Chromium's composited pixels
|
|
798
|
+
// for the complete isolation subtree at its paint-order position.
|
|
799
|
+
backdropFilterRaster: (function () {
|
|
800
|
+
const value = cs.backdropFilter || cs.webkitBackdropFilter || '';
|
|
801
|
+
if (value === '' || value === 'none' || rect.width <= 0 || rect.height <= 0)
|
|
802
|
+
return undefined;
|
|
803
|
+
const token = 'bf' + (_backdropRasterSeq++);
|
|
804
|
+
el.setAttribute('data-domotion-backdrop-raster', token);
|
|
805
|
+
return { x: rect.left - vp.x, y: rect.top - vp.y, width: rect.width, height: rect.height, token };
|
|
772
806
|
})(),
|
|
773
807
|
// DM-680: per-axis cumulative ancestor scale, exposed ONLY when
|
|
774
808
|
// anisotropic (sx ≠ sy within a small epsilon). The geometric mean is
|
|
@@ -32,25 +32,7 @@ export declare const createFormControlsHandler: ({ normColor, resolvePseudo }: {
|
|
|
32
32
|
} | undefined;
|
|
33
33
|
selectChevron: boolean;
|
|
34
34
|
selectDisplayText: any;
|
|
35
|
-
selectListboxOptions:
|
|
36
|
-
text: any;
|
|
37
|
-
selected: boolean;
|
|
38
|
-
disabled: boolean;
|
|
39
|
-
isOptgroupLabel: boolean;
|
|
40
|
-
isOptgroupChild?: undefined;
|
|
41
|
-
} | {
|
|
42
|
-
text: any;
|
|
43
|
-
selected: boolean;
|
|
44
|
-
disabled: boolean;
|
|
45
|
-
isOptgroupChild: boolean;
|
|
46
|
-
isOptgroupLabel?: undefined;
|
|
47
|
-
} | {
|
|
48
|
-
text: any;
|
|
49
|
-
selected: boolean;
|
|
50
|
-
disabled: boolean;
|
|
51
|
-
isOptgroupLabel?: undefined;
|
|
52
|
-
isOptgroupChild?: undefined;
|
|
53
|
-
})[] | undefined;
|
|
35
|
+
selectListboxOptions: any[] | undefined;
|
|
54
36
|
accentColor: any;
|
|
55
37
|
caretColor: any;
|
|
56
38
|
inputValue: any;
|
|
@@ -147,21 +147,52 @@ export const createFormControlsHandler = ({ normColor, resolvePseudo }) => {
|
|
|
147
147
|
selectListboxOptions: tag === 'select' && (el.size > 1 || el.multiple)
|
|
148
148
|
? (function () {
|
|
149
149
|
const list = [];
|
|
150
|
+
const hostRect = el.getBoundingClientRect();
|
|
151
|
+
const row = (o, extra) => {
|
|
152
|
+
const ocs = window.getComputedStyle(o);
|
|
153
|
+
const or = o.getBoundingClientRect();
|
|
154
|
+
const canvas = document.createElement('canvas');
|
|
155
|
+
const ctx = canvas.getContext('2d');
|
|
156
|
+
let fontAscent;
|
|
157
|
+
if (ctx != null) {
|
|
158
|
+
ctx.font = `${ocs.fontStyle} ${ocs.fontWeight} ${ocs.fontSize} ${ocs.fontFamily}`;
|
|
159
|
+
fontAscent = ctx.measureText('Mxgp').fontBoundingBoxAscent;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
text: (o.textContent || '').trim(), selected: !!o.selected,
|
|
163
|
+
disabled: !!o.disabled, ...extra,
|
|
164
|
+
// Unlike closed-select shadow parts, listbox OPTION elements
|
|
165
|
+
// expose real layout rects and computed author paint. Capture
|
|
166
|
+
// those browser-resolved facts instead of rebuilding row
|
|
167
|
+
// height and :checked styling from host-font heuristics.
|
|
168
|
+
x: or.left - hostRect.left, y: or.top - hostRect.top,
|
|
169
|
+
width: or.width, height: or.height,
|
|
170
|
+
backgroundColor: normColor(ocs.backgroundColor),
|
|
171
|
+
color: normColor(ocs.color),
|
|
172
|
+
paddingLeft: parseFloat(ocs.paddingLeft) || 0,
|
|
173
|
+
paddingTop: parseFloat(ocs.paddingTop) || 0,
|
|
174
|
+
fontSize: parseFloat(ocs.fontSize) || undefined,
|
|
175
|
+
fontFamily: ocs.fontFamily || undefined,
|
|
176
|
+
fontWeight: ocs.fontWeight || undefined,
|
|
177
|
+
fontStyle: ocs.fontStyle || undefined,
|
|
178
|
+
fontAscent,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
150
181
|
const kids = el.children;
|
|
151
182
|
for (let ki = 0; ki < kids.length; ki++) {
|
|
152
183
|
const c = kids[ki];
|
|
153
184
|
if (c.tagName === 'OPTGROUP') {
|
|
154
|
-
list.push({ text: c.label || '', selected: false,
|
|
185
|
+
list.push(row(c, { text: c.label || '', selected: false, isOptgroupLabel: true }));
|
|
155
186
|
const og = c.children;
|
|
156
187
|
for (let gi = 0; gi < og.length; gi++) {
|
|
157
188
|
const o = og[gi];
|
|
158
189
|
if (o.tagName !== 'OPTION')
|
|
159
190
|
continue;
|
|
160
|
-
list.push(
|
|
191
|
+
list.push(row(o, { isOptgroupChild: true }));
|
|
161
192
|
}
|
|
162
193
|
}
|
|
163
194
|
else if (c.tagName === 'OPTION') {
|
|
164
|
-
list.push(
|
|
195
|
+
list.push(row(c, {}));
|
|
165
196
|
}
|
|
166
197
|
}
|
|
167
198
|
return list;
|
|
@@ -7,6 +7,66 @@
|
|
|
7
7
|
// across more than one line-box / column fragment. See the inline comments for
|
|
8
8
|
// the trigger conditions (DM-754 / DM-937).
|
|
9
9
|
export const detectInlineFragments = (el, cs, vp, captured) => {
|
|
10
|
+
// CSS Multi-column Layout paints rules in each column row. A
|
|
11
|
+
// column-span:all child ends the current row and starts another, so a
|
|
12
|
+
// rule reconstructed from the container's union box would incorrectly
|
|
13
|
+
// cross the spanner. Capture the physical row intervals here while the
|
|
14
|
+
// DOM's computed margins and border/padding geometry are available.
|
|
15
|
+
var _ownCount = parseInt(cs.columnCount, 10);
|
|
16
|
+
var _ownColumnWidth = parseFloat(cs.columnWidth || '');
|
|
17
|
+
var _ownGap = parseFloat(cs.columnGap || '');
|
|
18
|
+
if (!Number.isFinite(_ownGap))
|
|
19
|
+
_ownGap = parseFloat(cs.fontSize || '16') || 16;
|
|
20
|
+
var _ruleWidth = parseFloat(cs.columnRuleWidth || '0') || 0;
|
|
21
|
+
var _ruleStyle = cs.columnRuleStyle || 'none';
|
|
22
|
+
if (_ruleWidth > 0 && _ruleStyle !== 'none' && _ruleStyle !== 'hidden') {
|
|
23
|
+
var _box = el.getBoundingClientRect();
|
|
24
|
+
var _contentLeft = _box.left + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.paddingLeft) || 0);
|
|
25
|
+
var _contentRight = _box.right - (parseFloat(cs.borderRightWidth) || 0) - (parseFloat(cs.paddingRight) || 0);
|
|
26
|
+
var _contentTop = _box.top + (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.paddingTop) || 0);
|
|
27
|
+
var _contentBottom = _box.bottom - (parseFloat(cs.borderBottomWidth) || 0) - (parseFloat(cs.paddingBottom) || 0);
|
|
28
|
+
var _contentWidth = _contentRight - _contentLeft;
|
|
29
|
+
if (!(Number.isFinite(_ownCount) && _ownCount > 1) && Number.isFinite(_ownColumnWidth) && _ownColumnWidth > 0) {
|
|
30
|
+
_ownCount = Math.max(1, Math.floor((_contentWidth + _ownGap) / (_ownColumnWidth + _ownGap)));
|
|
31
|
+
}
|
|
32
|
+
if (Number.isFinite(_ownCount) && _ownCount > 1 && _contentWidth > 0 && _contentBottom > _contentTop) {
|
|
33
|
+
var _rows = [];
|
|
34
|
+
var _rowStart = _contentTop;
|
|
35
|
+
for (var _si = 0; _si < el.children.length; _si++) {
|
|
36
|
+
var _span = el.children[_si];
|
|
37
|
+
var _spanCs = window.getComputedStyle(_span);
|
|
38
|
+
if (_spanCs.columnSpan !== 'all')
|
|
39
|
+
continue;
|
|
40
|
+
var _spanRect = _span.getBoundingClientRect();
|
|
41
|
+
var _spanTop = _spanRect.top - (parseFloat(_spanCs.marginTop) || 0);
|
|
42
|
+
var _spanBottom = _spanRect.bottom + (parseFloat(_spanCs.marginBottom) || 0);
|
|
43
|
+
if (_spanTop > _rowStart)
|
|
44
|
+
_rows.push([_rowStart, Math.min(_spanTop, _contentBottom)]);
|
|
45
|
+
_rowStart = Math.max(_rowStart, _spanBottom);
|
|
46
|
+
}
|
|
47
|
+
if (_contentBottom > _rowStart)
|
|
48
|
+
_rows.push([_rowStart, _contentBottom]);
|
|
49
|
+
var _columnWidth = (_contentWidth - _ownGap * (_ownCount - 1)) / _ownCount;
|
|
50
|
+
var _rules = [];
|
|
51
|
+
for (var _ri = 1; _ri < _ownCount; _ri++) {
|
|
52
|
+
var _ruleX = _contentLeft + _ri * _columnWidth + (_ri - 0.5) * _ownGap - vp.x;
|
|
53
|
+
for (var _rsi = 0; _rsi < _rows.length; _rsi++) {
|
|
54
|
+
if (_rows[_rsi][1] <= _rows[_rsi][0])
|
|
55
|
+
continue;
|
|
56
|
+
_rules.push({
|
|
57
|
+
x: _ruleX,
|
|
58
|
+
y1: _rows[_rsi][0] - vp.y,
|
|
59
|
+
y2: _rows[_rsi][1] - vp.y,
|
|
60
|
+
width: _ruleWidth,
|
|
61
|
+
color: cs.columnRuleColor,
|
|
62
|
+
style: _ruleStyle,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (_rules.length > 0)
|
|
67
|
+
captured.columnRules = _rules;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
10
70
|
var _bgC = captured.styles.backgroundColor;
|
|
11
71
|
var _hasBg = _bgC != null && _bgC !== '' && _bgC !== 'transparent' && _bgC !== 'rgba(0, 0, 0, 0)';
|
|
12
72
|
var _hasBgImage = captured.styles.backgroundImage != null
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export declare const createListsCountersHandler: ({ normColor, resolveCounterStyle, isCustomCounterStyle }: {
|
|
1
|
+
export declare const createListsCountersHandler: ({ normColor, resolveCounterStyle, isCustomCounterStyle, measureFontMetrics }: {
|
|
2
2
|
normColor: any;
|
|
3
3
|
resolveCounterStyle: any;
|
|
4
4
|
isCustomCounterStyle: any;
|
|
5
|
+
measureFontMetrics: any;
|
|
5
6
|
}) => {
|
|
6
7
|
captureListsCounters: (el: any, cs: any, tag: any) => {
|
|
7
8
|
listMarkerIntrinsic: {
|
|
@@ -12,6 +13,7 @@ export declare const createListsCountersHandler: ({ normColor, resolveCounterSty
|
|
|
12
13
|
markerColor: any;
|
|
13
14
|
markerFontWeight: string | undefined;
|
|
14
15
|
markerFontSize: string | undefined;
|
|
16
|
+
markerFontAscent: any;
|
|
15
17
|
markerContent: string | undefined;
|
|
16
18
|
markerFontFamily: string | undefined;
|
|
17
19
|
markerFirstLineDy: number | undefined;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// no runtime imports of its own. Use `// @ts-nocheck` at the top because the
|
|
14
14
|
// outer-page environment exposes `document` / `window` / `Image` without the
|
|
15
15
|
// project's tsconfig DOM lib applying.
|
|
16
|
-
export const createListsCountersHandler = ({ normColor, resolveCounterStyle, isCustomCounterStyle }) => {
|
|
16
|
+
export const createListsCountersHandler = ({ normColor, resolveCounterStyle, isCustomCounterStyle, measureFontMetrics }) => {
|
|
17
17
|
const captureListsCounters = (el, cs, tag) => {
|
|
18
18
|
// CSS treats any element with display:list-item as a list item — the tag
|
|
19
19
|
// alone isn't enough. An <li> with display:inline-block (e.g. a horizontal
|
|
@@ -124,6 +124,7 @@ export const createListsCountersHandler = ({ normColor, resolveCounterStyle, isC
|
|
|
124
124
|
markerColor: markerCs ? normColor(markerCs.color) : undefined,
|
|
125
125
|
markerFontWeight: markerCs ? markerCs.fontWeight : undefined,
|
|
126
126
|
markerFontSize: markerCs ? markerCs.fontSize : undefined,
|
|
127
|
+
markerFontAscent: markerCs ? measureFontMetrics(markerCs).ascent : undefined,
|
|
127
128
|
markerContent,
|
|
128
129
|
markerFontFamily: markerCs ? markerCs.fontFamily : undefined,
|
|
129
130
|
markerFirstLineDy,
|
|
@@ -51,6 +51,8 @@ export declare const createPseudoContentHandler: (dependencies: any) => {
|
|
|
51
51
|
borB: number;
|
|
52
52
|
lineH: number;
|
|
53
53
|
fontSize: number;
|
|
54
|
+
measuredWidth: number;
|
|
55
|
+
measuredHeight: number;
|
|
54
56
|
backgroundColor: any;
|
|
55
57
|
backgroundImage: string | undefined;
|
|
56
58
|
borderRadius: number | undefined;
|
|
@@ -73,32 +73,43 @@ const buildPseudoContentHandler = ({ vp, normColor, measureFontMetrics, textNeed
|
|
|
73
73
|
_emojiAdvCtx.font = pseudoCanvasFont(pcs);
|
|
74
74
|
return _emojiAdvCtx.measureText(s).width;
|
|
75
75
|
};
|
|
76
|
-
// DM-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
// matters for `width: auto` absolute pseudos — the DM-507 numeric `pcs.width`
|
|
83
|
-
// path is still authoritative when present.
|
|
84
|
-
const probePseudoTextWidth = (text, pcs) => {
|
|
76
|
+
// DM-2191: DOM APIs expose a pseudo's computed style, but not its layout
|
|
77
|
+
// object or rect. Materialize that resolved style on a real child and let
|
|
78
|
+
// Blink lay out the generated content. Copying every computed property (not
|
|
79
|
+
// a hand-picked font/box subset) preserves the resolved cascade, variables,
|
|
80
|
+
// calc(), writing mode, font features, intrinsic sizing, and logical props.
|
|
81
|
+
const probeGeneratedPseudoLayout = (el, pseudo, text, pcs) => {
|
|
85
82
|
const span = document.createElement('span');
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
span.style.
|
|
93
|
-
span.style.
|
|
94
|
-
span.style.
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
for (let i = 0; i < pcs.length; i++) {
|
|
84
|
+
const name = pcs.item(i);
|
|
85
|
+
span.style.setProperty(name, pcs.getPropertyValue(name));
|
|
86
|
+
}
|
|
87
|
+
// Generated `content` has no effect on a real element; materialize it as a
|
|
88
|
+
// text node. Visibility keeps the probe non-painting while retaining layout.
|
|
89
|
+
span.style.setProperty('content', 'normal');
|
|
90
|
+
span.style.setProperty('visibility', 'hidden');
|
|
91
|
+
span.style.setProperty('pointer-events', 'none');
|
|
92
|
+
// Transforms affect only paint and turn getBoundingClientRect into an AABB;
|
|
93
|
+
// renderer applies them later around the untransformed layout box.
|
|
94
|
+
span.style.setProperty('transform', 'none');
|
|
95
|
+
span.style.setProperty('translate', 'none');
|
|
96
|
+
span.style.setProperty('rotate', 'none');
|
|
97
|
+
span.style.setProperty('scale', 'none');
|
|
97
98
|
span.textContent = text;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
if (pseudo === '::before')
|
|
100
|
+
el.insertBefore(span, el.firstChild);
|
|
101
|
+
else
|
|
102
|
+
el.appendChild(span);
|
|
103
|
+
const borderRect = span.getBoundingClientRect();
|
|
104
|
+
const range = document.createRange();
|
|
105
|
+
range.selectNodeContents(span);
|
|
106
|
+
const textRect = range.getBoundingClientRect();
|
|
107
|
+
const result = {
|
|
108
|
+
borderRect: { left: borderRect.left, top: borderRect.top, width: borderRect.width, height: borderRect.height },
|
|
109
|
+
textRect: { left: textRect.left, top: textRect.top, width: textRect.width, height: textRect.height },
|
|
110
|
+
};
|
|
111
|
+
span.remove();
|
|
112
|
+
return result;
|
|
102
113
|
};
|
|
103
114
|
// DM-768: when a static-flow pseudo declares `display: inline-block` (or
|
|
104
115
|
// inline-flex / inline-grid / inline-table) the box participates in Chrome's
|
|
@@ -618,17 +629,22 @@ const buildPseudoContentHandler = ({ vp, normColor, measureFontMetrics, textNeed
|
|
|
618
629
|
}
|
|
619
630
|
if (text === '')
|
|
620
631
|
continue;
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
+
const pseudoLayout = probeGeneratedPseudoLayout(el, pseudo, text, pcs);
|
|
633
|
+
// TextSegment dimensions are physical viewport dimensions. Range geometry
|
|
634
|
+
// therefore handles vertical text without pretending its inline advance is
|
|
635
|
+
// always an x-axis width. A zero range (e.g. whitespace-only content)
|
|
636
|
+
// falls back to the measured content box.
|
|
637
|
+
const pPadLMeasured = parseFloat(pcs.paddingLeft) || 0;
|
|
638
|
+
const pPadRMeasured = parseFloat(pcs.paddingRight) || 0;
|
|
639
|
+
const pBorLMeasured = parseFloat(pcs.borderLeftWidth) || 0;
|
|
640
|
+
const pBorRMeasured = parseFloat(pcs.borderRightWidth) || 0;
|
|
641
|
+
const measuredContentWidth = Math.max(0, pseudoLayout.borderRect.width
|
|
642
|
+
- pPadLMeasured - pPadRMeasured - pBorLMeasured - pBorRMeasured);
|
|
643
|
+
const hasUsedBoxWidth = pcs.width !== '' && pcs.width !== 'auto'
|
|
644
|
+
&& (pcs.position === 'absolute' || pcs.position === 'fixed');
|
|
645
|
+
const pseudoWidth = hasUsedBoxWidth
|
|
646
|
+
? measuredContentWidth
|
|
647
|
+
: (pseudoLayout.textRect.width > 0 ? pseudoLayout.textRect.width : measuredContentWidth);
|
|
632
648
|
// Position: ::before sits at the START of the host's text/content.
|
|
633
649
|
// ::after sits at the END. We fall back to (elLeft, elTop) here
|
|
634
650
|
// because the host's textLeft/textWidth aren't yet known — the
|
|
@@ -818,6 +834,8 @@ const buildPseudoContentHandler = ({ vp, normColor, measureFontMetrics, textNeed
|
|
|
818
834
|
// on the line box).
|
|
819
835
|
lineH,
|
|
820
836
|
fontSize: elFontSize,
|
|
837
|
+
measuredWidth: pseudoLayout.borderRect.width,
|
|
838
|
+
measuredHeight: pseudoLayout.borderRect.height,
|
|
821
839
|
backgroundColor: pseudoBgColor !== '' ? pseudoBgColor : undefined,
|
|
822
840
|
backgroundImage: hasPseudoBgImg ? pseudoBgImgRaw : undefined,
|
|
823
841
|
borderRadius: pseudoBR > 0 ? pseudoBR : undefined,
|
|
@@ -316,8 +316,10 @@ export const createPseudoInjectHandler = () => {
|
|
|
316
316
|
const lineCenter = p.seg.y + bs.fontSize / 2;
|
|
317
317
|
const boxTop = lineCenter - bs.lineH / 2 - bs.padT - bs.borT;
|
|
318
318
|
const bx = p.seg.x - bs.padL - bs.borL;
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
// DM-2191: prefer Blink's measured generated-content border box. The
|
|
320
|
+
// arithmetic fallback exists only for older captured trees.
|
|
321
|
+
const bw = bs.measuredWidth || (p.seg.width + bs.padL + bs.padR + bs.borL + bs.borR);
|
|
322
|
+
const bh = bs.measuredHeight || (bs.lineH + bs.padT + bs.padB + bs.borT + bs.borB);
|
|
321
323
|
if (bw > 0 && bh > 0) {
|
|
322
324
|
p.seg.pseudoBox = {
|
|
323
325
|
x: bx, y: boxTop, width: bw, height: bh,
|