@vyaz/renderer 0.0.14 → 0.3.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.
@@ -22,6 +22,19 @@ var PRESETS = {
22
22
  preserve: { structure: "expanded", spacing: "preserve", defaultFit: "frag" },
23
23
  glyph: { structure: "glyph", spacing: "preserve", defaultFit: "none" }
24
24
  };
25
+ function normalizeDeg(d) {
26
+ const r = d % 360;
27
+ return r < 0 ? r + 360 : r;
28
+ }
29
+ function rotationTransform(rotate, w, h) {
30
+ if (rotate === 90)
31
+ return { transform: `translate(${fmt(h)} 0) rotate(90)`, width: h, height: w };
32
+ if (rotate === 270)
33
+ return { transform: `translate(0 ${fmt(w)}) rotate(270)`, width: h, height: w };
34
+ if (rotate === 180)
35
+ return { transform: `translate(${fmt(w)} ${fmt(h)}) rotate(180)`, width: w, height: h };
36
+ return { transform: `rotate(${fmt(rotate)} ${fmt(w / 2)} ${fmt(h / 2)})`, width: w, height: h };
37
+ }
25
38
  function escapeXml(text) {
26
39
  return text.replace(/&/g, "&#38;").replace(/</g, "&#60;").replace(/>/g, "&#62;").replace(/"/g, "&#34;");
27
40
  }
@@ -93,7 +106,7 @@ function resolveOptions(opts) {
93
106
  console.warn(`SVGRenderer: fit="frag" downgraded to "text" when structure="flat"`);
94
107
  fit = "text";
95
108
  }
96
- return { structure, spacing, style, fit, sizingHorizontal, sizingVertical, width: opts.width, height: opts.height, className: opts.className, contentPadding: opts.contentPadding ?? 0, debug: opts.debug };
109
+ return { structure, spacing, style, fit, sizingHorizontal, sizingVertical, width: opts.width, height: opts.height, className: opts.className, contentPadding: opts.contentPadding ?? 0, debug: opts.debug, glyphDecorations: opts.glyphDecorations ?? true, inlineBoxes: opts.inlineBoxes };
97
110
  }
98
111
  function resolveSize(lines, opts) {
99
112
  const needsBBox = opts.sizingHorizontal === "content" || opts.sizingVertical === "content";
@@ -223,13 +236,12 @@ function buildTspanAttrs(span, x, currentStyle) {
223
236
  }
224
237
  return { attrs, newStyle: s };
225
238
  }
226
- function buildGlyphPositions(span, _lineX) {
239
+ function buildGlyphPositions(span, spanDX) {
227
240
  if (!span.glyphAdvances || span.glyphAdvances.length === 0) {
228
- return "";
241
+ return (span.type === "marker" || span.type === "space") && span.text ? fmt(span.x + spanDX, 1) : "";
229
242
  }
230
243
  const ls = span.style.letterSpacing || 0;
231
- const spanX = span.x;
232
- let xPos = spanX;
244
+ let xPos = span.x + spanDX;
233
245
  const positions = [fmt(xPos, 1)];
234
246
  for (let i = 0;i < span.glyphAdvances.length - 1; i++) {
235
247
  xPos += span.glyphAdvances[i] + ls;
@@ -324,6 +336,8 @@ class SvgAstBuilder {
324
336
  x: fmt(x),
325
337
  y: fmt(yOverride)
326
338
  };
339
+ if (runId)
340
+ attrs.id = runId;
327
341
  if (this.opts.style === "css") {
328
342
  let css = `font-family: '${s.fontFamily}', sans-serif; font-size: ${fmt(fontSizeOverride)}px; fill: ${colorToRGB(s.color)}; font-weight: ${s.fontWeight}`;
329
343
  if (s.fontStyle === "italic")
@@ -399,6 +413,34 @@ ${debugMarkup}
399
413
  });
400
414
  this.root.children.push(rect);
401
415
  }
416
+ addInlineBox(x, y, width, height, fragment) {
417
+ this.closeText();
418
+ if (fragment) {
419
+ this.root.children.push(rawNode(` <g transform="translate(${fmt(x)} ${fmt(y)})">${fragment}</g>
420
+ `));
421
+ return;
422
+ }
423
+ this.root.children.push(el("rect", {
424
+ x: fmt(x),
425
+ y: fmt(y),
426
+ width: fmt(width),
427
+ height: fmt(height),
428
+ fill: "#ccc",
429
+ stroke: "#999",
430
+ "stroke-dasharray": "2,2"
431
+ }));
432
+ }
433
+ addDecorationLine(x, width, y, color, thickness) {
434
+ this.closeText();
435
+ this.root.children.push(el("line", {
436
+ x1: fmt(x),
437
+ y1: fmt(y),
438
+ x2: fmt(x + width),
439
+ y2: fmt(y),
440
+ stroke: color,
441
+ "stroke-width": fmt(thickness)
442
+ }));
443
+ }
402
444
  addRawLine(lineStr) {
403
445
  this.closeText();
404
446
  this.root.children.push(rawNode(lineStr));
@@ -530,40 +572,127 @@ function getSpanBackgroundAttrs(span, baselineY) {
530
572
  const h = span.fontMetrics.ascent + span.fontMetrics.descent;
531
573
  return { x, y, w, h, fill: span.style.backgroundColor };
532
574
  }
533
- function renderToSVG(lines, options = {}) {
534
- const opts = resolveOptions(options);
575
+ function renderToSVG(input, options = {}) {
576
+ let lines;
577
+ let resolvedOptions;
578
+ const xf = options.transform ?? (Array.isArray(input) ? undefined : input.transform);
579
+ const rotate = xf ? normalizeDeg(xf.rotate) : 0;
580
+ const applyTransform = xf !== undefined && rotate !== 0;
581
+ if (Array.isArray(input)) {
582
+ lines = input;
583
+ resolvedOptions = options;
584
+ } else {
585
+ lines = input.lines;
586
+ const callerSizes = options.sizing !== undefined || options.width !== undefined || options.height !== undefined;
587
+ if (callerSizes) {
588
+ resolvedOptions = options;
589
+ } else if (applyTransform) {
590
+ resolvedOptions = {
591
+ sizing: { horizontal: "frame", vertical: "frame" },
592
+ width: xf.layoutBox.width,
593
+ height: xf.layoutBox.height,
594
+ ...options
595
+ };
596
+ } else {
597
+ const fw = input.fit.horizontal === "frame" && input.frame.width != null;
598
+ const fh = input.fit.vertical === "frame" && input.frame.height != null;
599
+ resolvedOptions = {
600
+ sizing: {
601
+ horizontal: fw ? "frame" : "content",
602
+ vertical: fh ? "frame" : "content"
603
+ },
604
+ width: fw ? input.frame.width : input.content.width,
605
+ height: fh ? input.frame.height : input.content.height,
606
+ ...options
607
+ };
608
+ }
609
+ }
610
+ const opts = resolveOptions(resolvedOptions);
535
611
  const { width: svgWidth, height: svgHeight, viewBox, frameWidth, frameHeight } = resolveSize(lines, opts);
536
612
  const builder = new SvgAstBuilder(svgWidth, svgHeight, opts, viewBox);
537
613
  for (const line of lines) {
538
614
  const baselineY = line.y + line.baseline;
615
+ const bgFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
539
616
  for (const span of line.spans) {
540
617
  if (!span.text || !span.style.backgroundColor)
541
618
  continue;
542
619
  const bg = getSpanBackgroundAttrs(span, baselineY);
543
620
  if (bg) {
544
- const rx = line.x + bg.x;
621
+ const rx = line.x + bg.x - bgFirstTextX;
545
622
  builder.addBackgroundRect(rx, bg.y, bg.w, bg.h, bg.fill);
546
623
  }
547
624
  }
625
+ for (const span of line.spans) {
626
+ const iw = span.inlineWidget;
627
+ if (!iw)
628
+ continue;
629
+ const ibx = line.x + span.x - bgFirstTextX;
630
+ const iby = baselineY - iw.height + (iw.baselineOffset || 0);
631
+ builder.addInlineBox(ibx, iby, iw.width, iw.height, iw.id ? opts.inlineBoxes?.[iw.id] : undefined);
632
+ }
548
633
  if (opts.structure === "glyph") {
634
+ const glyphFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
635
+ const glyphSpanDX = line.x - glyphFirstTextX;
549
636
  let currentRunIdx = -1;
637
+ let currentSig = "";
550
638
  for (const span of line.spans) {
551
- if (!span.text)
639
+ if (!span.text || span.inlineWidget)
552
640
  continue;
553
641
  const runIdx = span.itemIndex;
554
- if (runIdx !== currentRunIdx) {
642
+ const sig = styleSignature(span);
643
+ if (runIdx !== currentRunIdx || sig !== currentSig) {
555
644
  builder.closeText();
556
645
  const runId = span.tag ? `${span.tag}-${runIdx}` : undefined;
557
- builder.openText(line, span, runId);
646
+ const offset = span.fontMetrics.baselineOffset || 0;
647
+ if (offset !== 0) {
648
+ const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
649
+ builder.openText(line, span, runId, targetY, span.fontMetrics.fontSize);
650
+ } else {
651
+ builder.openText(line, span, runId);
652
+ }
558
653
  currentRunIdx = runIdx;
654
+ currentSig = sig;
559
655
  }
560
- builder.addGlyphTspan(span, line.x);
656
+ builder.addGlyphTspan(span, glyphSpanDX);
561
657
  }
562
658
  builder.closeText();
659
+ if (opts.glyphDecorations) {
660
+ let cur = null;
661
+ const flushDeco = () => {
662
+ if (!cur)
663
+ return;
664
+ const w = cur.end - cur.x;
665
+ if (cur.u)
666
+ builder.addDecorationLine(cur.x, w, cur.baseY + 2, cur.color, 1);
667
+ if (cur.s)
668
+ builder.addDecorationLine(cur.x, w, cur.baseY - cur.ascent * 0.4, cur.color, 1);
669
+ cur = null;
670
+ };
671
+ for (const span of line.spans) {
672
+ if (!span.text || span.inlineWidget)
673
+ continue;
674
+ const u = !!span.style.underline;
675
+ const s = !!span.style.strikethrough;
676
+ if (!u && !s) {
677
+ flushDeco();
678
+ continue;
679
+ }
680
+ const color = span.style.color || "#000000";
681
+ const x = span.x + glyphSpanDX;
682
+ const baseY = line.y + line.baseline;
683
+ if (cur && cur.u === u && cur.s === s && cur.color === color && Math.abs(cur.end - x) < 0.01) {
684
+ cur.end = x + span.width;
685
+ } else {
686
+ flushDeco();
687
+ cur = { x, end: x + span.width, baseY, ascent: span.fontMetrics.ascent, color, u, s };
688
+ }
689
+ }
690
+ flushDeco();
691
+ }
563
692
  } else if (opts.structure === "flat") {
564
693
  const groups = [];
565
694
  for (const span of line.spans) {
566
- if (!span.text)
695
+ if (!span.text || span.inlineWidget)
567
696
  continue;
568
697
  const offset = span.fontMetrics.baselineOffset || 0;
569
698
  const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
@@ -607,7 +736,7 @@ function renderToSVG(lines, options = {}) {
607
736
  } else {
608
737
  const groups = [];
609
738
  for (const span of line.spans) {
610
- if (!span.text)
739
+ if (!span.text || span.inlineWidget)
611
740
  continue;
612
741
  const offset = span.fontMetrics.baselineOffset || 0;
613
742
  const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
@@ -649,11 +778,13 @@ function renderToSVG(lines, options = {}) {
649
778
  } else {
650
779
  builder.openText(line, baseSpan);
651
780
  }
781
+ const lineFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
782
+ const spanDX = line.x - lineFirstTextX;
652
783
  let currentStyle = null;
653
784
  for (const span of group.spans) {
654
- if (!span.text)
785
+ if (!span.text || span.inlineWidget)
655
786
  continue;
656
- const x = span.x;
787
+ const x = span.x + spanDX;
657
788
  const shouldRender = span.type !== "space" || opts.spacing === "preserve";
658
789
  if (shouldRender) {
659
790
  const newStyle = builder.addTspan(span, x, currentStyle);
@@ -673,6 +804,14 @@ function renderToSVG(lines, options = {}) {
673
804
  const debugSvg = renderDebugToSVG(lines, opts.debug, frameSize, contentSize, options.columns, options.paddingLeft, 0);
674
805
  builder.addDebug(debugSvg);
675
806
  }
807
+ if (applyTransform) {
808
+ const { transform, width: visW, height: visH } = rotationTransform(rotate, xf.layoutBox.width, xf.layoutBox.height);
809
+ const kids = builder.root.children.splice(0, builder.root.children.length);
810
+ builder.root.children.push(el("g", { transform }, kids));
811
+ builder.root.attrs.width = fmt(visW);
812
+ builder.root.attrs.height = fmt(visH);
813
+ builder.root.attrs.viewBox = `0 0 ${fmt(visW)} ${fmt(visH)}`;
814
+ }
676
815
  return serializeSvg(builder.root);
677
816
  }
678
817
  function renderParagraphToSVG(lines, paragraphWidth, paragraphHeight, options) {
@@ -689,6 +828,147 @@ function renderResultToSVG(result, options) {
689
828
  ...options
690
829
  });
691
830
  }
831
+ // src/TableRenderer.ts
832
+ function withOverflowVisible(svg) {
833
+ return svg.replace(/^<svg /, '<svg style="overflow:visible" ');
834
+ }
835
+ function rect(x, y, w, h, attrs) {
836
+ const a = Object.entries(attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
837
+ return ` <rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(Math.max(0, w))}" height="${fmt(Math.max(0, h))}" ${a} />
838
+ `;
839
+ }
840
+ function line(x1, y1, x2, y2, color, width, extra = {}) {
841
+ const extraAttrs = Object.entries(extra).map(([k, v]) => ` ${k}="${v}"`).join("");
842
+ return ` <line x1="${fmt(x1)}" y1="${fmt(y1)}" x2="${fmt(x2)}" y2="${fmt(y2)}" stroke="${color}" stroke-width="${fmt(width)}"${extraAttrs} />
843
+ `;
844
+ }
845
+ function dashAttrs(b, side) {
846
+ const attrs = {};
847
+ const pattern = b.patterns?.[side];
848
+ if (pattern?.length)
849
+ attrs["stroke-dasharray"] = pattern.join(",");
850
+ const shape = b.shapes?.[side];
851
+ if (shape && shape !== "butt")
852
+ attrs["stroke-linecap"] = shape;
853
+ return attrs;
854
+ }
855
+ function dashUniform(b) {
856
+ const eq = (a, c) => (a?.join(",") ?? "") === (c?.join(",") ?? "");
857
+ const p = b.patterns;
858
+ const patternsUniform = !p || eq(p.top, p.right) && eq(p.right, p.bottom) && eq(p.bottom, p.left);
859
+ const s = b.shapes;
860
+ const shapesUniform = !s || s.top === s.right && s.right === s.bottom && s.bottom === s.left;
861
+ return patternsUniform && shapesUniform;
862
+ }
863
+ function borderMarkup(x, y, w, h, b) {
864
+ const { widths: wd, colors: co } = b;
865
+ const uniform = wd.top === wd.right && wd.right === wd.bottom && wd.bottom === wd.left && co.top === co.right && co.right === co.bottom && co.bottom === co.left && dashUniform(b);
866
+ if (uniform) {
867
+ if (wd.top <= 0)
868
+ return "";
869
+ const inset = wd.top / 2;
870
+ const attrs = { fill: "none", stroke: co.top, "stroke-width": fmt(wd.top), ...dashAttrs(b, "top") };
871
+ if (b.rx !== undefined)
872
+ attrs.rx = fmt(b.rx);
873
+ if (b.ry !== undefined)
874
+ attrs.ry = fmt(b.ry);
875
+ return rect(x + inset, y + inset, w - wd.top, h - wd.top, attrs);
876
+ }
877
+ let out = "";
878
+ if (wd.top > 0)
879
+ out += line(x, y + wd.top / 2, x + w, y + wd.top / 2, co.top, wd.top, dashAttrs(b, "top"));
880
+ if (wd.right > 0)
881
+ out += line(x + w - wd.right / 2, y, x + w - wd.right / 2, y + h, co.right, wd.right, dashAttrs(b, "right"));
882
+ if (wd.bottom > 0)
883
+ out += line(x, y + h - wd.bottom / 2, x + w, y + h - wd.bottom / 2, co.bottom, wd.bottom, dashAttrs(b, "bottom"));
884
+ if (wd.left > 0)
885
+ out += line(x + wd.left / 2, y, x + wd.left / 2, y + h, co.left, wd.left, dashAttrs(b, "left"));
886
+ return out;
887
+ }
888
+ function contentWidthOf(cell) {
889
+ const b = cell.border?.widths;
890
+ return Math.max(0, cell.width - cell.padding.left - cell.padding.right - (b ? b.left + b.right : 0));
891
+ }
892
+ function paintSlot(slot, preset, style) {
893
+ const { content, x, y } = slot;
894
+ if (content.content.width <= 0 || content.content.height <= 0 || content.lines.length === 0)
895
+ return "";
896
+ const svg = renderToSVG(content, { preset, style, sizing: "frame", width: content.content.width, height: content.content.height });
897
+ return ` <g transform="translate(${fmt(x)} ${fmt(y)})">${svg}</g>
898
+ `;
899
+ }
900
+ function renderTableToSVG(result, opts = {}) {
901
+ const preset = opts.preset ?? "browser";
902
+ const style = opts.style;
903
+ const parts = [];
904
+ if (result.bgColor)
905
+ parts.push(rect(0, 0, result.width, result.height, { fill: result.bgColor }));
906
+ for (const row of result.rows) {
907
+ if (row.bgColor)
908
+ parts.push(rect(result.contentBox.x, row.y, result.contentBox.width, row.height, { fill: row.bgColor }));
909
+ }
910
+ for (const row of result.rows) {
911
+ for (const cell of row.cells) {
912
+ if (cell.bgColor)
913
+ parts.push(rect(cell.x, cell.y, cell.width, cell.height, { fill: cell.bgColor }));
914
+ }
915
+ }
916
+ for (const row of result.rows) {
917
+ if (row.border)
918
+ parts.push(borderMarkup(result.contentBox.x, row.y, result.contentBox.width, row.height, row.border));
919
+ }
920
+ for (const row of result.rows) {
921
+ for (const cell of row.cells) {
922
+ if (cell.border)
923
+ parts.push(borderMarkup(cell.x, cell.y, cell.width, cell.height, cell.border));
924
+ }
925
+ }
926
+ if (result.border) {
927
+ parts.push(borderMarkup(result.contentBox.x, result.contentBox.y, result.contentBox.width, result.contentBox.height, result.border));
928
+ }
929
+ for (const row of result.rows) {
930
+ for (const cell of row.cells) {
931
+ if (cell.before)
932
+ parts.push(paintSlot(cell.before, preset, style));
933
+ const originX = cell.x + cell.padding.left + (cell.border?.widths.left ?? 0) + cell.cx;
934
+ const originY = cell.y + cell.padding.top + (cell.border?.widths.top ?? 0) + cell.verticalOffset + cell.cy;
935
+ if (cell.nestedTable) {
936
+ const nestedSvg = renderTableToSVG(cell.nestedTable, { preset, style, fragment: true });
937
+ parts.push(` <g transform="translate(${fmt(originX)} ${fmt(originY)})">${nestedSvg}</g>
938
+ `);
939
+ } else {
940
+ const cw = contentWidthOf(cell);
941
+ const ch = cell.content.content.height;
942
+ if (cw > 0 && ch > 0 && cell.content.lines.length > 0) {
943
+ let svg = renderToSVG(cell.content, { preset, style, sizing: "frame", width: cw, height: ch });
944
+ if (cell.allowOverflow)
945
+ svg = withOverflowVisible(svg);
946
+ parts.push(` <g transform="translate(${fmt(originX)} ${fmt(originY)})">${svg}</g>
947
+ `);
948
+ }
949
+ }
950
+ if (cell.after)
951
+ parts.push(paintSlot(cell.after, preset, style));
952
+ }
953
+ }
954
+ if (opts.fragment) {
955
+ const gAttrs = opts.className ? ` class="${opts.className}"` : "";
956
+ return `<g${gAttrs}>
957
+ ${parts.join("")}</g>
958
+ `;
959
+ }
960
+ const attrs = [
961
+ 'xmlns="http://www.w3.org/2000/svg"',
962
+ `width="${fmt(result.width)}"`,
963
+ `height="${fmt(result.height)}"`,
964
+ `viewBox="0 0 ${fmt(result.width)} ${fmt(result.height)}"`
965
+ ];
966
+ if (opts.className)
967
+ attrs.push(`class="${opts.className}"`);
968
+ return `<svg ${attrs.join(" ")}>
969
+ ${parts.join("")}</svg>
970
+ `;
971
+ }
692
972
  // src/CanvasRenderer.ts
693
973
  function fontWeightNumeric2(weight) {
694
974
  if (weight === "bold")
@@ -709,13 +989,13 @@ function buildFontString(span) {
709
989
  const family = span.style.fontFamily;
710
990
  return `${style} ${weight} ${size}px ${family}`;
711
991
  }
712
- function groupSpansByBaseline(line, spans) {
992
+ function groupSpansByBaseline(line2, spans) {
713
993
  const groups = [];
714
994
  for (const span of spans) {
715
995
  if (!span.text)
716
996
  continue;
717
997
  const offset = span.fontMetrics.baselineOffset || 0;
718
- const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
998
+ const targetY = Math.round((line2.y + line2.baseline + offset) * 100) / 100;
719
999
  const last = groups[groups.length - 1];
720
1000
  if (last && last.targetY === targetY) {
721
1001
  last.spans.push(span);
@@ -725,9 +1005,9 @@ function groupSpansByBaseline(line, spans) {
725
1005
  }
726
1006
  return groups;
727
1007
  }
728
- function drawSpanBackground(ctx, line, span) {
729
- const baselineY = line.y + line.baseline;
730
- const x = line.x + span.x;
1008
+ function drawSpanBackground(ctx, line2, span) {
1009
+ const baselineY = line2.y + line2.baseline;
1010
+ const x = line2.x + span.x;
731
1011
  const y = baselineY - span.fontMetrics.ascent;
732
1012
  const w = span.width;
733
1013
  const h = span.fontMetrics.ascent + span.fontMetrics.descent;
@@ -736,9 +1016,9 @@ function drawSpanBackground(ctx, line, span) {
736
1016
  ctx.fillRect(x, y, w, h);
737
1017
  }
738
1018
  }
739
- function renderSpan(ctx, line, span, baselineY, preserveSpaces) {
740
- const x = line.x + span.x;
741
- drawSpanBackground(ctx, line, span);
1019
+ function renderSpan(ctx, line2, span, baselineY, preserveSpaces) {
1020
+ const x = line2.x + span.x;
1021
+ drawSpanBackground(ctx, line2, span);
742
1022
  ctx.font = buildFontString(span);
743
1023
  ctx.fillStyle = span.style.color || "#000000";
744
1024
  ctx.textBaseline = "alphabetic";
@@ -798,14 +1078,14 @@ function renderToCanvas(ctx, lines, options = {}) {
798
1078
  } else {
799
1079
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
800
1080
  }
801
- for (const line of lines) {
802
- const drawableSpans = line.spans.filter((s) => preserveSpaces || s.type !== "space");
1081
+ for (const line2 of lines) {
1082
+ const drawableSpans = line2.spans.filter((s) => preserveSpaces || s.type !== "space");
803
1083
  if (drawableSpans.length === 0)
804
1084
  continue;
805
- const groups = groupSpansByBaseline(line, drawableSpans);
1085
+ const groups = groupSpansByBaseline(line2, drawableSpans);
806
1086
  for (const group of groups) {
807
1087
  for (const span of group.spans) {
808
- renderSpan(ctx, line, span, group.targetY, preserveSpaces);
1088
+ renderSpan(ctx, line2, span, group.targetY, preserveSpaces);
809
1089
  }
810
1090
  }
811
1091
  }
@@ -867,12 +1147,12 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
867
1147
  ctx.fillText(`⚠ content overflow: ${warnParts.join(" ")}`, 4, _height + 4);
868
1148
  }
869
1149
  }
870
- for (const line of lines) {
871
- const bx = line.x;
872
- const by = line.y;
873
- const bw = line.width;
874
- const bh = line.height;
875
- const baselineY = line.y + line.baseline;
1150
+ for (const line2 of lines) {
1151
+ const bx = line2.x;
1152
+ const by = line2.y;
1153
+ const bw = line2.width;
1154
+ const bh = line2.height;
1155
+ const baselineY = line2.y + line2.baseline;
876
1156
  if (flags.lineGap) {
877
1157
  ctx.fillStyle = "rgba(0,150,255,0.10)";
878
1158
  ctx.fillRect(bx, by, bw, bh);
@@ -891,8 +1171,8 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
891
1171
  ctx.stroke();
892
1172
  }
893
1173
  if (flags.ascentDescent) {
894
- const ascentY = baselineY - line.ascent;
895
- const descentY = baselineY + line.descent;
1174
+ const ascentY = baselineY - line2.ascent;
1175
+ const descentY = baselineY + line2.descent;
896
1176
  ctx.strokeStyle = "rgba(100,255,100,0.4)";
897
1177
  ctx.lineWidth = sw;
898
1178
  ctx.setLineDash([3, 2]);
@@ -915,10 +1195,10 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
915
1195
  ctx.fillText(label, bx, labelY);
916
1196
  }
917
1197
  if (flags.runs) {
918
- for (const span of line.spans) {
1198
+ for (const span of line2.spans) {
919
1199
  if (span.width <= 0)
920
1200
  continue;
921
- const rx = line.x + span.x;
1201
+ const rx = line2.x + span.x;
922
1202
  const ry = baselineY - span.fontMetrics.ascent;
923
1203
  const rw = span.width;
924
1204
  const rh = span.fontMetrics.ascent + span.fontMetrics.descent;
@@ -936,33 +1216,33 @@ function renderSelection(ctx, lines, start, end, color = "rgba(100, 150, 255, 0.
936
1216
  const liMin = Math.min(start.lineIndex, end.lineIndex);
937
1217
  const liMax = Math.max(start.lineIndex, end.lineIndex);
938
1218
  for (let li = liMin;li <= liMax; li++) {
939
- const line = lines[li];
940
- if (!line)
1219
+ const line2 = lines[li];
1220
+ if (!line2)
941
1221
  continue;
942
- const baselineY = line.y + line.baseline;
1222
+ const baselineY = line2.y + line2.baseline;
943
1223
  let xStart;
944
1224
  let xEnd;
945
1225
  if (li === liMin && li === liMax) {
946
1226
  xStart = Math.min(start.x, end.x);
947
1227
  xEnd = Math.max(start.x, end.x);
948
1228
  if (xEnd === xStart) {
949
- const spansEnd = line.x + line.spans[line.spans.length - 1].x + line.spans[line.spans.length - 1].width;
1229
+ const spansEnd = line2.x + line2.spans[line2.spans.length - 1].x + line2.spans[line2.spans.length - 1].width;
950
1230
  xEnd = spansEnd;
951
1231
  }
952
1232
  } else if (li === liMin) {
953
1233
  xStart = start.x;
954
- const lastSpan = line.spans[line.spans.length - 1];
955
- xEnd = line.x + lastSpan.x + lastSpan.width;
1234
+ const lastSpan = line2.spans[line2.spans.length - 1];
1235
+ xEnd = line2.x + lastSpan.x + lastSpan.width;
956
1236
  } else if (li === liMax) {
957
- xStart = line.x;
1237
+ xStart = line2.x;
958
1238
  xEnd = end.x;
959
1239
  } else {
960
- xStart = line.x;
961
- const lastSpan = line.spans[line.spans.length - 1];
962
- xEnd = line.x + lastSpan.x + lastSpan.width;
1240
+ xStart = line2.x;
1241
+ const lastSpan = line2.spans[line2.spans.length - 1];
1242
+ xEnd = line2.x + lastSpan.x + lastSpan.width;
963
1243
  }
964
- const y = baselineY - line.ascent;
965
- const h = line.ascent + line.descent;
1244
+ const y = baselineY - line2.ascent;
1245
+ const h = line2.ascent + line2.descent;
966
1246
  const w = xEnd - xStart;
967
1247
  if (w > 0) {
968
1248
  ctx.fillRect(xStart, y, w, h);
@@ -978,8 +1258,8 @@ function renderCursor(ctx, lines, pos, options = {}) {
978
1258
  if (options.height !== undefined) {
979
1259
  cursorHeight = options.height;
980
1260
  } else {
981
- const line = lines[pos.lineIndex];
982
- cursorHeight = line.ascent + line.descent;
1261
+ const line2 = lines[pos.lineIndex];
1262
+ cursorHeight = line2.ascent + line2.descent;
983
1263
  }
984
1264
  const topY = pos.y - (options.height !== undefined ? options.height : lines[pos.lineIndex].ascent);
985
1265
  ctx.strokeStyle = color;
@@ -1004,13 +1284,13 @@ function getCharAdvances(span) {
1004
1284
  function buildCharSegments(lines) {
1005
1285
  const segments = [];
1006
1286
  for (let li = 0;li < lines.length; li++) {
1007
- const line = lines[li];
1008
- const baselineY = line.y + line.baseline;
1009
- for (let si = 0;si < line.spans.length; si++) {
1010
- const span = line.spans[si];
1287
+ const line2 = lines[li];
1288
+ const baselineY = line2.y + line2.baseline;
1289
+ for (let si = 0;si < line2.spans.length; si++) {
1290
+ const span = line2.spans[si];
1011
1291
  const advances = getCharAdvances(span);
1012
1292
  const text = span.text;
1013
- let charX = line.x + span.x;
1293
+ let charX = line2.x + span.x;
1014
1294
  for (let ci = 0;ci < text.length; ci++) {
1015
1295
  const advance = ci < advances.length ? advances[ci] : 0;
1016
1296
  segments.push({
@@ -1022,7 +1302,7 @@ function buildCharSegments(lines) {
1022
1302
  y: baselineY,
1023
1303
  width: advance,
1024
1304
  char: text[ci],
1025
- line,
1305
+ line: line2,
1026
1306
  span
1027
1307
  });
1028
1308
  charX += advance;
@@ -1037,29 +1317,29 @@ function charAtPoint(lines, px, py) {
1037
1317
  let nearestLineIdx = 0;
1038
1318
  let minYDist = Infinity;
1039
1319
  for (let li = 0;li < lines.length; li++) {
1040
- const line2 = lines[li];
1041
- const baselineY2 = line2.y + line2.baseline;
1320
+ const line3 = lines[li];
1321
+ const baselineY2 = line3.y + line3.baseline;
1042
1322
  const yDist = Math.abs(py - baselineY2);
1043
- const inLineBox = py >= line2.y && py <= line2.y + line2.height;
1323
+ const inLineBox = py >= line3.y && py <= line3.y + line3.height;
1044
1324
  const distance = inLineBox ? 0 : yDist;
1045
1325
  if (distance < minYDist) {
1046
1326
  minYDist = distance;
1047
1327
  nearestLineIdx = li;
1048
1328
  }
1049
1329
  }
1050
- const line = lines[nearestLineIdx];
1051
- const baselineY = line.y + line.baseline;
1330
+ const line2 = lines[nearestLineIdx];
1331
+ const baselineY = line2.y + line2.baseline;
1052
1332
  let nearestSpanIdx = 0;
1053
1333
  let nearestCharIdx = 0;
1054
- let nearestX = line.x;
1334
+ let nearestX = line2.x;
1055
1335
  let nearestWidth = 0;
1056
- let nearestSpan = line.spans[0];
1336
+ let nearestSpan = line2.spans[0];
1057
1337
  let minXDist = Infinity;
1058
- for (let si = 0;si < line.spans.length; si++) {
1059
- const span = line.spans[si];
1338
+ for (let si = 0;si < line2.spans.length; si++) {
1339
+ const span = line2.spans[si];
1060
1340
  const advances = getCharAdvances(span);
1061
1341
  const text = span.text;
1062
- let charX = line.x + span.x;
1342
+ let charX = line2.x + span.x;
1063
1343
  for (let ci = 0;ci < text.length; ci++) {
1064
1344
  const advance = ci < advances.length ? advances[ci] : 0;
1065
1345
  const charCenter = charX + advance / 2;
@@ -1083,7 +1363,7 @@ function charAtPoint(lines, px, py) {
1083
1363
  x: nearestX,
1084
1364
  y: baselineY,
1085
1365
  width: nearestWidth,
1086
- line,
1366
+ line: line2,
1087
1367
  span: nearestSpan
1088
1368
  };
1089
1369
  }
@@ -1125,26 +1405,62 @@ function charIndexToPos(lines, charIndex) {
1125
1405
  function posToCharIndex(lines, pos) {
1126
1406
  let index = 0;
1127
1407
  for (let li = 0;li < pos.lineIndex; li++) {
1128
- const line2 = lines[li];
1129
- for (const span of line2.spans) {
1408
+ const line3 = lines[li];
1409
+ for (const span of line3.spans) {
1130
1410
  index += span.text.length;
1131
1411
  }
1132
1412
  }
1133
- const line = lines[pos.lineIndex];
1413
+ const line2 = lines[pos.lineIndex];
1134
1414
  for (let si = 0;si < pos.spanIndex; si++) {
1135
- index += line.spans[si].text.length;
1415
+ index += line2.spans[si].text.length;
1136
1416
  }
1137
1417
  index += pos.charIndex;
1138
1418
  return index;
1139
1419
  }
1420
+ // src/register-font.ts
1421
+ import { fontMetricsProvider } from "@vyaz/core";
1422
+ var CSS_GENERIC = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-serif|ui-sans-serif|ui-monospace|ui-rounded|math|emoji|fangsong)$/i;
1423
+ function normWeight(w) {
1424
+ if (w == null)
1425
+ return "normal";
1426
+ return typeof w === "number" ? String(w) : w;
1427
+ }
1428
+ async function registerFont(family, source, opts = {}) {
1429
+ const weight = normWeight(opts.weight);
1430
+ const style = opts.style ?? "normal";
1431
+ if (CSS_GENERIC.test(family)) {
1432
+ const proc = typeof globalThis !== "undefined" ? globalThis.process : undefined;
1433
+ if (proc?.env?.NODE_ENV !== "production") {
1434
+ console.warn(`[vyaz] registerFont("${family}"): "${family}" is a CSS generic keyword. ` + 'A browser paints `font-family="' + family + '"` with the OS default for that ' + "generic and ignores this @font-face, so the SVG will not match the metrics. " + "Register under a concrete family name instead.");
1435
+ }
1436
+ }
1437
+ const bytes = typeof source === "string" ? new Uint8Array(await (await fetch(source)).arrayBuffer()) : source instanceof Uint8Array ? source : new Uint8Array(source);
1438
+ await fontMetricsProvider.registerFont(family, { weight, style, ...opts.variation ? { variation: opts.variation } : {} }, bytes);
1439
+ let browser = false;
1440
+ if (!opts.engineOnly && typeof document !== "undefined" && document.fonts && typeof FontFace !== "undefined") {
1441
+ try {
1442
+ const face = new FontFace(family, bytes, {
1443
+ weight: opts.variation ? "1 1000" : weight === "normal" ? "400" : weight,
1444
+ style,
1445
+ display: "swap"
1446
+ });
1447
+ await face.load();
1448
+ document.fonts.add(face);
1449
+ browser = true;
1450
+ } catch {}
1451
+ }
1452
+ return { family, engine: true, browser };
1453
+ }
1140
1454
  export {
1141
1455
  renderToSVG,
1142
1456
  renderToCanvas,
1457
+ renderTableToSVG,
1143
1458
  renderSelection,
1144
1459
  renderResultToSVG,
1145
1460
  renderParagraphToSVG,
1146
1461
  renderDebugToCanvas,
1147
1462
  renderCursor,
1463
+ registerFont,
1148
1464
  posToCharIndex,
1149
1465
  charIndexToPos,
1150
1466
  charAtPoint