domotion-svg 0.2.2 → 0.3.2
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/FEATURES.md +1 -0
- package/README.md +29 -0
- package/dist/animation/animator.js +25 -14
- package/dist/animation/animator.test.js +54 -21
- package/dist/animation/cursor-overlay.js +0 -2
- package/dist/capture/emoji.js +29 -18
- package/dist/capture/index.js +5 -4
- package/dist/capture/script/color-norm.d.ts +1 -0
- package/dist/capture/script/color-norm.js +43 -1
- package/dist/capture/script/emoji-detect.js +14 -0
- package/dist/capture/script/index.js +593 -65
- package/dist/capture/script/walker/borders-backgrounds.d.ts +24 -17
- package/dist/capture/script/walker/borders-backgrounds.js +123 -7
- package/dist/capture/script/walker/counter-style-resolver.d.ts +7 -0
- package/dist/capture/script/walker/counter-style-resolver.js +218 -0
- package/dist/capture/script/walker/input-value.js +14 -1
- package/dist/capture/script/walker/lists-counters.d.ts +3 -1
- package/dist/capture/script/walker/lists-counters.js +22 -2
- package/dist/capture/script/walker/masks-clips.d.ts +2 -0
- package/dist/capture/script/walker/masks-clips.js +41 -1
- package/dist/capture/script/walker/pseudo-content.d.ts +14 -1
- package/dist/capture/script/walker/pseudo-content.js +301 -61
- package/dist/capture/script/walker/pseudo-inject.js +20 -0
- package/dist/capture/script/walker/text-segments.js +98 -4
- package/dist/capture/script/walker/transforms.d.ts +1 -0
- package/dist/capture/script/walker/transforms.js +16 -0
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +213 -2
- package/dist/cli/animate.js +151 -15
- package/dist/mask.test.js +12 -7
- package/dist/render/borders.d.ts +9 -13
- package/dist/render/borders.js +379 -14
- package/dist/render/element-tree-to-svg.d.ts +11 -12
- package/dist/render/element-tree-to-svg.js +2046 -241
- package/dist/render/embedded-font-builder.d.ts +49 -0
- package/dist/render/embedded-font-builder.js +149 -0
- package/dist/render/form-controls.js +45 -24
- package/dist/render/gradients.d.ts +15 -0
- package/dist/render/gradients.js +103 -2
- package/dist/render/gradients.test.js +34 -0
- package/dist/render/text-to-path.d.ts +38 -1
- package/dist/render/text-to-path.js +654 -29
- package/dist/render/text-to-path.test.js +230 -9
- package/dist/render/text.d.ts +14 -0
- package/dist/render/text.js +344 -40
- package/dist/scroll/composer.d.ts +26 -0
- package/dist/scroll/composer.js +199 -11
- package/dist/scroll/composer.test.js +293 -16
- package/dist/scroll/executor.d.ts +3 -1
- package/dist/scroll/executor.js +15 -6
- package/dist/scroll/executor.test.js +25 -0
- package/dist/scroll/hoist-fixed.d.ts +48 -0
- package/dist/scroll/hoist-fixed.js +85 -0
- package/dist/scroll/hoist-fixed.test.d.ts +1 -0
- package/dist/scroll/hoist-fixed.test.js +103 -0
- package/dist/scroll/hoist-sticky.d.ts +45 -0
- package/dist/scroll/hoist-sticky.js +157 -0
- package/dist/scroll/hoist-sticky.test.d.ts +1 -0
- package/dist/scroll/hoist-sticky.test.js +154 -0
- package/dist/scroll/pattern.d.ts +22 -5
- package/dist/scroll/pattern.js +55 -7
- package/dist/scroll/pattern.test.js +48 -1
- package/dist/tree-ops/frame-merge.d.ts +10 -0
- package/dist/tree-ops/frame-merge.js +23 -5
- package/dist/tree-ops/frame-merge.test.js +45 -0
- package/dist/tree-ops/tree-diff.js +1 -1
- package/dist/tree-ops/viewbox-culling.js +32 -18
- package/dist/tree-ops/viewbox-culling.test.js +40 -6
- package/package.json +8 -2
- package/src/animation/animator.test.ts +56 -21
- package/src/animation/animator.ts +25 -14
- package/src/animation/cursor-overlay.ts +0 -2
- package/src/capture/emoji.ts +28 -18
- package/src/capture/index.ts +15 -14
- package/src/capture/script/color-norm.ts +38 -1
- package/src/capture/script/emoji-detect.ts +14 -0
- package/src/capture/script/index.ts +555 -48
- package/src/capture/script/walker/borders-backgrounds.ts +114 -7
- package/src/capture/script/walker/counter-style-resolver.ts +184 -0
- package/src/capture/script/walker/input-value.ts +14 -1
- package/src/capture/script/walker/lists-counters.ts +24 -2
- package/src/capture/script/walker/masks-clips.ts +40 -1
- package/src/capture/script/walker/pseudo-content.ts +297 -55
- package/src/capture/script/walker/pseudo-inject.ts +20 -0
- package/src/capture/script/walker/text-segments.ts +93 -4
- package/src/capture/script/walker/transforms.ts +14 -0
- package/src/capture/script.generated.ts +1 -1
- package/src/capture/types.ts +202 -2
- package/src/cli/animate.ts +135 -15
- package/src/mask.test.ts +12 -7
- package/src/render/borders.ts +383 -17
- package/src/render/element-tree-to-svg.ts +2051 -238
- package/src/render/embedded-font-builder.ts +221 -0
- package/src/render/form-controls.ts +45 -24
- package/src/render/gradients.test.ts +46 -0
- package/src/render/gradients.ts +94 -2
- package/src/render/opentype.js.d.ts +7 -0
- package/src/render/text-to-path.test.ts +246 -9
- package/src/render/text-to-path.ts +702 -31
- package/src/render/text.ts +344 -40
- package/src/scroll/composer.test.ts +322 -16
- package/src/scroll/composer.ts +246 -13
- package/src/scroll/executor.test.ts +27 -0
- package/src/scroll/executor.ts +19 -10
- package/src/scroll/hoist-fixed.test.ts +117 -0
- package/src/scroll/hoist-fixed.ts +95 -0
- package/src/scroll/hoist-sticky.test.ts +173 -0
- package/src/scroll/hoist-sticky.ts +193 -0
- package/src/scroll/pattern.test.ts +58 -1
- package/src/scroll/pattern.ts +71 -8
- package/src/tree-ops/frame-merge.test.ts +51 -0
- package/src/tree-ops/frame-merge.ts +24 -6
- package/src/tree-ops/tree-diff.ts +3 -1
- package/src/tree-ops/viewbox-culling.test.ts +42 -6
- package/src/tree-ops/viewbox-culling.ts +32 -18
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export declare const createPseudoContentHandler: ({ vp, normColor, measureFontMetrics, textNeedsRaster }: {
|
|
1
|
+
export declare const createPseudoContentHandler: ({ vp, normColor, measureFontMetrics, textNeedsRaster, resolveCounterValue, isCustomCounterStyle }: {
|
|
2
2
|
vp: any;
|
|
3
3
|
normColor: any;
|
|
4
4
|
measureFontMetrics: any;
|
|
5
5
|
textNeedsRaster: any;
|
|
6
|
+
resolveCounterValue: any;
|
|
7
|
+
isCustomCounterStyle: any;
|
|
6
8
|
}) => {
|
|
7
9
|
capturePseudoContent: (el: any, cs: any, rect: any, counterSnapshot: any) => {
|
|
8
10
|
pseudoSegments: ({
|
|
@@ -38,6 +40,7 @@ export declare const createPseudoContentHandler: ({ vp, normColor, measureFontMe
|
|
|
38
40
|
fontSize: number;
|
|
39
41
|
fontWeight: string;
|
|
40
42
|
fontFamily: string;
|
|
43
|
+
fontStyle: string;
|
|
41
44
|
fontAscent: any;
|
|
42
45
|
};
|
|
43
46
|
color: string;
|
|
@@ -54,9 +57,16 @@ export declare const createPseudoContentHandler: ({ vp, normColor, measureFontMe
|
|
|
54
57
|
lineH: number;
|
|
55
58
|
fontSize: number;
|
|
56
59
|
backgroundColor: any;
|
|
60
|
+
backgroundImage: string | undefined;
|
|
57
61
|
borderRadius: number | undefined;
|
|
58
62
|
borderWidth: number | undefined;
|
|
59
63
|
borderColor: any;
|
|
64
|
+
transform: string | undefined;
|
|
65
|
+
transformOrigin: string | undefined;
|
|
66
|
+
borderTopColor: any;
|
|
67
|
+
borderRightColor: any;
|
|
68
|
+
borderBottomColor: any;
|
|
69
|
+
borderLeftColor: any;
|
|
60
70
|
} | null;
|
|
61
71
|
imageUrl?: undefined;
|
|
62
72
|
renderWidth?: undefined;
|
|
@@ -74,6 +84,7 @@ export declare const createPseudoContentHandler: ({ vp, normColor, measureFontMe
|
|
|
74
84
|
width: number;
|
|
75
85
|
height: number;
|
|
76
86
|
backgroundColor: any;
|
|
87
|
+
backgroundImage: string | undefined;
|
|
77
88
|
borderTopWidth: number;
|
|
78
89
|
borderTopColor: any;
|
|
79
90
|
borderTopStyle: string;
|
|
@@ -87,6 +98,8 @@ export declare const createPseudoContentHandler: ({ vp, normColor, measureFontMe
|
|
|
87
98
|
borderLeftColor: any;
|
|
88
99
|
borderLeftStyle: string;
|
|
89
100
|
borderRadius: number;
|
|
101
|
+
transform: string | undefined;
|
|
102
|
+
transformOrigin: string | undefined;
|
|
90
103
|
}[];
|
|
91
104
|
};
|
|
92
105
|
};
|
|
@@ -52,7 +52,125 @@
|
|
|
52
52
|
// entries), and everything that mixes pseudo positioning with the
|
|
53
53
|
// element's own textSegments. Those depend on text shaping state that
|
|
54
54
|
// hasn't been pulled out of captureInner yet — follow-up.
|
|
55
|
-
export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics, textNeedsRaster }) => {
|
|
55
|
+
export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics, textNeedsRaster, resolveCounterValue, isCustomCounterStyle }) => {
|
|
56
|
+
// DM-785: Chrome's HarfBuzz-shaped layout width differs from
|
|
57
|
+
// `canvas.measureText` by ~1-3px on bold uppercase short strings (the
|
|
58
|
+
// gradient-pill / MOST POPULAR / NEW badge pattern). Measuring via an
|
|
59
|
+
// off-screen <span> with the pseudo's resolved font properties and reading
|
|
60
|
+
// `getBoundingClientRect().width` matches the painted width exactly because
|
|
61
|
+
// it goes through the same shaping pipeline Chrome uses for layout. Only
|
|
62
|
+
// matters for `width: auto` absolute pseudos — the DM-507 numeric `pcs.width`
|
|
63
|
+
// path is still authoritative when present.
|
|
64
|
+
const probePseudoTextWidth = (text, pcs) => {
|
|
65
|
+
const span = document.createElement('span');
|
|
66
|
+
span.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;left:-99999px;top:-99999px;white-space:pre;line-height:normal;margin:0;padding:0;border:0;text-indent:0';
|
|
67
|
+
span.style.fontFamily = pcs.fontFamily || '';
|
|
68
|
+
span.style.fontSize = pcs.fontSize || '';
|
|
69
|
+
span.style.fontWeight = pcs.fontWeight || '';
|
|
70
|
+
span.style.fontStyle = pcs.fontStyle || '';
|
|
71
|
+
span.style.fontStretch = pcs.fontStretch || '';
|
|
72
|
+
span.style.fontVariant = pcs.fontVariant || '';
|
|
73
|
+
span.style.fontFeatureSettings = pcs.fontFeatureSettings || '';
|
|
74
|
+
span.style.fontVariationSettings = pcs.fontVariationSettings || '';
|
|
75
|
+
span.style.letterSpacing = pcs.letterSpacing || '';
|
|
76
|
+
span.style.wordSpacing = pcs.wordSpacing || '';
|
|
77
|
+
span.textContent = text;
|
|
78
|
+
document.body.appendChild(span);
|
|
79
|
+
const w = span.getBoundingClientRect().width;
|
|
80
|
+
document.body.removeChild(span);
|
|
81
|
+
return w;
|
|
82
|
+
};
|
|
83
|
+
// DM-768: when a static-flow pseudo declares `display: inline-block` (or
|
|
84
|
+
// inline-flex / inline-grid / inline-table) the box participates in Chrome's
|
|
85
|
+
// inline vertical-align math — `vertical-align: middle` aligns the pseudo's
|
|
86
|
+
// mid-point with the parent's baseline + 0.5 × x-height, `baseline` aligns
|
|
87
|
+
// the pseudo's bottom to the parent baseline, etc. The earlier formula
|
|
88
|
+
// (`rect.top + hostBorT + hostPadT + pMarT`) ignores that and places the
|
|
89
|
+
// pseudo at the host's content-area top — i.e. the line-box top — so an
|
|
90
|
+
// inline-block down-caret with `border-top: 5px solid` paints 6-7 px too
|
|
91
|
+
// high inside its parent button. Probe instead: insert a real sentinel
|
|
92
|
+
// mirroring the pseudo's box (display / size / borders / padding / margin /
|
|
93
|
+
// vertical-align) at the pseudo's logical position in the host and read its
|
|
94
|
+
// `getBoundingClientRect()`. Chrome lays out the sentinel exactly where the
|
|
95
|
+
// pseudo would have gone, so we get the correct x/y without re-deriving
|
|
96
|
+
// font metrics + vertical-align semantics ourselves.
|
|
97
|
+
const probePseudoStaticBoxRect = (el, pseudo, pcs) => {
|
|
98
|
+
const probe = document.createElement('span');
|
|
99
|
+
probe.style.cssText = 'pointer-events:none;visibility:hidden;box-sizing:content-box';
|
|
100
|
+
probe.style.display = pcs.display;
|
|
101
|
+
probe.style.width = pcs.width;
|
|
102
|
+
probe.style.height = pcs.height;
|
|
103
|
+
probe.style.paddingTop = pcs.paddingTop;
|
|
104
|
+
probe.style.paddingRight = pcs.paddingRight;
|
|
105
|
+
probe.style.paddingBottom = pcs.paddingBottom;
|
|
106
|
+
probe.style.paddingLeft = pcs.paddingLeft;
|
|
107
|
+
probe.style.borderTopWidth = pcs.borderTopWidth;
|
|
108
|
+
probe.style.borderRightWidth = pcs.borderRightWidth;
|
|
109
|
+
probe.style.borderBottomWidth = pcs.borderBottomWidth;
|
|
110
|
+
probe.style.borderLeftWidth = pcs.borderLeftWidth;
|
|
111
|
+
probe.style.borderStyle = 'solid';
|
|
112
|
+
probe.style.borderColor = 'transparent';
|
|
113
|
+
probe.style.marginTop = pcs.marginTop;
|
|
114
|
+
probe.style.marginRight = pcs.marginRight;
|
|
115
|
+
probe.style.marginBottom = pcs.marginBottom;
|
|
116
|
+
probe.style.marginLeft = pcs.marginLeft;
|
|
117
|
+
probe.style.verticalAlign = pcs.verticalAlign;
|
|
118
|
+
probe.style.font = ''; // inherit so line-box metrics match the pseudo's parent
|
|
119
|
+
if (pseudo === '::before')
|
|
120
|
+
el.insertBefore(probe, el.firstChild);
|
|
121
|
+
else
|
|
122
|
+
el.appendChild(probe);
|
|
123
|
+
const r = probe.getBoundingClientRect();
|
|
124
|
+
probe.remove();
|
|
125
|
+
return r;
|
|
126
|
+
};
|
|
127
|
+
// For `position: absolute` / `position: fixed` pseudos, the containing block
|
|
128
|
+
// is the nearest positioned ancestor of the host (NOT the host itself when
|
|
129
|
+
// the host is `position: static`). NYT's mobile nav `.css-sdhjrl::after`
|
|
130
|
+
// fade-out is `position: absolute; right: 0; top:0; width: 24px; height: 40px`
|
|
131
|
+
// on a `position: static; display: flex; overflow: scroll` NAV — the pseudo's
|
|
132
|
+
// computed `top` / `left` resolve against a far-up ancestor, so naïvely adding
|
|
133
|
+
// them to the host's padding-box origin places the gradient ~3088px below the
|
|
134
|
+
// NAV (where there's no NAV to fade over). Instead, inject a real absolutely-
|
|
135
|
+
// positioned sentinel as a child of the host: it inherits the same containing
|
|
136
|
+
// block the pseudo would have, and Chrome lays it out at the exact rect the
|
|
137
|
+
// pseudo paints to. Read its `getBoundingClientRect` directly.
|
|
138
|
+
const probePseudoAbsoluteBoxRect = (el, pseudo, pcs) => {
|
|
139
|
+
const probe = document.createElement('div');
|
|
140
|
+
probe.style.cssText = 'pointer-events:none;visibility:hidden;box-sizing:content-box;margin:0';
|
|
141
|
+
probe.style.position = pcs.position;
|
|
142
|
+
probe.style.top = pcs.top;
|
|
143
|
+
probe.style.right = pcs.right;
|
|
144
|
+
probe.style.bottom = pcs.bottom;
|
|
145
|
+
probe.style.left = pcs.left;
|
|
146
|
+
probe.style.width = pcs.width;
|
|
147
|
+
probe.style.height = pcs.height;
|
|
148
|
+
probe.style.paddingTop = pcs.paddingTop;
|
|
149
|
+
probe.style.paddingRight = pcs.paddingRight;
|
|
150
|
+
probe.style.paddingBottom = pcs.paddingBottom;
|
|
151
|
+
probe.style.paddingLeft = pcs.paddingLeft;
|
|
152
|
+
probe.style.borderTopWidth = pcs.borderTopWidth;
|
|
153
|
+
probe.style.borderRightWidth = pcs.borderRightWidth;
|
|
154
|
+
probe.style.borderBottomWidth = pcs.borderBottomWidth;
|
|
155
|
+
probe.style.borderLeftWidth = pcs.borderLeftWidth;
|
|
156
|
+
probe.style.borderStyle = 'solid';
|
|
157
|
+
probe.style.borderColor = 'transparent';
|
|
158
|
+
probe.style.marginTop = pcs.marginTop;
|
|
159
|
+
probe.style.marginRight = pcs.marginRight;
|
|
160
|
+
probe.style.marginBottom = pcs.marginBottom;
|
|
161
|
+
probe.style.marginLeft = pcs.marginLeft;
|
|
162
|
+
probe.style.transform = pcs.transform && pcs.transform !== 'none' ? pcs.transform : '';
|
|
163
|
+
probe.style.transformOrigin = pcs.transformOrigin || '';
|
|
164
|
+
// Pseudo lives logically inside the host; an absolute child of the host
|
|
165
|
+
// inherits the same containing-block lookup.
|
|
166
|
+
if (pseudo === '::before')
|
|
167
|
+
el.insertBefore(probe, el.firstChild);
|
|
168
|
+
else
|
|
169
|
+
el.appendChild(probe);
|
|
170
|
+
const r = probe.getBoundingClientRect();
|
|
171
|
+
probe.remove();
|
|
172
|
+
return r;
|
|
173
|
+
};
|
|
56
174
|
const pickQuoteChar = (forEl, isOpen) => {
|
|
57
175
|
// Count q-element ancestors above this element (depth=0 = the first q
|
|
58
176
|
// not inside another q). The pseudo lives ON forEl so when forEl IS a
|
|
@@ -113,6 +231,17 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
113
231
|
const content = pcs.content;
|
|
114
232
|
if (content == null || content === 'none' || content === 'normal' || content === '')
|
|
115
233
|
continue;
|
|
234
|
+
// DM-665 / DM-677: pseudos with computed `opacity: 0` paint nothing in
|
|
235
|
+
// Chrome (Material-style ripple / hover overlays use this — Google's
|
|
236
|
+
// `a.gb_C::before` is the empty-content variant we already skipped;
|
|
237
|
+
// `a.gb_A::before` on the mobile "Sign in" pill is the same idea but
|
|
238
|
+
// with `content: " "` (a single space, non-empty) so it slipped past
|
|
239
|
+
// the previous gate). Skip ALL opacity-zero pseudos before doing any
|
|
240
|
+
// measurement / box-rect work; capturing them anyway would paint an
|
|
241
|
+
// opaque box over the host's actual content.
|
|
242
|
+
const opacityNum = parseFloat(pcs.opacity);
|
|
243
|
+
if (Number.isFinite(opacityNum) && opacityNum === 0)
|
|
244
|
+
continue;
|
|
116
245
|
let text = '';
|
|
117
246
|
let imageUrl = '';
|
|
118
247
|
let i = 0;
|
|
@@ -161,16 +290,28 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
161
290
|
});
|
|
162
291
|
const cname = args[0];
|
|
163
292
|
const sep = isCounters ? (args[1] ?? '') : '';
|
|
164
|
-
//
|
|
165
|
-
// counter
|
|
166
|
-
//
|
|
293
|
+
// DM-788: third arg of counters() / second arg of counter() is a
|
|
294
|
+
// `<counter-style>` name. When that name matches a custom
|
|
295
|
+
// `@counter-style` rule captured in the pre-walk, run each value
|
|
296
|
+
// through the resolver so prefix / suffix / pad / negative / range
|
|
297
|
+
// / fallback descriptors apply — e.g. `counter(step, prefixed)`
|
|
298
|
+
// produces "Step 01: " instead of plain decimal "1".
|
|
299
|
+
const styleArg = isCounters ? args[2] : args[1];
|
|
300
|
+
const useCustomStyle = styleArg != null && styleArg !== ''
|
|
301
|
+
&& isCustomCounterStyle != null && isCustomCounterStyle(styleArg);
|
|
302
|
+
const format = (v) => {
|
|
303
|
+
if (!useCustomStyle)
|
|
304
|
+
return String(v);
|
|
305
|
+
const out = resolveCounterValue(styleArg, v);
|
|
306
|
+
return out != null ? out : String(v);
|
|
307
|
+
};
|
|
167
308
|
const snapshot = counterSnapshot.get(el) || [];
|
|
168
|
-
const matches = snapshot.filter((s) => s.name === cname).map((s) =>
|
|
309
|
+
const matches = snapshot.filter((s) => s.name === cname).map((s) => format(s.value));
|
|
169
310
|
if (isCounters) {
|
|
170
|
-
text += matches.length > 0 ? matches.join(sep) :
|
|
311
|
+
text += matches.length > 0 ? matches.join(sep) : format(0);
|
|
171
312
|
}
|
|
172
313
|
else {
|
|
173
|
-
text += matches.length > 0 ? matches[matches.length - 1] :
|
|
314
|
+
text += matches.length > 0 ? matches[matches.length - 1] : format(0);
|
|
174
315
|
}
|
|
175
316
|
i = closeIdx + 1;
|
|
176
317
|
}
|
|
@@ -202,13 +343,29 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
202
343
|
// side or a visible background.
|
|
203
344
|
const bgRaw = pcs.backgroundColor;
|
|
204
345
|
const hasBg = bgRaw && bgRaw !== '' && bgRaw !== 'rgba(0, 0, 0, 0)' && bgRaw !== 'transparent';
|
|
346
|
+
// DM-767: capture background-image (linear-gradient / radial-gradient /
|
|
347
|
+
// url) on empty-content pseudos too. The `.corner::after` accent stripe
|
|
348
|
+
// pattern in `24-deep-pseudo-shapes` is an absolutely-positioned 4 px
|
|
349
|
+
// strip with a `linear-gradient` background and no color / border —
|
|
350
|
+
// without this check the pseudoBox emit was skipped entirely.
|
|
351
|
+
const bgImgRaw = pcs.backgroundImage;
|
|
352
|
+
const hasBgImg = bgImgRaw != null && bgImgRaw !== '' && bgImgRaw !== 'none';
|
|
205
353
|
const bwT = parseFloat(pcs.borderTopWidth) || 0;
|
|
206
354
|
const bwR = parseFloat(pcs.borderRightWidth) || 0;
|
|
207
355
|
const bwB = parseFloat(pcs.borderBottomWidth) || 0;
|
|
208
356
|
const bwL = parseFloat(pcs.borderLeftWidth) || 0;
|
|
209
357
|
const hasBorder = bwT > 0 || bwR > 0 || bwB > 0 || bwL > 0;
|
|
210
358
|
const isBlockLike = pcs.display === 'block' || pcs.display === 'inline-block' || pcs.display === 'flex';
|
|
211
|
-
|
|
359
|
+
// DM-665: `opacity: 0` pseudos paint nothing (Material-style ripple
|
|
360
|
+
// / hover overlays use this pattern — Google's `a.gb_C::before` is
|
|
361
|
+
// a 40×40 dark-grey absolute box that's invisible at rest). Capturing
|
|
362
|
+
// them anyway would paint an opaque box over the host's content
|
|
363
|
+
// (the apps-grid SVG underneath the anchor). Skip empty-content
|
|
364
|
+
// pseudos whose `opacity: 0` makes them visually a no-op.
|
|
365
|
+
const opacityNum = parseFloat(pcs.opacity);
|
|
366
|
+
if (Number.isFinite(opacityNum) && opacityNum === 0)
|
|
367
|
+
continue;
|
|
368
|
+
if (isBlockLike && (hasBg || hasBgImg || hasBorder)) {
|
|
212
369
|
const hostPadL = parseFloat(cs.paddingLeft) || 0;
|
|
213
370
|
const hostPadT = parseFloat(cs.paddingTop) || 0;
|
|
214
371
|
const hostBorL = parseFloat(cs.borderLeftWidth) || 0;
|
|
@@ -235,44 +392,100 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
235
392
|
let borderBoxX;
|
|
236
393
|
let borderBoxY;
|
|
237
394
|
if (pcs.position === 'absolute' || pcs.position === 'fixed') {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!isNaN(pcsTop))
|
|
253
|
-
borderBoxY = paddingBoxT + pcsTop;
|
|
254
|
-
else if (!isNaN(pcsBottom))
|
|
255
|
-
borderBoxY = paddingBoxB - pcsBottom - borderBoxH;
|
|
256
|
-
else
|
|
257
|
-
borderBoxY = paddingBoxT;
|
|
395
|
+
// Use a real positioned sentinel to find the pseudo's true painted
|
|
396
|
+
// rect. Chrome's containing-block lookup walks up from the host
|
|
397
|
+
// looking for a positioned ancestor (or a transformed / filtered
|
|
398
|
+
// / contained ancestor); the same lookup applies to a real child
|
|
399
|
+
// of the host. Probing avoids re-implementing that walk + all the
|
|
400
|
+
// containing-block-establishing properties. NYT mobile's nav fade-
|
|
401
|
+
// out `.css-sdhjrl::after` (`position: absolute; right: 0`) on a
|
|
402
|
+
// `position: static` NAV is the trigger case — the pseudo's
|
|
403
|
+
// resolved `top` / `left` are relative to a far-up positioned
|
|
404
|
+
// ancestor, not the NAV, so the prior additive math placed the
|
|
405
|
+
// gradient thousands of pixels off the NAV.
|
|
406
|
+
const pr = probePseudoAbsoluteBoxRect(el, pseudo, pcs);
|
|
407
|
+
borderBoxX = pr.left - vp.x;
|
|
408
|
+
borderBoxY = pr.top - vp.y;
|
|
258
409
|
}
|
|
259
410
|
else {
|
|
260
411
|
const pMarT = parseFloat(pcs.marginTop) || 0;
|
|
261
412
|
borderBoxX = rect.left - vp.x + hostBorL + hostPadL + pMarL;
|
|
262
413
|
borderBoxY = rect.top - vp.y + hostBorT + hostPadT + pMarT;
|
|
414
|
+
// DM-768: static `display: inline-block` (and inline-flex / inline-grid /
|
|
415
|
+
// inline-table) pseudos participate in Chrome's inline vertical-align
|
|
416
|
+
// math — the formula above ignores `vertical-align` and pins the box to
|
|
417
|
+
// the host's content-area top, which is 6-7 px too high for a typical
|
|
418
|
+
// `vertical-align: middle` down-caret. Probe with a real sentinel that
|
|
419
|
+
// mirrors the pseudo's box properties.
|
|
420
|
+
//
|
|
421
|
+
// CSS render order on the line is: ::before → real children → ::after.
|
|
422
|
+
// The sentinel is a real child:
|
|
423
|
+
// - For ::before, the sentinel renders AFTER the pseudo. The
|
|
424
|
+
// pseudo's own position is unchanged; the sentinel just shifts
|
|
425
|
+
// subsequent content. So the pseudo's border-box left =
|
|
426
|
+
// probe.left − pMarR − borderBoxW − pMarL (back out the sentinel
|
|
427
|
+
// gap), and the pseudo's top equals probe.top (both lay out
|
|
428
|
+
// on the same line with matching `vertical-align`).
|
|
429
|
+
// - For ::after, the sentinel renders BEFORE the pseudo. Without
|
|
430
|
+
// the sentinel, the pseudo would take the slot the sentinel
|
|
431
|
+
// now occupies, so the pseudo's border-box left = probe.left
|
|
432
|
+
// and top = probe.top.
|
|
433
|
+
const dispIsInline = pcs.display === 'inline-block' || pcs.display === 'inline-flex' || pcs.display === 'inline-grid' || pcs.display === 'inline-table';
|
|
434
|
+
if (dispIsInline) {
|
|
435
|
+
const pr = probePseudoStaticBoxRect(el, pseudo, pcs);
|
|
436
|
+
borderBoxY = pr.top - vp.y;
|
|
437
|
+
if (pseudo === '::after') {
|
|
438
|
+
borderBoxX = pr.left - vp.x;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const pMarR = parseFloat(pcs.marginRight) || 0;
|
|
442
|
+
borderBoxX = pr.left - vp.x - pMarR - borderBoxW - pMarL;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// DM-710: if the host has a CSS transform whose 2D submatrix is
|
|
447
|
+
// singular (zero determinant), the host's painted area collapses
|
|
448
|
+
// to a point / line and the pseudo paints nothing visible —
|
|
449
|
+
// Apple's `.globalnav-bag-badge` carries `transform: matrix(0, 0,
|
|
450
|
+
// 0, 0, 0, 0)` as the "no items in cart" state, and the empty
|
|
451
|
+
// ::before with `width: 13px; background: black; border-radius:
|
|
452
|
+
// 13px` would otherwise emit as a visible dot. Skip the pseudoBox
|
|
453
|
+
// in that case; the live-rect model already drops the host itself.
|
|
454
|
+
let degenerateHostTransform = false;
|
|
455
|
+
if (cs.transform && cs.transform !== 'none') {
|
|
456
|
+
const m2 = /^matrix\(\s*([-\d.eE]+)\s*,\s*([-\d.eE]+)\s*,\s*([-\d.eE]+)\s*,\s*([-\d.eE]+)/.exec(cs.transform);
|
|
457
|
+
if (m2) {
|
|
458
|
+
const a = parseFloat(m2[1]);
|
|
459
|
+
const b = parseFloat(m2[2]);
|
|
460
|
+
const c = parseFloat(m2[3]);
|
|
461
|
+
const d = parseFloat(m2[4]);
|
|
462
|
+
if (Math.abs(a * d - b * c) < 1e-9)
|
|
463
|
+
degenerateHostTransform = true;
|
|
464
|
+
}
|
|
263
465
|
}
|
|
264
|
-
if (borderBoxW > 0 && borderBoxH > 0) {
|
|
466
|
+
if (borderBoxW > 0 && borderBoxH > 0 && !degenerateHostTransform) {
|
|
467
|
+
// DM-783: the pseudo's own `transform` (rotate/scale/translate/
|
|
468
|
+
// matrix) wraps the pseudoBox at render time. getComputedStyle
|
|
469
|
+
// returns the resolved matrix() form, and transformOrigin returns
|
|
470
|
+
// resolved px values relative to the pseudo's box top-left — both
|
|
471
|
+
// can be pasted directly into an SVG `<g>` wrapper. Captured only
|
|
472
|
+
// when non-`none` to keep the captured tree compact.
|
|
473
|
+
const pcsTransform = pcs.transform && pcs.transform !== 'none' ? pcs.transform : undefined;
|
|
474
|
+
const pcsTransformOrigin = pcsTransform != null ? (pcs.transformOrigin || undefined) : undefined;
|
|
265
475
|
pseudoBoxes.push({
|
|
266
476
|
x: borderBoxX,
|
|
267
477
|
y: borderBoxY,
|
|
268
478
|
width: borderBoxW,
|
|
269
479
|
height: borderBoxH,
|
|
270
480
|
backgroundColor: hasBg ? normColor(bgRaw) : undefined,
|
|
481
|
+
backgroundImage: hasBgImg ? bgImgRaw : undefined,
|
|
271
482
|
borderTopWidth: bwT, borderTopColor: bwT > 0 ? normColor(pcs.borderTopColor) : undefined, borderTopStyle: pcs.borderTopStyle,
|
|
272
483
|
borderRightWidth: bwR, borderRightColor: bwR > 0 ? normColor(pcs.borderRightColor) : undefined, borderRightStyle: pcs.borderRightStyle,
|
|
273
484
|
borderBottomWidth: bwB, borderBottomColor: bwB > 0 ? normColor(pcs.borderBottomColor) : undefined, borderBottomStyle: pcs.borderBottomStyle,
|
|
274
485
|
borderLeftWidth: bwL, borderLeftColor: bwL > 0 ? normColor(pcs.borderLeftColor) : undefined, borderLeftStyle: pcs.borderLeftStyle,
|
|
275
486
|
borderRadius: parseFloat(pcs.borderRadius) || 0,
|
|
487
|
+
transform: pcsTransform,
|
|
488
|
+
transformOrigin: pcsTransformOrigin,
|
|
276
489
|
});
|
|
277
490
|
}
|
|
278
491
|
}
|
|
@@ -332,22 +545,12 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
332
545
|
}
|
|
333
546
|
if (text === '')
|
|
334
547
|
continue;
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// canvas.measureText when available. For position:absolute pseudos
|
|
342
|
-
// with auto-width Chrome shrink-to-fits the box and
|
|
343
|
-
// getComputedStyle returns the resolved content-box width.
|
|
344
|
-
// canvas.measureText drifts ~1-2px from Chrome's actual layout in
|
|
345
|
-
// common bold / symbol-mix fixtures because the canvas font-
|
|
346
|
-
// shaping path differs slightly from Chrome's HarfBuzz paint
|
|
347
|
-
// pipeline. Falling back to canvas measurement when pcs.width is
|
|
348
|
-
// unavailable (typical for non-positioned inline pseudos) keeps
|
|
349
|
-
// the existing path.
|
|
350
|
-
let pseudoWidth = mctx.measureText(text).width;
|
|
548
|
+
// DM-785: probe-span measurement matches Chrome's HarfBuzz-shaped
|
|
549
|
+
// layout width — canvas.measureText drifted ~1-3px on bold uppercase
|
|
550
|
+
// short strings (visible on rotated gradient pills as the text
|
|
551
|
+
// overflowing the badge). DM-507 numeric-pcs.width override still
|
|
552
|
+
// wins when the pseudo's box has an authored fixed width.
|
|
553
|
+
let pseudoWidth = probePseudoTextWidth(text, pcs);
|
|
351
554
|
if (pcs.position === 'absolute' || pcs.position === 'fixed') {
|
|
352
555
|
const pcsW = parseFloat(pcs.width);
|
|
353
556
|
if (!isNaN(pcsW) && pcsW > 0)
|
|
@@ -449,12 +652,15 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
449
652
|
width: pseudoWidth,
|
|
450
653
|
height: elFontSize,
|
|
451
654
|
// Carry pseudo-specific typography so the renderer can respect
|
|
452
|
-
// per-pseudo color, font-size, font-weight, font-family
|
|
453
|
-
// lets pseudos style independently of their parent
|
|
655
|
+
// per-pseudo color, font-size, font-weight, font-family, font-style
|
|
656
|
+
// (CSS lets pseudos style independently of their parent — Slashdot's
|
|
657
|
+
// "Most Discussed" carousel heading is a ::after that's italic+bordered
|
|
658
|
+
// on a non-italic host div).
|
|
454
659
|
color: pcs.color,
|
|
455
660
|
fontSize: elFontSize,
|
|
456
661
|
fontWeight: pcs.fontWeight,
|
|
457
662
|
fontFamily: pcs.fontFamily,
|
|
663
|
+
fontStyle: pcs.fontStyle,
|
|
458
664
|
fontAscent: pseudoMetrics.ascent,
|
|
459
665
|
};
|
|
460
666
|
// DM-497: stash pseudo's own background / border-radius on the
|
|
@@ -466,26 +672,49 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
466
672
|
const pseudoBgColor = pseudoBgRaw && pseudoBgRaw !== '' && pseudoBgRaw !== 'rgba(0, 0, 0, 0)' && pseudoBgRaw !== 'transparent'
|
|
467
673
|
? normColor(pseudoBgRaw) : '';
|
|
468
674
|
const pseudoBR = parseFloat(pcs.borderRadius) || 0;
|
|
469
|
-
// Capture a uniform border when all four sides match
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
675
|
+
// Capture a uniform border when all four sides match (renders as
|
|
676
|
+
// `<rect stroke=…>`). When a single side carries a border (e.g.
|
|
677
|
+
// `border-bottom: 1px solid rgba(255,255,255,0.5)` on Slashdot's
|
|
678
|
+
// `.carouselHeading::after`) we still capture per-side widths +
|
|
679
|
+
// colors so the renderer can emit a `<line>` for the visible side.
|
|
680
|
+
const bwTop = parseFloat(pcs.borderTopWidth) || 0;
|
|
681
|
+
const bwRight = parseFloat(pcs.borderRightWidth) || 0;
|
|
682
|
+
const bwBottom = parseFloat(pcs.borderBottomWidth) || 0;
|
|
683
|
+
const bwLeft = parseFloat(pcs.borderLeftWidth) || 0;
|
|
684
|
+
const bwUniform = bwTop > 0 && bwRight === bwTop && bwBottom === bwTop && bwLeft === bwTop;
|
|
477
685
|
const pseudoBC = bwUniform ? normColor(pcs.borderTopColor) : '';
|
|
686
|
+
const colorIsPaintable = (raw) => raw !== '' && raw !== 'rgba(0, 0, 0, 0)' && raw !== 'transparent';
|
|
687
|
+
const sideBorderTopColor = bwTop > 0 ? normColor(pcs.borderTopColor) : '';
|
|
688
|
+
const sideBorderRightColor = bwRight > 0 ? normColor(pcs.borderRightColor) : '';
|
|
689
|
+
const sideBorderBottomColor = bwBottom > 0 ? normColor(pcs.borderBottomColor) : '';
|
|
690
|
+
const sideBorderLeftColor = bwLeft > 0 ? normColor(pcs.borderLeftColor) : '';
|
|
691
|
+
const hasPerSideBorder = !bwUniform && ((bwTop > 0 && colorIsPaintable(sideBorderTopColor))
|
|
692
|
+
|| (bwRight > 0 && colorIsPaintable(sideBorderRightColor))
|
|
693
|
+
|| (bwBottom > 0 && colorIsPaintable(sideBorderBottomColor))
|
|
694
|
+
|| (bwLeft > 0 && colorIsPaintable(sideBorderLeftColor)));
|
|
695
|
+
// DM-782: background-image (linear-gradient / radial-gradient / url())
|
|
696
|
+
// on text-content pseudos. The empty-content path already plumbs this
|
|
697
|
+
// (DM-767); the text-content path was dropping it, so "gradient badge"
|
|
698
|
+
// patterns (`.tier.popular::before { content: "MOST POPULAR"; background:
|
|
699
|
+
// linear-gradient(135deg, ...) }`) lost the pill bg behind the white
|
|
700
|
+
// glyphs.
|
|
701
|
+
const pseudoBgImgRaw = pcs.backgroundImage;
|
|
702
|
+
const hasPseudoBgImg = pseudoBgImgRaw != null && pseudoBgImgRaw !== '' && pseudoBgImgRaw !== 'none';
|
|
703
|
+
// DM-783: pseudo's own `transform` (rotate/scale/translate/matrix)
|
|
704
|
+
// wraps both the paint box AND the glyph emit at render time.
|
|
705
|
+
const pseudoTransform = pcs.transform && pcs.transform !== 'none' ? pcs.transform : undefined;
|
|
706
|
+
const pseudoTransformOrigin = pseudoTransform != null ? (pcs.transformOrigin || undefined) : undefined;
|
|
478
707
|
let pseudoBoxStyles = null;
|
|
479
|
-
if (pseudoBgColor !== '' || pseudoBR > 0 || (bwUniform && pseudoBC !== '' && pseudoBC !== 'rgba(0, 0, 0, 0)')) {
|
|
708
|
+
if (pseudoBgColor !== '' || hasPseudoBgImg || pseudoBR > 0 || (bwUniform && pseudoBC !== '' && pseudoBC !== 'rgba(0, 0, 0, 0)') || hasPerSideBorder || pseudoTransform != null) {
|
|
480
709
|
pseudoBoxStyles = {
|
|
481
710
|
padL: parseFloat(pcs.paddingLeft) || 0,
|
|
482
711
|
padR: parseFloat(pcs.paddingRight) || 0,
|
|
483
712
|
padT: parseFloat(pcs.paddingTop) || 0,
|
|
484
713
|
padB: parseFloat(pcs.paddingBottom) || 0,
|
|
485
|
-
borL:
|
|
486
|
-
borR:
|
|
487
|
-
borT:
|
|
488
|
-
borB:
|
|
714
|
+
borL: bwLeft,
|
|
715
|
+
borR: bwRight,
|
|
716
|
+
borT: bwTop,
|
|
717
|
+
borB: bwBottom,
|
|
489
718
|
// Inline-box bg paints at line-height, not at font-size — so
|
|
490
719
|
// the box's vertical extent is lineH + padding + border (not
|
|
491
720
|
// fontSize). Capture lineH alongside the metrics; the post-
|
|
@@ -494,9 +723,20 @@ export const createPseudoContentHandler = ({ vp, normColor, measureFontMetrics,
|
|
|
494
723
|
lineH,
|
|
495
724
|
fontSize: elFontSize,
|
|
496
725
|
backgroundColor: pseudoBgColor !== '' ? pseudoBgColor : undefined,
|
|
726
|
+
backgroundImage: hasPseudoBgImg ? pseudoBgImgRaw : undefined,
|
|
497
727
|
borderRadius: pseudoBR > 0 ? pseudoBR : undefined,
|
|
498
|
-
borderWidth: bwUniform ?
|
|
728
|
+
borderWidth: bwUniform ? bwTop : undefined,
|
|
499
729
|
borderColor: bwUniform && pseudoBC !== '' && pseudoBC !== 'rgba(0, 0, 0, 0)' ? pseudoBC : undefined,
|
|
730
|
+
transform: pseudoTransform,
|
|
731
|
+
transformOrigin: pseudoTransformOrigin,
|
|
732
|
+
// Per-side colors. Renderer reads these when no uniform border
|
|
733
|
+
// is set and emits a `<line>` for each side whose width > 0 and
|
|
734
|
+
// color is paintable. Undefined when the side has no visible
|
|
735
|
+
// border, keeping the captured tree compact in the common case.
|
|
736
|
+
borderTopColor: hasPerSideBorder && bwTop > 0 && colorIsPaintable(sideBorderTopColor) ? sideBorderTopColor : undefined,
|
|
737
|
+
borderRightColor: hasPerSideBorder && bwRight > 0 && colorIsPaintable(sideBorderRightColor) ? sideBorderRightColor : undefined,
|
|
738
|
+
borderBottomColor: hasPerSideBorder && bwBottom > 0 && colorIsPaintable(sideBorderBottomColor) ? sideBorderBottomColor : undefined,
|
|
739
|
+
borderLeftColor: hasPerSideBorder && bwLeft > 0 && colorIsPaintable(sideBorderLeftColor) ? sideBorderLeftColor : undefined,
|
|
500
740
|
};
|
|
501
741
|
}
|
|
502
742
|
// If the pseudo contains any codepoint Chrome paints via a color-
|
|
@@ -168,9 +168,29 @@ export const createPseudoInjectHandler = () => {
|
|
|
168
168
|
p.seg.pseudoBox = {
|
|
169
169
|
x: bx, y: boxTop, width: bw, height: bh,
|
|
170
170
|
backgroundColor: bs.backgroundColor,
|
|
171
|
+
// DM-782: gradient/url() bg-image plumbing — renderer threads
|
|
172
|
+
// each comma-separated layer through `buildBackgroundLayerDef`
|
|
173
|
+
// and paints rect(s) behind the glyphs (mirrors the empty-
|
|
174
|
+
// content pseudoBox path in `element-tree-to-svg.ts`).
|
|
175
|
+
backgroundImage: bs.backgroundImage,
|
|
171
176
|
borderRadius: bs.borderRadius,
|
|
172
177
|
borderWidth: bs.borderWidth,
|
|
173
178
|
borderColor: bs.borderColor,
|
|
179
|
+
// Per-side widths + colors for non-uniform borders (e.g. a
|
|
180
|
+
// bare `border-bottom` on a pseudo). Width fields are always
|
|
181
|
+
// emitted so the renderer doesn't have to fall back to zero
|
|
182
|
+
// when a `borderWidth` (uniform) shorthand is absent.
|
|
183
|
+
borL: bs.borL, borR: bs.borR, borT: bs.borT, borB: bs.borB,
|
|
184
|
+
borderTopColor: bs.borderTopColor,
|
|
185
|
+
borderRightColor: bs.borderRightColor,
|
|
186
|
+
borderBottomColor: bs.borderBottomColor,
|
|
187
|
+
borderLeftColor: bs.borderLeftColor,
|
|
188
|
+
// DM-783: pseudo's `transform` + `transformOrigin`. Renderer
|
|
189
|
+
// wraps the box + glyphs in a pre-baked
|
|
190
|
+
// translate-(transform)-translate matrix so the rotation/scale
|
|
191
|
+
// pivots around the box-relative origin instead of (0,0).
|
|
192
|
+
transform: bs.transform,
|
|
193
|
+
transformOrigin: bs.transformOrigin,
|
|
174
194
|
};
|
|
175
195
|
}
|
|
176
196
|
}
|