render-tag 0.1.22 → 0.1.24
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/lib/layout.d.ts +4 -0
- package/lib/layout.d.ts.map +1 -1
- package/lib/layout.js +160 -21
- package/lib/layout.js.map +1 -1
- package/lib/path/glyph-layout.d.ts +22 -0
- package/lib/path/glyph-layout.d.ts.map +1 -1
- package/lib/path/glyph-layout.js +41 -4
- package/lib/path/glyph-layout.js.map +1 -1
- package/lib/path/index.js +142 -60
- package/lib/path/index.js.map +1 -1
- package/lib/render-tag.umd.js +5 -5
- package/lib/render-tag.umd.js.map +1 -1
- package/lib/render.d.ts +1 -1
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +52 -32
- package/lib/render.js.map +1 -1
- package/lib/types.d.ts +26 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +2 -2
package/lib/layout.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare function getFontMetrics(ctx: CanvasRenderingContext2D, style: Res
|
|
|
9
9
|
ascent: number;
|
|
10
10
|
descent: number;
|
|
11
11
|
};
|
|
12
|
+
/** True for an element declaring `background-clip:text` with a visible
|
|
13
|
+
* background (gradient image or solid color) — the fill/decorations of every
|
|
14
|
+
* glyph it covers must sample that background instead of painting it as a box. */
|
|
15
|
+
export declare function hasTextClip(style: ResolvedStyle): boolean;
|
|
12
16
|
/**
|
|
13
17
|
* Build the layout tree from the styled tree using pure canvas measurement.
|
|
14
18
|
* No DOM measurements used — all positions computed from CSS values + canvas.measureText.
|
package/lib/layout.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,SAAS,EAAc,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA4C3G;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAGnF;AAgBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAc5D;AA8HD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAMD,wBAAgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAWvH;
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,SAAS,EAAc,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA4C3G;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAGnF;AAgBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAc5D;AA8HD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAMD,wBAAgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAWvH;AAmFD;;kFAEkF;AAClF,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAIzD;AAsnED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,wBAAwB,EAC7B,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,kBAAkB,UAAO,EACzB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,YAAY,EAAE,UAAU,KAAK,IAAI,GACvD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,CAmD1D"}
|
package/lib/layout.js
CHANGED
|
@@ -280,6 +280,14 @@ function hasVisibleBoxStyles(style) {
|
|
|
280
280
|
return true;
|
|
281
281
|
return false;
|
|
282
282
|
}
|
|
283
|
+
/** True for an element declaring `background-clip:text` with a visible
|
|
284
|
+
* background (gradient image or solid color) — the fill/decorations of every
|
|
285
|
+
* glyph it covers must sample that background instead of painting it as a box. */
|
|
286
|
+
export function hasTextClip(style) {
|
|
287
|
+
return style.webkitBackgroundClip === 'text' &&
|
|
288
|
+
((!!style.backgroundImage && style.backgroundImage !== 'none') ||
|
|
289
|
+
!isTransparent(style.backgroundColor));
|
|
290
|
+
}
|
|
283
291
|
/** True for atomic inline-block words (boxOpen && boxClose && text together). */
|
|
284
292
|
function isAtomicInlineBlock(w) {
|
|
285
293
|
return !!(w.boxOpen && w.boxClose && w.text);
|
|
@@ -364,15 +372,21 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
364
372
|
*/
|
|
365
373
|
function collectTextRuns(node) {
|
|
366
374
|
const runs = [];
|
|
367
|
-
function walk(n, boxStyle) {
|
|
375
|
+
function walk(n, boxStyle, clipStyle, strokeImageStyle) {
|
|
368
376
|
if (n.tagName === '#text' && n.textContent) {
|
|
369
|
-
runs.push({ text: n.textContent, style: n.style, boxStyle });
|
|
377
|
+
runs.push({ text: n.textContent, style: n.style, boxStyle, clipStyle, strokeImageStyle });
|
|
370
378
|
return;
|
|
371
379
|
}
|
|
372
380
|
const isInlineBlock = n.style.display === 'inline-block';
|
|
373
381
|
// Inline-block always needs box treatment (padding/margin affect layout)
|
|
374
382
|
const isBox = isInlineBlock || (isInline(n) && hasVisibleBoxStyles(n.style));
|
|
375
383
|
const newBoxStyle = isBox ? n.style : boxStyle;
|
|
384
|
+
// Track the nearest inline element declaring a background-clip:text
|
|
385
|
+
// background or a --rt-text-stroke-image, so those paints reach descendant
|
|
386
|
+
// runs that don't carry the (non-inheriting) properties themselves.
|
|
387
|
+
const newClipStyle = isInline(n) && hasTextClip(n.style) ? n.style : clipStyle;
|
|
388
|
+
const newStrokeImageStyle = isInline(n) && n.style.webkitTextStrokeImage && n.style.webkitTextStrokeImage !== 'none'
|
|
389
|
+
? n.style : strokeImageStyle;
|
|
376
390
|
const hasHorizSpacing = isBox && (n.style.paddingLeft > 0 || n.style.paddingRight > 0 ||
|
|
377
391
|
n.style.borderLeftWidth > 0 || n.style.borderRightWidth > 0);
|
|
378
392
|
if (isInlineBlock) {
|
|
@@ -384,6 +398,8 @@ function collectTextRuns(node) {
|
|
|
384
398
|
text: allText,
|
|
385
399
|
style: n.style,
|
|
386
400
|
boxStyle: newBoxStyle,
|
|
401
|
+
clipStyle: newClipStyle,
|
|
402
|
+
strokeImageStyle: newStrokeImageStyle,
|
|
387
403
|
// Store the full box info for atomic inline-block handling
|
|
388
404
|
boxOpen: n.style, // signals this is a boxed element
|
|
389
405
|
boxClose: n.style,
|
|
@@ -401,7 +417,7 @@ function collectTextRuns(node) {
|
|
|
401
417
|
runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxOpen: n.style });
|
|
402
418
|
}
|
|
403
419
|
for (const child of n.children) {
|
|
404
|
-
walk(child, isBox ? newBoxStyle : boxStyle);
|
|
420
|
+
walk(child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle);
|
|
405
421
|
}
|
|
406
422
|
if (hasHorizSpacing) {
|
|
407
423
|
runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxClose: n.style });
|
|
@@ -509,6 +525,8 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
509
525
|
isSpace: true,
|
|
510
526
|
isTab: true,
|
|
511
527
|
boxStyle: run.boxStyle,
|
|
528
|
+
clipStyle: run.clipStyle,
|
|
529
|
+
strokeImageStyle: run.strokeImageStyle,
|
|
512
530
|
});
|
|
513
531
|
continue;
|
|
514
532
|
}
|
|
@@ -519,6 +537,8 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
519
537
|
style: run.style,
|
|
520
538
|
isSpace,
|
|
521
539
|
boxStyle: run.boxStyle,
|
|
540
|
+
clipStyle: run.clipStyle,
|
|
541
|
+
strokeImageStyle: run.strokeImageStyle,
|
|
522
542
|
});
|
|
523
543
|
}
|
|
524
544
|
}
|
|
@@ -554,6 +574,8 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
554
574
|
style: run.style,
|
|
555
575
|
isSpace: true,
|
|
556
576
|
boxStyle: run.boxStyle,
|
|
577
|
+
clipStyle: run.clipStyle,
|
|
578
|
+
strokeImageStyle: run.strokeImageStyle,
|
|
557
579
|
});
|
|
558
580
|
continue;
|
|
559
581
|
}
|
|
@@ -572,6 +594,8 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
572
594
|
style: run.style,
|
|
573
595
|
isSpace: false,
|
|
574
596
|
boxStyle: run.boxStyle,
|
|
597
|
+
clipStyle: run.clipStyle,
|
|
598
|
+
strokeImageStyle: run.strokeImageStyle,
|
|
575
599
|
});
|
|
576
600
|
}
|
|
577
601
|
continue;
|
|
@@ -595,6 +619,8 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
595
619
|
style: run.style,
|
|
596
620
|
isSpace: false,
|
|
597
621
|
boxStyle: run.boxStyle,
|
|
622
|
+
clipStyle: run.clipStyle,
|
|
623
|
+
strokeImageStyle: run.strokeImageStyle,
|
|
598
624
|
});
|
|
599
625
|
}
|
|
600
626
|
// Propagate cumulative state back to caller (for \u200B/\u00AD splits)
|
|
@@ -638,6 +664,8 @@ function tokenizeRuns(ctx, runs) {
|
|
|
638
664
|
boxStyle: run.boxStyle,
|
|
639
665
|
boxOpen: run.boxOpen,
|
|
640
666
|
boxClose: run.boxClose,
|
|
667
|
+
clipStyle: run.clipStyle,
|
|
668
|
+
strokeImageStyle: run.strokeImageStyle,
|
|
641
669
|
});
|
|
642
670
|
continue;
|
|
643
671
|
}
|
|
@@ -1292,6 +1320,12 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1292
1320
|
*/
|
|
1293
1321
|
function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = false, clamp) {
|
|
1294
1322
|
const results = [];
|
|
1323
|
+
// Text nodes covered by an inline element declaring background-clip:text
|
|
1324
|
+
// (clipRuns) or --rt-text-stroke-image (strokeImageRuns), mapped to that
|
|
1325
|
+
// declaring element's style. A post-pass turns each per-line run of
|
|
1326
|
+
// same-declarer nodes into a fragment-spanning paint box.
|
|
1327
|
+
const clipRuns = new Map();
|
|
1328
|
+
const strokeImageRuns = new Map();
|
|
1295
1329
|
if (clamp && (clamp.exhausted || clamp.remaining <= 0)) {
|
|
1296
1330
|
// An ancestor's clamp already used its line budget — drop this content.
|
|
1297
1331
|
clamp.exhausted = true;
|
|
@@ -1575,7 +1609,7 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1575
1609
|
else {
|
|
1576
1610
|
if (currentGroup)
|
|
1577
1611
|
groups.push(currentGroup);
|
|
1578
|
-
currentGroup = { text: word.text, style: word.style, width: word.width, boxStyle: word.boxStyle, x: 0, padBefore: pendingPad };
|
|
1612
|
+
currentGroup = { text: word.text, style: word.style, width: word.width, boxStyle: word.boxStyle, clipStyle: word.clipStyle, strokeImageStyle: word.strokeImageStyle, x: 0, padBefore: pendingPad };
|
|
1579
1613
|
pendingPad = 0;
|
|
1580
1614
|
}
|
|
1581
1615
|
}
|
|
@@ -1604,14 +1638,19 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1604
1638
|
}
|
|
1605
1639
|
// Emit text groups
|
|
1606
1640
|
for (const group of groups) {
|
|
1607
|
-
|
|
1641
|
+
const node = {
|
|
1608
1642
|
type: 'text',
|
|
1609
1643
|
text: group.text,
|
|
1610
1644
|
x: group.x + group.width, // x = right edge for RTL textAlign
|
|
1611
1645
|
y: lineBaselineY,
|
|
1612
1646
|
width: group.width,
|
|
1613
1647
|
style: { ...group.style, direction: 'rtl' },
|
|
1614
|
-
}
|
|
1648
|
+
};
|
|
1649
|
+
results.push(node);
|
|
1650
|
+
if (group.clipStyle)
|
|
1651
|
+
clipRuns.set(node, group.clipStyle);
|
|
1652
|
+
if (group.strokeImageStyle)
|
|
1653
|
+
strokeImageRuns.set(node, group.strokeImageStyle);
|
|
1615
1654
|
}
|
|
1616
1655
|
}
|
|
1617
1656
|
else {
|
|
@@ -1633,14 +1672,19 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1633
1672
|
// right-aligns the whole line at the left edge (x=curX), drawing it
|
|
1634
1673
|
// off-screen. The canvas BiDi engine still reorders the embedded
|
|
1635
1674
|
// Arabic/Hebrew runs within the LTR line.
|
|
1636
|
-
|
|
1675
|
+
const node = {
|
|
1637
1676
|
type: 'text',
|
|
1638
1677
|
text: lineText,
|
|
1639
1678
|
x: curX,
|
|
1640
1679
|
y: lineBaselineY,
|
|
1641
1680
|
width: measuredWidth,
|
|
1642
1681
|
style: { ...textWords[0].style, direction: 'ltr' },
|
|
1643
|
-
}
|
|
1682
|
+
};
|
|
1683
|
+
results.push(node);
|
|
1684
|
+
if (textWords[0].clipStyle)
|
|
1685
|
+
clipRuns.set(node, textWords[0].clipStyle);
|
|
1686
|
+
if (textWords[0].strokeImageStyle)
|
|
1687
|
+
strokeImageRuns.set(node, textWords[0].strokeImageStyle);
|
|
1644
1688
|
}
|
|
1645
1689
|
else {
|
|
1646
1690
|
// Mixed styles: word by word
|
|
@@ -1653,14 +1697,19 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1653
1697
|
if (word.boxOpen && word.boxClose) {
|
|
1654
1698
|
const s = word.style;
|
|
1655
1699
|
const textX = curX + s.marginLeft + s.borderLeftWidth + s.paddingLeft;
|
|
1656
|
-
|
|
1700
|
+
const node = {
|
|
1657
1701
|
type: 'text',
|
|
1658
1702
|
text: word.text,
|
|
1659
1703
|
x: textX,
|
|
1660
1704
|
y: lineBaselineY,
|
|
1661
1705
|
width: cachedMeasureWidth(ctx, word.text),
|
|
1662
1706
|
style: word.style,
|
|
1663
|
-
}
|
|
1707
|
+
};
|
|
1708
|
+
results.push(node);
|
|
1709
|
+
if (word.clipStyle)
|
|
1710
|
+
clipRuns.set(node, word.clipStyle);
|
|
1711
|
+
if (word.strokeImageStyle)
|
|
1712
|
+
strokeImageRuns.set(node, word.strokeImageStyle);
|
|
1664
1713
|
curX += word.width;
|
|
1665
1714
|
continue;
|
|
1666
1715
|
}
|
|
@@ -1673,14 +1722,19 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1673
1722
|
baselineY += verticalAlignShift(va, wA, wD, pfs, maxAscent, maxDescent, lineHeight);
|
|
1674
1723
|
}
|
|
1675
1724
|
const effectiveWidth = word.width + (word.isSpace ? justifyExtraPerSpace : 0);
|
|
1676
|
-
|
|
1725
|
+
const node = {
|
|
1677
1726
|
type: 'text',
|
|
1678
1727
|
text: word.text,
|
|
1679
1728
|
x: curX,
|
|
1680
1729
|
y: baselineY,
|
|
1681
1730
|
width: effectiveWidth,
|
|
1682
1731
|
style: word.style,
|
|
1683
|
-
}
|
|
1732
|
+
};
|
|
1733
|
+
results.push(node);
|
|
1734
|
+
if (word.clipStyle)
|
|
1735
|
+
clipRuns.set(node, word.clipStyle);
|
|
1736
|
+
if (word.strokeImageStyle)
|
|
1737
|
+
strokeImageRuns.set(node, word.strokeImageStyle);
|
|
1684
1738
|
curX += effectiveWidth;
|
|
1685
1739
|
}
|
|
1686
1740
|
}
|
|
@@ -1705,8 +1759,70 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1705
1759
|
});
|
|
1706
1760
|
curY += effectiveLineHeight;
|
|
1707
1761
|
}
|
|
1762
|
+
assignInlineFragmentBoxes(ctx, results, clipRuns, (node, s, box) => {
|
|
1763
|
+
node.clip = {
|
|
1764
|
+
image: s.backgroundImage && s.backgroundImage !== 'none' ? s.backgroundImage : undefined,
|
|
1765
|
+
color: !isTransparent(s.backgroundColor) ? s.backgroundColor : undefined,
|
|
1766
|
+
...box,
|
|
1767
|
+
};
|
|
1768
|
+
});
|
|
1769
|
+
assignInlineFragmentBoxes(ctx, results, strokeImageRuns, (node, s, box) => {
|
|
1770
|
+
node.strokeImage = { image: s.webkitTextStrokeImage, ...box };
|
|
1771
|
+
});
|
|
1708
1772
|
return { nodes: results, height: curY - y };
|
|
1709
1773
|
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Give each text run covered by an inline paint declarer (background-clip:text
|
|
1776
|
+
* background, --rt-text-stroke-image) a paint box spanning the declaring
|
|
1777
|
+
* element's fragment on its line.
|
|
1778
|
+
*
|
|
1779
|
+
* Browsers paint the declaring element's background over its inline fragment
|
|
1780
|
+
* (the run of glyphs it covers on one line) and clip it to the text; with
|
|
1781
|
+
* `background-size:100% 100%` the gradient fills that fragment box. Consecutive
|
|
1782
|
+
* text nodes sharing the same declaring element (same style object) on the
|
|
1783
|
+
* same baseline form one fragment; a wrap to the next line starts a new one
|
|
1784
|
+
* (box-decoration-break:clone semantics — Chrome's default `slice` continues
|
|
1785
|
+
* the gradient across line fragments; accepted approximation), and unlike a
|
|
1786
|
+
* per-run gradient it never restarts per word.
|
|
1787
|
+
*/
|
|
1788
|
+
function assignInlineFragmentBoxes(ctx, results, runs, assign) {
|
|
1789
|
+
if (runs.size === 0)
|
|
1790
|
+
return;
|
|
1791
|
+
const edges = (n) => n.style.direction === 'rtl'
|
|
1792
|
+
? { left: n.x - n.width, right: n.x } // RTL x is the right edge
|
|
1793
|
+
: { left: n.x, right: n.x + n.width };
|
|
1794
|
+
for (let i = 0; i < results.length;) {
|
|
1795
|
+
const first = results[i];
|
|
1796
|
+
const declarer = first.type === 'text' ? runs.get(first) : undefined;
|
|
1797
|
+
if (!declarer) {
|
|
1798
|
+
i++;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
let j = i;
|
|
1802
|
+
let left = Infinity, right = -Infinity;
|
|
1803
|
+
while (j < results.length) {
|
|
1804
|
+
const n = results[j];
|
|
1805
|
+
if (n.type !== 'text' || runs.get(n) !== declarer || n.y !== first.y)
|
|
1806
|
+
break;
|
|
1807
|
+
const e = edges(n);
|
|
1808
|
+
if (e.left < left)
|
|
1809
|
+
left = e.left;
|
|
1810
|
+
if (e.right > right)
|
|
1811
|
+
right = e.right;
|
|
1812
|
+
j++;
|
|
1813
|
+
}
|
|
1814
|
+
const { ascent, descent } = getFontMetrics(ctx, declarer);
|
|
1815
|
+
const box = {
|
|
1816
|
+
x: left,
|
|
1817
|
+
y: first.y - ascent,
|
|
1818
|
+
width: right - left,
|
|
1819
|
+
height: ascent + descent,
|
|
1820
|
+
};
|
|
1821
|
+
for (let k = i; k < j; k++)
|
|
1822
|
+
assign(results[k], declarer, box);
|
|
1823
|
+
i = j;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1710
1826
|
// ─── Block layout ──────────────────────────────────────────────────────
|
|
1711
1827
|
/**
|
|
1712
1828
|
* Collapse margins between two adjacent block elements.
|
|
@@ -2019,21 +2135,41 @@ function addListMarker(ctx, box, node) {
|
|
|
2019
2135
|
let markerX;
|
|
2020
2136
|
let markerY = baselineY;
|
|
2021
2137
|
let markerDirection = 'ltr';
|
|
2138
|
+
// Style/width the marker glyph is actually DRAWN with. Numbers draw at the
|
|
2139
|
+
// li font (unchanged); bullets scale up (see below), so keep these separate.
|
|
2140
|
+
let markerDrawStyle = markerStyleObj;
|
|
2141
|
+
let markerDrawWidth = markerWidth;
|
|
2022
2142
|
const contentStartX = box.x + style.borderLeftWidth + style.paddingLeft;
|
|
2023
2143
|
const boxRightEdge = box.x + box.width;
|
|
2024
2144
|
if (isBullet) {
|
|
2025
2145
|
const { ascent } = getFontMetrics(ctx, markerStyleObj);
|
|
2026
2146
|
const m = ctx.measureText(node.listMarker);
|
|
2027
|
-
|
|
2147
|
+
// Blink's marker unit: the disc DIAMETER, the variable part of the gap, and
|
|
2148
|
+
// the vertical centering all key off this one value (a 2/3·ascent marker
|
|
2149
|
+
// box with a half-filling disc → ascent/3). Named once so tuning one keeps
|
|
2150
|
+
// the trio in sync.
|
|
2151
|
+
const markerUnit = ascent / 3;
|
|
2152
|
+
const gap = explicitGap !== undefined ? explicitGap : 7 + markerUnit;
|
|
2153
|
+
// Chrome paints bullet symbols (disc/circle/square) as a SYNTHETIC shape of
|
|
2154
|
+
// that diameter, NOT the font's smaller '•'/'○'/'■' glyph (Roboto's '•' ink
|
|
2155
|
+
// is ~0.22em vs Chrome's ~0.31em disc). Match it by scaling the glyph so its
|
|
2156
|
+
// ink height equals markerUnit. Keeping the marker a text node means fill /
|
|
2157
|
+
// stroke / shadow / gradient still apply exactly as before.
|
|
2158
|
+
const inkH = (m.actualBoundingBoxAscent ?? 0) + (m.actualBoundingBoxDescent ?? 0);
|
|
2159
|
+
const scale = inkH > 0 ? markerUnit / inkH : 1;
|
|
2160
|
+
const inkRight = (m.actualBoundingBoxRight ?? markerWidth) * scale;
|
|
2161
|
+
const inkLeft = (m.actualBoundingBoxLeft ?? 0) * scale;
|
|
2162
|
+
const glyphInkCenter = (((m.actualBoundingBoxAscent ?? 0) - (m.actualBoundingBoxDescent ?? 0)) / 2) * scale;
|
|
2028
2163
|
if (isRTL) {
|
|
2029
2164
|
// actualBoundingBoxLeft is positive when ink extends left of origin
|
|
2030
|
-
markerX = boxRightEdge + gap +
|
|
2165
|
+
markerX = boxRightEdge + gap + inkLeft;
|
|
2031
2166
|
}
|
|
2032
2167
|
else {
|
|
2033
|
-
markerX = contentStartX - gap -
|
|
2168
|
+
markerX = contentStartX - gap - inkRight;
|
|
2034
2169
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2170
|
+
markerY = baselineY - markerUnit + glyphInkCenter;
|
|
2171
|
+
markerDrawStyle = { ...markerStyleObj, fontSize: markerStyleObj.fontSize * scale };
|
|
2172
|
+
markerDrawWidth = markerWidth * scale;
|
|
2037
2173
|
}
|
|
2038
2174
|
else {
|
|
2039
2175
|
const gap = explicitGap !== undefined
|
|
@@ -2062,22 +2198,25 @@ function addListMarker(ctx, box, node) {
|
|
|
2062
2198
|
text: node.listMarker,
|
|
2063
2199
|
x: markerX,
|
|
2064
2200
|
y: markerY,
|
|
2065
|
-
width:
|
|
2066
|
-
style: { ...
|
|
2201
|
+
width: markerDrawWidth,
|
|
2202
|
+
style: { ...markerDrawStyle, textDecorationLine: 'none', textDecorations: [], fontWeight: ms?.fontWeight ?? 400, fontStyle: ms?.fontStyle ?? 'normal', direction: markerDirection },
|
|
2067
2203
|
});
|
|
2068
2204
|
// Also publish the marker through the LayoutLine stream so result.lines
|
|
2069
2205
|
// sees the bullet/number alongside the item text. Markers are added AFTER
|
|
2070
2206
|
// inline content is laid out, so they don't go through layoutInlineContent.
|
|
2071
2207
|
// The buildLayoutTree sort+merge step picks up the marker by its baseline.
|
|
2072
2208
|
// RTL numbered markers store their right edge in markerX (textAlign trick).
|
|
2073
|
-
|
|
2209
|
+
// bounds.width is the (scaled) glyph ADVANCE, not its ink extent — same
|
|
2210
|
+
// convention as numbered markers; the ink right edge itself is pinned to
|
|
2211
|
+
// contentStart - gap above.
|
|
2212
|
+
const markerLeftX = markerDirection === 'rtl' ? markerX - markerDrawWidth : markerX;
|
|
2074
2213
|
_lines.push({
|
|
2075
2214
|
y: Math.round(baselineY),
|
|
2076
2215
|
text: node.listMarker,
|
|
2077
2216
|
bounds: {
|
|
2078
2217
|
x: markerLeftX,
|
|
2079
2218
|
y: box.y + style.borderTopWidth + style.paddingTop,
|
|
2080
|
-
width:
|
|
2219
|
+
width: markerDrawWidth,
|
|
2081
2220
|
height: lineHeight,
|
|
2082
2221
|
},
|
|
2083
2222
|
});
|