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.
Files changed (115) hide show
  1. package/FEATURES.md +1 -0
  2. package/README.md +29 -0
  3. package/dist/animation/animator.js +25 -14
  4. package/dist/animation/animator.test.js +54 -21
  5. package/dist/animation/cursor-overlay.js +0 -2
  6. package/dist/capture/emoji.js +29 -18
  7. package/dist/capture/index.js +5 -4
  8. package/dist/capture/script/color-norm.d.ts +1 -0
  9. package/dist/capture/script/color-norm.js +43 -1
  10. package/dist/capture/script/emoji-detect.js +14 -0
  11. package/dist/capture/script/index.js +593 -65
  12. package/dist/capture/script/walker/borders-backgrounds.d.ts +24 -17
  13. package/dist/capture/script/walker/borders-backgrounds.js +123 -7
  14. package/dist/capture/script/walker/counter-style-resolver.d.ts +7 -0
  15. package/dist/capture/script/walker/counter-style-resolver.js +218 -0
  16. package/dist/capture/script/walker/input-value.js +14 -1
  17. package/dist/capture/script/walker/lists-counters.d.ts +3 -1
  18. package/dist/capture/script/walker/lists-counters.js +22 -2
  19. package/dist/capture/script/walker/masks-clips.d.ts +2 -0
  20. package/dist/capture/script/walker/masks-clips.js +41 -1
  21. package/dist/capture/script/walker/pseudo-content.d.ts +14 -1
  22. package/dist/capture/script/walker/pseudo-content.js +301 -61
  23. package/dist/capture/script/walker/pseudo-inject.js +20 -0
  24. package/dist/capture/script/walker/text-segments.js +98 -4
  25. package/dist/capture/script/walker/transforms.d.ts +1 -0
  26. package/dist/capture/script/walker/transforms.js +16 -0
  27. package/dist/capture/script.generated.js +1 -1
  28. package/dist/capture/types.d.ts +213 -2
  29. package/dist/cli/animate.js +151 -15
  30. package/dist/mask.test.js +12 -7
  31. package/dist/render/borders.d.ts +9 -13
  32. package/dist/render/borders.js +379 -14
  33. package/dist/render/element-tree-to-svg.d.ts +11 -12
  34. package/dist/render/element-tree-to-svg.js +2046 -241
  35. package/dist/render/embedded-font-builder.d.ts +49 -0
  36. package/dist/render/embedded-font-builder.js +149 -0
  37. package/dist/render/form-controls.js +45 -24
  38. package/dist/render/gradients.d.ts +15 -0
  39. package/dist/render/gradients.js +103 -2
  40. package/dist/render/gradients.test.js +34 -0
  41. package/dist/render/text-to-path.d.ts +38 -1
  42. package/dist/render/text-to-path.js +654 -29
  43. package/dist/render/text-to-path.test.js +230 -9
  44. package/dist/render/text.d.ts +14 -0
  45. package/dist/render/text.js +344 -40
  46. package/dist/scroll/composer.d.ts +26 -0
  47. package/dist/scroll/composer.js +199 -11
  48. package/dist/scroll/composer.test.js +293 -16
  49. package/dist/scroll/executor.d.ts +3 -1
  50. package/dist/scroll/executor.js +15 -6
  51. package/dist/scroll/executor.test.js +25 -0
  52. package/dist/scroll/hoist-fixed.d.ts +48 -0
  53. package/dist/scroll/hoist-fixed.js +85 -0
  54. package/dist/scroll/hoist-fixed.test.d.ts +1 -0
  55. package/dist/scroll/hoist-fixed.test.js +103 -0
  56. package/dist/scroll/hoist-sticky.d.ts +45 -0
  57. package/dist/scroll/hoist-sticky.js +157 -0
  58. package/dist/scroll/hoist-sticky.test.d.ts +1 -0
  59. package/dist/scroll/hoist-sticky.test.js +154 -0
  60. package/dist/scroll/pattern.d.ts +22 -5
  61. package/dist/scroll/pattern.js +55 -7
  62. package/dist/scroll/pattern.test.js +48 -1
  63. package/dist/tree-ops/frame-merge.d.ts +10 -0
  64. package/dist/tree-ops/frame-merge.js +23 -5
  65. package/dist/tree-ops/frame-merge.test.js +45 -0
  66. package/dist/tree-ops/tree-diff.js +1 -1
  67. package/dist/tree-ops/viewbox-culling.js +32 -18
  68. package/dist/tree-ops/viewbox-culling.test.js +40 -6
  69. package/package.json +8 -2
  70. package/src/animation/animator.test.ts +56 -21
  71. package/src/animation/animator.ts +25 -14
  72. package/src/animation/cursor-overlay.ts +0 -2
  73. package/src/capture/emoji.ts +28 -18
  74. package/src/capture/index.ts +15 -14
  75. package/src/capture/script/color-norm.ts +38 -1
  76. package/src/capture/script/emoji-detect.ts +14 -0
  77. package/src/capture/script/index.ts +555 -48
  78. package/src/capture/script/walker/borders-backgrounds.ts +114 -7
  79. package/src/capture/script/walker/counter-style-resolver.ts +184 -0
  80. package/src/capture/script/walker/input-value.ts +14 -1
  81. package/src/capture/script/walker/lists-counters.ts +24 -2
  82. package/src/capture/script/walker/masks-clips.ts +40 -1
  83. package/src/capture/script/walker/pseudo-content.ts +297 -55
  84. package/src/capture/script/walker/pseudo-inject.ts +20 -0
  85. package/src/capture/script/walker/text-segments.ts +93 -4
  86. package/src/capture/script/walker/transforms.ts +14 -0
  87. package/src/capture/script.generated.ts +1 -1
  88. package/src/capture/types.ts +202 -2
  89. package/src/cli/animate.ts +135 -15
  90. package/src/mask.test.ts +12 -7
  91. package/src/render/borders.ts +383 -17
  92. package/src/render/element-tree-to-svg.ts +2051 -238
  93. package/src/render/embedded-font-builder.ts +221 -0
  94. package/src/render/form-controls.ts +45 -24
  95. package/src/render/gradients.test.ts +46 -0
  96. package/src/render/gradients.ts +94 -2
  97. package/src/render/opentype.js.d.ts +7 -0
  98. package/src/render/text-to-path.test.ts +246 -9
  99. package/src/render/text-to-path.ts +702 -31
  100. package/src/render/text.ts +344 -40
  101. package/src/scroll/composer.test.ts +322 -16
  102. package/src/scroll/composer.ts +246 -13
  103. package/src/scroll/executor.test.ts +27 -0
  104. package/src/scroll/executor.ts +19 -10
  105. package/src/scroll/hoist-fixed.test.ts +117 -0
  106. package/src/scroll/hoist-fixed.ts +95 -0
  107. package/src/scroll/hoist-sticky.test.ts +173 -0
  108. package/src/scroll/hoist-sticky.ts +193 -0
  109. package/src/scroll/pattern.test.ts +58 -1
  110. package/src/scroll/pattern.ts +71 -8
  111. package/src/tree-ops/frame-merge.test.ts +51 -0
  112. package/src/tree-ops/frame-merge.ts +24 -6
  113. package/src/tree-ops/tree-diff.ts +3 -1
  114. package/src/tree-ops/viewbox-culling.test.ts +42 -6
  115. 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
- const u = /^\s*url\(\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^)\s]+))\s*\)/.exec(layer);
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
- borderTopStyle: cs.borderTopStyle,
139
- borderRightStyle: cs.borderRightStyle,
140
- borderBottomStyle: cs.borderBottomStyle,
141
- borderLeftStyle: cs.borderLeftStyle,
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,7 @@
1
+ export declare const createCounterStyleResolver: ({ counterStyles }: {
2
+ counterStyles: any;
3
+ }) => {
4
+ resolveCounterStyle: (name: any, n: any) => any;
5
+ resolveCounterValue: (name: any, n: any) => any;
6
+ isCustomCounterStyle: (name: any) => boolean;
7
+ };
@@ -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
- if (isFlexLike && cs.alignItems === 'center') {
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: markerCs ? markerCs.content : undefined,
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
- return { discoverMasks, maskDefs, maskRasters };
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
  };