@weasel-js/text 1.9.4 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
- import { resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, DECORATION_KINDS, decorationRule } from './chunk-VZNK3C6F.js';
2
- export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_DECORATION_METRICS, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, decorationMetrics, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-VZNK3C6F.js';
3
- import { resolveScreenLength } from '@weasel-js/paint';
4
- import { faceMetricsOf, resolveFontVariant, faceMetricsFor, cssFamilyName } from '@weasel-js/font';
1
+ import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs, layoutRuns } from './chunk-EFPJZYNI.js';
2
+ export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-EFPJZYNI.js';
3
+ import { resolveScreenLength, fillHasInk, strokeHasInk, paintKey, strokeKey } from '@weasel-js/paint';
4
+ import { faceMetricsOf, resolveFontVariant, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
5
+ import { MIXED, isPlainObject } from '@weasel-js/registry';
6
+ export { MIXED } from '@weasel-js/registry';
7
+ import { transformPath, poseFrameOf, pathFromD, pathSignedArea, reversePath, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
5
8
 
6
9
  // src/runs.ts
7
10
  function toRuns(input) {
@@ -368,6 +371,66 @@ function resolveRuns(runs, style, viewScale = 1) {
368
371
  return out;
369
372
  }
370
373
 
374
+ // src/layout/decorationMetrics.ts
375
+ var DECORATION_KINDS = ["underline", "strikethrough", "overline"];
376
+ var DEFAULT_DECORATION_METRICS = Object.freeze({
377
+ underline: Object.freeze({ offset: 0.1, thickness: 0.05 }),
378
+ strikethrough: Object.freeze({ offset: -0.3, thickness: 0.05 }),
379
+ overline: Object.freeze({ offset: -0.9, thickness: 0.05 })
380
+ });
381
+ var resolved = /* @__PURE__ */ new WeakMap();
382
+ function decorationMetrics(face) {
383
+ if (!face) return DEFAULT_DECORATION_METRICS;
384
+ let out = resolved.get(face);
385
+ if (!out) {
386
+ const underline = face.underline ?? DEFAULT_DECORATION_METRICS.underline;
387
+ out = Object.freeze({
388
+ underline,
389
+ strikethrough: face.strikethrough ?? DEFAULT_DECORATION_METRICS.strikethrough,
390
+ overline: Object.freeze({ offset: DEFAULT_DECORATION_METRICS.overline.offset, thickness: underline.thickness })
391
+ });
392
+ resolved.set(face, out);
393
+ }
394
+ return out;
395
+ }
396
+ function decorationRule(kind, baselineY, fontSize, face) {
397
+ const m = decorationMetrics(face)[kind];
398
+ const y0 = baselineY + fontSize * m.offset;
399
+ return { y0, y1: y0 + fontSize * m.thickness };
400
+ }
401
+ function lineDecorations(glyphs, runs, faceOf, paintOf) {
402
+ const rules = [];
403
+ let open = null;
404
+ const close = () => {
405
+ const s = open;
406
+ open = null;
407
+ if (!s || s.x1 <= s.x0) return;
408
+ for (const kind of DECORATION_KINDS) {
409
+ if (!s.style[kind]) continue;
410
+ const { y0, y1 } = decorationRule(kind, s.baselineY, s.style.fontSize, s.face);
411
+ rules.push({ kind, x0: s.x0, y0, x1: s.x1, y1, run: s.run });
412
+ }
413
+ };
414
+ for (const g of glyphs) {
415
+ const style = runs[g.run];
416
+ const paint = paintOf(g.run);
417
+ if (paint === null || !DECORATION_KINDS.some((kind) => style[kind])) {
418
+ close();
419
+ continue;
420
+ }
421
+ const face = faceOf(g.run);
422
+ const joins = open !== null && DECORATION_KINDS.every((kind) => open.style[kind] === style[kind]) && open.style.fontSize === style.fontSize && open.face === face && open.baselineY === g.baselineY && open.paint === paint;
423
+ if (joins) {
424
+ open.x1 = g.x + g.step;
425
+ } else {
426
+ close();
427
+ open = { run: g.run, style, face, paint, baselineY: g.baselineY, x0: g.x, x1: g.x + g.step };
428
+ }
429
+ }
430
+ close();
431
+ return rules;
432
+ }
433
+
371
434
  // src/measure/verticalAlign.ts
372
435
  function verticalAlignOffset(align, boxHeight, textHeight) {
373
436
  if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
@@ -402,6 +465,135 @@ function layoutTextPose(pose, scale = 1) {
402
465
  y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
403
466
  };
404
467
  }
468
+ function glyphOutlineD(g, run, minSize) {
469
+ if (g.glyph?.color) return null;
470
+ const { family, weight, style } = g.resolved.resolved;
471
+ if (g.resolved.source === "outline") return glyphOutline(family, weight, style, g.cp);
472
+ if (g.glyph === null) return null;
473
+ if (minSize === void 0) return null;
474
+ if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
475
+ if (g.resolved.synthetic.bold) return null;
476
+ return glyphOutline(family, weight, style, g.cp);
477
+ }
478
+ function glyphQuad(g) {
479
+ const { font, glyph } = g;
480
+ if (font === null || glyph === null || glyph.width === 0 || glyph.page < 0) return null;
481
+ const scale = g.fontSize / g.metrics.size;
482
+ const x0 = g.x + glyph.xoffset * scale;
483
+ const y0 = g.baselineY + (glyph.yoffset - g.metrics.base) * scale;
484
+ return {
485
+ x0,
486
+ y0,
487
+ x1: x0 + glyph.width * scale,
488
+ y1: y0 + glyph.height * scale,
489
+ u0: glyph.x / font.common.scaleW,
490
+ v0: glyph.y / font.common.scaleH,
491
+ u1: (glyph.x + glyph.width) / font.common.scaleW,
492
+ v1: (glyph.y + glyph.height) / font.common.scaleH,
493
+ baselineY: g.baselineY
494
+ };
495
+ }
496
+ function runFaces(runs) {
497
+ const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
498
+ return (run) => faces[run];
499
+ }
500
+ function groupFor(groups, g, run, source) {
501
+ const { family, weight, style } = g.resolved.resolved;
502
+ const { synthetic } = g.resolved;
503
+ const page = source === "outline" ? 0 : g.glyph.page;
504
+ const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
505
+ const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
506
+ let group = groups.get(key);
507
+ if (!group) {
508
+ group = {
509
+ family,
510
+ weight,
511
+ style,
512
+ synthetic: { ...synthetic },
513
+ source,
514
+ page,
515
+ fill: run.fill,
516
+ ...stroke ? { stroke } : {},
517
+ quads: [],
518
+ glyphs: []
519
+ };
520
+ groups.set(key, group);
521
+ }
522
+ return group;
523
+ }
524
+ function paintRuns(laid, runs, opts = {}) {
525
+ const groups = /* @__PURE__ */ new Map();
526
+ const decorations = [];
527
+ const faceOf = runFaces(runs);
528
+ const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
529
+ for (const line of laid.lines) {
530
+ for (const g of line.glyphs) {
531
+ const run = runs[g.run];
532
+ const d = glyphOutlineD(g, run, opts.outlineMinSize);
533
+ if (!fillHasInk(run.fill) && !(d !== null && strokeHasInk(run.stroke))) continue;
534
+ if (d !== null) {
535
+ const { family, weight, style } = g.resolved.resolved;
536
+ groupFor(groups, g, run, "outline").glyphs.push({
537
+ d,
538
+ key: `${family}|${weight}|${style}|${g.cp}`,
539
+ x: g.x,
540
+ baselineY: g.baselineY,
541
+ scale: g.fontSize
542
+ });
543
+ continue;
544
+ }
545
+ const quad = glyphQuad(g);
546
+ if (quad) groupFor(groups, g, run, g.glyph?.color ? "color" : g.resolved.source).quads.push(quad);
547
+ }
548
+ for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
549
+ const { run, ...rect } = rule;
550
+ decorations.push({ ...rect, fill: runs[run].fill });
551
+ }
552
+ }
553
+ return { groups: [...groups.values()], decorations };
554
+ }
555
+ var PAINT_CACHE_VARIANT_LIMIT = 32;
556
+ var own = /* @__PURE__ */ new WeakMap();
557
+ var shared = /* @__PURE__ */ new WeakMap();
558
+ function outlineBucket(runs, min) {
559
+ if (min === void 0) return -1;
560
+ const sizes = /* @__PURE__ */ new Set();
561
+ for (const r of runs) sizes.add(r.fontSize);
562
+ let n = 0;
563
+ for (const size of sizes) if (size >= min) n++;
564
+ return n;
565
+ }
566
+ function variantKey(runs, opts) {
567
+ let out = `${outlineBucket(runs, opts.outlineMinSize)}`;
568
+ for (const r of runs) {
569
+ out += `|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}${paintKey(r.fill?.paint)}|${strokeKey(r.stroke)}`;
570
+ }
571
+ return out;
572
+ }
573
+ function cachedPaintRuns(laid, runs, opts = {}) {
574
+ const generation = glyphGeneration();
575
+ const key = variantKey(runs, opts);
576
+ const fresh = (p) => p !== void 0 && p.generation === generation && p.key === key;
577
+ let byLayout = own.get(runs);
578
+ if (byLayout === void 0) {
579
+ byLayout = /* @__PURE__ */ new WeakMap();
580
+ own.set(runs, byLayout);
581
+ }
582
+ const mine = byLayout.get(laid);
583
+ if (fresh(mine)) return mine.painted;
584
+ let byKey = shared.get(laid);
585
+ if (byKey === void 0) {
586
+ byKey = /* @__PURE__ */ new Map();
587
+ shared.set(laid, byKey);
588
+ }
589
+ let painting = byKey.get(key);
590
+ byKey.delete(key);
591
+ if (!fresh(painting)) painting = { generation, key, painted: paintRuns(laid, runs, opts) };
592
+ if (byKey.size >= PAINT_CACHE_VARIANT_LIMIT) byKey.delete(byKey.keys().next().value);
593
+ byKey.set(key, painting);
594
+ byLayout.set(laid, painting);
595
+ return painting.painted;
596
+ }
405
597
 
406
598
  // src/measure/measureText.ts
407
599
  function measuredWidth(ctx, text, style) {
@@ -417,27 +609,40 @@ function measureText(ctx, text, maxWidth, style) {
417
609
  u += ch.length;
418
610
  }
419
611
  at.push(u);
420
- const wrapped = wrapLines(cps, maxWidth, (start, end) => measuredWidth(ctx, text.slice(at[start], at[end]), style));
421
- const lines = wrapped.map((l) => {
612
+ const flatWidth = (start, end) => measuredWidth(ctx, text.slice(at[start], at[end]), style);
613
+ const tabs = [];
614
+ cps.forEach((cp, k) => {
615
+ if (cp === 9) tabs.push(k);
616
+ });
617
+ const space = tabs.length === 0 ? 0 : ctx.measureText(" ").width;
618
+ const tab = { stop: style.tabSize * (space + style.letterSpacing), min: space / 2 };
619
+ const widthOf = (start, end, piece) => widthWithTabs(start, end, tabs, () => tab, flatWidth, piece);
620
+ const wrapped = wrapLines(cps, maxWidth, (start, end) => widthOf(start, end));
621
+ const lines = [];
622
+ const spans = [];
623
+ for (const l of wrapped) {
422
624
  let end = l.end;
423
625
  while (end > l.start && hangs(cps[end - 1])) end--;
424
- return text.slice(at[l.start], at[end]);
425
- });
626
+ lines.push(text.slice(at[l.start], at[end]));
627
+ const row = [];
628
+ widthOf(l.start, end, (from, to, x) => row.push({ text: text.slice(at[from], at[to]), x }));
629
+ spans.push(row);
630
+ }
426
631
  const lineStarts = wrapped.map((l) => at[l.start]);
427
632
  const height = lines.length * style.fontSize * style.lineHeight;
428
- return { lines, lineStarts, height };
633
+ return { lines, spans, lineStarts, height };
429
634
  }
430
635
 
431
636
  // src/measure/measureTextBounds.ts
432
637
  function measureTextBounds(text, style, opts) {
433
- const resolved = resolveTextStyle(style, void 0, opts?.scale);
434
- const runs = resolveRuns([{ text }], resolved, opts?.scale);
638
+ const resolved2 = resolveTextStyle(style, void 0, opts?.scale);
639
+ const runs = resolveRuns([{ text }], resolved2, opts?.scale);
435
640
  const { bounds } = cachedLayoutRuns(runs, {
436
641
  maxWidth: opts?.maxWidth ?? Infinity,
437
- lineHeight: opts?.lineHeight ?? resolved.lineHeight,
438
- align: resolved.align,
439
- direction: resolved.direction,
440
- tabSize: resolved.tabSize
642
+ lineHeight: opts?.lineHeight ?? resolved2.lineHeight,
643
+ align: resolved2.align,
644
+ direction: resolved2.direction,
645
+ tabSize: resolved2.tabSize
441
646
  });
442
647
  return { width: bounds.width, height: bounds.height };
443
648
  }
@@ -458,6 +663,115 @@ function textLineBoxes(pose, opts = {}) {
458
663
  }
459
664
  return out;
460
665
  }
666
+
667
+ // src/hitTest.ts
668
+ function caretCells(cells) {
669
+ const out = [];
670
+ for (const c of cells) {
671
+ const prev = out[out.length - 1];
672
+ if (prev && (!prev.clusterEnd || prev.srcIndex === c.srcIndex && prev.srcEnd === c.srcEnd)) {
673
+ const x0 = Math.min(prev.x, c.x);
674
+ const x1 = Math.max(prev.x + prev.advance, c.x + c.advance);
675
+ out[out.length - 1] = {
676
+ ...prev,
677
+ x: x0,
678
+ advance: x1 - x0,
679
+ srcIndex: Math.min(prev.srcIndex, c.srcIndex),
680
+ srcEnd: Math.max(prev.srcEnd, c.srcEnd),
681
+ clusterEnd: c.clusterEnd
682
+ };
683
+ } else {
684
+ out.push(c);
685
+ }
686
+ }
687
+ return out;
688
+ }
689
+ function pointInTextPose(x, y, pose, opts = {}) {
690
+ const p = opts.padding ?? 0;
691
+ return x >= pose.x - p && x <= pose.x + pose.width + p && y >= pose.y - p && y <= pose.y + pose.height + p;
692
+ }
693
+ function caretIndexAt(x, y, pose) {
694
+ const { laid, x: dx, y: dy } = layoutTextPose(pose);
695
+ const lines = laid.lines;
696
+ if (lines.length === 0) return 0;
697
+ const last = lines[lines.length - 1];
698
+ if (y < dy + lines[0].y0) return lines[0].cells[0]?.srcIndex ?? lines[0].srcEnd;
699
+ if (y >= dy + last.y1) return last.srcEnd;
700
+ let line = last;
701
+ for (const candidate of lines) {
702
+ if (y < dy + candidate.y1) {
703
+ line = candidate;
704
+ break;
705
+ }
706
+ }
707
+ if (line.cells.length === 0) return line.srcEnd;
708
+ const cells = caretCells(line.cells);
709
+ const visual = cells.map((_, i) => i).sort((a, b) => cells[a].x - cells[b].x);
710
+ for (const i of visual) {
711
+ const c = cells[i];
712
+ if (x >= dx + c.x + c.advance / 2) continue;
713
+ return c.level % 2 === 1 ? c.srcEnd : c.srcIndex;
714
+ }
715
+ const trailing = cells[visual[visual.length - 1]];
716
+ return trailing.level % 2 === 1 ? trailing.srcIndex : trailing.srcEnd;
717
+ }
718
+
719
+ // src/fitTextPose.ts
720
+ function fitTextPose(pose, opts = {}) {
721
+ const axis = opts.axis ?? "height";
722
+ const { padX, padY } = resolvePadding(opts.padding);
723
+ const { bounds } = layoutTextPose(axis === "height" ? { ...pose, width: pose.width - padX * 2 } : { ...pose, style: { ...pose.style, wrap: false } }).laid;
724
+ if (axis === "height") return { ...pose, height: bounds.height + padY * 2 };
725
+ return {
726
+ ...pose,
727
+ width: bounds.width + padX * 2,
728
+ height: bounds.height + padY * 2
729
+ };
730
+ }
731
+ function resolvePadding(p) {
732
+ if (p == null) return { padX: 0, padY: 0 };
733
+ if (typeof p === "number") return { padX: p, padY: p };
734
+ return { padX: p.x ?? 0, padY: p.y ?? 0 };
735
+ }
736
+
737
+ // src/renderLabel.ts
738
+ function renderLabel(ctx, text, x, y, options = {}) {
739
+ const {
740
+ align = "center",
741
+ fontSize = 13,
742
+ renderText,
743
+ width,
744
+ height,
745
+ padX = 4,
746
+ padY = 1,
747
+ cornerRadius = 3
748
+ } = options;
749
+ ctx.save();
750
+ ctx.font = `${fontSize}px sans-serif`;
751
+ ctx.textBaseline = "top";
752
+ const contentW = width ?? ctx.measureText(text).width;
753
+ const contentH = height ?? fontSize;
754
+ const w = contentW + padX * 2;
755
+ const h = contentH + padY * 2;
756
+ const rx = align === "center" ? x - w / 2 : x - padX;
757
+ ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
758
+ ctx.beginPath();
759
+ ctx.roundRect(rx, y - padY, w, h, cornerRadius);
760
+ ctx.fill();
761
+ ctx.textAlign = align === "center" ? "center" : "left";
762
+ const tx = align === "center" ? x : x;
763
+ (renderText ?? defaultLabelTextRenderer)(ctx, text, tx, y);
764
+ ctx.restore();
765
+ }
766
+ var defaultLabelTextRenderer = (ctx, text, x, y) => {
767
+ ctx.fillStyle = "#FFFFFF";
768
+ ctx.fillText(text, x, y);
769
+ };
770
+
771
+ // src/isTextData.ts
772
+ function isTextData(data) {
773
+ return typeof data === "object" && data !== null && typeof data.text === "string";
774
+ }
461
775
  function runMetrics(run, fontSize, face) {
462
776
  const script = run.script ? scriptMetrics(face)[run.script] : void 0;
463
777
  const scale = run.fontScale ?? script?.size ?? 1;
@@ -469,7 +783,7 @@ function runMetrics(run, fontSize, face) {
469
783
  y: shiftEm === 0 ? 0 : -shiftEm * fontSize
470
784
  };
471
785
  }
472
- function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf) {
786
+ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf, tabSize = DEFAULT_TAB_SIZE) {
473
787
  if (runs.length === 0) return { lines: [], width: 0, height: 0 };
474
788
  const shown = transformRunTexts(runs.map((r) => r.text), runs.map((r) => r.textTransform ?? "none"));
475
789
  const segs = [];
@@ -520,7 +834,7 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
520
834
  const out = [];
521
835
  for (let k = from; k < to; k++) {
522
836
  const last = out[out.length - 1];
523
- if (last && last.run === runOf[k] && last.end === startOf[k]) {
837
+ if (last && last.run === runOf[k] && last.end === startOf[k] && cps[k] !== 9 && cps[k - 1] !== 9) {
524
838
  out[out.length - 1] = { ...last, end: endOf[k] };
525
839
  } else {
526
840
  out.push({ run: runOf[k], start: startOf[k], end: endOf[k] });
@@ -535,14 +849,25 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
535
849
  const positioned = [];
536
850
  for (const p of line) {
537
851
  const { run, face, size, y, runSize } = placed[p.run];
538
- const width2 = pieceWidth(p);
852
+ const tab = run.text[p.start] === " " ? stopOf(p.run) : null;
853
+ const width2 = tab ? tabAdvance(x, tab.stop, tab.min) : pieceWidth(p);
539
854
  positioned.push({ ...run, text: run.text.slice(p.start, p.end), x, width: width2, y, size, ...face ? { face } : {} });
540
855
  x += width2;
541
856
  maxSize = Math.max(maxSize, runSize);
542
857
  }
543
858
  lines.push({ runs: positioned, width: x, height: (maxSize > 0 ? maxSize : fontSize) * lineHeightFactor });
544
859
  }
545
- const widthOf = (start, end) => piecesOf(start, end).reduce((w, p) => w + pieceWidth(p), 0);
860
+ const stopOf = (run) => {
861
+ const { run: r, size } = placed[run];
862
+ const space = measure(" ", size, r.bold ?? false, r.italic ?? false);
863
+ return { stop: tabSize * space, min: space / 2 };
864
+ };
865
+ const tabs = [];
866
+ cps.forEach((cp, k) => {
867
+ if (cp === 9) tabs.push(k);
868
+ });
869
+ const flatWidth = (start, end) => piecesOf(start, end).reduce((w, p) => w + pieceWidth(p), 0);
870
+ const widthOf = (start, end) => widthWithTabs(start, end, tabs, (k) => stopOf(runOf[k]), flatWidth);
546
871
  for (const line of wrapLines(cps, maxWidth, widthOf)) {
547
872
  let end = line.end;
548
873
  if (line.wrapped) while (end > line.start && hangs(cps[end - 1])) end--;
@@ -599,7 +924,7 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
599
924
  const family = fontOpts.family ?? "sans-serif";
600
925
  const weight = numericWeight(fontOpts.weight ?? 400);
601
926
  const faceOf = (bold, italic) => faceMetricsFor(family, bold ? 700 : weight, italic ? "italic" : "normal");
602
- const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf);
927
+ const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf, fontOpts.tabSize);
603
928
  function walk(c, x, y, drawRun) {
604
929
  const align = c.textAlign;
605
930
  const left = blockLeft(c, x, layout.width);
@@ -657,7 +982,452 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
657
982
  };
658
983
  return { renderer, strokeRenderer, width: layout.width, height: layout.height };
659
984
  }
985
+ var STYLE_KEYS = [
986
+ "bold",
987
+ "fontWeight",
988
+ "italic",
989
+ "underline",
990
+ "strikethrough",
991
+ "overline",
992
+ "fontFamily",
993
+ "fontSize",
994
+ "letterSpacing",
995
+ "fill",
996
+ "stroke",
997
+ "script",
998
+ "baselineShift",
999
+ "fontScale",
1000
+ "textTransform",
1001
+ "fontVariantCaps"
1002
+ ];
1003
+ var FLAG_KEYS = /* @__PURE__ */ new Set([
1004
+ "bold",
1005
+ "italic",
1006
+ "underline",
1007
+ "strikethrough",
1008
+ "overline"
1009
+ ]);
1010
+ function deepEqual(a, b) {
1011
+ if (Object.is(a, b)) return true;
1012
+ if (Array.isArray(a) && Array.isArray(b)) {
1013
+ return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
1014
+ }
1015
+ if (isPlainObject(a) && isPlainObject(b)) {
1016
+ const keys = Object.keys(a);
1017
+ if (keys.length !== Object.keys(b).length) return false;
1018
+ return keys.every((k) => k in b && deepEqual(a[k], b[k]));
1019
+ }
1020
+ return false;
1021
+ }
1022
+ function valueAt(run, key) {
1023
+ const value = run[key];
1024
+ if (FLAG_KEYS.has(key)) return value ?? false;
1025
+ return value;
1026
+ }
1027
+ var STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["fill", "stroke"]);
1028
+ function sameValue(key, a, b) {
1029
+ return STRUCTURAL_KEYS.has(key) ? deepEqual(a, b) : Object.is(a, b);
1030
+ }
1031
+ function sameStyle(a, b) {
1032
+ return STYLE_KEYS.every((key) => sameValue(key, valueAt(a, key), valueAt(b, key)));
1033
+ }
1034
+ function totalLength(runs) {
1035
+ let n = 0;
1036
+ for (const run of runs) n += run.text.length;
1037
+ return n;
1038
+ }
1039
+ function styleAtRange(runs, start, end) {
1040
+ const lo = Math.max(0, start);
1041
+ const hi = end;
1042
+ if (!(lo < hi)) return {};
1043
+ const first = /* @__PURE__ */ new Map();
1044
+ const mixed = /* @__PURE__ */ new Set();
1045
+ let seen = false;
1046
+ let pos = 0;
1047
+ for (const run of runs) {
1048
+ const runEnd = pos + run.text.length;
1049
+ const a = Math.max(pos, lo);
1050
+ const b = Math.min(runEnd, hi);
1051
+ pos = runEnd;
1052
+ if (!(a < b)) continue;
1053
+ for (const key of STYLE_KEYS) {
1054
+ const value = valueAt(run, key);
1055
+ if (!seen) first.set(key, value);
1056
+ else if (!mixed.has(key) && !sameValue(key, first.get(key), value)) {
1057
+ mixed.add(key);
1058
+ }
1059
+ }
1060
+ seen = true;
1061
+ }
1062
+ if (!seen) return {};
1063
+ const style = {};
1064
+ for (const key of STYLE_KEYS) {
1065
+ if (mixed.has(key)) style[key] = MIXED;
1066
+ else {
1067
+ const value = first.get(key);
1068
+ if (value !== void 0) style[key] = value;
1069
+ }
1070
+ }
1071
+ return style;
1072
+ }
1073
+ function runsCarryStyling(runs) {
1074
+ return runs.some(
1075
+ (run) => STYLE_KEYS.some((key) => {
1076
+ const value = run[key];
1077
+ return value !== void 0 && value !== false;
1078
+ })
1079
+ );
1080
+ }
1081
+ function supersededKey(patch) {
1082
+ if ("bold" in patch && !("fontWeight" in patch)) return "fontWeight";
1083
+ if (patch.fontWeight !== void 0 && !("bold" in patch)) return "bold";
1084
+ return void 0;
1085
+ }
1086
+ function patchRangeStyle(range, patch) {
1087
+ const out = { ...range };
1088
+ const superseded = supersededKey(patch);
1089
+ if (superseded !== void 0) delete out[superseded];
1090
+ for (const [key, value] of Object.entries(patch)) {
1091
+ if (value !== void 0) out[key] = value;
1092
+ }
1093
+ return out;
1094
+ }
1095
+ function patchRun(run, patch) {
1096
+ const next = { ...run };
1097
+ const superseded = supersededKey(patch);
1098
+ if (superseded !== void 0) delete next[superseded];
1099
+ for (const key of STYLE_KEYS) {
1100
+ if (!(key in patch)) continue;
1101
+ const value = patch[key];
1102
+ if (value === void 0 || value === false) delete next[key];
1103
+ else next[key] = value;
1104
+ }
1105
+ return next;
1106
+ }
1107
+ function coalesceRuns(runs) {
1108
+ const out = [];
1109
+ for (const run of runs) {
1110
+ if (run.text.length === 0) continue;
1111
+ const prev = out[out.length - 1];
1112
+ if (prev && sameStyle(prev, run)) prev.text += run.text;
1113
+ else out.push({ ...run });
1114
+ }
1115
+ return out;
1116
+ }
1117
+ function applyStyleToRange(runs, start, end, patch) {
1118
+ const lo = Math.max(0, start);
1119
+ const hi = Math.min(totalLength(runs), end);
1120
+ if (!(lo < hi)) return coalesceRuns(runs);
1121
+ const out = [];
1122
+ let pos = 0;
1123
+ for (const run of runs) {
1124
+ const runEnd = pos + run.text.length;
1125
+ const a = Math.max(pos, lo);
1126
+ const b = Math.min(runEnd, hi);
1127
+ if (a < b) {
1128
+ if (pos < a) out.push({ ...run, text: run.text.slice(0, a - pos) });
1129
+ out.push(patchRun({ ...run, text: run.text.slice(a - pos, b - pos) }, patch));
1130
+ if (b < runEnd) out.push({ ...run, text: run.text.slice(b - pos) });
1131
+ } else {
1132
+ out.push({ ...run });
1133
+ }
1134
+ pos = runEnd;
1135
+ }
1136
+ return coalesceRuns(out);
1137
+ }
1138
+
1139
+ // src/runs/flagRange.ts
1140
+ function nodeHasFlag(style, key) {
1141
+ switch (key) {
1142
+ case "bold":
1143
+ return style.fontWeight !== void 0 && isBoldWeight(style.fontWeight);
1144
+ case "italic":
1145
+ return style.fontStyle === "italic";
1146
+ case "underline":
1147
+ return style.underline === true;
1148
+ case "strikethrough":
1149
+ return style.strikethrough === true;
1150
+ case "overline":
1151
+ return style.overline === true;
1152
+ }
1153
+ }
1154
+ function unboldPatch(style) {
1155
+ return nodeHasFlag(style, "bold") ? { bold: false, fontWeight: 400 } : { bold: false };
1156
+ }
1157
+ function clearNodeFlag(style, key) {
1158
+ const next = { ...style };
1159
+ switch (key) {
1160
+ case "italic":
1161
+ next.fontStyle = "normal";
1162
+ break;
1163
+ case "underline":
1164
+ delete next.underline;
1165
+ break;
1166
+ case "strikethrough":
1167
+ delete next.strikethrough;
1168
+ break;
1169
+ case "overline":
1170
+ delete next.overline;
1171
+ break;
1172
+ }
1173
+ return next;
1174
+ }
1175
+ function setFlagOverRange(runs, style, start, end, key, value) {
1176
+ if (value || !nodeHasFlag(style, key)) {
1177
+ return { runs: applyStyleToRange(runs, start, end, { [key]: value }), style };
1178
+ }
1179
+ if (key === "bold") {
1180
+ return { runs: applyStyleToRange(runs, start, end, unboldPatch(style)), style };
1181
+ }
1182
+ const total = runs.reduce((n, r) => n + r.text.length, 0);
1183
+ const lo = Math.max(0, start);
1184
+ const hi = Math.min(total, end);
1185
+ let next = applyStyleToRange(runs, 0, total, { [key]: true });
1186
+ if (lo < hi) next = applyStyleToRange(next, lo, hi, { [key]: false });
1187
+ return { runs: next, style: clearNodeFlag(style, key) };
1188
+ }
1189
+
1190
+ // src/runs/effectiveRangeStyle.ts
1191
+ var FLAGS = ["italic", "underline", "strikethrough", "overline"];
1192
+ function rangeWeight(range, style) {
1193
+ const node = numericWeight(style?.fontWeight ?? 400);
1194
+ const own2 = range?.fontWeight;
1195
+ if (own2 === MIXED) return { fontWeight: MIXED, bold: MIXED };
1196
+ if (own2 !== void 0) return { fontWeight: own2, bold: isBoldWeight(own2) };
1197
+ const flag = range?.bold;
1198
+ if (flag === MIXED) {
1199
+ if (node === 700) return { fontWeight: 700, bold: true };
1200
+ return { fontWeight: MIXED, bold: isBoldWeight(node) ? true : MIXED };
1201
+ }
1202
+ const weight = flag === true ? 700 : node;
1203
+ return { fontWeight: weight, bold: isBoldWeight(weight) };
1204
+ }
1205
+ var OVERRIDES = [
1206
+ "fontFamily",
1207
+ "fontSize",
1208
+ "letterSpacing",
1209
+ "fill",
1210
+ "script",
1211
+ "baselineShift",
1212
+ "fontScale",
1213
+ "textTransform",
1214
+ "fontVariantCaps"
1215
+ ];
1216
+ function effectiveRangeStyle(range, style, paint) {
1217
+ const resolved2 = resolveTextStyle(style, paint);
1218
+ const node = style ?? {};
1219
+ const out = {
1220
+ fontFamily: resolved2.fontFamily,
1221
+ fontSize: resolved2.fontSize,
1222
+ letterSpacing: resolved2.letterSpacing,
1223
+ textTransform: resolved2.textTransform,
1224
+ fontVariantCaps: resolved2.fontVariantCaps
1225
+ };
1226
+ if (resolved2.fill !== null) out.fill = resolved2.fill;
1227
+ Object.assign(out, rangeWeight(range, style));
1228
+ for (const key of FLAGS) {
1229
+ const on = nodeHasFlag(node, key);
1230
+ const run = range?.[key];
1231
+ out[key] = on || run === void 0 ? on : run;
1232
+ }
1233
+ if (range === null) return out;
1234
+ for (const key of OVERRIDES) {
1235
+ if (range[key] !== void 0) out[key] = range[key];
1236
+ }
1237
+ return out;
1238
+ }
1239
+
1240
+ // src/syntheticItalic.ts
1241
+ var SYNTHETIC_ITALIC_RADIANS = 0.2094;
1242
+ var TextOutlinesError = class extends Error {
1243
+ reason;
1244
+ family;
1245
+ weight;
1246
+ style;
1247
+ constructor(reason, family, weight, style, detail) {
1248
+ super(`textToPath: "${family}" ${weight}/${style} \u2014 ${detail}`);
1249
+ this.name = "TextOutlinesError";
1250
+ this.reason = reason;
1251
+ this.family = family;
1252
+ this.weight = weight;
1253
+ this.style = style;
1254
+ }
1255
+ };
1256
+ function facesOf(runs) {
1257
+ const out = /* @__PURE__ */ new Map();
1258
+ const add = (family, weight, style) => {
1259
+ out.set(`${family}|${weight}|${style}`, { family, weight, style });
1260
+ };
1261
+ for (const r of runs) {
1262
+ add(r.fontFamily, r.fontWeight, r.fontStyle);
1263
+ const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
1264
+ add(family, weight, style);
1265
+ }
1266
+ return [...out.values()];
1267
+ }
1268
+ function assertOutlinable(run) {
1269
+ const { fontFamily, fontWeight, fontStyle } = run;
1270
+ const fail = (reason, detail) => {
1271
+ throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail);
1272
+ };
1273
+ const requested = outlineStatus(fontFamily, fontWeight, fontStyle);
1274
+ if (requested === "idle" || requested === "loading") {
1275
+ void loadFontOutlines(fontFamily, { weight: fontWeight, style: fontStyle });
1276
+ fail("outlines-loading", "its outlines have not loaded yet; await loadTextOutlines first.");
1277
+ }
1278
+ const r = resolveFontVariant(fontFamily, fontWeight, fontStyle);
1279
+ const { family, weight, style } = r.resolved;
1280
+ if (r.synthetic.bold) {
1281
+ fail("synthetic-bold", `drawn as a faux bold of ${family} ${weight}/${style}, which has no geometry to embolden. Register outlines for the real bold face.`);
1282
+ }
1283
+ if (r.source === "outline") return;
1284
+ const status = outlineStatus(family, weight, style);
1285
+ if (status === null) {
1286
+ fail("no-outlines", `served by ${family} ${weight}/${style}, which has no outlines registered. Call registerFontOutlines for it.`);
1287
+ }
1288
+ if (status === "idle" || status === "loading") {
1289
+ void loadFontOutlines(family, { weight, style });
1290
+ fail("outlines-loading", `the outlines of ${family} ${weight}/${style} have not loaded yet; await loadTextOutlines first.`);
1291
+ }
1292
+ if (status === "failed") {
1293
+ fail("outlines-failed", `the outlines registered for ${family} ${weight}/${style} failed to load.`);
1294
+ }
1295
+ }
1296
+ var EM_CACHE_LIMIT = 4096;
1297
+ var emCache = /* @__PURE__ */ new Map();
1298
+ function emGlyph(d) {
1299
+ let p = emCache.get(d);
1300
+ if (p) return p;
1301
+ p = pathFromD(d);
1302
+ if (pathSignedArea(p) < 0) p = reversePath(p);
1303
+ if (emCache.size >= EM_CACHE_LIMIT) emCache.clear();
1304
+ emCache.set(d, p);
1305
+ return p;
1306
+ }
1307
+ function concat(parts) {
1308
+ let nc = 0, nk = 0;
1309
+ for (const p of parts) {
1310
+ nc += p.commands.length;
1311
+ nk += p.coords.length;
1312
+ }
1313
+ const commands = new Uint8Array(nc);
1314
+ const coords = new Float32Array(nk);
1315
+ let ci = 0, ki = 0;
1316
+ for (const p of parts) {
1317
+ commands.set(p.commands, ci);
1318
+ ci += p.commands.length;
1319
+ coords.set(p.coords, ki);
1320
+ ki += p.coords.length;
1321
+ }
1322
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
1323
+ }
1324
+ function ruleContour(x0, y0, x1, y1) {
1325
+ return {
1326
+ kind: "polygon",
1327
+ commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
1328
+ coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
1329
+ fillRule: "nonzero"
1330
+ };
1331
+ }
1332
+ function walkOutlines(data, pose, opts) {
1333
+ const textPose = {
1334
+ x: pose.x,
1335
+ y: pose.y,
1336
+ width: pose.width,
1337
+ height: pose.height,
1338
+ text: data.text,
1339
+ runs: data.runs,
1340
+ style: data.style,
1341
+ verticalAlign: data.verticalAlign,
1342
+ fill: data.fill,
1343
+ stroke: data.stroke
1344
+ };
1345
+ const { runs, opts: layoutOpts } = textPoseLayoutInput(textPose, opts.scale ?? 1);
1346
+ for (const run of runs) assertOutlinable(run);
1347
+ const paints = [];
1348
+ const indexOf = /* @__PURE__ */ new Map();
1349
+ const paintOf = runs.map((run) => {
1350
+ const key = `${paintKey(run.fill?.paint)}|${strokeKey(run.stroke)}`;
1351
+ let i = indexOf.get(key);
1352
+ if (i === void 0) {
1353
+ i = paints.length;
1354
+ indexOf.set(key, i);
1355
+ paints.push(run.stroke !== void 0 ? { fill: run.fill, stroke: run.stroke } : { fill: run.fill });
1356
+ }
1357
+ return i;
1358
+ });
1359
+ const laid = layoutRuns(runs, layoutOpts);
1360
+ const dx = pose.x;
1361
+ const dy = pose.y + verticalAlignOffset(data.verticalAlign, pose.height, laid.bounds.height);
1362
+ const parts = paints.map(() => []);
1363
+ const rules = paints.map(() => []);
1364
+ const faceOf = runFaces(runs);
1365
+ for (const line of laid.lines) {
1366
+ for (const g of line.glyphs) {
1367
+ if (g.glyph?.color) {
1368
+ const { fontFamily, fontWeight, fontStyle } = runs[g.run];
1369
+ throw new TextOutlinesError(
1370
+ "color-glyph",
1371
+ fontFamily,
1372
+ fontWeight,
1373
+ fontStyle,
1374
+ "the text holds an emoji, which is drawn in its own colors and has no outline."
1375
+ );
1376
+ }
1377
+ const d = glyphOutlineD(g, runs[g.run], 0);
1378
+ if (d === null) {
1379
+ if (glyphQuad(g) !== null) {
1380
+ const { family, weight, style } = g.resolved.resolved;
1381
+ throw new TextOutlinesError(
1382
+ "no-outlines",
1383
+ family,
1384
+ weight,
1385
+ style,
1386
+ "a fallback face serving some of the text's characters has no outlines registered."
1387
+ );
1388
+ }
1389
+ continue;
1390
+ }
1391
+ const s = g.fontSize;
1392
+ const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1393
+ const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
1394
+ parts[paintOf[g.run]].push(transformPath(emGlyph(d), m));
1395
+ }
1396
+ for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
1397
+ rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
1398
+ }
1399
+ }
1400
+ const frame = poseFrameOf(pose);
1401
+ const paths = parts.map((glyphs, i) => {
1402
+ const path = concat([...glyphs, ...rules[i]]);
1403
+ if (!frame || path.commands.length === 0) return path;
1404
+ return transformPath(path, frame);
1405
+ });
1406
+ return { paints, paths };
1407
+ }
1408
+ function textToPath(data, pose, opts = {}) {
1409
+ return concat(walkOutlines(data, pose, opts).paths);
1410
+ }
1411
+ function textToPathsByPaint(data, pose, opts = {}) {
1412
+ const { paints, paths } = walkOutlines(data, pose, opts);
1413
+ const out = [];
1414
+ paints.forEach((paint, i) => {
1415
+ if (paths[i].commands.length > 0) out.push({ ...paint, path: paths[i] });
1416
+ });
1417
+ return out;
1418
+ }
1419
+ async function loadTextOutlines(data, opts = {}) {
1420
+ const textPose = {
1421
+ width: 0,
1422
+ text: data.text,
1423
+ runs: data.runs,
1424
+ style: data.style
1425
+ };
1426
+ const { runs } = textPoseLayoutInput(textPose, opts.scale ?? 1);
1427
+ await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1428
+ await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1429
+ }
660
1430
 
661
- export { MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, createMarkdownRenderer, isBoldWeight, isSmallCapsLetter, layoutMarkdown, layoutTextPose, markdownToRuns, measureText, measureTextBounds, measuredWidth, numericWeight, resolveRunFace, resolveRuns, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, smallCapsScale, smallCapsScaleFor, smallCapsText, textLineBoxes, textPoseLayoutInput, toRuns, transformRunTexts, verticalAlignOffset };
1431
+ export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
662
1432
  //# sourceMappingURL=index.js.map
663
1433
  //# sourceMappingURL=index.js.map