@weasel-js/text 1.9.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,48 @@
1
- import { resolveScreenLength } from '@weasel-js/paint';
2
- import { cssFamilyName, resolveFontVariant, faceMetricsOf, fontPending, glyphGeneration, glyphOutline, resolveGlyphFallback } from '@weasel-js/font';
1
+ import { isSolidPaint, resolveScreenLength } from '@weasel-js/paint';
2
+ import { cssFamilyName, faceGeneration, resolveFontVariant, metricsOf, warnNoMetrics, resolveColorCluster, glyphIn, resolveGlyph } from '@weasel-js/font';
3
3
 
4
4
  var __defProp = Object.defineProperty;
5
5
  var __export = (target, all) => {
6
6
  for (var name in all)
7
7
  __defProp(target, name, { get: all[name], enumerable: true });
8
8
  };
9
+
10
+ // src/layout/tabStops.ts
11
+ var DEFAULT_TAB_SIZE = 8;
12
+ function tabAdvance(x, stop, min) {
13
+ if (!(stop > 0)) return 0;
14
+ const toNext = (Math.floor(x / stop) + 1) * stop - x;
15
+ return toNext < min ? toNext + stop : toNext;
16
+ }
17
+ function widthWithTabs(start, end, tabs, stopOf, flatWidth, piece) {
18
+ let x = 0;
19
+ let from = start;
20
+ const lay = (to) => {
21
+ if (to <= from) return;
22
+ piece?.(from, to, x);
23
+ x += flatWidth(from, to);
24
+ };
25
+ for (const t of tabs) {
26
+ if (t < start) continue;
27
+ if (t >= end) break;
28
+ lay(t);
29
+ const { stop, min } = stopOf(t);
30
+ x += tabAdvance(x, stop, min);
31
+ from = t + 1;
32
+ }
33
+ lay(end);
34
+ return x;
35
+ }
36
+
37
+ // src/textStyle.ts
9
38
  function resolveAlign(align, direction) {
10
39
  if (align === "start" || align === "justify") return direction === "rtl" ? "right" : "left";
11
40
  if (align === "end") return direction === "rtl" ? "left" : "right";
12
41
  return align;
13
42
  }
14
- var DEFAULT_FILL = { fill: "solid", color: "#000" };
43
+ var DEFAULT_FILL = { kind: "solid", color: "#000" };
15
44
  function paintColor(p) {
16
- return p !== null && "color" in p ? p.color : "#000";
45
+ return isSolidPaint(p) ? p.color : "#000";
17
46
  }
18
47
  function defaultSelectionBackground(textColor) {
19
48
  return `color-mix(in srgb, ${textColor} 25%, transparent)`;
@@ -24,10 +53,11 @@ var DEFAULT_TEXT_STYLE = {
24
53
  fontWeight: 400,
25
54
  fontStyle: "normal",
26
55
  align: "left",
27
- direction: "ltr",
56
+ direction: "auto",
28
57
  lineHeight: 1.2,
58
+ tabSize: DEFAULT_TAB_SIZE,
29
59
  wrap: false,
30
- fill: DEFAULT_FILL,
60
+ fill: { paint: DEFAULT_FILL },
31
61
  caretColor: paintColor(DEFAULT_FILL),
32
62
  selectionBackground: defaultSelectionBackground(paintColor(DEFAULT_FILL)),
33
63
  selectionColor: null,
@@ -45,12 +75,12 @@ function resolveTextStyle(style, paint, scale = 1) {
45
75
  return fill === DEFAULT_TEXT_STYLE.fill && stroke === void 0 ? DEFAULT_TEXT_STYLE : {
46
76
  ...DEFAULT_TEXT_STYLE,
47
77
  fill,
48
- caretColor: paintColor(fill),
49
- selectionBackground: defaultSelectionBackground(paintColor(fill)),
78
+ caretColor: paintColor(fill?.paint ?? null),
79
+ selectionBackground: defaultSelectionBackground(paintColor(fill?.paint ?? null)),
50
80
  ...stroke !== void 0 ? { stroke } : {}
51
81
  };
52
82
  }
53
- const caretColor = style.caretColor ?? paintColor(fill);
83
+ const caretColor = style.caretColor ?? paintColor(fill?.paint ?? null);
54
84
  let selectionBackground;
55
85
  if (style.selectionBackground === "none") {
56
86
  selectionBackground = null;
@@ -67,6 +97,7 @@ function resolveTextStyle(style, paint, scale = 1) {
67
97
  align: style.align ?? DEFAULT_TEXT_STYLE.align,
68
98
  direction: style.direction ?? DEFAULT_TEXT_STYLE.direction,
69
99
  lineHeight: style.lineHeight ?? DEFAULT_TEXT_STYLE.lineHeight,
100
+ tabSize: style.tabSize ?? DEFAULT_TEXT_STYLE.tabSize,
70
101
  wrap: style.wrap ?? DEFAULT_TEXT_STYLE.wrap,
71
102
  fill,
72
103
  caretColor,
@@ -86,34 +117,6 @@ function fontString(s) {
86
117
  return `${s.fontStyle} ${s.fontWeight} ${s.fontSize}px ${cssFamilyName(s.fontFamily)}`;
87
118
  }
88
119
 
89
- // src/layout/decorationMetrics.ts
90
- var DECORATION_KINDS = ["underline", "strikethrough", "overline"];
91
- var DEFAULT_DECORATION_METRICS = Object.freeze({
92
- underline: Object.freeze({ offset: 0.1, thickness: 0.05 }),
93
- strikethrough: Object.freeze({ offset: -0.3, thickness: 0.05 }),
94
- overline: Object.freeze({ offset: -0.9, thickness: 0.05 })
95
- });
96
- var resolved = /* @__PURE__ */ new WeakMap();
97
- function decorationMetrics(face) {
98
- if (!face) return DEFAULT_DECORATION_METRICS;
99
- let out = resolved.get(face);
100
- if (!out) {
101
- const underline = face.underline ?? DEFAULT_DECORATION_METRICS.underline;
102
- out = Object.freeze({
103
- underline,
104
- strikethrough: face.strikethrough ?? DEFAULT_DECORATION_METRICS.strikethrough,
105
- overline: Object.freeze({ offset: DEFAULT_DECORATION_METRICS.overline.offset, thickness: underline.thickness })
106
- });
107
- resolved.set(face, out);
108
- }
109
- return out;
110
- }
111
- function decorationRule(kind, baselineY, fontSize, face) {
112
- const m = decorationMetrics(face)[kind];
113
- const y0 = baselineY + fontSize * m.offset;
114
- return { y0, y1: y0 + fontSize * m.thickness };
115
- }
116
-
117
120
  // src/layout/lineBreak/lineBreakTable.ts
118
121
  var lineBreakTable_exports = {};
119
122
  __export(lineBreakTable_exports, {
@@ -468,8 +471,156 @@ function lineBreakOpportunities(cps) {
468
471
  }
469
472
  }
470
473
 
474
+ // src/layout/bidiRegistry.ts
475
+ var registered = null;
476
+ function registerBidi(engine) {
477
+ registered = engine;
478
+ return () => {
479
+ if (registered === engine) registered = null;
480
+ };
481
+ }
482
+ function registeredBidi() {
483
+ return registered;
484
+ }
485
+ function directionOfLevel(level) {
486
+ return level % 2 === 1 ? "rtl" : "ltr";
487
+ }
488
+ function paragraphDirection(text, direction = "auto", engine = registered) {
489
+ if (direction !== "auto") return direction;
490
+ if (engine === null) return "ltr";
491
+ const cps = [];
492
+ for (const ch of text) {
493
+ const cp = ch.codePointAt(0);
494
+ if (isHardLineBreak(cp)) break;
495
+ cps.push(cp);
496
+ }
497
+ return directionOfLevel(engine.baseLevel(engine.analyze(cps, "auto")));
498
+ }
499
+ var RTL_BLOCKS = [
500
+ [1424, 1535],
501
+ [1536, 1983],
502
+ [2144, 2303],
503
+ [64285, 65023],
504
+ [65136, 65279],
505
+ [67584, 69631],
506
+ [124928, 126975]
507
+ ];
508
+ var RTL_BLOCK_NUMBERS = [
509
+ [1536, 1541],
510
+ [1632, 1644],
511
+ [1757, 1757],
512
+ [1776, 1785],
513
+ [2274, 2274],
514
+ [68912, 68921],
515
+ [69216, 69246]
516
+ ];
517
+ var RTL_BLOCK_NEUTRALS = [
518
+ [1548, 1548],
519
+ [64830, 64831],
520
+ [65279, 65279]
521
+ ];
522
+ var within = (ranges, cp) => ranges.some(([lo, hi]) => cp >= lo && cp <= hi);
523
+ var warnedLogicalRtl = false;
524
+ function warnRtlWithoutEngine(codePoints) {
525
+ if (warnedLogicalRtl) return;
526
+ for (const cp of codePoints) {
527
+ if (!within(RTL_BLOCKS, cp) || within(RTL_BLOCK_NUMBERS, cp) || within(RTL_BLOCK_NEUTRALS, cp)) continue;
528
+ warnedLogicalRtl = true;
529
+ console.warn(
530
+ // Naming the import in `from "<pkg>"` form would trip labkit's
531
+ // consumer smoke test, which greps dist for that exact shape.
532
+ "weasel layoutRuns: this text contains right-to-left characters but no `bidi` engine is registered, so it is being laid out in logical order \u2014 the glyphs will appear reversed. Import `bidi` out of the `@weasel-js/bidi` package and call `registerBidi(bidi)` once at startup."
533
+ );
534
+ return;
535
+ }
536
+ }
537
+ function _resetBidiWarningForTests() {
538
+ warnedLogicalRtl = false;
539
+ }
540
+
541
+ // src/layout/bidiLines.ts
542
+ function analyzeParagraphs(engine, codePoints, isBreak, direction) {
543
+ const paragraphs = [];
544
+ let start = 0;
545
+ for (let i = 0; i <= codePoints.length; i++) {
546
+ if (i < codePoints.length && !isBreak(i)) continue;
547
+ if (i > start) {
548
+ paragraphs.push({ start, end: i, analysis: engine.analyze(codePoints.slice(start, i), direction) });
549
+ }
550
+ start = i + 1;
551
+ }
552
+ const holding = (at) => {
553
+ let lo = 0;
554
+ let hi = paragraphs.length - 1;
555
+ while (lo < hi) {
556
+ const mid = lo + hi >> 1;
557
+ if (paragraphs[mid].end <= at) lo = mid + 1;
558
+ else hi = mid;
559
+ }
560
+ const p = paragraphs[lo];
561
+ return p !== void 0 && at >= p.start && at < p.end ? p : void 0;
562
+ };
563
+ return {
564
+ reorder(from, to) {
565
+ const p = holding(from);
566
+ if (p === void 0 || to > p.end) return { order: [], levels: [] };
567
+ const r = engine.reorder(p.analysis, from - p.start, to - p.start);
568
+ return { order: r.order.map((i) => i + p.start), levels: r.levels };
569
+ },
570
+ baseLevel(at) {
571
+ const p = holding(at);
572
+ return p === void 0 ? 0 : engine.baseLevel(p.analysis);
573
+ }
574
+ };
575
+ }
576
+ function restoreRemoved(order, count, rtl) {
577
+ if (order.length === count) return [...order];
578
+ const kept = new Set(order);
579
+ const leading = [];
580
+ const following = /* @__PURE__ */ new Map();
581
+ let anchor = -1;
582
+ let firstKept = -1;
583
+ for (let k = 0; k < count; k++) {
584
+ if (kept.has(k)) {
585
+ anchor = k;
586
+ if (firstKept < 0) firstKept = k;
587
+ } else if (anchor < 0) {
588
+ leading.push(k);
589
+ } else {
590
+ const list = following.get(anchor);
591
+ if (list) list.push(k);
592
+ else following.set(anchor, [k]);
593
+ }
594
+ }
595
+ if (firstKept < 0) return leading;
596
+ const out = [];
597
+ for (const p of order) {
598
+ const after = following.get(p) ?? [];
599
+ const before = p === firstKept ? leading : [];
600
+ if (rtl(p)) out.push(...[...after].reverse(), p, ...[...before].reverse());
601
+ else out.push(...before, p, ...after);
602
+ }
603
+ return out;
604
+ }
605
+ function orderLine(paragraphs, start, count) {
606
+ const levels = new Array(count).fill(0);
607
+ if (paragraphs === null || count === 0) return { visual: levels.map((_, pos) => pos), levels };
608
+ const r = paragraphs.reorder(start, start + count);
609
+ for (let pos = 0; pos < count; pos++) {
610
+ const level = r.levels[pos];
611
+ if (typeof level === "number" && level >= 0) levels[pos] = level;
612
+ }
613
+ const kept = /* @__PURE__ */ new Set();
614
+ for (const at of r.order) {
615
+ const pos = at - start;
616
+ if (Number.isInteger(pos) && pos >= 0 && pos < count) kept.add(pos);
617
+ }
618
+ return { visual: restoreRemoved([...kept], count, (pos) => levels[pos] % 2 === 1), levels };
619
+ }
620
+
471
621
  // src/measure/graphemes.ts
472
622
  var segmenter;
623
+ var NEEDS_SEGMENTING = /[\r\u0300-\uffff]/;
473
624
  function graphemeSegmenter() {
474
625
  if (segmenter === void 0) {
475
626
  segmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
@@ -478,6 +629,10 @@ function graphemeSegmenter() {
478
629
  }
479
630
  function graphemeEnds(text) {
480
631
  const out = [];
632
+ if (!NEEDS_SEGMENTING.test(text)) {
633
+ for (let i = 1; i <= text.length; i++) out.push(i);
634
+ return out;
635
+ }
481
636
  const seg = graphemeSegmenter();
482
637
  if (seg) {
483
638
  for (const s of seg.segment(text)) out.push(s.index + s.segment.length);
@@ -491,8 +646,125 @@ function graphemeCount(text) {
491
646
  return graphemeEnds(text).length;
492
647
  }
493
648
 
649
+ // src/layout/layoutEntries.ts
650
+ function measureRuns(runs, tabSize) {
651
+ const entries = [];
652
+ let prev;
653
+ let srcIndex = 0;
654
+ runs.forEach((run, runIndex) => {
655
+ const resolved = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
656
+ const metrics = metricsOf(resolved);
657
+ if (!metrics) {
658
+ warnNoMetrics(run.fontFamily, run.fontWeight, run.fontStyle);
659
+ srcIndex += run.srcMap?.length ?? run.text.length;
660
+ prev = void 0;
661
+ return;
662
+ }
663
+ const runBase = srcIndex;
664
+ const map = run.srcMap;
665
+ const clusterEnds = graphemeEnds(run.text);
666
+ let cluster = 0;
667
+ let clusterStart = 0;
668
+ let color = null;
669
+ let at = 0;
670
+ for (const ch of run.text) {
671
+ const cp = ch.codePointAt(0);
672
+ const fontSize = run.sizeMap ? run.sizeMap[at] : run.fontSize;
673
+ if (at === clusterStart) {
674
+ color = resolveColorCluster(
675
+ resolved,
676
+ run.fontFamily,
677
+ run.fontWeight,
678
+ run.fontStyle,
679
+ run.text.slice(at, clusterEnds[cluster])
680
+ );
681
+ }
682
+ const clusterEnd = at + ch.length === clusterEnds[cluster];
683
+ if (clusterEnd) clusterStart = clusterEnds[cluster++];
684
+ const tracking = clusterEnd ? run.letterSpacing : 0;
685
+ const srcStart = map ? runBase + map.starts[at] : runBase + at;
686
+ const srcEnd = map ? runBase + map.ends[at] : runBase + at + ch.length;
687
+ at += ch.length;
688
+ srcIndex = map ? runBase + map.length : srcEnd;
689
+ const kerningBefore = prev ? prev.metrics.kernOf(prev.cp, cp) * (prev.fontSize / prev.metrics.size) : 0;
690
+ const blank = {
691
+ run: runIndex,
692
+ cp,
693
+ fontSize,
694
+ lineSize: Math.max(run.fontSize, run.strutSize ?? 0),
695
+ // Optional on the authored run type; absent, every y would go NaN.
696
+ baselineShift: run.baselineShift ?? 0,
697
+ srcIndex: srcStart,
698
+ srcEnd,
699
+ clusterEnd,
700
+ resolved,
701
+ metrics,
702
+ font: null,
703
+ glyph: null,
704
+ advance: 0,
705
+ tracking: 0,
706
+ kerningBefore: 0,
707
+ drawsInk: false
708
+ };
709
+ const inked = (hit, kerning) => ({
710
+ ...blank,
711
+ resolved: hit.resolved,
712
+ metrics: hit.metrics,
713
+ font: hit.font,
714
+ glyph: hit.glyph,
715
+ advance: hit.advance * (fontSize / hit.metrics.size),
716
+ tracking,
717
+ kerningBefore: kerning,
718
+ drawsInk: true
719
+ });
720
+ if (color) {
721
+ entries.push(clusterEnd ? inked(color, 0) : blank);
722
+ prev = void 0;
723
+ } else if (isHardLineBreak(cp)) {
724
+ entries.push(blank);
725
+ prev = void 0;
726
+ } else if (cp === 9 || cp === 32) {
727
+ const space = metrics.advanceOf(32);
728
+ const advance = space !== null ? space * (fontSize / metrics.size) : fontSize * 0.25;
729
+ if (cp === 9) {
730
+ entries.push({ ...blank, tab: { stop: tabSize * (advance + run.letterSpacing), min: advance / 2 } });
731
+ prev = void 0;
732
+ } else {
733
+ const own = glyphIn(resolved, 32);
734
+ prev = {
735
+ ...blank,
736
+ font: own?.font ?? null,
737
+ glyph: own?.glyph ?? null,
738
+ advance,
739
+ tracking,
740
+ kerningBefore
741
+ };
742
+ entries.push(prev);
743
+ }
744
+ } else {
745
+ const hit = resolveGlyph(resolved, run.fontFamily, run.fontWeight, run.fontStyle, cp);
746
+ prev = hit ? inked(hit, kerningBefore) : blank;
747
+ entries.push(prev);
748
+ }
749
+ }
750
+ });
751
+ return entries;
752
+ }
753
+ function mirrorEntries(entries, levels, mirror, runs) {
754
+ return entries.map((entry, pos) => {
755
+ if (levels[pos] % 2 === 0) return entry;
756
+ const cp = mirror(entry.cp);
757
+ if (cp === null || cp === entry.cp) return entry;
758
+ if (entry.font === null) return { ...entry, cp };
759
+ const run = runs[entry.run];
760
+ const hit = resolveGlyph(entry.resolved, run.fontFamily, run.fontWeight, run.fontStyle, cp, false);
761
+ if (!hit) return entry;
762
+ return { ...entry, cp, resolved: hit.resolved, metrics: hit.metrics, font: hit.font, glyph: hit.glyph };
763
+ });
764
+ }
765
+
494
766
  // src/layout/lineBreak/wrapLines.ts
495
- var SPACE = 32;
767
+ var hangs = (cp) => cp === 32 || cp === 9;
496
768
  function wrapLines(cps, maxWidth, widthOf) {
497
769
  const n = cps.length;
498
770
  const breaks = Number.isFinite(maxWidth) ? lineBreakOpportunities(cps) : null;
@@ -507,14 +779,14 @@ function wrapLines(cps, maxWidth, widthOf) {
507
779
  start = i;
508
780
  continue;
509
781
  }
510
- if (cp === SPACE) {
782
+ if (hangs(cp)) {
511
783
  i++;
512
784
  continue;
513
785
  }
514
786
  let j = i + 1;
515
787
  let ink = i + 1;
516
788
  while (j < n && !isHardLineBreak(cps[j]) && (!breaks || breaks[j] === NO_BREAK)) {
517
- if (cps[j] !== SPACE) ink = j + 1;
789
+ if (!hangs(cps[j])) ink = j + 1;
518
790
  j++;
519
791
  }
520
792
  if (breaks && i > start && widthOf(start, ink) > maxWidth) {
@@ -527,609 +799,175 @@ function wrapLines(cps, maxWidth, widthOf) {
527
799
  return lines;
528
800
  }
529
801
 
530
- // src/layout/layoutRuns.ts
531
- function atlasMetrics(font) {
532
- const size = font.info.size;
533
- const { ascent, descent } = font.faceMetrics ?? {};
534
- return {
535
- size,
536
- base: font.common.base,
537
- ...ascent !== void 0 && descent !== void 0 ? { ascent: ascent * size, descent: descent * size } : { ascent: font.common.base },
538
- advanceOf: (cp) => font.charMap.get(cp)?.xadvance ?? null,
539
- kernOf: (l, r) => font.kerningMap.get(l)?.get(r) ?? 0
540
- };
541
- }
542
- function fillKey(p) {
543
- if (p === null) return "none";
544
- if ("color" in p) return `s:${p.color}:${p.opacity ?? 1}`;
545
- return `nx:${Math.random()}`;
546
- }
547
- function sameFill(a, b) {
548
- if (a === b) return true;
549
- if (a !== null && b !== null && "color" in a && "color" in b) {
550
- return a.color === b.color && (a.opacity ?? 1) === (b.opacity ?? 1);
551
- }
552
- return false;
553
- }
554
- function strokePaints(s) {
555
- if (s?.paint === void 0) return false;
556
- const w = s.width ?? 1;
557
- return (typeof w === "number" ? w : w.px) > 0;
558
- }
559
- function strokeKey(s) {
560
- if (!strokePaints(s)) return "-";
561
- const width = s.width ?? 1;
562
- return [
563
- // A screen-pixel width keys apart from the equal world-unit number: the two
564
- // resolve to different ribbons, so they must not share a draw call.
565
- fillKey(s.paint),
566
- typeof width === "number" ? width : `px${width.px}`,
567
- s.join ?? "miter",
568
- s.cap ?? "butt",
569
- s.miterLimit ?? "",
570
- s.align ?? "center",
571
- (s.dash ?? []).join(",")
572
- ].join(":");
573
- }
574
- function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
575
- return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
576
- }
577
- function getOrCreateGroup(ctx, run, resolved2, page, sourceOverride) {
578
- const atlasFamily = resolved2.resolved.family;
579
- const resolvedWeight = resolved2.resolved.weight;
580
- const resolvedStyle = resolved2.resolved.style;
581
- const source = sourceOverride ?? resolved2.source;
582
- const key = groupKey(
583
- atlasFamily,
584
- resolvedWeight,
585
- resolvedStyle,
586
- resolved2.synthetic,
587
- run.fill,
588
- source === "outline" && strokePaints(run.stroke) ? run.stroke : void 0,
589
- source,
590
- page
591
- );
592
- let g = ctx.groups.get(key);
593
- if (!g) {
594
- g = {
595
- family: atlasFamily,
596
- weight: resolvedWeight,
597
- style: resolvedStyle,
598
- synthetic: { ...resolved2.synthetic },
599
- source,
600
- page,
601
- fill: run.fill,
602
- // Only the outline tier can paint it, and carrying it on an SDF group
603
- // would be a promise the renderer cannot keep.
604
- ...source === "outline" && strokePaints(run.stroke) ? { stroke: run.stroke } : {},
605
- quads: [],
606
- glyphs: []
607
- };
608
- ctx.groups.set(key, g);
609
- }
610
- return g;
611
- }
612
- function resolveGlyph(run, font, resolved2, cp) {
613
- const direct = font.charMap.get(cp);
614
- if (direct) return { glyph: direct, font, resolved: resolved2 };
615
- const fallback = resolveGlyphFallback(run.fontFamily, run.fontWeight, run.fontStyle);
616
- const face = fallback?.dynamicFace;
617
- if (fallback && face) {
618
- const glyph = face.requestGlyph(cp);
619
- if (glyph.xadvance > 0 || glyph.width > 0) {
620
- return { glyph, font: face.font, resolved: fallback };
621
- }
622
- }
623
- warnMissingGlyphOnce(resolved2.resolved.family, cp);
624
- return null;
625
- }
626
- var RTL_BLOCKS = [
627
- [1424, 1535],
628
- [1536, 1983],
629
- [2144, 2303],
630
- [64285, 65023],
631
- [65136, 65279],
632
- [67584, 69631],
633
- [124928, 126975]
634
- ];
635
- var warnedLogicalRtl = false;
636
- function warnLogicalRtlOnce() {
637
- if (warnedLogicalRtl) return;
638
- warnedLogicalRtl = true;
639
- console.warn(
640
- // Naming the import in `from "<pkg>"` form would trip labkit's
641
- // consumer smoke test, which greps dist for that exact shape.
642
- "weasel layoutRuns: this text contains right-to-left characters but no `bidi` engine was supplied, so it is being laid out in logical order \u2014 the glyphs will appear reversed. Import `bidi` out of the `@weasel-js/bidi` package and add it to the layout options."
643
- );
644
- }
645
- var warnedMissingGlyphs = /* @__PURE__ */ new Set();
646
- function warnMissingGlyphOnce(family, cp) {
647
- const key = `${family}|${cp}`;
648
- if (warnedMissingGlyphs.has(key)) return;
649
- warnedMissingGlyphs.add(key);
650
- const ch = String.fromCodePoint(cp);
651
- console.warn(
652
- `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
653
- );
802
+ // src/layout/layoutLines.ts
803
+ function aboveBaseline(e, lineHeight) {
804
+ const k = e.lineSize / e.metrics.size;
805
+ const ascent = e.metrics.ascent * k;
806
+ if (e.metrics.descent === void 0) return ascent;
807
+ return (e.lineSize * lineHeight - ascent - e.metrics.descent * k) / 2 + ascent;
654
808
  }
655
- var warnedNoMetrics = /* @__PURE__ */ new Set();
656
- function warnNoMetricsOnce(family, weight, style) {
657
- const key = `${family}|${weight}|${style}`;
658
- if (warnedNoMetrics.has(key)) return;
659
- warnedNoMetrics.add(key);
660
- console.warn(
661
- `weasel layoutRuns: no metrics for "${family}" ${weight}/${style} \u2014 neither an atlas nor an outline face resolved, so every run set in it lays out as nothing. Register it with registerFont / registerCanvasFont / registerFontOutlines. If you did, check for a duplicated @weasel-js/font in node_modules \u2014 its registry is module state, so a second copy is a second, empty one.`
662
- );
663
- }
664
- function layoutRuns(runs, opts) {
665
- const ctx = { groups: /* @__PURE__ */ new Map() };
666
- const align = resolveAlign(opts.align, opts.direction ?? "ltr");
667
- const justify = opts.justify === true || opts.align === "justify";
668
- const lineSize = (e) => Math.max(e.run.fontSize, e.run.strutSize ?? 0);
669
- const aboveBaseline = (e) => {
670
- const k = lineSize(e) / e.metrics.size;
671
- const ascent = e.metrics.ascent * k;
672
- if (e.metrics.descent === void 0) return ascent;
673
- return (lineSize(e) * opts.lineHeight - ascent - e.metrics.descent * k) / 2 + ascent;
674
- };
675
- const entries = [];
676
- const runFaces = /* @__PURE__ */ new Map();
677
- let prevCp;
678
- let prevMetrics;
679
- let prevFontSize;
680
- let srcIndex = 0;
681
- for (const run of runs) {
682
- const resolved2 = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
683
- runFaces.set(run, faceMetricsOf(resolved2));
684
- const outlineFace = resolved2.outlineFace;
685
- const font = resolved2.entry?.font ?? resolved2.dynamicFace?.font ?? null;
686
- const metrics = outlineFace ? {
687
- size: 1,
688
- base: outlineFace.ascender,
689
- ascent: outlineFace.faceMetrics?.ascent ?? outlineFace.ascender,
690
- ...outlineFace.faceMetrics?.descent !== void 0 ? { descent: outlineFace.faceMetrics.descent } : {},
691
- advanceOf: (cp) => outlineFace.advanceOf(cp),
692
- kernOf: (l, r) => outlineFace.kernOf(l, r)
693
- } : font ? atlasMetrics(font) : void 0;
694
- if (!metrics) {
695
- if (!fontPending(run.fontFamily, run.fontWeight, run.fontStyle)) {
696
- warnNoMetricsOnce(run.fontFamily, run.fontWeight, run.fontStyle);
697
- }
698
- srcIndex += run.srcMap?.length ?? run.text.length;
699
- prevCp = void 0;
700
- prevMetrics = void 0;
701
- prevFontSize = void 0;
702
- continue;
703
- }
704
- const runBase = srcIndex;
705
- const map = run.srcMap;
706
- const sizes = run.sizeMap;
707
- let at = 0;
708
- const runTracking = run.letterSpacing;
709
- const clusterEnds = runTracking === 0 ? null : new Set(graphemeEnds(run.text));
710
- for (const ch of [...run.text]) {
711
- const cp = ch.codePointAt(0);
712
- const fontSize = sizes ? sizes[at] : run.fontSize;
713
- const scale = fontSize / metrics.size;
714
- const tracking = clusterEnds === null || clusterEnds.has(at + ch.length) ? runTracking : 0;
715
- const isNewline = isHardLineBreak(cp);
716
- const isSpace = cp === 32;
717
- const srcStart = map ? runBase + map.starts[at] : runBase + at;
718
- const srcEnd = map ? runBase + map.ends[at] : runBase + at + ch.length;
719
- at += ch.length;
720
- srcIndex = map ? runBase + map.length : srcEnd;
721
- if (isNewline) {
722
- entries.push({
723
- run,
724
- font,
725
- metrics,
726
- glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
727
- // A newline consumes no advance, so it takes no tracking either.
728
- cp,
729
- advance: 0,
730
- tracking: 0,
731
- kerningBefore: 0,
732
- isSpace: false,
733
- isNewline: true,
734
- drawsInk: false,
735
- resolved: resolved2,
736
- fontSize,
737
- srcIndex: srcStart,
738
- srcEnd,
739
- flat: 0
740
- });
741
- prevCp = void 0;
742
- prevMetrics = void 0;
743
- prevFontSize = void 0;
744
- continue;
745
- }
746
- if (isSpace) {
747
- const spaceGlyph = resolved2.dynamicFace ? resolved2.dynamicFace.requestGlyph(32) : font?.charMap.get(32) ?? null;
748
- const spaceAdvance = spaceGlyph ? spaceGlyph.xadvance : metrics.advanceOf(32);
749
- const advance = spaceAdvance !== null ? spaceAdvance * scale : fontSize * 0.25;
750
- let kerningBefore2 = 0;
751
- if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
752
- kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
753
- }
754
- entries.push({
755
- run,
756
- font,
757
- metrics,
758
- glyph: spaceGlyph ?? { id: 32, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
759
- cp,
760
- advance,
761
- tracking,
762
- kerningBefore: kerningBefore2,
763
- isSpace: true,
764
- isNewline: false,
765
- drawsInk: false,
766
- resolved: resolved2,
767
- fontSize,
768
- srcIndex: srcStart,
769
- srcEnd,
770
- flat: 0
771
- });
772
- prevCp = cp;
773
- prevMetrics = metrics;
774
- prevFontSize = fontSize;
775
- continue;
776
- }
777
- if (outlineFace) {
778
- const adv = metrics.advanceOf(cp);
779
- if (adv === null) {
780
- entries.push({
781
- run,
782
- font: null,
783
- metrics,
784
- glyph: null,
785
- cp,
786
- advance: 0,
787
- tracking: 0,
788
- kerningBefore: 0,
789
- isSpace,
790
- isNewline: false,
791
- drawsInk: false,
792
- resolved: resolved2,
793
- fontSize,
794
- srcIndex: srcStart,
795
- srcEnd,
796
- flat: 0
797
- });
798
- prevCp = cp;
799
- prevMetrics = metrics;
800
- prevFontSize = fontSize;
801
- continue;
802
- }
803
- let kerningBefore2 = 0;
804
- if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
805
- kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
806
- }
807
- entries.push({
808
- run,
809
- font: null,
810
- metrics,
811
- glyph: null,
812
- cp,
813
- advance: adv * scale,
814
- tracking,
815
- kerningBefore: kerningBefore2,
816
- isSpace,
817
- isNewline: false,
818
- drawsInk: !isSpace,
819
- resolved: resolved2,
820
- fontSize,
821
- srcIndex: srcStart,
822
- srcEnd,
823
- flat: 0
824
- });
825
- prevCp = cp;
826
- prevMetrics = metrics;
827
- prevFontSize = fontSize;
828
- continue;
829
- }
830
- const hit = resolved2.dynamicFace ? { glyph: resolved2.dynamicFace.requestGlyph(cp), font, resolved: resolved2 } : resolveGlyph(run, font, resolved2, cp);
831
- if (!hit) {
832
- entries.push({
833
- run,
834
- font: null,
835
- metrics,
836
- glyph: null,
837
- cp,
838
- advance: 0,
839
- tracking: 0,
840
- kerningBefore: 0,
841
- isSpace,
842
- isNewline: false,
843
- drawsInk: false,
844
- resolved: resolved2,
845
- fontSize,
846
- srcIndex: srcStart,
847
- srcEnd,
848
- flat: 0
849
- });
850
- prevCp = cp;
851
- prevMetrics = metrics;
852
- prevFontSize = fontSize;
853
- continue;
854
- }
855
- const glyphFont = hit.font;
856
- const glyphMetrics = glyphFont === font ? metrics : atlasMetrics(glyphFont);
857
- const glyphScale = fontSize / glyphFont.info.size;
858
- let kerningBefore = 0;
859
- if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
860
- kerningBefore = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
861
- }
862
- entries.push({
863
- run,
864
- font: glyphFont,
865
- metrics: glyphMetrics,
866
- glyph: hit.glyph,
867
- cp,
868
- advance: hit.glyph.xadvance * glyphScale,
869
- tracking,
870
- kerningBefore,
871
- isSpace,
872
- isNewline: false,
873
- drawsInk: !isSpace,
874
- resolved: hit.resolved,
875
- fontSize,
876
- srcIndex: srcStart,
877
- srcEnd,
878
- flat: 0
879
- });
880
- prevCp = cp;
881
- prevMetrics = glyphMetrics;
882
- prevFontSize = fontSize;
883
- }
884
- }
885
- for (let i = 0; i < entries.length; i++) entries[i].flat = i;
886
- const analysis = opts.bidi ? opts.bidi.analyze(entries.map((e) => e.cp), opts.direction ?? "auto") : null;
887
- if (!opts.bidi && entries.some((e) => RTL_BLOCKS.some(([lo, hi]) => e.cp >= lo && e.cp <= hi))) {
888
- warnLogicalRtlOnce();
889
- }
890
- const openingWidth = (e) => e.isSpace ? 0 : e.advance + e.tracking;
809
+ function breakLines(entries, maxWidth, lineHeight) {
891
810
  const sum = new Float64Array(entries.length + 1);
892
- for (let k = 0; k < entries.length; k++) {
893
- const e = entries[k];
811
+ const tabs = [];
812
+ entries.forEach((e, k) => {
894
813
  sum[k + 1] = sum[k] + e.kerningBefore + e.advance + e.tracking;
895
- }
896
- const widthOf = (start, end) => openingWidth(entries[start]) + sum[end] - sum[start + 1];
897
- const lines = [];
898
- for (const span2 of wrapLines(entries.map((e) => e.cp), opts.maxWidth, widthOf)) {
899
- const line = { entries: [], width: 0, height: 0, ...span2.wrapped ? { wrapped: true } : {} };
900
- if (span2.start === span2.end) {
901
- const brk = entries[span2.end];
902
- line.height = lineSize(brk) * opts.lineHeight;
903
- line.blank = brk;
814
+ if (e.tab) tabs.push(k);
815
+ });
816
+ const flatWidth = (start, end) => entries[start].advance + entries[start].tracking + sum[end] - sum[start + 1];
817
+ const widthOf = tabs.length === 0 ? flatWidth : (start, end) => widthWithTabs(start, end, tabs, (k) => entries[k].tab, flatWidth);
818
+ return wrapLines(entries.map((e) => e.cp), maxWidth, widthOf).map((span) => {
819
+ const line = {
820
+ start: span.start,
821
+ entries: [],
822
+ width: 0,
823
+ height: 0,
824
+ ascent: 0,
825
+ wrapped: span.wrapped,
826
+ srcEnd: 0
827
+ };
828
+ if (span.start === span.end) {
829
+ const brk = entries[span.end];
830
+ line.height = brk.lineSize * lineHeight;
831
+ line.ascent = aboveBaseline(brk, lineHeight);
832
+ line.srcEnd = brk.srcIndex;
833
+ return line;
904
834
  }
905
- for (let k = span2.start; k < span2.end; k++) {
835
+ for (let k = span.start; k < span.end; k++) {
906
836
  const e = entries[k];
907
- const placed = k > span2.start ? e : e.isSpace ? { ...e, kerningBefore: 0, advance: 0, tracking: 0 } : { ...e, kerningBefore: 0 };
837
+ const opened = k > span.start ? e : { ...e, kerningBefore: 0 };
838
+ const placed = e.tab ? { ...opened, advance: tabAdvance(line.width, e.tab.stop, e.tab.min) } : opened;
908
839
  line.entries.push(placed);
909
840
  line.width += placed.kerningBefore + placed.advance + placed.tracking;
910
- line.height = Math.max(line.height, lineSize(e) * opts.lineHeight);
911
- }
912
- lines.push(line);
913
- }
914
- function outlineFor(e) {
915
- const r0 = e.resolved.resolved;
916
- if (e.resolved.source === "outline") {
917
- return glyphOutline(r0.family, r0.weight, r0.style, e.cp);
918
- }
919
- const min = opts.outlineMinSize;
920
- if (min === void 0) return null;
921
- if (e.run.fontSize < min && !strokePaints(e.run.stroke)) return null;
922
- if (e.resolved.synthetic.bold) return null;
923
- const r = e.resolved.resolved;
924
- return glyphOutline(r.family, r.weight, r.style, e.cp);
925
- }
926
- const decorations = [];
927
- let span = null;
928
- function flushSpan() {
929
- const s = span;
930
- span = null;
931
- if (!s || s.x1 <= s.x0) return;
932
- for (const kind of DECORATION_KINDS) {
933
- if (!s[kind]) continue;
934
- const { y0, y1 } = decorationRule(kind, s.baselineY, s.fontSize, s.face);
935
- decorations.push({ kind, x0: s.x0, y0, x1: s.x1, y1, fill: s.fill });
841
+ line.height = Math.max(line.height, e.lineSize * lineHeight);
842
+ line.ascent = Math.max(line.ascent, aboveBaseline(placed, lineHeight));
936
843
  }
844
+ line.srcEnd = entries[span.end - 1].srcEnd;
845
+ return line;
846
+ });
847
+ }
848
+ function hungWidth(entries) {
849
+ let hung = 0;
850
+ for (let k = entries.length - 1; k >= 0 && hangs(entries[k].cp); k--) {
851
+ const e = entries[k];
852
+ hung += e.kerningBefore + e.advance + e.tracking;
937
853
  }
938
- const lineBoxes = [];
939
- let penY = 0;
940
- let maxLineWidth = 0;
941
- const alignWidth = opts.alignWidth ?? opts.maxWidth;
942
- for (const line of lines) {
943
- let hung = 0;
944
- for (let k = line.entries.length - 1; k >= 0; k--) {
945
- const e = line.entries[k];
946
- if (!e.isSpace) break;
947
- hung += e.kerningBefore + e.advance + e.tracking;
948
- }
949
- const inkWidth = line.width - hung;
950
- const gaps = /* @__PURE__ */ new Set();
951
- let gapExtra = 0;
952
- if (justify && line.wrapped && Number.isFinite(alignWidth) && alignWidth > inkWidth) {
953
- let first = 0;
954
- while (first < line.entries.length && line.entries[first].isSpace) first++;
955
- let last = line.entries.length - 1;
956
- while (last > first && line.entries[last].isSpace) last--;
957
- for (let k = first + 1; k < last; k++) if (line.entries[k].isSpace) gaps.add(k);
958
- if (gaps.size > 0) gapExtra = (alignWidth - inkWidth) / gaps.size;
959
- }
960
- const spread = gapExtra * gaps.size;
961
- const alignShift = (() => {
962
- if (spread > 0) return 0;
963
- if (align === "left") return 0;
964
- if (!Number.isFinite(alignWidth)) {
965
- return align === "center" ? -inkWidth / 2 : -inkWidth;
966
- }
967
- const slack = alignWidth - inkWidth;
968
- return align === "center" ? slack / 2 : slack;
969
- })();
970
- const lineX0 = alignShift;
971
- let lineAscent = 0;
972
- for (const e of line.entries) lineAscent = Math.max(lineAscent, aboveBaseline(e));
973
- if (line.entries.length === 0 && line.blank) lineAscent = aboveBaseline(line.blank);
974
- const lineBaselineY = penY + lineAscent;
975
- const cells = new Array(line.entries.length);
976
- const flatOf = line.entries.map((e) => e.flat);
977
- const posOfFlat = /* @__PURE__ */ new Map();
978
- flatOf.forEach((f, pos) => posOfFlat.set(f, pos));
979
- let visual = line.entries.map((_, k) => k);
980
- let levelOf = () => 0;
981
- if (opts.bidi && analysis !== null && line.entries.length > 0) {
982
- const from = flatOf[0];
983
- const to = flatOf[flatOf.length - 1] + 1;
984
- const r = opts.bidi.reorder(analysis, from, to);
985
- const seen = /* @__PURE__ */ new Set();
986
- const ordered = [];
987
- for (const f of r.order) {
988
- const pos = posOfFlat.get(f);
989
- if (pos !== void 0 && !seen.has(pos)) {
990
- ordered.push(pos);
991
- seen.add(pos);
992
- }
993
- }
994
- for (let k = 0; k < line.entries.length; k++) if (!seen.has(k)) ordered.push(k);
995
- visual = ordered;
996
- levelOf = (pos) => {
997
- const l = r.levels[flatOf[pos] - from];
998
- return typeof l === "number" && l >= 0 ? l : 0;
999
- };
1000
- }
1001
- if (opts.bidi) {
1002
- for (let pos = 0; pos < line.entries.length; pos++) {
1003
- if (levelOf(pos) % 2 === 0) continue;
1004
- const e = line.entries[pos];
1005
- const m = opts.bidi.mirror(e.cp);
1006
- if (m === null || m === e.cp) continue;
1007
- if (e.font !== null) {
1008
- const hit = resolveGlyph(e.run, e.font, e.resolved, m);
1009
- if (!hit) continue;
1010
- e.glyph = hit.glyph;
1011
- }
1012
- e.cp = m;
1013
- }
1014
- }
1015
- let penX = lineX0;
1016
- let prevPos = -1;
1017
- for (const pos of visual) {
1018
- const e = line.entries[pos];
1019
- penX += prevPos < 0 ? 0 : pos === prevPos + 1 ? e.kerningBefore : prevPos === pos + 1 ? line.entries[prevPos].kerningBefore : 0;
1020
- prevPos = pos;
1021
- const step = e.advance + e.tracking + (gaps.has(pos) ? gapExtra : 0);
1022
- const cell = {
1023
- srcIndex: e.srcIndex,
1024
- srcEnd: e.srcEnd,
1025
- cp: e.cp,
1026
- x: penX,
1027
- advance: step,
1028
- level: levelOf(pos),
1029
- drawsInk: e.drawsInk
1030
- };
1031
- cells[pos] = cell;
1032
- const scale = e.fontSize / e.metrics.size;
1033
- const baselineY = lineBaselineY - (e.run.baselineShift ?? 0);
1034
- const decoFill = e.run.fill;
1035
- if (decoFill !== null && (e.run.underline || e.run.strikethrough || e.run.overline)) {
1036
- const face = runFaces.get(e.run);
1037
- if (span !== null && span.underline === e.run.underline && span.strikethrough === e.run.strikethrough && span.overline === e.run.overline && span.fontSize === e.run.fontSize && span.face === face && span.baselineY === baselineY && sameFill(span.fill, e.run.fill)) {
1038
- span.x1 = penX + step;
1039
- } else {
1040
- flushSpan();
1041
- span = {
1042
- underline: e.run.underline,
1043
- strikethrough: e.run.strikethrough,
1044
- overline: e.run.overline,
1045
- fill: decoFill,
1046
- // The run's size, not the glyph's: a small-caps word is one rule.
1047
- fontSize: e.run.fontSize,
1048
- face,
1049
- baselineY,
1050
- x0: penX,
1051
- x1: penX + step
1052
- };
1053
- }
1054
- } else {
1055
- flushSpan();
1056
- }
1057
- const outlineD = outlineFor(e);
1058
- if (e.run.fill === null && !(outlineD !== null && strokePaints(e.run.stroke))) {
1059
- penX += step;
1060
- continue;
1061
- }
1062
- if (outlineD !== null) {
1063
- const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
1064
- group2.glyphs.push({
1065
- d: outlineD,
1066
- key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
1067
- x: penX,
1068
- baselineY,
1069
- // Em space is unit-scale, so world units per em is just the size.
1070
- scale: e.fontSize
1071
- });
1072
- penX += step;
1073
- continue;
1074
- }
1075
- if (e.font === null || e.glyph === null || e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
1076
- penX += step;
1077
- continue;
1078
- }
1079
- const group = getOrCreateGroup(ctx, e.run, e.resolved, e.glyph.page);
1080
- const atlasW = e.font.common.scaleW;
1081
- const atlasH = e.font.common.scaleH;
1082
- const qx0 = penX + e.glyph.xoffset * scale;
1083
- const qy0 = baselineY + (e.glyph.yoffset - e.metrics.base) * scale;
1084
- const qx1 = qx0 + e.glyph.width * scale;
1085
- const qy1 = qy0 + e.glyph.height * scale;
1086
- const u0 = e.glyph.x / atlasW;
1087
- const v0 = e.glyph.y / atlasH;
1088
- const u1 = (e.glyph.x + e.glyph.width) / atlasW;
1089
- const v1 = (e.glyph.y + e.glyph.height) / atlasH;
1090
- group.quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0, v0, u1, v1, baselineY });
1091
- penX += step;
1092
- }
1093
- flushSpan();
1094
- const lastCell = line.entries[line.entries.length - 1];
1095
- const srcEnd = lastCell ? lastCell.srcEnd : line.blank?.srcIndex ?? 0;
1096
- lineBoxes.push({
1097
- x0: lineX0,
1098
- y0: penY,
1099
- x1: lineX0 + line.width + spread,
1100
- y1: penY + line.height,
1101
- baselineY: lineBaselineY,
1102
- cells,
1103
- srcEnd
854
+ return hung;
855
+ }
856
+ function wordGaps(entries) {
857
+ const gaps = /* @__PURE__ */ new Set();
858
+ let first = 0;
859
+ while (first < entries.length && entries[first].cp === 32) first++;
860
+ let last = entries.length - 1;
861
+ while (last > first && entries[last].cp === 32) last--;
862
+ for (let k = first + 1; k < last; k++) if (entries[k].cp === 32) gaps.add(k);
863
+ return gaps;
864
+ }
865
+ function alignOffset(align, alignWidth, inkWidth) {
866
+ if (align === "left") return 0;
867
+ if (!Number.isFinite(alignWidth)) return align === "center" ? -inkWidth / 2 : -inkWidth;
868
+ const slack = alignWidth - inkWidth;
869
+ return align === "center" ? slack / 2 : slack;
870
+ }
871
+ function placeLine(line, y, order, align, alignWidth, justify) {
872
+ const { entries } = line;
873
+ const hung = hungWidth(entries);
874
+ const inkWidth = line.width - hung;
875
+ const slack = alignWidth - inkWidth;
876
+ const gaps = justify && line.wrapped && Number.isFinite(alignWidth) && slack > 0 ? wordGaps(entries) : /* @__PURE__ */ new Set();
877
+ const gapExtra = gaps.size > 0 ? slack / gaps.size : 0;
878
+ const spread = gapExtra * gaps.size;
879
+ const inkLeft = spread > 0 ? 0 : alignOffset(align, alignWidth, inkWidth);
880
+ const hangsLeft = hung > 0 && order.levels[entries.length - 1] % 2 === 1;
881
+ const left = hangsLeft ? inkLeft - hung : inkLeft;
882
+ const lineBaselineY = y + line.ascent;
883
+ const cells = new Array(entries.length);
884
+ const glyphs = [];
885
+ let penX = left;
886
+ let prevPos = -1;
887
+ for (const pos of order.visual) {
888
+ const e = entries[pos];
889
+ penX += prevPos < 0 ? 0 : pos === prevPos + 1 ? e.kerningBefore : prevPos === pos + 1 ? entries[prevPos].kerningBefore : 0;
890
+ prevPos = pos;
891
+ const step = e.advance + e.tracking + (gaps.has(pos) ? gapExtra : 0);
892
+ cells[pos] = {
893
+ srcIndex: e.srcIndex,
894
+ srcEnd: e.srcEnd,
895
+ cp: e.cp,
896
+ x: penX,
897
+ advance: step,
898
+ level: order.levels[pos],
899
+ drawsInk: e.drawsInk,
900
+ clusterEnd: e.clusterEnd
901
+ };
902
+ glyphs.push({
903
+ run: e.run,
904
+ cp: e.cp,
905
+ fontSize: e.fontSize,
906
+ resolved: e.resolved,
907
+ metrics: e.metrics,
908
+ font: e.font,
909
+ glyph: e.glyph,
910
+ x: penX,
911
+ // Positive raises, and y grows down, so the shift subtracts.
912
+ baselineY: lineBaselineY - e.baselineShift,
913
+ advance: e.advance,
914
+ step
1104
915
  });
1105
- maxLineWidth = Math.max(maxLineWidth, inkWidth + spread);
1106
- penY += line.height;
916
+ penX += step;
1107
917
  }
1108
918
  return {
1109
- groups: [...ctx.groups.values()],
1110
- decorations,
1111
- lines: lineBoxes,
1112
- bounds: { width: maxLineWidth, height: penY }
919
+ box: {
920
+ x0: left,
921
+ y0: y,
922
+ x1: left + line.width + spread,
923
+ y1: y + line.height,
924
+ baselineY: lineBaselineY,
925
+ cells,
926
+ glyphs,
927
+ srcEnd: line.srcEnd
928
+ },
929
+ inkWidth: inkWidth + spread
1113
930
  };
1114
931
  }
1115
- var LAYOUT_CACHE_VARIANT_LIMIT = 8;
1116
- var LAYOUT_CACHE_STRUCTURAL_LIMIT = 64;
1117
- var cache = /* @__PURE__ */ new WeakMap();
1118
- var structural = /* @__PURE__ */ new Map();
1119
- var structuralGeneration = -1;
1120
- function outlineBucket(runs, min) {
1121
- if (min === void 0) return -1;
1122
- const sizes = /* @__PURE__ */ new Set();
1123
- for (const r of runs) sizes.add(r.fontSize);
1124
- let n = 0;
1125
- for (const size of sizes) if (size >= min) n++;
1126
- return n;
932
+
933
+ // src/layout/layoutOpts.ts
934
+ function resolveLayoutOpts(opts) {
935
+ return {
936
+ maxWidth: opts.maxWidth,
937
+ alignWidth: opts.alignWidth ?? opts.maxWidth,
938
+ lineHeight: opts.lineHeight,
939
+ align: opts.align,
940
+ justify: opts.justify === true || opts.align === "justify",
941
+ bidi: opts.bidi ?? registeredBidi(),
942
+ direction: opts.direction ?? "auto",
943
+ tabSize: opts.tabSize ?? DEFAULT_TAB_SIZE
944
+ };
1127
945
  }
1128
- function variantKey(runs, opts) {
1129
- const alignWidth = opts.alignWidth ?? opts.maxWidth;
1130
- const justify = opts.justify === true || opts.align === "justify";
1131
- return `${opts.maxWidth}|${alignWidth}|${opts.lineHeight}|${opts.align}|${justify ? "j" : ""}|${outlineBucket(runs, opts.outlineMinSize)}`;
946
+
947
+ // src/layout/layoutRuns.ts
948
+ function layoutRuns(runs, opts) {
949
+ const o = resolveLayoutOpts(opts);
950
+ const entries = measureRuns(runs, o.tabSize);
951
+ const cps = entries.map((e) => e.cp);
952
+ const { bidi } = o;
953
+ const paragraphs = bidi ? analyzeParagraphs(bidi, cps, (i) => isHardLineBreak(cps[i]), o.direction) : null;
954
+ if (!bidi) warnRtlWithoutEngine(cps);
955
+ const lines = [];
956
+ let y = 0;
957
+ let width = 0;
958
+ for (const line of breakLines(entries, o.maxWidth, o.lineHeight)) {
959
+ const order = orderLine(paragraphs, line.start, line.entries.length);
960
+ if (bidi) line.entries = mirrorEntries(line.entries, order.levels, (cp) => bidi.mirror(cp), runs);
961
+ const reads = o.direction !== "auto" ? o.direction : directionOfLevel(line.entries.length > 0 ? paragraphs?.baseLevel(line.start) ?? 0 : 0);
962
+ const placed = placeLine(line, y, order, resolveAlign(o.align, reads), o.alignWidth, o.justify);
963
+ lines.push(placed.box);
964
+ width = Math.max(width, placed.inkWidth);
965
+ y += line.height;
966
+ }
967
+ return { lines, bounds: { width, height: y } };
1132
968
  }
969
+
970
+ // src/layout/refId.ts
1133
971
  var nextRefId = 1;
1134
972
  var refIds = /* @__PURE__ */ new WeakMap();
1135
973
  function refId(o) {
@@ -1140,32 +978,26 @@ function refId(o) {
1140
978
  }
1141
979
  return id;
1142
980
  }
1143
- function paintKey(p) {
1144
- if (p === null) return "none";
1145
- return "color" in p ? `s${p.color}:${p.opacity ?? 1}` : `#${refId(p)}`;
1146
- }
1147
- function strokeKey2(s) {
1148
- if (!s?.paint) return "-";
1149
- const width = s.width ?? 1;
1150
- return [
1151
- paintKey(s.paint),
1152
- typeof width === "number" ? width : `px${width.px}`,
1153
- s.join ?? "miter",
1154
- s.cap ?? "butt",
1155
- s.miterLimit ?? "",
1156
- s.align ?? "center",
1157
- (s.dash ?? []).join(",")
1158
- ].join(":");
981
+
982
+ // src/layout/layoutCache.ts
983
+ var LAYOUT_CACHE_VARIANT_LIMIT = 8;
984
+ var LAYOUT_CACHE_STRUCTURAL_LIMIT = 64;
985
+ var cache = /* @__PURE__ */ new WeakMap();
986
+ var structural = /* @__PURE__ */ new Map();
987
+ var structuralGeneration = -1;
988
+ function variantKey(opts) {
989
+ const o = resolveLayoutOpts(opts);
990
+ return `${o.maxWidth}|${o.alignWidth}|${o.lineHeight}|${o.align}|${o.justify ? "j" : ""}|${o.direction}|${o.bidi ? refId(o.bidi) : ""}|${o.tabSize}`;
1159
991
  }
1160
992
  function runsKey(runs) {
1161
993
  let out = "";
1162
994
  for (const r of runs) {
1163
- out += `${r.text.length}:${r.text}|${r.fontFamily.length}:${r.fontFamily}|${r.fontSize}|${r.fontWeight}|${r.fontStyle}|${r.letterSpacing}|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}|${r.baselineShift}|${paintKey(r.fill)}|${strokeKey2(r.stroke)}|${r.srcMap ? `${r.srcMap.length}:${r.srcMap.starts.join(",")}` : ""}|${r.sizeMap ? r.sizeMap.join(",") : ""}|`;
995
+ out += `${r.text.length}:${r.text}|${r.fontFamily.length}:${r.fontFamily}|${r.fontSize}|${r.fontWeight}|${r.fontStyle}|${r.letterSpacing}|${r.baselineShift}|${r.strutSize ?? ""}|${r.srcMap ? `${r.srcMap.length}:${r.srcMap.starts.join(",")}` : ""}|${r.sizeMap ? r.sizeMap.join(",") : ""}|`;
1164
996
  }
1165
997
  return out;
1166
998
  }
1167
999
  function cachedLayoutRuns(runs, opts) {
1168
- const fonts = glyphGeneration();
1000
+ const fonts = faceGeneration();
1169
1001
  let entry = cache.get(runs);
1170
1002
  if (entry === void 0) {
1171
1003
  entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
@@ -1174,7 +1006,7 @@ function cachedLayoutRuns(runs, opts) {
1174
1006
  entry.generation = fonts;
1175
1007
  entry.byVariant.clear();
1176
1008
  }
1177
- const key = variantKey(runs, opts);
1009
+ const key = variantKey(opts);
1178
1010
  const hit = entry.byVariant.get(key);
1179
1011
  if (hit !== void 0) return hit;
1180
1012
  if (structuralGeneration !== fonts) {
@@ -1202,6 +1034,6 @@ function _resetLayoutCacheForTests() {
1202
1034
  structuralGeneration = -1;
1203
1035
  }
1204
1036
 
1205
- export { BREAK_ALLOWED, BREAK_MANDATORY, DECORATION_KINDS, DEFAULT_DECORATION_METRICS, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, _resetLayoutCacheForTests, cachedLayoutRuns, decorationMetrics, decorationRule, fontString, graphemeCount, isHardLineBreak, layoutRuns, lineBreakOpportunities, resolveAlign, resolveTextStyle, wrapLines };
1206
- //# sourceMappingURL=chunk-L6UJ4RQK.js.map
1207
- //# sourceMappingURL=chunk-L6UJ4RQK.js.map
1037
+ export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TAB_SIZE, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, _resetBidiWarningForTests, _resetLayoutCacheForTests, cachedLayoutRuns, fontString, graphemeCount, hangs, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle, tabAdvance, widthWithTabs, wrapLines };
1038
+ //# sourceMappingURL=chunk-Z6YRJ33J.js.map
1039
+ //# sourceMappingURL=chunk-Z6YRJ33J.js.map