@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.
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
- import { resolveTextStyle, resolveAlign, cachedLayoutRuns, graphemeCount, wrapLines, DECORATION_KINDS, decorationRule } from './chunk-L6UJ4RQK.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, resolveAlign, resolveTextStyle } from './chunk-L6UJ4RQK.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-Z6YRJ33J.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-Z6YRJ33J.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) {
@@ -52,6 +55,7 @@ function runsToMarkdown(runs, grammar = MARKDOWN_RUN_GRAMMAR) {
52
55
  const chars = escapees(grammar);
53
56
  let out = "";
54
57
  for (const r of runs) {
58
+ if (r.text.length === 0) continue;
55
59
  const escaped = escapeFor(r.text, chars);
56
60
  const on = flagsOf(r, grammar);
57
61
  if (on.size === 0) {
@@ -104,7 +108,12 @@ function markdownToRuns(input, grammar = MARKDOWN_RUN_GRAMMAR) {
104
108
  if (markers) {
105
109
  let count = 0;
106
110
  while (i + count < input.length && input[i + count] === ch) count++;
107
- const marker = markers.find((m) => m.repeat <= count) ?? markers[markers.length - 1];
111
+ const marker = markers.find((m) => m.repeat <= count);
112
+ if (!marker) {
113
+ buf += ch.repeat(count);
114
+ i += count;
115
+ continue;
116
+ }
108
117
  flush();
109
118
  for (const f of marker.flags) {
110
119
  if (active.has(f)) active.delete(f);
@@ -346,7 +355,7 @@ function resolveRuns(runs, style, viewScale = 1) {
346
355
  ...sizeMap ? { sizeMap } : {},
347
356
  fontWeight,
348
357
  fontStyle,
349
- fill: run.fill ?? style.fill,
358
+ fill: run.fill !== void 0 ? run.fill : style.fill,
350
359
  // Unlike the decorations below, a run's stroke *replaces* the node's
351
360
  // rather than adding to it — there is only one outline to paint.
352
361
  ...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : {},
@@ -362,6 +371,66 @@ function resolveRuns(runs, style, viewScale = 1) {
362
371
  return out;
363
372
  }
364
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
+
365
434
  // src/measure/verticalAlign.ts
366
435
  function verticalAlignOffset(align, boxHeight, textHeight) {
367
436
  if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
@@ -380,7 +449,9 @@ function textPoseLayoutInput(pose, scale = 1) {
380
449
  maxWidth: style.wrap ? pose.width : Infinity,
381
450
  alignWidth: pose.width,
382
451
  lineHeight: style.lineHeight,
383
- align: resolveAlign(style.align, style.direction),
452
+ align: style.align,
453
+ direction: style.direction,
454
+ tabSize: style.tabSize,
384
455
  justify: style.align === "justify"
385
456
  }
386
457
  };
@@ -394,6 +465,135 @@ function layoutTextPose(pose, scale = 1) {
394
465
  y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
395
466
  };
396
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
+ }
397
597
 
398
598
  // src/measure/measureText.ts
399
599
  function measuredWidth(ctx, text, style) {
@@ -409,21 +609,40 @@ function measureText(ctx, text, maxWidth, style) {
409
609
  u += ch.length;
410
610
  }
411
611
  at.push(u);
412
- const wrapped = wrapLines(cps, maxWidth, (start, end) => measuredWidth(ctx, text.slice(at[start], at[end]), style));
413
- const lines = wrapped.map((l) => text.slice(at[l.start], at[l.end]).replace(/ +$/, ""));
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) {
624
+ let end = l.end;
625
+ while (end > l.start && hangs(cps[end - 1])) end--;
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
+ }
414
631
  const lineStarts = wrapped.map((l) => at[l.start]);
415
632
  const height = lines.length * style.fontSize * style.lineHeight;
416
- return { lines, lineStarts, height };
633
+ return { lines, spans, lineStarts, height };
417
634
  }
418
635
 
419
636
  // src/measure/measureTextBounds.ts
420
637
  function measureTextBounds(text, style, opts) {
421
- const resolved = resolveTextStyle(style, void 0, opts?.scale);
422
- const runs = resolveRuns([{ text }], resolved, opts?.scale);
638
+ const resolved2 = resolveTextStyle(style, void 0, opts?.scale);
639
+ const runs = resolveRuns([{ text }], resolved2, opts?.scale);
423
640
  const { bounds } = cachedLayoutRuns(runs, {
424
641
  maxWidth: opts?.maxWidth ?? Infinity,
425
- lineHeight: opts?.lineHeight ?? resolved.lineHeight,
426
- align: resolved.align
642
+ lineHeight: opts?.lineHeight ?? resolved2.lineHeight,
643
+ align: resolved2.align,
644
+ direction: resolved2.direction,
645
+ tabSize: resolved2.tabSize
427
646
  });
428
647
  return { width: bounds.width, height: bounds.height };
429
648
  }
@@ -444,6 +663,115 @@ function textLineBoxes(pose, opts = {}) {
444
663
  }
445
664
  return out;
446
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
+ }
447
775
  function runMetrics(run, fontSize, face) {
448
776
  const script = run.script ? scriptMetrics(face)[run.script] : void 0;
449
777
  const scale = run.fontScale ?? script?.size ?? 1;
@@ -455,7 +783,7 @@ function runMetrics(run, fontSize, face) {
455
783
  y: shiftEm === 0 ? 0 : -shiftEm * fontSize
456
784
  };
457
785
  }
458
- 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) {
459
787
  if (runs.length === 0) return { lines: [], width: 0, height: 0 };
460
788
  const shown = transformRunTexts(runs.map((r) => r.text), runs.map((r) => r.textTransform ?? "none"));
461
789
  const segs = [];
@@ -506,7 +834,7 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
506
834
  const out = [];
507
835
  for (let k = from; k < to; k++) {
508
836
  const last = out[out.length - 1];
509
- 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) {
510
838
  out[out.length - 1] = { ...last, end: endOf[k] };
511
839
  } else {
512
840
  out.push({ run: runOf[k], start: startOf[k], end: endOf[k] });
@@ -521,19 +849,31 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
521
849
  const positioned = [];
522
850
  for (const p of line) {
523
851
  const { run, face, size, y, runSize } = placed[p.run];
524
- 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);
525
854
  positioned.push({ ...run, text: run.text.slice(p.start, p.end), x, width: width2, y, size, ...face ? { face } : {} });
526
855
  x += width2;
527
856
  maxSize = Math.max(maxSize, runSize);
528
857
  }
529
858
  lines.push({ runs: positioned, width: x, height: (maxSize > 0 ? maxSize : fontSize) * lineHeightFactor });
530
859
  }
531
- 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);
532
871
  for (const line of wrapLines(cps, maxWidth, widthOf)) {
533
872
  let end = line.end;
534
- if (line.wrapped) while (end > line.start && cps[end - 1] === 32) end--;
873
+ if (line.wrapped) while (end > line.start && hangs(cps[end - 1])) end--;
535
874
  commitLine(piecesOf(line.start, end));
536
875
  }
876
+ if (lines.length === 0) return { lines: [], width: 0, height: 0 };
537
877
  const width = Math.max(...lines.map((l) => l.width));
538
878
  const height = lines.reduce((sum, l) => sum + l.height, 0);
539
879
  return { lines, width, height };
@@ -584,7 +924,7 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
584
924
  const family = fontOpts.family ?? "sans-serif";
585
925
  const weight = numericWeight(fontOpts.weight ?? 400);
586
926
  const faceOf = (bold, italic) => faceMetricsFor(family, bold ? 700 : weight, italic ? "italic" : "normal");
587
- const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf);
927
+ const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf, fontOpts.tabSize);
588
928
  function walk(c, x, y, drawRun) {
589
929
  const align = c.textAlign;
590
930
  const left = blockLeft(c, x, layout.width);
@@ -642,7 +982,442 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
642
982
  };
643
983
  return { renderer, strokeRenderer, width: layout.width, height: layout.height };
644
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
+ const d = glyphOutlineD(g, runs[g.run], 0);
1368
+ if (d === null) {
1369
+ if (glyphQuad(g) !== null) {
1370
+ const { family, weight, style } = g.resolved.resolved;
1371
+ throw new TextOutlinesError(
1372
+ "no-outlines",
1373
+ family,
1374
+ weight,
1375
+ style,
1376
+ "a fallback face serving some of the text's characters has no outlines registered."
1377
+ );
1378
+ }
1379
+ continue;
1380
+ }
1381
+ const s = g.fontSize;
1382
+ const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1383
+ const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
1384
+ parts[paintOf[g.run]].push(transformPath(emGlyph(d), m));
1385
+ }
1386
+ for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
1387
+ rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
1388
+ }
1389
+ }
1390
+ const frame = poseFrameOf(pose);
1391
+ const paths = parts.map((glyphs, i) => {
1392
+ const path = concat([...glyphs, ...rules[i]]);
1393
+ if (!frame || path.commands.length === 0) return path;
1394
+ return transformPath(path, frame);
1395
+ });
1396
+ return { paints, paths };
1397
+ }
1398
+ function textToPath(data, pose, opts = {}) {
1399
+ return concat(walkOutlines(data, pose, opts).paths);
1400
+ }
1401
+ function textToPathsByPaint(data, pose, opts = {}) {
1402
+ const { paints, paths } = walkOutlines(data, pose, opts);
1403
+ const out = [];
1404
+ paints.forEach((paint, i) => {
1405
+ if (paths[i].commands.length > 0) out.push({ ...paint, path: paths[i] });
1406
+ });
1407
+ return out;
1408
+ }
1409
+ async function loadTextOutlines(data, opts = {}) {
1410
+ const textPose = {
1411
+ width: 0,
1412
+ text: data.text,
1413
+ runs: data.runs,
1414
+ style: data.style
1415
+ };
1416
+ const { runs } = textPoseLayoutInput(textPose, opts.scale ?? 1);
1417
+ await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1418
+ await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1419
+ }
645
1420
 
646
- 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 };
1421
+ 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 };
647
1422
  //# sourceMappingURL=index.js.map
648
1423
  //# sourceMappingURL=index.js.map