@weasel-js/text 1.9.4 → 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,5 +1,5 @@
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) => {
@@ -14,18 +14,24 @@ function tabAdvance(x, stop, min) {
14
14
  const toNext = (Math.floor(x / stop) + 1) * stop - x;
15
15
  return toNext < min ? toNext + stop : toNext;
16
16
  }
17
- function widthWithTabs(start, end, tabs, stopOf, flatWidth) {
17
+ function widthWithTabs(start, end, tabs, stopOf, flatWidth, piece) {
18
18
  let x = 0;
19
19
  let from = start;
20
+ const lay = (to) => {
21
+ if (to <= from) return;
22
+ piece?.(from, to, x);
23
+ x += flatWidth(from, to);
24
+ };
20
25
  for (const t of tabs) {
21
26
  if (t < start) continue;
22
27
  if (t >= end) break;
23
- if (t > from) x += flatWidth(from, t);
28
+ lay(t);
24
29
  const { stop, min } = stopOf(t);
25
30
  x += tabAdvance(x, stop, min);
26
31
  from = t + 1;
27
32
  }
28
- return from < end ? x + flatWidth(from, end) : x;
33
+ lay(end);
34
+ return x;
29
35
  }
30
36
 
31
37
  // src/textStyle.ts
@@ -34,9 +40,9 @@ function resolveAlign(align, direction) {
34
40
  if (align === "end") return direction === "rtl" ? "left" : "right";
35
41
  return align;
36
42
  }
37
- var DEFAULT_FILL = { fill: "solid", color: "#000" };
43
+ var DEFAULT_FILL = { kind: "solid", color: "#000" };
38
44
  function paintColor(p) {
39
- return p !== null && "color" in p ? p.color : "#000";
45
+ return isSolidPaint(p) ? p.color : "#000";
40
46
  }
41
47
  function defaultSelectionBackground(textColor) {
42
48
  return `color-mix(in srgb, ${textColor} 25%, transparent)`;
@@ -51,7 +57,7 @@ var DEFAULT_TEXT_STYLE = {
51
57
  lineHeight: 1.2,
52
58
  tabSize: DEFAULT_TAB_SIZE,
53
59
  wrap: false,
54
- fill: DEFAULT_FILL,
60
+ fill: { paint: DEFAULT_FILL },
55
61
  caretColor: paintColor(DEFAULT_FILL),
56
62
  selectionBackground: defaultSelectionBackground(paintColor(DEFAULT_FILL)),
57
63
  selectionColor: null,
@@ -69,12 +75,12 @@ function resolveTextStyle(style, paint, scale = 1) {
69
75
  return fill === DEFAULT_TEXT_STYLE.fill && stroke === void 0 ? DEFAULT_TEXT_STYLE : {
70
76
  ...DEFAULT_TEXT_STYLE,
71
77
  fill,
72
- caretColor: paintColor(fill),
73
- selectionBackground: defaultSelectionBackground(paintColor(fill)),
78
+ caretColor: paintColor(fill?.paint ?? null),
79
+ selectionBackground: defaultSelectionBackground(paintColor(fill?.paint ?? null)),
74
80
  ...stroke !== void 0 ? { stroke } : {}
75
81
  };
76
82
  }
77
- const caretColor = style.caretColor ?? paintColor(fill);
83
+ const caretColor = style.caretColor ?? paintColor(fill?.paint ?? null);
78
84
  let selectionBackground;
79
85
  if (style.selectionBackground === "none") {
80
86
  selectionBackground = null;
@@ -111,34 +117,6 @@ function fontString(s) {
111
117
  return `${s.fontStyle} ${s.fontWeight} ${s.fontSize}px ${cssFamilyName(s.fontFamily)}`;
112
118
  }
113
119
 
114
- // src/layout/decorationMetrics.ts
115
- var DECORATION_KINDS = ["underline", "strikethrough", "overline"];
116
- var DEFAULT_DECORATION_METRICS = Object.freeze({
117
- underline: Object.freeze({ offset: 0.1, thickness: 0.05 }),
118
- strikethrough: Object.freeze({ offset: -0.3, thickness: 0.05 }),
119
- overline: Object.freeze({ offset: -0.9, thickness: 0.05 })
120
- });
121
- var resolved = /* @__PURE__ */ new WeakMap();
122
- function decorationMetrics(face) {
123
- if (!face) return DEFAULT_DECORATION_METRICS;
124
- let out = resolved.get(face);
125
- if (!out) {
126
- const underline = face.underline ?? DEFAULT_DECORATION_METRICS.underline;
127
- out = Object.freeze({
128
- underline,
129
- strikethrough: face.strikethrough ?? DEFAULT_DECORATION_METRICS.strikethrough,
130
- overline: Object.freeze({ offset: DEFAULT_DECORATION_METRICS.overline.offset, thickness: underline.thickness })
131
- });
132
- resolved.set(face, out);
133
- }
134
- return out;
135
- }
136
- function decorationRule(kind, baselineY, fontSize, face) {
137
- const m = decorationMetrics(face)[kind];
138
- const y0 = baselineY + fontSize * m.offset;
139
- return { y0, y1: y0 + fontSize * m.thickness };
140
- }
141
-
142
120
  // src/layout/lineBreak/lineBreakTable.ts
143
121
  var lineBreakTable_exports = {};
144
122
  __export(lineBreakTable_exports, {
@@ -518,6 +496,47 @@ function paragraphDirection(text, direction = "auto", engine = registered) {
518
496
  }
519
497
  return directionOfLevel(engine.baseLevel(engine.analyze(cps, "auto")));
520
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
+ }
521
540
 
522
541
  // src/layout/bidiLines.ts
523
542
  function analyzeParagraphs(engine, codePoints, isBreak, direction) {
@@ -583,17 +602,20 @@ function restoreRemoved(order, count, rtl) {
583
602
  }
584
603
  return out;
585
604
  }
586
-
587
- // src/layout/refId.ts
588
- var nextRefId = 1;
589
- var refIds = /* @__PURE__ */ new WeakMap();
590
- function refId(o) {
591
- let id = refIds.get(o);
592
- if (id === void 0) {
593
- id = nextRefId++;
594
- refIds.set(o, id);
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;
595
612
  }
596
- return id;
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 };
597
619
  }
598
620
 
599
621
  // src/measure/graphemes.ts
@@ -624,6 +646,123 @@ function graphemeCount(text) {
624
646
  return graphemeEnds(text).length;
625
647
  }
626
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
+
627
766
  // src/layout/lineBreak/wrapLines.ts
628
767
  var hangs = (cp) => cp === 32 || cp === 9;
629
768
  function wrapLines(cps, maxWidth, widthOf) {
@@ -660,681 +799,205 @@ function wrapLines(cps, maxWidth, widthOf) {
660
799
  return lines;
661
800
  }
662
801
 
663
- // src/layout/layoutRuns.ts
664
- function atlasMetrics(font) {
665
- const size = font.info.size;
666
- const { ascent, descent } = font.faceMetrics ?? {};
667
- return {
668
- size,
669
- base: font.common.base,
670
- ...ascent !== void 0 && descent !== void 0 ? { ascent: ascent * size, descent: descent * size } : { ascent: font.common.base },
671
- advanceOf: (cp) => font.charMap.get(cp)?.xadvance ?? null,
672
- kernOf: (l, r) => font.kerningMap.get(l)?.get(r) ?? 0
673
- };
674
- }
675
- function fillKey(p) {
676
- if (p === null) return "none";
677
- if ("color" in p) return `s:${p.color}:${p.opacity ?? 1}`;
678
- return `nx:${refId(p)}`;
679
- }
680
- function sameFill(a, b) {
681
- if (a === b) return true;
682
- if (a !== null && b !== null && "color" in a && "color" in b) {
683
- return a.color === b.color && (a.opacity ?? 1) === (b.opacity ?? 1);
684
- }
685
- return false;
686
- }
687
- function strokePaints(s) {
688
- if (s?.paint === void 0) return false;
689
- const w = s.width ?? 1;
690
- return (typeof w === "number" ? w : w.px) > 0;
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;
691
808
  }
692
- function strokeKey(s) {
693
- if (!strokePaints(s)) return "-";
694
- const width = s.width ?? 1;
695
- return [
696
- // A screen-pixel width keys apart from the equal world-unit number: the two
697
- // resolve to different ribbons, so they must not share a draw call.
698
- fillKey(s.paint),
699
- typeof width === "number" ? width : `px${width.px}`,
700
- s.join ?? "miter",
701
- s.cap ?? "butt",
702
- s.miterLimit ?? "",
703
- s.align ?? "center",
704
- (s.dash ?? []).join(",")
705
- ].join(":");
706
- }
707
- function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
708
- return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
709
- }
710
- function getOrCreateGroup(ctx, run, resolved2, page, sourceOverride) {
711
- const atlasFamily = resolved2.resolved.family;
712
- const resolvedWeight = resolved2.resolved.weight;
713
- const resolvedStyle = resolved2.resolved.style;
714
- const source = sourceOverride ?? resolved2.source;
715
- const key = groupKey(
716
- atlasFamily,
717
- resolvedWeight,
718
- resolvedStyle,
719
- resolved2.synthetic,
720
- run.fill,
721
- source === "outline" && strokePaints(run.stroke) ? run.stroke : void 0,
722
- source,
723
- page
724
- );
725
- let g = ctx.groups.get(key);
726
- if (!g) {
727
- g = {
728
- family: atlasFamily,
729
- weight: resolvedWeight,
730
- style: resolvedStyle,
731
- synthetic: { ...resolved2.synthetic },
732
- source,
733
- page,
734
- fill: run.fill,
735
- // Only the outline tier can paint it, and carrying it on an SDF group
736
- // would be a promise the renderer cannot keep.
737
- ...source === "outline" && strokePaints(run.stroke) ? { stroke: run.stroke } : {},
738
- quads: [],
739
- glyphs: []
740
- };
741
- ctx.groups.set(key, g);
742
- }
743
- return g;
744
- }
745
- function resolveGlyph(run, font, resolved2, cp, warn = true) {
746
- const direct = font.charMap.get(cp);
747
- if (direct) return { glyph: direct, font, resolved: resolved2 };
748
- const fallback = resolveGlyphFallback(run.fontFamily, run.fontWeight, run.fontStyle);
749
- const face = fallback?.dynamicFace;
750
- if (fallback && face) {
751
- const glyph = face.requestGlyph(cp);
752
- if (glyph.xadvance > 0 || glyph.width > 0) {
753
- return { glyph, font: face.font, resolved: fallback };
754
- }
755
- }
756
- if (warn) warnMissingGlyphOnce(resolved2.resolved.family, cp);
757
- return null;
758
- }
759
- var RTL_BLOCKS = [
760
- [1424, 1535],
761
- [1536, 1983],
762
- [2144, 2303],
763
- [64285, 65023],
764
- [65136, 65279],
765
- [67584, 69631],
766
- [124928, 126975]
767
- ];
768
- var warnedLogicalRtl = false;
769
- function warnLogicalRtlOnce() {
770
- if (warnedLogicalRtl) return;
771
- warnedLogicalRtl = true;
772
- console.warn(
773
- // Naming the import in `from "<pkg>"` form would trip labkit's
774
- // consumer smoke test, which greps dist for that exact shape.
775
- "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."
776
- );
777
- }
778
- var warnedMissingGlyphs = /* @__PURE__ */ new Set();
779
- function warnMissingGlyphOnce(family, cp) {
780
- const key = `${family}|${cp}`;
781
- if (warnedMissingGlyphs.has(key)) return;
782
- warnedMissingGlyphs.add(key);
783
- const ch = String.fromCodePoint(cp);
784
- console.warn(
785
- `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.`
786
- );
787
- }
788
- var warnedNoMetrics = /* @__PURE__ */ new Set();
789
- function warnNoMetricsOnce(family, weight, style) {
790
- const key = `${family}|${weight}|${style}`;
791
- if (warnedNoMetrics.has(key)) return;
792
- warnedNoMetrics.add(key);
793
- console.warn(
794
- `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.`
795
- );
796
- }
797
- function layoutRuns(runs, opts) {
798
- const ctx = { groups: /* @__PURE__ */ new Map() };
799
- const bidi = opts.bidi ?? registeredBidi();
800
- const direction = opts.direction ?? "auto";
801
- const justify = opts.justify === true || opts.align === "justify";
802
- const tabSize = opts.tabSize ?? DEFAULT_TAB_SIZE;
803
- const lineSize = (e) => Math.max(e.run.fontSize, e.run.strutSize ?? 0);
804
- const aboveBaseline = (e) => {
805
- const k = lineSize(e) / e.metrics.size;
806
- const ascent = e.metrics.ascent * k;
807
- if (e.metrics.descent === void 0) return ascent;
808
- return (lineSize(e) * opts.lineHeight - ascent - e.metrics.descent * k) / 2 + ascent;
809
- };
810
- const entries = [];
811
- const closesCluster = [];
812
- const runFaces = /* @__PURE__ */ new Map();
813
- let prevCp;
814
- let prevMetrics;
815
- let prevFontSize;
816
- let srcIndex = 0;
817
- for (const run of runs) {
818
- const resolved2 = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
819
- runFaces.set(run, faceMetricsOf(resolved2));
820
- const outlineFace = resolved2.outlineFace;
821
- const font = resolved2.entry?.font ?? resolved2.dynamicFace?.font ?? null;
822
- const metrics = outlineFace ? {
823
- size: 1,
824
- base: outlineFace.ascender,
825
- ascent: outlineFace.faceMetrics?.ascent ?? outlineFace.ascender,
826
- ...outlineFace.faceMetrics?.descent !== void 0 ? { descent: outlineFace.faceMetrics.descent } : {},
827
- advanceOf: (cp) => outlineFace.advanceOf(cp),
828
- kernOf: (l, r) => outlineFace.kernOf(l, r)
829
- } : font ? atlasMetrics(font) : void 0;
830
- if (!metrics) {
831
- if (!fontPending(run.fontFamily, run.fontWeight, run.fontStyle)) {
832
- warnNoMetricsOnce(run.fontFamily, run.fontWeight, run.fontStyle);
833
- }
834
- srcIndex += run.srcMap?.length ?? run.text.length;
835
- prevCp = void 0;
836
- prevMetrics = void 0;
837
- prevFontSize = void 0;
838
- continue;
839
- }
840
- const runBase = srcIndex;
841
- const map = run.srcMap;
842
- const sizes = run.sizeMap;
843
- let at = 0;
844
- const runTracking = run.letterSpacing;
845
- const clusterEnds = new Set(graphemeEnds(run.text));
846
- for (const ch of [...run.text]) {
847
- const cp = ch.codePointAt(0);
848
- const fontSize = sizes ? sizes[at] : run.fontSize;
849
- const scale = fontSize / metrics.size;
850
- const clusterEnd = clusterEnds.has(at + ch.length);
851
- closesCluster.push(clusterEnd);
852
- const tracking = clusterEnd ? runTracking : 0;
853
- const isNewline = isHardLineBreak(cp);
854
- const isSpace = cp === 32;
855
- const srcStart = map ? runBase + map.starts[at] : runBase + at;
856
- const srcEnd = map ? runBase + map.ends[at] : runBase + at + ch.length;
857
- at += ch.length;
858
- srcIndex = map ? runBase + map.length : srcEnd;
859
- if (isNewline) {
860
- entries.push({
861
- run,
862
- font,
863
- metrics,
864
- glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
865
- // A newline consumes no advance, so it takes no tracking either.
866
- cp,
867
- advance: 0,
868
- tracking: 0,
869
- kerningBefore: 0,
870
- isSpace: false,
871
- isNewline: true,
872
- drawsInk: false,
873
- resolved: resolved2,
874
- fontSize,
875
- srcIndex: srcStart,
876
- srcEnd,
877
- flat: 0
878
- });
879
- prevCp = void 0;
880
- prevMetrics = void 0;
881
- prevFontSize = void 0;
882
- continue;
883
- }
884
- const isTab = cp === 9;
885
- if (isSpace || isTab) {
886
- const spaceGlyph = resolved2.dynamicFace ? resolved2.dynamicFace.requestGlyph(32) : font?.charMap.get(32) ?? null;
887
- const spaceAdvance = spaceGlyph ? spaceGlyph.xadvance : metrics.advanceOf(32);
888
- const advance = spaceAdvance !== null ? spaceAdvance * scale : fontSize * 0.25;
889
- if (isTab) {
890
- entries.push({
891
- run,
892
- font,
893
- metrics,
894
- glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
895
- cp,
896
- advance: 0,
897
- tracking: 0,
898
- kerningBefore: 0,
899
- isSpace: false,
900
- isNewline: false,
901
- tab: { stop: tabSize * (advance + runTracking), min: advance / 2 },
902
- drawsInk: false,
903
- resolved: resolved2,
904
- fontSize,
905
- srcIndex: srcStart,
906
- srcEnd,
907
- flat: 0
908
- });
909
- prevCp = void 0;
910
- prevMetrics = void 0;
911
- prevFontSize = void 0;
912
- continue;
913
- }
914
- let kerningBefore2 = 0;
915
- if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
916
- kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
917
- }
918
- entries.push({
919
- run,
920
- font,
921
- metrics,
922
- glyph: spaceGlyph ?? { id: 32, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
923
- cp,
924
- advance,
925
- tracking,
926
- kerningBefore: kerningBefore2,
927
- isSpace: true,
928
- isNewline: false,
929
- drawsInk: false,
930
- resolved: resolved2,
931
- fontSize,
932
- srcIndex: srcStart,
933
- srcEnd,
934
- flat: 0
935
- });
936
- prevCp = cp;
937
- prevMetrics = metrics;
938
- prevFontSize = fontSize;
939
- continue;
940
- }
941
- if (outlineFace) {
942
- const adv = metrics.advanceOf(cp);
943
- if (adv === null) {
944
- entries.push({
945
- run,
946
- font: null,
947
- metrics,
948
- glyph: null,
949
- cp,
950
- advance: 0,
951
- tracking: 0,
952
- kerningBefore: 0,
953
- isSpace,
954
- isNewline: false,
955
- drawsInk: false,
956
- resolved: resolved2,
957
- fontSize,
958
- srcIndex: srcStart,
959
- srcEnd,
960
- flat: 0
961
- });
962
- prevCp = cp;
963
- prevMetrics = metrics;
964
- prevFontSize = fontSize;
965
- continue;
966
- }
967
- let kerningBefore2 = 0;
968
- if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
969
- kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
970
- }
971
- entries.push({
972
- run,
973
- font: null,
974
- metrics,
975
- glyph: null,
976
- cp,
977
- advance: adv * scale,
978
- tracking,
979
- kerningBefore: kerningBefore2,
980
- isSpace,
981
- isNewline: false,
982
- drawsInk: !isSpace,
983
- resolved: resolved2,
984
- fontSize,
985
- srcIndex: srcStart,
986
- srcEnd,
987
- flat: 0
988
- });
989
- prevCp = cp;
990
- prevMetrics = metrics;
991
- prevFontSize = fontSize;
992
- continue;
993
- }
994
- const hit = resolved2.dynamicFace ? { glyph: resolved2.dynamicFace.requestGlyph(cp), font, resolved: resolved2 } : resolveGlyph(run, font, resolved2, cp);
995
- if (!hit) {
996
- entries.push({
997
- run,
998
- font: null,
999
- metrics,
1000
- glyph: null,
1001
- cp,
1002
- advance: 0,
1003
- tracking: 0,
1004
- kerningBefore: 0,
1005
- isSpace,
1006
- isNewline: false,
1007
- drawsInk: false,
1008
- resolved: resolved2,
1009
- fontSize,
1010
- srcIndex: srcStart,
1011
- srcEnd,
1012
- flat: 0
1013
- });
1014
- prevCp = cp;
1015
- prevMetrics = metrics;
1016
- prevFontSize = fontSize;
1017
- continue;
1018
- }
1019
- const glyphFont = hit.font;
1020
- const glyphMetrics = glyphFont === font ? metrics : atlasMetrics(glyphFont);
1021
- const glyphScale = fontSize / glyphFont.info.size;
1022
- let kerningBefore = 0;
1023
- if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
1024
- kerningBefore = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
1025
- }
1026
- entries.push({
1027
- run,
1028
- font: glyphFont,
1029
- metrics: glyphMetrics,
1030
- glyph: hit.glyph,
1031
- cp,
1032
- advance: hit.glyph.xadvance * glyphScale,
1033
- tracking,
1034
- kerningBefore,
1035
- isSpace,
1036
- isNewline: false,
1037
- drawsInk: !isSpace,
1038
- resolved: hit.resolved,
1039
- fontSize,
1040
- srcIndex: srcStart,
1041
- srcEnd,
1042
- flat: 0
1043
- });
1044
- prevCp = cp;
1045
- prevMetrics = glyphMetrics;
1046
- prevFontSize = fontSize;
1047
- }
1048
- }
1049
- for (let i = 0; i < entries.length; i++) entries[i].flat = i;
1050
- const paragraphs = bidi ? analyzeParagraphs(bidi, entries.map((e) => e.cp), (i) => entries[i].isNewline, direction) : null;
1051
- if (!bidi && entries.some((e) => RTL_BLOCKS.some(([lo, hi]) => e.cp >= lo && e.cp <= hi))) {
1052
- warnLogicalRtlOnce();
1053
- }
1054
- const openingWidth = (e) => e.advance + e.tracking;
809
+ function breakLines(entries, maxWidth, lineHeight) {
1055
810
  const sum = new Float64Array(entries.length + 1);
1056
- for (let k = 0; k < entries.length; k++) {
1057
- const e = entries[k];
1058
- sum[k + 1] = sum[k] + e.kerningBefore + e.advance + e.tracking;
1059
- }
1060
- const flatWidth = (start, end) => openingWidth(entries[start]) + sum[end] - sum[start + 1];
1061
811
  const tabs = [];
1062
812
  entries.forEach((e, k) => {
813
+ sum[k + 1] = sum[k] + e.kerningBefore + e.advance + e.tracking;
1063
814
  if (e.tab) tabs.push(k);
1064
815
  });
816
+ const flatWidth = (start, end) => entries[start].advance + entries[start].tracking + sum[end] - sum[start + 1];
1065
817
  const widthOf = tabs.length === 0 ? flatWidth : (start, end) => widthWithTabs(start, end, tabs, (k) => entries[k].tab, flatWidth);
1066
- const lines = [];
1067
- for (const span2 of wrapLines(entries.map((e) => e.cp), opts.maxWidth, widthOf)) {
1068
- const line = { entries: [], width: 0, height: 0, ...span2.wrapped ? { wrapped: true } : {} };
1069
- if (span2.start === span2.end) {
1070
- const brk = entries[span2.end];
1071
- line.height = lineSize(brk) * opts.lineHeight;
1072
- line.blank = brk;
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;
1073
834
  }
1074
- for (let k = span2.start; k < span2.end; k++) {
835
+ for (let k = span.start; k < span.end; k++) {
1075
836
  const e = entries[k];
1076
- const opened = k > span2.start ? e : { ...e, kerningBefore: 0 };
837
+ const opened = k > span.start ? e : { ...e, kerningBefore: 0 };
1077
838
  const placed = e.tab ? { ...opened, advance: tabAdvance(line.width, e.tab.stop, e.tab.min) } : opened;
1078
839
  line.entries.push(placed);
1079
840
  line.width += placed.kerningBefore + placed.advance + placed.tracking;
1080
- line.height = Math.max(line.height, lineSize(e) * opts.lineHeight);
1081
- }
1082
- lines.push(line);
1083
- }
1084
- function outlineFor(e) {
1085
- const r0 = e.resolved.resolved;
1086
- if (e.resolved.source === "outline") {
1087
- return glyphOutline(r0.family, r0.weight, r0.style, e.cp);
841
+ line.height = Math.max(line.height, e.lineSize * lineHeight);
842
+ line.ascent = Math.max(line.ascent, aboveBaseline(placed, lineHeight));
1088
843
  }
1089
- const min = opts.outlineMinSize;
1090
- if (min === void 0) return null;
1091
- if (e.run.fontSize < min && !strokePaints(e.run.stroke)) return null;
1092
- if (e.resolved.synthetic.bold) return null;
1093
- const r = e.resolved.resolved;
1094
- return glyphOutline(r.family, r.weight, r.style, e.cp);
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;
1095
853
  }
1096
- const decorations = [];
1097
- let span = null;
1098
- function flushSpan() {
1099
- const s = span;
1100
- span = null;
1101
- if (!s || s.x1 <= s.x0) return;
1102
- for (const kind of DECORATION_KINDS) {
1103
- if (!s[kind]) continue;
1104
- const { y0, y1 } = decorationRule(kind, s.baselineY, s.fontSize, s.face);
1105
- decorations.push({ kind, x0: s.x0, y0, x1: s.x1, y1, fill: s.fill });
1106
- }
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
915
+ });
916
+ penX += step;
1107
917
  }
1108
- const lineBoxes = [];
1109
- let penY = 0;
1110
- let maxLineWidth = 0;
1111
- const alignWidth = opts.alignWidth ?? opts.maxWidth;
1112
- for (const line of lines) {
1113
- let hung = 0;
1114
- for (let k = line.entries.length - 1; k >= 0; k--) {
1115
- const e = line.entries[k];
1116
- if (!e.isSpace && !e.tab) break;
1117
- hung += e.kerningBefore + e.advance + e.tracking;
1118
- }
1119
- const inkWidth = line.width - hung;
1120
- const gaps = /* @__PURE__ */ new Set();
1121
- let gapExtra = 0;
1122
- if (justify && line.wrapped && Number.isFinite(alignWidth) && alignWidth > inkWidth) {
1123
- let first = 0;
1124
- while (first < line.entries.length && line.entries[first].isSpace) first++;
1125
- let last = line.entries.length - 1;
1126
- while (last > first && line.entries[last].isSpace) last--;
1127
- for (let k = first + 1; k < last; k++) if (line.entries[k].isSpace) gaps.add(k);
1128
- if (gaps.size > 0) gapExtra = (alignWidth - inkWidth) / gaps.size;
1129
- }
1130
- const spread = gapExtra * gaps.size;
1131
- const align = resolveAlign(opts.align, direction !== "auto" ? direction : directionOfLevel(line.entries.length > 0 ? paragraphs?.baseLevel(line.entries[0].flat) ?? 0 : 0));
1132
- const alignShift = (() => {
1133
- if (spread > 0) return 0;
1134
- if (align === "left") return 0;
1135
- if (!Number.isFinite(alignWidth)) {
1136
- return align === "center" ? -inkWidth / 2 : -inkWidth;
1137
- }
1138
- const slack = alignWidth - inkWidth;
1139
- return align === "center" ? slack / 2 : slack;
1140
- })();
1141
- const lineX0 = alignShift;
1142
- let lineAscent = 0;
1143
- for (const e of line.entries) lineAscent = Math.max(lineAscent, aboveBaseline(e));
1144
- if (line.entries.length === 0 && line.blank) lineAscent = aboveBaseline(line.blank);
1145
- const lineBaselineY = penY + lineAscent;
1146
- const cells = new Array(line.entries.length);
1147
- const flatOf = line.entries.map((e) => e.flat);
1148
- const posOfFlat = /* @__PURE__ */ new Map();
1149
- flatOf.forEach((f, pos) => posOfFlat.set(f, pos));
1150
- let visual = line.entries.map((_, k) => k);
1151
- let levelOf = () => 0;
1152
- if (paragraphs !== null && line.entries.length > 0) {
1153
- const from = flatOf[0];
1154
- const to = flatOf[flatOf.length - 1] + 1;
1155
- const r = paragraphs.reorder(from, to);
1156
- const seen = /* @__PURE__ */ new Set();
1157
- const ordered = [];
1158
- for (const f of r.order) {
1159
- const pos = posOfFlat.get(f);
1160
- if (pos !== void 0 && !seen.has(pos)) {
1161
- ordered.push(pos);
1162
- seen.add(pos);
1163
- }
1164
- }
1165
- levelOf = (pos) => {
1166
- const l = r.levels[flatOf[pos] - from];
1167
- return typeof l === "number" && l >= 0 ? l : 0;
1168
- };
1169
- visual = restoreRemoved(ordered, line.entries.length, (pos) => levelOf(pos) % 2 === 1);
1170
- }
1171
- if (bidi) {
1172
- for (let pos = 0; pos < line.entries.length; pos++) {
1173
- if (levelOf(pos) % 2 === 0) continue;
1174
- const e = line.entries[pos];
1175
- const m = bidi.mirror(e.cp);
1176
- if (m === null || m === e.cp) continue;
1177
- if (e.font !== null) {
1178
- const hit = resolveGlyph(e.run, e.font, e.resolved, m, false);
1179
- if (!hit) continue;
1180
- if (hit.font !== e.font) {
1181
- e.font = hit.font;
1182
- e.metrics = atlasMetrics(hit.font);
1183
- e.resolved = hit.resolved;
1184
- }
1185
- e.glyph = hit.glyph;
1186
- }
1187
- e.cp = m;
1188
- }
1189
- }
1190
- const hangsLeft = hung > 0 && levelOf(line.entries.length - 1) % 2 === 1;
1191
- const lineLeft = hangsLeft ? lineX0 - hung : lineX0;
1192
- let penX = lineLeft;
1193
- let prevPos = -1;
1194
- for (const pos of visual) {
1195
- const e = line.entries[pos];
1196
- penX += prevPos < 0 ? 0 : pos === prevPos + 1 ? e.kerningBefore : prevPos === pos + 1 ? line.entries[prevPos].kerningBefore : 0;
1197
- prevPos = pos;
1198
- const step = e.advance + e.tracking + (gaps.has(pos) ? gapExtra : 0);
1199
- const cell = {
1200
- srcIndex: e.srcIndex,
1201
- srcEnd: e.srcEnd,
1202
- cp: e.cp,
1203
- x: penX,
1204
- advance: step,
1205
- level: levelOf(pos),
1206
- drawsInk: e.drawsInk,
1207
- clusterEnd: closesCluster[e.flat]
1208
- };
1209
- cells[pos] = cell;
1210
- const scale = e.fontSize / e.metrics.size;
1211
- const baselineY = lineBaselineY - (e.run.baselineShift ?? 0);
1212
- const decoFill = e.run.fill;
1213
- if (decoFill !== null && (e.run.underline || e.run.strikethrough || e.run.overline)) {
1214
- const face = runFaces.get(e.run);
1215
- 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)) {
1216
- span.x1 = penX + step;
1217
- } else {
1218
- flushSpan();
1219
- span = {
1220
- underline: e.run.underline,
1221
- strikethrough: e.run.strikethrough,
1222
- overline: e.run.overline,
1223
- fill: decoFill,
1224
- // The run's size, not the glyph's: a small-caps word is one rule.
1225
- fontSize: e.run.fontSize,
1226
- face,
1227
- baselineY,
1228
- x0: penX,
1229
- x1: penX + step
1230
- };
1231
- }
1232
- } else {
1233
- flushSpan();
1234
- }
1235
- const outlineD = outlineFor(e);
1236
- if (e.run.fill === null && !(outlineD !== null && strokePaints(e.run.stroke))) {
1237
- penX += step;
1238
- continue;
1239
- }
1240
- if (outlineD !== null) {
1241
- const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
1242
- group2.glyphs.push({
1243
- d: outlineD,
1244
- key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
1245
- x: penX,
1246
- baselineY,
1247
- // Em space is unit-scale, so world units per em is just the size.
1248
- scale: e.fontSize
1249
- });
1250
- penX += step;
1251
- continue;
1252
- }
1253
- if (e.font === null || e.glyph === null || e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
1254
- penX += step;
1255
- continue;
1256
- }
1257
- const group = getOrCreateGroup(ctx, e.run, e.resolved, e.glyph.page);
1258
- const atlasW = e.font.common.scaleW;
1259
- const atlasH = e.font.common.scaleH;
1260
- const qx0 = penX + e.glyph.xoffset * scale;
1261
- const qy0 = baselineY + (e.glyph.yoffset - e.metrics.base) * scale;
1262
- const qx1 = qx0 + e.glyph.width * scale;
1263
- const qy1 = qy0 + e.glyph.height * scale;
1264
- const u0 = e.glyph.x / atlasW;
1265
- const v0 = e.glyph.y / atlasH;
1266
- const u1 = (e.glyph.x + e.glyph.width) / atlasW;
1267
- const v1 = (e.glyph.y + e.glyph.height) / atlasH;
1268
- group.quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0, v0, u1, v1, baselineY });
1269
- penX += step;
1270
- }
1271
- flushSpan();
1272
- const lastCell = line.entries[line.entries.length - 1];
1273
- const srcEnd = lastCell ? lastCell.srcEnd : line.blank?.srcIndex ?? 0;
1274
- lineBoxes.push({
1275
- x0: lineLeft,
1276
- y0: penY,
1277
- x1: lineLeft + line.width + spread,
1278
- y1: penY + line.height,
918
+ return {
919
+ box: {
920
+ x0: left,
921
+ y0: y,
922
+ x1: left + line.width + spread,
923
+ y1: y + line.height,
1279
924
  baselineY: lineBaselineY,
1280
925
  cells,
1281
- srcEnd
1282
- });
1283
- maxLineWidth = Math.max(maxLineWidth, inkWidth + spread);
1284
- penY += line.height;
1285
- }
926
+ glyphs,
927
+ srcEnd: line.srcEnd
928
+ },
929
+ inkWidth: inkWidth + spread
930
+ };
931
+ }
932
+
933
+ // src/layout/layoutOpts.ts
934
+ function resolveLayoutOpts(opts) {
1286
935
  return {
1287
- groups: [...ctx.groups.values()],
1288
- decorations,
1289
- lines: lineBoxes,
1290
- bounds: { width: maxLineWidth, height: penY }
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
1291
944
  };
1292
945
  }
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 } };
968
+ }
969
+
970
+ // src/layout/refId.ts
971
+ var nextRefId = 1;
972
+ var refIds = /* @__PURE__ */ new WeakMap();
973
+ function refId(o) {
974
+ let id = refIds.get(o);
975
+ if (id === void 0) {
976
+ id = nextRefId++;
977
+ refIds.set(o, id);
978
+ }
979
+ return id;
980
+ }
981
+
982
+ // src/layout/layoutCache.ts
1293
983
  var LAYOUT_CACHE_VARIANT_LIMIT = 8;
1294
984
  var LAYOUT_CACHE_STRUCTURAL_LIMIT = 64;
1295
985
  var cache = /* @__PURE__ */ new WeakMap();
1296
986
  var structural = /* @__PURE__ */ new Map();
1297
987
  var structuralGeneration = -1;
1298
- function outlineBucket(runs, min) {
1299
- if (min === void 0) return -1;
1300
- const sizes = /* @__PURE__ */ new Set();
1301
- for (const r of runs) sizes.add(r.fontSize);
1302
- let n = 0;
1303
- for (const size of sizes) if (size >= min) n++;
1304
- return n;
1305
- }
1306
- function variantKey(runs, opts) {
1307
- const alignWidth = opts.alignWidth ?? opts.maxWidth;
1308
- const justify = opts.justify === true || opts.align === "justify";
1309
- const bidi = opts.bidi ?? registeredBidi();
1310
- return `${opts.maxWidth}|${alignWidth}|${opts.lineHeight}|${opts.align}|${justify ? "j" : ""}|${outlineBucket(runs, opts.outlineMinSize)}|${opts.direction ?? ""}|${bidi ? refId(bidi) : ""}|${opts.tabSize ?? ""}`;
1311
- }
1312
- function paintKey(p) {
1313
- if (p === null) return "none";
1314
- return "color" in p ? `s${p.color}:${p.opacity ?? 1}` : `#${refId(p)}`;
1315
- }
1316
- function strokeKey2(s) {
1317
- if (!s?.paint) return "-";
1318
- const width = s.width ?? 1;
1319
- return [
1320
- paintKey(s.paint),
1321
- typeof width === "number" ? width : `px${width.px}`,
1322
- s.join ?? "miter",
1323
- s.cap ?? "butt",
1324
- s.miterLimit ?? "",
1325
- s.align ?? "center",
1326
- (s.dash ?? []).join(",")
1327
- ].join(":");
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}`;
1328
991
  }
1329
992
  function runsKey(runs) {
1330
993
  let out = "";
1331
994
  for (const r of runs) {
1332
- 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}|${r.strutSize ?? ""}|${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(",") : ""}|`;
1333
996
  }
1334
997
  return out;
1335
998
  }
1336
999
  function cachedLayoutRuns(runs, opts) {
1337
- const fonts = glyphGeneration();
1000
+ const fonts = faceGeneration();
1338
1001
  let entry = cache.get(runs);
1339
1002
  if (entry === void 0) {
1340
1003
  entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
@@ -1343,7 +1006,7 @@ function cachedLayoutRuns(runs, opts) {
1343
1006
  entry.generation = fonts;
1344
1007
  entry.byVariant.clear();
1345
1008
  }
1346
- const key = variantKey(runs, opts);
1009
+ const key = variantKey(opts);
1347
1010
  const hit = entry.byVariant.get(key);
1348
1011
  if (hit !== void 0) return hit;
1349
1012
  if (structuralGeneration !== fonts) {
@@ -1371,6 +1034,6 @@ function _resetLayoutCacheForTests() {
1371
1034
  structuralGeneration = -1;
1372
1035
  }
1373
1036
 
1374
- 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, hangs, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle, wrapLines };
1375
- //# sourceMappingURL=chunk-VZNK3C6F.js.map
1376
- //# sourceMappingURL=chunk-VZNK3C6F.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