@vyaz/renderer 0.1.0 → 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, glyphDecorations: opts.glyphDecorations ?? true };
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";
@@ -400,6 +413,23 @@ ${debugMarkup}
400
413
  });
401
414
  this.root.children.push(rect);
402
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
+ }
403
433
  addDecorationLine(x, width, y, color, thickness) {
404
434
  this.closeText();
405
435
  this.root.children.push(el("line", {
@@ -545,6 +575,9 @@ function getSpanBackgroundAttrs(span, baselineY) {
545
575
  function renderToSVG(input, options = {}) {
546
576
  let lines;
547
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;
548
581
  if (Array.isArray(input)) {
549
582
  lines = input;
550
583
  resolvedOptions = options;
@@ -553,6 +586,13 @@ function renderToSVG(input, options = {}) {
553
586
  const callerSizes = options.sizing !== undefined || options.width !== undefined || options.height !== undefined;
554
587
  if (callerSizes) {
555
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
+ };
556
596
  } else {
557
597
  const fw = input.fit.horizontal === "frame" && input.frame.width != null;
558
598
  const fh = input.fit.vertical === "frame" && input.frame.height != null;
@@ -582,13 +622,21 @@ function renderToSVG(input, options = {}) {
582
622
  builder.addBackgroundRect(rx, bg.y, bg.w, bg.h, bg.fill);
583
623
  }
584
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
+ }
585
633
  if (opts.structure === "glyph") {
586
634
  const glyphFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
587
635
  const glyphSpanDX = line.x - glyphFirstTextX;
588
636
  let currentRunIdx = -1;
589
637
  let currentSig = "";
590
638
  for (const span of line.spans) {
591
- if (!span.text)
639
+ if (!span.text || span.inlineWidget)
592
640
  continue;
593
641
  const runIdx = span.itemIndex;
594
642
  const sig = styleSignature(span);
@@ -621,7 +669,7 @@ function renderToSVG(input, options = {}) {
621
669
  cur = null;
622
670
  };
623
671
  for (const span of line.spans) {
624
- if (!span.text)
672
+ if (!span.text || span.inlineWidget)
625
673
  continue;
626
674
  const u = !!span.style.underline;
627
675
  const s = !!span.style.strikethrough;
@@ -644,7 +692,7 @@ function renderToSVG(input, options = {}) {
644
692
  } else if (opts.structure === "flat") {
645
693
  const groups = [];
646
694
  for (const span of line.spans) {
647
- if (!span.text)
695
+ if (!span.text || span.inlineWidget)
648
696
  continue;
649
697
  const offset = span.fontMetrics.baselineOffset || 0;
650
698
  const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
@@ -688,7 +736,7 @@ function renderToSVG(input, options = {}) {
688
736
  } else {
689
737
  const groups = [];
690
738
  for (const span of line.spans) {
691
- if (!span.text)
739
+ if (!span.text || span.inlineWidget)
692
740
  continue;
693
741
  const offset = span.fontMetrics.baselineOffset || 0;
694
742
  const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
@@ -734,7 +782,7 @@ function renderToSVG(input, options = {}) {
734
782
  const spanDX = line.x - lineFirstTextX;
735
783
  let currentStyle = null;
736
784
  for (const span of group.spans) {
737
- if (!span.text)
785
+ if (!span.text || span.inlineWidget)
738
786
  continue;
739
787
  const x = span.x + spanDX;
740
788
  const shouldRender = span.type !== "space" || opts.spacing === "preserve";
@@ -756,6 +804,14 @@ function renderToSVG(input, options = {}) {
756
804
  const debugSvg = renderDebugToSVG(lines, opts.debug, frameSize, contentSize, options.columns, options.paddingLeft, 0);
757
805
  builder.addDebug(debugSvg);
758
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
+ }
759
815
  return serializeSvg(builder.root);
760
816
  }
761
817
  function renderParagraphToSVG(lines, paragraphWidth, paragraphHeight, options) {
@@ -772,6 +828,147 @@ function renderResultToSVG(result, options) {
772
828
  ...options
773
829
  });
774
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
+ }
775
972
  // src/CanvasRenderer.ts
776
973
  function fontWeightNumeric2(weight) {
777
974
  if (weight === "bold")
@@ -792,13 +989,13 @@ function buildFontString(span) {
792
989
  const family = span.style.fontFamily;
793
990
  return `${style} ${weight} ${size}px ${family}`;
794
991
  }
795
- function groupSpansByBaseline(line, spans) {
992
+ function groupSpansByBaseline(line2, spans) {
796
993
  const groups = [];
797
994
  for (const span of spans) {
798
995
  if (!span.text)
799
996
  continue;
800
997
  const offset = span.fontMetrics.baselineOffset || 0;
801
- const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
998
+ const targetY = Math.round((line2.y + line2.baseline + offset) * 100) / 100;
802
999
  const last = groups[groups.length - 1];
803
1000
  if (last && last.targetY === targetY) {
804
1001
  last.spans.push(span);
@@ -808,9 +1005,9 @@ function groupSpansByBaseline(line, spans) {
808
1005
  }
809
1006
  return groups;
810
1007
  }
811
- function drawSpanBackground(ctx, line, span) {
812
- const baselineY = line.y + line.baseline;
813
- 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;
814
1011
  const y = baselineY - span.fontMetrics.ascent;
815
1012
  const w = span.width;
816
1013
  const h = span.fontMetrics.ascent + span.fontMetrics.descent;
@@ -819,9 +1016,9 @@ function drawSpanBackground(ctx, line, span) {
819
1016
  ctx.fillRect(x, y, w, h);
820
1017
  }
821
1018
  }
822
- function renderSpan(ctx, line, span, baselineY, preserveSpaces) {
823
- const x = line.x + span.x;
824
- drawSpanBackground(ctx, line, span);
1019
+ function renderSpan(ctx, line2, span, baselineY, preserveSpaces) {
1020
+ const x = line2.x + span.x;
1021
+ drawSpanBackground(ctx, line2, span);
825
1022
  ctx.font = buildFontString(span);
826
1023
  ctx.fillStyle = span.style.color || "#000000";
827
1024
  ctx.textBaseline = "alphabetic";
@@ -881,14 +1078,14 @@ function renderToCanvas(ctx, lines, options = {}) {
881
1078
  } else {
882
1079
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
883
1080
  }
884
- for (const line of lines) {
885
- 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");
886
1083
  if (drawableSpans.length === 0)
887
1084
  continue;
888
- const groups = groupSpansByBaseline(line, drawableSpans);
1085
+ const groups = groupSpansByBaseline(line2, drawableSpans);
889
1086
  for (const group of groups) {
890
1087
  for (const span of group.spans) {
891
- renderSpan(ctx, line, span, group.targetY, preserveSpaces);
1088
+ renderSpan(ctx, line2, span, group.targetY, preserveSpaces);
892
1089
  }
893
1090
  }
894
1091
  }
@@ -950,12 +1147,12 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
950
1147
  ctx.fillText(`⚠ content overflow: ${warnParts.join(" ")}`, 4, _height + 4);
951
1148
  }
952
1149
  }
953
- for (const line of lines) {
954
- const bx = line.x;
955
- const by = line.y;
956
- const bw = line.width;
957
- const bh = line.height;
958
- 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;
959
1156
  if (flags.lineGap) {
960
1157
  ctx.fillStyle = "rgba(0,150,255,0.10)";
961
1158
  ctx.fillRect(bx, by, bw, bh);
@@ -974,8 +1171,8 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
974
1171
  ctx.stroke();
975
1172
  }
976
1173
  if (flags.ascentDescent) {
977
- const ascentY = baselineY - line.ascent;
978
- const descentY = baselineY + line.descent;
1174
+ const ascentY = baselineY - line2.ascent;
1175
+ const descentY = baselineY + line2.descent;
979
1176
  ctx.strokeStyle = "rgba(100,255,100,0.4)";
980
1177
  ctx.lineWidth = sw;
981
1178
  ctx.setLineDash([3, 2]);
@@ -998,10 +1195,10 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
998
1195
  ctx.fillText(label, bx, labelY);
999
1196
  }
1000
1197
  if (flags.runs) {
1001
- for (const span of line.spans) {
1198
+ for (const span of line2.spans) {
1002
1199
  if (span.width <= 0)
1003
1200
  continue;
1004
- const rx = line.x + span.x;
1201
+ const rx = line2.x + span.x;
1005
1202
  const ry = baselineY - span.fontMetrics.ascent;
1006
1203
  const rw = span.width;
1007
1204
  const rh = span.fontMetrics.ascent + span.fontMetrics.descent;
@@ -1019,33 +1216,33 @@ function renderSelection(ctx, lines, start, end, color = "rgba(100, 150, 255, 0.
1019
1216
  const liMin = Math.min(start.lineIndex, end.lineIndex);
1020
1217
  const liMax = Math.max(start.lineIndex, end.lineIndex);
1021
1218
  for (let li = liMin;li <= liMax; li++) {
1022
- const line = lines[li];
1023
- if (!line)
1219
+ const line2 = lines[li];
1220
+ if (!line2)
1024
1221
  continue;
1025
- const baselineY = line.y + line.baseline;
1222
+ const baselineY = line2.y + line2.baseline;
1026
1223
  let xStart;
1027
1224
  let xEnd;
1028
1225
  if (li === liMin && li === liMax) {
1029
1226
  xStart = Math.min(start.x, end.x);
1030
1227
  xEnd = Math.max(start.x, end.x);
1031
1228
  if (xEnd === xStart) {
1032
- 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;
1033
1230
  xEnd = spansEnd;
1034
1231
  }
1035
1232
  } else if (li === liMin) {
1036
1233
  xStart = start.x;
1037
- const lastSpan = line.spans[line.spans.length - 1];
1038
- xEnd = line.x + lastSpan.x + lastSpan.width;
1234
+ const lastSpan = line2.spans[line2.spans.length - 1];
1235
+ xEnd = line2.x + lastSpan.x + lastSpan.width;
1039
1236
  } else if (li === liMax) {
1040
- xStart = line.x;
1237
+ xStart = line2.x;
1041
1238
  xEnd = end.x;
1042
1239
  } else {
1043
- xStart = line.x;
1044
- const lastSpan = line.spans[line.spans.length - 1];
1045
- 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;
1046
1243
  }
1047
- const y = baselineY - line.ascent;
1048
- const h = line.ascent + line.descent;
1244
+ const y = baselineY - line2.ascent;
1245
+ const h = line2.ascent + line2.descent;
1049
1246
  const w = xEnd - xStart;
1050
1247
  if (w > 0) {
1051
1248
  ctx.fillRect(xStart, y, w, h);
@@ -1061,8 +1258,8 @@ function renderCursor(ctx, lines, pos, options = {}) {
1061
1258
  if (options.height !== undefined) {
1062
1259
  cursorHeight = options.height;
1063
1260
  } else {
1064
- const line = lines[pos.lineIndex];
1065
- cursorHeight = line.ascent + line.descent;
1261
+ const line2 = lines[pos.lineIndex];
1262
+ cursorHeight = line2.ascent + line2.descent;
1066
1263
  }
1067
1264
  const topY = pos.y - (options.height !== undefined ? options.height : lines[pos.lineIndex].ascent);
1068
1265
  ctx.strokeStyle = color;
@@ -1087,13 +1284,13 @@ function getCharAdvances(span) {
1087
1284
  function buildCharSegments(lines) {
1088
1285
  const segments = [];
1089
1286
  for (let li = 0;li < lines.length; li++) {
1090
- const line = lines[li];
1091
- const baselineY = line.y + line.baseline;
1092
- for (let si = 0;si < line.spans.length; si++) {
1093
- 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];
1094
1291
  const advances = getCharAdvances(span);
1095
1292
  const text = span.text;
1096
- let charX = line.x + span.x;
1293
+ let charX = line2.x + span.x;
1097
1294
  for (let ci = 0;ci < text.length; ci++) {
1098
1295
  const advance = ci < advances.length ? advances[ci] : 0;
1099
1296
  segments.push({
@@ -1105,7 +1302,7 @@ function buildCharSegments(lines) {
1105
1302
  y: baselineY,
1106
1303
  width: advance,
1107
1304
  char: text[ci],
1108
- line,
1305
+ line: line2,
1109
1306
  span
1110
1307
  });
1111
1308
  charX += advance;
@@ -1120,29 +1317,29 @@ function charAtPoint(lines, px, py) {
1120
1317
  let nearestLineIdx = 0;
1121
1318
  let minYDist = Infinity;
1122
1319
  for (let li = 0;li < lines.length; li++) {
1123
- const line2 = lines[li];
1124
- const baselineY2 = line2.y + line2.baseline;
1320
+ const line3 = lines[li];
1321
+ const baselineY2 = line3.y + line3.baseline;
1125
1322
  const yDist = Math.abs(py - baselineY2);
1126
- const inLineBox = py >= line2.y && py <= line2.y + line2.height;
1323
+ const inLineBox = py >= line3.y && py <= line3.y + line3.height;
1127
1324
  const distance = inLineBox ? 0 : yDist;
1128
1325
  if (distance < minYDist) {
1129
1326
  minYDist = distance;
1130
1327
  nearestLineIdx = li;
1131
1328
  }
1132
1329
  }
1133
- const line = lines[nearestLineIdx];
1134
- const baselineY = line.y + line.baseline;
1330
+ const line2 = lines[nearestLineIdx];
1331
+ const baselineY = line2.y + line2.baseline;
1135
1332
  let nearestSpanIdx = 0;
1136
1333
  let nearestCharIdx = 0;
1137
- let nearestX = line.x;
1334
+ let nearestX = line2.x;
1138
1335
  let nearestWidth = 0;
1139
- let nearestSpan = line.spans[0];
1336
+ let nearestSpan = line2.spans[0];
1140
1337
  let minXDist = Infinity;
1141
- for (let si = 0;si < line.spans.length; si++) {
1142
- const span = line.spans[si];
1338
+ for (let si = 0;si < line2.spans.length; si++) {
1339
+ const span = line2.spans[si];
1143
1340
  const advances = getCharAdvances(span);
1144
1341
  const text = span.text;
1145
- let charX = line.x + span.x;
1342
+ let charX = line2.x + span.x;
1146
1343
  for (let ci = 0;ci < text.length; ci++) {
1147
1344
  const advance = ci < advances.length ? advances[ci] : 0;
1148
1345
  const charCenter = charX + advance / 2;
@@ -1166,7 +1363,7 @@ function charAtPoint(lines, px, py) {
1166
1363
  x: nearestX,
1167
1364
  y: baselineY,
1168
1365
  width: nearestWidth,
1169
- line,
1366
+ line: line2,
1170
1367
  span: nearestSpan
1171
1368
  };
1172
1369
  }
@@ -1208,26 +1405,62 @@ function charIndexToPos(lines, charIndex) {
1208
1405
  function posToCharIndex(lines, pos) {
1209
1406
  let index = 0;
1210
1407
  for (let li = 0;li < pos.lineIndex; li++) {
1211
- const line2 = lines[li];
1212
- for (const span of line2.spans) {
1408
+ const line3 = lines[li];
1409
+ for (const span of line3.spans) {
1213
1410
  index += span.text.length;
1214
1411
  }
1215
1412
  }
1216
- const line = lines[pos.lineIndex];
1413
+ const line2 = lines[pos.lineIndex];
1217
1414
  for (let si = 0;si < pos.spanIndex; si++) {
1218
- index += line.spans[si].text.length;
1415
+ index += line2.spans[si].text.length;
1219
1416
  }
1220
1417
  index += pos.charIndex;
1221
1418
  return index;
1222
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
+ }
1223
1454
  export {
1224
1455
  renderToSVG,
1225
1456
  renderToCanvas,
1457
+ renderTableToSVG,
1226
1458
  renderSelection,
1227
1459
  renderResultToSVG,
1228
1460
  renderParagraphToSVG,
1229
1461
  renderDebugToCanvas,
1230
1462
  renderCursor,
1463
+ registerFont,
1231
1464
  posToCharIndex,
1232
1465
  charIndexToPos,
1233
1466
  charAtPoint
package/dist/index.d.ts CHANGED
@@ -5,9 +5,13 @@
5
5
  */
6
6
  export { renderToSVG, renderParagraphToSVG, renderResultToSVG } from './SVGRenderer.js';
7
7
  export type { SVGRenderOptions, SvgPreset, SvgStyle, SvgFit, SvgSizing } from './SVGRenderer.js';
8
+ export { renderTableToSVG } from './TableRenderer.js';
9
+ export type { TableRenderOptions } from './TableRenderer.js';
8
10
  export { renderToCanvas, renderDebugToCanvas, renderSelection, renderCursor } from './CanvasRenderer.js';
9
11
  export type { CanvasRenderOptions, CursorOptions } from './CanvasRenderer.js';
10
12
  export { charAtPoint, charIndexToPos, posToCharIndex } from './interactive.js';
11
13
  export type { CharPos } from './interactive.js';
14
+ export { registerFont } from './register-font.js';
15
+ export type { RegisterFontOptions, RegisterFontResult } from './register-font.js';
12
16
  export type { DebugFlags } from './types.js';
13
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACxF,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEjG,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACzG,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE9E,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC/E,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACxF,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACzG,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE9E,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC/E,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAElF,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}