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,25 +1,10 @@
|
|
|
1
|
-
export declare const createBordersBackgroundsHandler: ({ normColor, resolvePlaceholderShownBg, resolveCornerRadius }: {
|
|
1
|
+
export declare const createBordersBackgroundsHandler: ({ normColor, normGradientColors, resolvePlaceholderShownBg, resolveCornerRadius }: {
|
|
2
2
|
normColor: any;
|
|
3
|
+
normGradientColors: any;
|
|
3
4
|
resolvePlaceholderShownBg: any;
|
|
4
5
|
resolveCornerRadius: any;
|
|
5
6
|
}) => {
|
|
6
7
|
captureBordersBackgrounds: (el: any, cs: any, tag: any, rect: any, isPlaceholderCapture: any) => {
|
|
7
|
-
backgroundColor: any;
|
|
8
|
-
borderColor: any;
|
|
9
|
-
borderWidth: any;
|
|
10
|
-
borderRadius: any;
|
|
11
|
-
borderTopLeftRadius: any;
|
|
12
|
-
borderTopRightRadius: any;
|
|
13
|
-
borderBottomRightRadius: any;
|
|
14
|
-
borderBottomLeftRadius: any;
|
|
15
|
-
borderTopWidth: any;
|
|
16
|
-
borderRightWidth: any;
|
|
17
|
-
borderBottomWidth: any;
|
|
18
|
-
borderLeftWidth: any;
|
|
19
|
-
borderTopStyle: any;
|
|
20
|
-
borderRightStyle: any;
|
|
21
|
-
borderBottomStyle: any;
|
|
22
|
-
borderLeftStyle: any;
|
|
23
8
|
borderTopColor: any;
|
|
24
9
|
borderRightColor: any;
|
|
25
10
|
borderBottomColor: any;
|
|
@@ -31,7 +16,12 @@ export declare const createBordersBackgroundsHandler: ({ normColor, resolvePlace
|
|
|
31
16
|
backgroundPosition: any;
|
|
32
17
|
backgroundRepeat: any;
|
|
33
18
|
backgroundClip: any;
|
|
19
|
+
backgroundBlendMode: any;
|
|
34
20
|
webkitTextFillColor: any;
|
|
21
|
+
inheritedTextFillGradient: string | undefined;
|
|
22
|
+
webkitTextStrokeWidth: any;
|
|
23
|
+
webkitTextStrokeColor: any;
|
|
24
|
+
paintOrder: any;
|
|
35
25
|
backgroundOrigin: any;
|
|
36
26
|
backgroundAttachment: any;
|
|
37
27
|
backgroundIntrinsic: ({
|
|
@@ -50,5 +40,22 @@ export declare const createBordersBackgroundsHandler: ({ normColor, resolvePlace
|
|
|
50
40
|
outlineColor: any;
|
|
51
41
|
outlineOffset: any;
|
|
52
42
|
boxShadow: any;
|
|
43
|
+
boxDecorationBreak: any;
|
|
44
|
+
borderTopStyle: any;
|
|
45
|
+
borderRightStyle: any;
|
|
46
|
+
borderBottomStyle: any;
|
|
47
|
+
borderLeftStyle: any;
|
|
48
|
+
backgroundColor: any;
|
|
49
|
+
borderColor: any;
|
|
50
|
+
borderWidth: any;
|
|
51
|
+
borderRadius: any;
|
|
52
|
+
borderTopLeftRadius: any;
|
|
53
|
+
borderTopRightRadius: any;
|
|
54
|
+
borderBottomRightRadius: any;
|
|
55
|
+
borderBottomLeftRadius: any;
|
|
56
|
+
borderTopWidth: any;
|
|
57
|
+
borderRightWidth: any;
|
|
58
|
+
borderBottomWidth: any;
|
|
59
|
+
borderLeftWidth: any;
|
|
53
60
|
};
|
|
54
61
|
};
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
//
|
|
40
40
|
// - **borderImageIntrinsicWidth / Height**: same pattern for border-
|
|
41
41
|
// image-source url().
|
|
42
|
-
export const createBordersBackgroundsHandler = ({ normColor, resolvePlaceholderShownBg, resolveCornerRadius }) => {
|
|
42
|
+
export const createBordersBackgroundsHandler = ({ normColor, normGradientColors, resolvePlaceholderShownBg, resolveCornerRadius }) => {
|
|
43
43
|
const isUaColorBorder = (tag, el, cs, side) => tag === 'input' && el.type === 'color'
|
|
44
44
|
&& normColor(cs[side], cs.color).replace(/\s+/g, '') === 'rgb(0,0,0)';
|
|
45
45
|
const tintedBorderColor = (tag, el, cs, side) => isUaColorBorder(tag, el, cs, side) ? 'rgb(118,118,118)' : normColor(cs[side], cs.color);
|
|
@@ -90,10 +90,26 @@ export const createBordersBackgroundsHandler = ({ normColor, resolvePlaceholderS
|
|
|
90
90
|
}
|
|
91
91
|
layers.push(bgImage.slice(start));
|
|
92
92
|
return layers.map((layer) => {
|
|
93
|
+
// DM-759: `image-set(url(...) 1x, url(...) 2x, ...)` layers wrap their
|
|
94
|
+
// url() candidates inside the function call; the top-level regex
|
|
95
|
+
// below only matches a bare `url(...)` at layer start. Probe the
|
|
96
|
+
// FIRST url() inside the image-set instead so the renderer's `cover`
|
|
97
|
+
// / `contain` math has the right aspect ratio. Picking any candidate
|
|
98
|
+
// works because Chrome's image-set candidates are conventionally the
|
|
99
|
+
// SAME image at different resolutions / formats, so the intrinsic
|
|
100
|
+
// aspect is consistent across them; the absolute scale may differ
|
|
101
|
+
// by the 1x/2x factor but `cover` / `contain` are aspect-driven.
|
|
102
|
+
let searchLayer = layer;
|
|
103
|
+
const imgSet = /^\s*(?:-webkit-)?image-set\(\s*([\s\S]+)\s*\)\s*$/i.exec(layer);
|
|
104
|
+
if (imgSet != null)
|
|
105
|
+
searchLayer = imgSet[1];
|
|
93
106
|
// Match all three url() forms: "...", '...', and bare. Data: URLs
|
|
94
107
|
// with embedded HTML attribute quotes (escaped as \") were silently
|
|
95
108
|
// truncated by a prior single-regex implementation. DM-308.
|
|
96
|
-
|
|
109
|
+
// For image-set candidates the url() may appear anywhere in the
|
|
110
|
+
// inner string, so anchor at the start of the SEARCH LAYER but
|
|
111
|
+
// allow leading whitespace.
|
|
112
|
+
const u = /\burl\(\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^)\s]+))\s*\)/.exec(searchLayer);
|
|
97
113
|
if (u == null)
|
|
98
114
|
return null;
|
|
99
115
|
const raw = u[1] || u[2] || u[3] || '';
|
|
@@ -115,6 +131,55 @@ export const createBordersBackgroundsHandler = ({ normColor, resolvePlaceholderS
|
|
|
115
131
|
img.src = m[1];
|
|
116
132
|
return img[dim] || undefined;
|
|
117
133
|
};
|
|
134
|
+
// DM-690: CSS 2.1 §17.6.2.1 — `border-style: hidden` has the highest
|
|
135
|
+
// precedence in `border-collapse: collapse` mode and SUPPRESSES the
|
|
136
|
+
// neighbor cell's matching border too. Walk the table grid neighbors and
|
|
137
|
+
// return per-side flags so the caller can rewrite this cell's border-side
|
|
138
|
+
// styles to `'hidden'` (which the renderer already skips). Scope: simple
|
|
139
|
+
// tables (no rowspan / colspan); good enough for the bug-report fixture
|
|
140
|
+
// `04-deep-border-conflict` and the common-case marketing tables.
|
|
141
|
+
const cellHiddenNeighbors = (el, tag, cs) => {
|
|
142
|
+
const out = { top: false, right: false, bottom: false, left: false };
|
|
143
|
+
if ((tag !== 'td' && tag !== 'th') || cs.borderCollapse !== 'collapse')
|
|
144
|
+
return out;
|
|
145
|
+
const tr = el.parentElement;
|
|
146
|
+
if (tr == null || tr.tagName !== 'TR')
|
|
147
|
+
return out;
|
|
148
|
+
const rowCells = Array.from(tr.children).filter((c) => c.tagName === 'TD' || c.tagName === 'TH');
|
|
149
|
+
const colIdx = rowCells.indexOf(el);
|
|
150
|
+
const hiddenSide = (cell, side) => {
|
|
151
|
+
if (cell == null)
|
|
152
|
+
return false;
|
|
153
|
+
const cs2 = getComputedStyle(cell);
|
|
154
|
+
return cs2['border' + side + 'Style'] === 'hidden';
|
|
155
|
+
};
|
|
156
|
+
if (hiddenSide(rowCells[colIdx - 1], 'Right'))
|
|
157
|
+
out.left = true;
|
|
158
|
+
if (hiddenSide(rowCells[colIdx + 1], 'Left'))
|
|
159
|
+
out.right = true;
|
|
160
|
+
// Resolve the surrounding table to walk row-neighbors across
|
|
161
|
+
// thead/tbody/tfoot sections.
|
|
162
|
+
let table = tr.parentElement;
|
|
163
|
+
while (table != null && table.tagName !== 'TABLE')
|
|
164
|
+
table = table.parentElement;
|
|
165
|
+
if (table != null) {
|
|
166
|
+
const allRows = Array.from(table.querySelectorAll('tr')).filter((t) => t.closest('table') === table);
|
|
167
|
+
const rowIdx = allRows.indexOf(tr);
|
|
168
|
+
const above = allRows[rowIdx - 1];
|
|
169
|
+
const below = allRows[rowIdx + 1];
|
|
170
|
+
if (above != null) {
|
|
171
|
+
const aboveCells = Array.from(above.children).filter((c) => c.tagName === 'TD' || c.tagName === 'TH');
|
|
172
|
+
if (hiddenSide(aboveCells[colIdx], 'Bottom'))
|
|
173
|
+
out.top = true;
|
|
174
|
+
}
|
|
175
|
+
if (below != null) {
|
|
176
|
+
const belowCells = Array.from(below.children).filter((c) => c.tagName === 'TD' || c.tagName === 'TH');
|
|
177
|
+
if (hiddenSide(belowCells[colIdx], 'Top'))
|
|
178
|
+
out.bottom = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
};
|
|
118
183
|
const captureBordersBackgrounds = (el, cs, tag, rect, isPlaceholderCapture) => ({
|
|
119
184
|
backgroundColor: (function () {
|
|
120
185
|
if (isPlaceholderCapture) {
|
|
@@ -135,25 +200,73 @@ export const createBordersBackgroundsHandler = ({ normColor, resolvePlaceholderS
|
|
|
135
200
|
borderRightWidth: cs.borderRightWidth,
|
|
136
201
|
borderBottomWidth: cs.borderBottomWidth,
|
|
137
202
|
borderLeftWidth: cs.borderLeftWidth,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
203
|
+
// DM-690: when an adjacent collapsed-table cell declares its matching
|
|
204
|
+
// side as `border-style: hidden`, CSS 2.1 §17.6.2.1 says we MUST treat
|
|
205
|
+
// our side as hidden too (precedence: `hidden > widest > ...`). Override
|
|
206
|
+
// here at capture time so the renderer's existing `style === 'hidden'`
|
|
207
|
+
// skip suppresses the paint on this side. (Cells whose OWN side is
|
|
208
|
+
// already hidden are unaffected — they pass through.)
|
|
209
|
+
...(function () {
|
|
210
|
+
const hn = cellHiddenNeighbors(el, tag, cs);
|
|
211
|
+
return {
|
|
212
|
+
borderTopStyle: hn.top ? 'hidden' : cs.borderTopStyle,
|
|
213
|
+
borderRightStyle: hn.right ? 'hidden' : cs.borderRightStyle,
|
|
214
|
+
borderBottomStyle: hn.bottom ? 'hidden' : cs.borderBottomStyle,
|
|
215
|
+
borderLeftStyle: hn.left ? 'hidden' : cs.borderLeftStyle,
|
|
216
|
+
};
|
|
217
|
+
})(),
|
|
142
218
|
borderTopColor: tintedBorderColor(tag, el, cs, 'borderTopColor'),
|
|
143
219
|
borderRightColor: tintedBorderColor(tag, el, cs, 'borderRightColor'),
|
|
144
220
|
borderBottomColor: tintedBorderColor(tag, el, cs, 'borderBottomColor'),
|
|
145
221
|
borderLeftColor: tintedBorderColor(tag, el, cs, 'borderLeftColor'),
|
|
146
222
|
borderCollapse: cs.borderCollapse,
|
|
147
223
|
frostedBgFallback: computeFrostedBgFallback(cs),
|
|
148
|
-
backgroundImage: cs.backgroundImage,
|
|
224
|
+
backgroundImage: normGradientColors(cs.backgroundImage, cs.color),
|
|
149
225
|
backgroundSize: cs.backgroundSize,
|
|
150
226
|
backgroundPosition: cs.backgroundPosition,
|
|
151
227
|
backgroundRepeat: cs.backgroundRepeat,
|
|
152
228
|
backgroundClip: cs.backgroundClip,
|
|
229
|
+
backgroundBlendMode: cs.backgroundBlendMode,
|
|
153
230
|
// DM-462: -webkit-text-fill-color is the property that actually makes
|
|
154
231
|
// the headline text transparent in the background-clip:text idiom
|
|
155
232
|
// (cs.color may still report a normal value).
|
|
156
233
|
webkitTextFillColor: cs.webkitTextFillColor || cs.WebkitTextFillColor || undefined,
|
|
234
|
+
// DM-749: Stripe's keynote-speaker headline pattern — a span with
|
|
235
|
+
// `background-image: <gradient>; background-clip: text; -webkit-text-
|
|
236
|
+
// fill-color: transparent` wraps a child div that holds the actual
|
|
237
|
+
// text. The gradient is on the parent but Chrome lets it paint through
|
|
238
|
+
// the child's glyphs because background-clip: text masks the gradient
|
|
239
|
+
// by the union of all descendant text shapes. When the element's own
|
|
240
|
+
// bg-image is none AND its text-fill-color is transparent AND an
|
|
241
|
+
// ancestor has background-clip: text with a gradient, capture that
|
|
242
|
+
// ancestor's gradient so the renderer can use it as the glyph fill.
|
|
243
|
+
inheritedTextFillGradient: (function () {
|
|
244
|
+
const ownTfc = cs.webkitTextFillColor || cs.WebkitTextFillColor || '';
|
|
245
|
+
// Only meaningful when our own text is transparent.
|
|
246
|
+
if (!/^(rgba\(0[^)]*?,\s*0\)|transparent)$/i.test(ownTfc.trim()))
|
|
247
|
+
return undefined;
|
|
248
|
+
// Walk up at most 8 ancestors looking for `background-clip: text`
|
|
249
|
+
// + a non-none `background-image`. 8 covers the Stripe hds-heading
|
|
250
|
+
// depth-of-2 nesting comfortably without scanning the whole tree.
|
|
251
|
+
let p = el.parentElement;
|
|
252
|
+
let depth = 0;
|
|
253
|
+
while (p != null && depth < 8) {
|
|
254
|
+
const pcs = window.getComputedStyle(p);
|
|
255
|
+
const bc = (pcs.backgroundClip || '') + ' ' + (pcs.webkitBackgroundClip || '');
|
|
256
|
+
if (/\btext\b/i.test(bc) && pcs.backgroundImage && pcs.backgroundImage !== 'none' && pcs.backgroundImage !== '') {
|
|
257
|
+
return pcs.backgroundImage;
|
|
258
|
+
}
|
|
259
|
+
p = p.parentElement;
|
|
260
|
+
depth++;
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
})(),
|
|
264
|
+
// DM-719: `-webkit-text-stroke-width` / `-webkit-text-stroke-color` paint a
|
|
265
|
+
// stroke around each glyph outline. Captured so the renderer can add a
|
|
266
|
+
// `stroke` attribute to the text-path emission.
|
|
267
|
+
webkitTextStrokeWidth: cs.webkitTextStrokeWidth || cs.WebkitTextStrokeWidth || undefined,
|
|
268
|
+
webkitTextStrokeColor: cs.webkitTextStrokeColor || cs.WebkitTextStrokeColor || undefined,
|
|
269
|
+
paintOrder: cs.paintOrder || undefined,
|
|
157
270
|
backgroundOrigin: cs.backgroundOrigin,
|
|
158
271
|
backgroundAttachment: cs.backgroundAttachment,
|
|
159
272
|
backgroundIntrinsic: computeBackgroundIntrinsic(cs),
|
|
@@ -169,6 +282,9 @@ export const createBordersBackgroundsHandler = ({ normColor, resolvePlaceholderS
|
|
|
169
282
|
outlineColor: normColor(cs.outlineColor),
|
|
170
283
|
outlineOffset: cs.outlineOffset,
|
|
171
284
|
boxShadow: cs.boxShadow,
|
|
285
|
+
// box-decoration-break: 'slice' (default) vs 'clone'. Drives per-fragment
|
|
286
|
+
// paint of wrapped inline elements; see CapturedElement.inlineFragments.
|
|
287
|
+
boxDecorationBreak: cs.boxDecorationBreak || cs.webkitBoxDecorationBreak || 'slice',
|
|
172
288
|
});
|
|
173
289
|
return { captureBordersBackgrounds };
|
|
174
290
|
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
//
|
|
3
|
+
// DM-770 / DM-788: resolves a custom @counter-style name + 1-based index to
|
|
4
|
+
// the marker symbol string per the CSS Counter Styles algorithms (cyclic,
|
|
5
|
+
// fixed, numeric, alphabetic, symbolic, additive) plus prefix / suffix /
|
|
6
|
+
// pad / negative / range / fallback / extends descriptors.
|
|
7
|
+
//
|
|
8
|
+
// Used by:
|
|
9
|
+
// - lists-counters.ts to resolve `list-style-type: <custom-name>` markers
|
|
10
|
+
// on <li> elements (DM-770)
|
|
11
|
+
// - pseudo-content.ts to resolve `counter(name, custom-style)` /
|
|
12
|
+
// `counters(name, sep, custom-style)` inside `::before` / `::after`
|
|
13
|
+
// `content` declarations (DM-788)
|
|
14
|
+
//
|
|
15
|
+
// The counter-style rule map is populated by `_walkRulesForCounterStyles` in
|
|
16
|
+
// the orchestrator pre-walk (`src/capture/script/index.ts`); both walkers
|
|
17
|
+
// close over the same object reference so a single sweep of styleSheets is
|
|
18
|
+
// shared across the capture.
|
|
19
|
+
//
|
|
20
|
+
// Bundled into the page-context capture script via the index.ts orchestrator;
|
|
21
|
+
// no runtime imports of its own.
|
|
22
|
+
export const createCounterStyleResolver = ({ counterStyles }) => {
|
|
23
|
+
// Built-in counter-style names whose algorithm the render side already
|
|
24
|
+
// covers inside `formatListMarker`. When an `extends` / `fallback` chain
|
|
25
|
+
// bottoms out at one of these, return the formatted symbol so the caller
|
|
26
|
+
// can stamp it directly without recursing through this resolver.
|
|
27
|
+
const BUILTINS = new Set([
|
|
28
|
+
'decimal', 'decimal-leading-zero',
|
|
29
|
+
'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin',
|
|
30
|
+
'lower-roman', 'upper-roman',
|
|
31
|
+
'lower-greek',
|
|
32
|
+
'disc', 'circle', 'square', 'none',
|
|
33
|
+
]);
|
|
34
|
+
// Marker-context resolution (used by list-item ::marker). Returns the full
|
|
35
|
+
// string with prefix + pad + value + suffix wrapping per the CSS spec.
|
|
36
|
+
const resolveCounterStyle = (name, n) => _resolve(name, n, 0, true);
|
|
37
|
+
// Counter-function-context resolution (used by `counter()` / `counters()`
|
|
38
|
+
// inside `content`). Per Chrome's paint, the function returns only the
|
|
39
|
+
// pad-formatted value: prefix / suffix are NOT included. Matches DM-788
|
|
40
|
+
// empirical probe.
|
|
41
|
+
const resolveCounterValue = (name, n) => _resolve(name, n, 0, false);
|
|
42
|
+
const isCustomCounterStyle = (name) => counterStyles[name] != null;
|
|
43
|
+
const _resolve = (name, n, depth, wrap) => {
|
|
44
|
+
if (depth > 16)
|
|
45
|
+
return null; // fallback / extends loop guard
|
|
46
|
+
if (BUILTINS.has(name))
|
|
47
|
+
return _formatBuiltin(name, n);
|
|
48
|
+
const def = counterStyles[name];
|
|
49
|
+
if (def == null)
|
|
50
|
+
return _formatBuiltin('decimal', n);
|
|
51
|
+
if (def.system === 'extends' && def.extendsName != null) {
|
|
52
|
+
const childSym = _resolve(def.extendsName, n, depth + 1, wrap);
|
|
53
|
+
if (childSym == null)
|
|
54
|
+
return null;
|
|
55
|
+
const padded = _applyPad(childSym, def.padLen, def.padSym);
|
|
56
|
+
return wrap ? (def.prefix + padded + def.suffix) : padded;
|
|
57
|
+
}
|
|
58
|
+
if (n < def.rangeLo || n > def.rangeHi) {
|
|
59
|
+
return _resolve(def.fallback ?? 'decimal', n, depth + 1, wrap);
|
|
60
|
+
}
|
|
61
|
+
const negative = n < 0;
|
|
62
|
+
const abs = Math.abs(n);
|
|
63
|
+
let core = null;
|
|
64
|
+
switch (def.system) {
|
|
65
|
+
case 'cyclic':
|
|
66
|
+
if (def.symbols.length === 0)
|
|
67
|
+
break;
|
|
68
|
+
core = def.symbols[((abs - 1) % def.symbols.length + def.symbols.length) % def.symbols.length];
|
|
69
|
+
break;
|
|
70
|
+
case 'fixed': {
|
|
71
|
+
const idx0 = abs - 1;
|
|
72
|
+
if (idx0 >= 0 && idx0 < def.symbols.length)
|
|
73
|
+
core = def.symbols[idx0];
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'numeric': {
|
|
77
|
+
if (def.symbols.length < 2)
|
|
78
|
+
break;
|
|
79
|
+
if (abs === 0) {
|
|
80
|
+
core = def.symbols[0];
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const base = def.symbols.length;
|
|
84
|
+
let v = abs;
|
|
85
|
+
let s = '';
|
|
86
|
+
while (v > 0) {
|
|
87
|
+
s = def.symbols[v % base] + s;
|
|
88
|
+
v = Math.floor(v / base);
|
|
89
|
+
}
|
|
90
|
+
core = s;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'alphabetic': {
|
|
94
|
+
if (def.symbols.length === 0 || abs <= 0)
|
|
95
|
+
break;
|
|
96
|
+
const base = def.symbols.length;
|
|
97
|
+
let v = abs;
|
|
98
|
+
let s = '';
|
|
99
|
+
while (v > 0) {
|
|
100
|
+
v--;
|
|
101
|
+
s = def.symbols[v % base] + s;
|
|
102
|
+
v = Math.floor(v / base);
|
|
103
|
+
}
|
|
104
|
+
core = s;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'symbolic': {
|
|
108
|
+
if (def.symbols.length === 0 || abs <= 0)
|
|
109
|
+
break;
|
|
110
|
+
const base = def.symbols.length;
|
|
111
|
+
const copies = Math.ceil(abs / base);
|
|
112
|
+
const sym = def.symbols[(abs - 1) % base];
|
|
113
|
+
core = sym.repeat(copies);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'additive': {
|
|
117
|
+
if (def.additiveSymbols.length === 0)
|
|
118
|
+
break;
|
|
119
|
+
if (abs === 0) {
|
|
120
|
+
const zero = def.additiveSymbols.find((s) => s.weight === 0);
|
|
121
|
+
core = zero ? zero.sym : null;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
let v = abs;
|
|
125
|
+
let s = '';
|
|
126
|
+
for (const { weight, sym } of def.additiveSymbols) {
|
|
127
|
+
if (weight <= 0)
|
|
128
|
+
continue;
|
|
129
|
+
const count = Math.floor(v / weight);
|
|
130
|
+
for (let i = 0; i < count; i++)
|
|
131
|
+
s += sym;
|
|
132
|
+
v -= count * weight;
|
|
133
|
+
}
|
|
134
|
+
core = v === 0 ? s : null;
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (core == null)
|
|
140
|
+
return _resolve(def.fallback ?? 'decimal', n, depth + 1, wrap);
|
|
141
|
+
const padded = _applyPad(core, def.padLen, def.padSym);
|
|
142
|
+
const sign = negative ? def.negPrefix : '';
|
|
143
|
+
const signTail = negative ? def.negSuffix : '';
|
|
144
|
+
return wrap
|
|
145
|
+
? (def.prefix + sign + padded + signTail + def.suffix)
|
|
146
|
+
: (sign + padded + signTail);
|
|
147
|
+
};
|
|
148
|
+
const _applyPad = (s, len, sym) => {
|
|
149
|
+
if (!len || !sym)
|
|
150
|
+
return s;
|
|
151
|
+
while ([...s].length < len)
|
|
152
|
+
s = sym + s;
|
|
153
|
+
return s;
|
|
154
|
+
};
|
|
155
|
+
const _formatBuiltin = (type, n) => {
|
|
156
|
+
switch (type) {
|
|
157
|
+
case 'decimal': return String(n);
|
|
158
|
+
case 'decimal-leading-zero': return n < 10 && n >= 0 ? '0' + n : String(n);
|
|
159
|
+
case 'lower-alpha':
|
|
160
|
+
case 'lower-latin':
|
|
161
|
+
return _alphaMarker(n, false);
|
|
162
|
+
case 'upper-alpha':
|
|
163
|
+
case 'upper-latin':
|
|
164
|
+
return _alphaMarker(n, true);
|
|
165
|
+
case 'lower-roman': return _romanMarker(n).toLowerCase();
|
|
166
|
+
case 'upper-roman': return _romanMarker(n);
|
|
167
|
+
case 'lower-greek': return _greekMarker(n);
|
|
168
|
+
case 'disc':
|
|
169
|
+
case 'circle':
|
|
170
|
+
case 'square':
|
|
171
|
+
case 'none':
|
|
172
|
+
return null;
|
|
173
|
+
default: return String(n);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const _alphaMarker = (n, upper) => {
|
|
177
|
+
if (n <= 0)
|
|
178
|
+
return String(n);
|
|
179
|
+
const base = upper ? 65 : 97;
|
|
180
|
+
let s = '';
|
|
181
|
+
let v = n;
|
|
182
|
+
while (v > 0) {
|
|
183
|
+
v--;
|
|
184
|
+
s = String.fromCharCode(base + (v % 26)) + s;
|
|
185
|
+
v = Math.floor(v / 26);
|
|
186
|
+
}
|
|
187
|
+
return s;
|
|
188
|
+
};
|
|
189
|
+
const _greekMarker = (n) => {
|
|
190
|
+
if (n <= 0)
|
|
191
|
+
return String(n);
|
|
192
|
+
const greek = 'αβγδεζηθικλμνξοπρστυφχψω';
|
|
193
|
+
let s = '';
|
|
194
|
+
let v = n;
|
|
195
|
+
while (v > 0) {
|
|
196
|
+
v--;
|
|
197
|
+
s = greek.charAt(v % 24) + s;
|
|
198
|
+
v = Math.floor(v / 24);
|
|
199
|
+
}
|
|
200
|
+
return s;
|
|
201
|
+
};
|
|
202
|
+
const _romanMarker = (n) => {
|
|
203
|
+
if (n <= 0 || n >= 4000)
|
|
204
|
+
return String(n);
|
|
205
|
+
const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
|
|
206
|
+
const syms = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
|
|
207
|
+
let s = '';
|
|
208
|
+
let v = n;
|
|
209
|
+
for (let i = 0; i < vals.length; i++) {
|
|
210
|
+
while (v >= vals[i]) {
|
|
211
|
+
s += syms[i];
|
|
212
|
+
v -= vals[i];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return s;
|
|
216
|
+
};
|
|
217
|
+
return { resolveCounterStyle, resolveCounterValue, isCustomCounterStyle };
|
|
218
|
+
};
|
|
@@ -92,9 +92,22 @@ export const createInputValueHandler = ({ vp, normColor, measureFontMetrics }) =
|
|
|
92
92
|
// + ascent), so without this adjustment the text shows up at the top
|
|
93
93
|
// of the button instead of centered. Surfaced by framer-mobile-fold's
|
|
94
94
|
// "Okay" button (display: flex; align-items: center; height: 45px).
|
|
95
|
+
//
|
|
96
|
+
// DM-666: `<input type="submit" | "button" | "reset">` are button-type
|
|
97
|
+
// inputs whose value text Chrome ALWAYS centers vertically inside the
|
|
98
|
+
// content box — this is the UA-stylesheet `appearance: button`
|
|
99
|
+
// behaviour, independent of `display` / `align-items`. Google's
|
|
100
|
+
// homepage "Google Search" / "I'm Feeling Lucky" inputs are exactly
|
|
101
|
+
// this case: `display: inline-block`, no flex, 36-px tall with 14-px
|
|
102
|
+
// text — the value sits dead-centre in Chrome but anchored to
|
|
103
|
+
// content-top in the captured tree pre-fix. Extending the centring to
|
|
104
|
+
// these button types here keeps the renderer's textTop = line-box-top
|
|
105
|
+
// contract intact.
|
|
95
106
|
const display = cs.display;
|
|
96
107
|
const isFlexLike = display === 'flex' || display === 'inline-flex' || display === 'grid' || display === 'inline-grid';
|
|
97
|
-
|
|
108
|
+
const isButtonInput = tag === 'input'
|
|
109
|
+
&& (inputType === 'submit' || inputType === 'button' || inputType === 'reset');
|
|
110
|
+
if ((isFlexLike && cs.alignItems === 'center') || isButtonInput) {
|
|
98
111
|
const contentH = rect.height - bt - bb - pt - pb;
|
|
99
112
|
if (contentH > textHeight + 0.5) {
|
|
100
113
|
textTop = (rect.top - vp.y + bt + pt) + (contentH - textHeight) / 2;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare const createListsCountersHandler: ({ normColor }: {
|
|
1
|
+
export declare const createListsCountersHandler: ({ normColor, resolveCounterStyle, isCustomCounterStyle }: {
|
|
2
2
|
normColor: any;
|
|
3
|
+
resolveCounterStyle: any;
|
|
4
|
+
isCustomCounterStyle: any;
|
|
3
5
|
}) => {
|
|
4
6
|
captureListsCounters: (el: any, cs: any, tag: any) => {
|
|
5
7
|
listMarkerIntrinsic: {
|
|
@@ -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 }) => {
|
|
16
|
+
export const createListsCountersHandler = ({ normColor, resolveCounterStyle, isCustomCounterStyle }) => {
|
|
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
|
|
@@ -79,13 +79,33 @@ export const createListsCountersHandler = ({ normColor }) => {
|
|
|
79
79
|
// else the values come back equal to the element's own font and are
|
|
80
80
|
// quietly ignored at render time, so leave them undefined.
|
|
81
81
|
const markerCs = isListItem ? window.getComputedStyle(el, '::marker') : null;
|
|
82
|
+
let markerContent = markerCs ? markerCs.content : undefined;
|
|
83
|
+
// DM-770: if list-style-type names a custom @counter-style and the
|
|
84
|
+
// ::marker pseudo doesn't already define a `content` override, resolve
|
|
85
|
+
// the marker symbol from the captured rule definitions. Chrome's CSSOM
|
|
86
|
+
// returns the resolved `::marker { content }` as the literal `"normal"`
|
|
87
|
+
// even when the painted marker is a custom symbol, so we re-implement
|
|
88
|
+
// the resolution algorithm against the captured rule map.
|
|
89
|
+
if (isListItem && resolveCounterStyle != null && listItemIndex != null) {
|
|
90
|
+
const lsType = cs.listStyleType;
|
|
91
|
+
const isCustom = lsType != null && isCustomCounterStyle != null && isCustomCounterStyle(lsType);
|
|
92
|
+
const noAuthorContent = markerContent == null || markerContent === '' || markerContent === 'normal';
|
|
93
|
+
if (isCustom && noAuthorContent) {
|
|
94
|
+
const resolved = resolveCounterStyle(lsType, listItemIndex);
|
|
95
|
+
if (resolved != null) {
|
|
96
|
+
// Wrap as a CSS-string so the render-time `rawContent` parser
|
|
97
|
+
// (which strips surrounding quotes) accepts it.
|
|
98
|
+
markerContent = '"' + resolved.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
82
102
|
return {
|
|
83
103
|
listMarkerIntrinsic,
|
|
84
104
|
listItemIndex,
|
|
85
105
|
markerColor: markerCs ? normColor(markerCs.color) : undefined,
|
|
86
106
|
markerFontWeight: markerCs ? markerCs.fontWeight : undefined,
|
|
87
107
|
markerFontSize: markerCs ? markerCs.fontSize : undefined,
|
|
88
|
-
markerContent
|
|
108
|
+
markerContent,
|
|
89
109
|
markerFontFamily: markerCs ? markerCs.fontFamily : undefined,
|
|
90
110
|
};
|
|
91
111
|
};
|
|
@@ -3,6 +3,8 @@ export declare const createMasksClipsHandler: ({ vp, warn }: {
|
|
|
3
3
|
warn: any;
|
|
4
4
|
}) => {
|
|
5
5
|
discoverMasks: (el: any, cs: any, sel: any) => void;
|
|
6
|
+
discoverClipPaths: (el: any, cs: any, sel: any) => void;
|
|
6
7
|
maskDefs: Map<any, any>;
|
|
7
8
|
maskRasters: Map<any, any>;
|
|
9
|
+
clipPathDefs: Map<any, any>;
|
|
8
10
|
};
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
export const createMasksClipsHandler = ({ vp, warn }) => {
|
|
39
39
|
const maskDefs = new Map();
|
|
40
40
|
const maskRasters = new Map();
|
|
41
|
+
const clipPathDefs = new Map();
|
|
41
42
|
let maskRasterIdx = 0;
|
|
42
43
|
const discoverMasks = (el, cs, sel) => {
|
|
43
44
|
if (!cs.mask || cs.mask === 'none' || cs.mask === '')
|
|
@@ -134,5 +135,44 @@ export const createMasksClipsHandler = ({ vp, warn }) => {
|
|
|
134
135
|
warn(sel, 'mask', 'non-gradient/non-url()/non-element() mask source — not emitted');
|
|
135
136
|
}
|
|
136
137
|
};
|
|
137
|
-
|
|
138
|
+
// DM-826: clip-path: url("#id") same-document fragment ref. Resolves the
|
|
139
|
+
// fragment to an inline `<clipPath>` element and stashes its outerHTML so
|
|
140
|
+
// the renderer can emit it into the output SVG `<defs>`. See
|
|
141
|
+
// `docs/39-clip-path-fragment-references.md`.
|
|
142
|
+
//
|
|
143
|
+
// Scope (initial cut): same-document `<clipPath>` defs with
|
|
144
|
+
// `clipPathUnits="objectBoundingBox"` or the default `userSpaceOnUse`. The
|
|
145
|
+
// emitted def carries `clipPathUnits` through verbatim — SVG handles
|
|
146
|
+
// objectBoundingBox auto-scaling natively. userSpaceOnUse refs are recorded
|
|
147
|
+
// but the renderer currently passes coordinates through unchanged (best-
|
|
148
|
+
// effort; faithful support needs per-element translation, deferred).
|
|
149
|
+
const discoverClipPaths = (el, cs, sel) => {
|
|
150
|
+
const cp = cs.clipPath;
|
|
151
|
+
if (!cp || cp === 'none' || cp === '')
|
|
152
|
+
return;
|
|
153
|
+
// Strip an optional <geometry-box> keyword (`padding-box` / `border-box` /
|
|
154
|
+
// …) before the url(...) check — `clip-path: url(#id) padding-box` is
|
|
155
|
+
// valid per CSS Masking 1 §3.1. The renderer's geo-box handling is
|
|
156
|
+
// shape-side; here we only care about the url() form.
|
|
157
|
+
const cpShape = cp.replace(/\b(?:content-box|padding-box|border-box|margin-box|fill-box|stroke-box|view-box)\b/i, '').trim();
|
|
158
|
+
const fragMatch = /^url\(\s*(?:"|')?#([^"')\s]+)(?:"|')?\s*\)$/i.exec(cpShape);
|
|
159
|
+
if (fragMatch != null) {
|
|
160
|
+
const fragId = fragMatch[1];
|
|
161
|
+
if (!clipPathDefs.has(fragId)) {
|
|
162
|
+
const target = document.getElementById(fragId);
|
|
163
|
+
if (target != null && target.tagName.toLowerCase() === 'clippath') {
|
|
164
|
+
clipPathDefs.set(fragId, { id: fragId, outerHTML: target.outerHTML });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
warn(sel, 'clip-path', 'clip-path fragment "#' + fragId + '" did not resolve to an inline <clipPath> element');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const extFragMatch = /^url\(\s*(?:"|')?[^"')#]+#[^"')\s]+(?:"|')?\s*\)$/i.exec(cpShape);
|
|
173
|
+
if (extFragMatch != null) {
|
|
174
|
+
warn(sel, 'clip-path', 'external-file SVG fragment refs (url("./file.svg#id")) are not yet emitted');
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return { discoverMasks, discoverClipPaths, maskDefs, maskRasters, clipPathDefs };
|
|
138
178
|
};
|