@vyaz/renderer 0.1.0 → 0.4.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";
@@ -164,7 +177,10 @@ function equalStyle(a, b) {
164
177
  }
165
178
  function styleSignature(span) {
166
179
  const s = defaultStyleState(span);
167
- return `${s.fontFamily}|${s.fontSize}|${s.fontWeight}|${s.color}|${s.fontStyle}|${s.decoration}|${s.letterSpacing ?? ""}|${s.backgroundColor ?? ""}`;
180
+ return `${s.fontFamily}|${s.fontSize}|${s.fontWeight}|${s.color}|${s.fontStyle}|${s.decoration}|${s.letterSpacing ?? ""}|${s.backgroundColor ?? ""}|${span.style.data?.href ?? ""}`;
181
+ }
182
+ function hrefOf(span) {
183
+ return span.style.data?.href;
168
184
  }
169
185
  function buildTextAttrs(line, span, opts, runId) {
170
186
  const x = line.x;
@@ -400,6 +416,29 @@ ${debugMarkup}
400
416
  });
401
417
  this.root.children.push(rect);
402
418
  }
419
+ addInlineBox(x, y, width, height, fragment) {
420
+ this.closeText();
421
+ if (fragment) {
422
+ this.root.children.push(rawNode(` <g transform="translate(${fmt(x)} ${fmt(y)})">${fragment}</g>
423
+ `));
424
+ return;
425
+ }
426
+ this.root.children.push(el("rect", {
427
+ x: fmt(x),
428
+ y: fmt(y),
429
+ width: fmt(width),
430
+ height: fmt(height),
431
+ fill: "#ccc",
432
+ stroke: "#999",
433
+ "stroke-dasharray": "2,2"
434
+ }));
435
+ }
436
+ openLink(href) {
437
+ this.root.children.push(rawNode(`<a href="${escapeXml(href)}">`));
438
+ }
439
+ closeLink() {
440
+ this.root.children.push(rawNode("</a>"));
441
+ }
403
442
  addDecorationLine(x, width, y, color, thickness) {
404
443
  this.closeText();
405
444
  this.root.children.push(el("line", {
@@ -545,6 +584,9 @@ function getSpanBackgroundAttrs(span, baselineY) {
545
584
  function renderToSVG(input, options = {}) {
546
585
  let lines;
547
586
  let resolvedOptions;
587
+ const xf = options.transform ?? (Array.isArray(input) ? undefined : input.transform);
588
+ const rotate = xf ? normalizeDeg(xf.rotate) : 0;
589
+ const applyTransform = xf !== undefined && rotate !== 0;
548
590
  if (Array.isArray(input)) {
549
591
  lines = input;
550
592
  resolvedOptions = options;
@@ -553,6 +595,13 @@ function renderToSVG(input, options = {}) {
553
595
  const callerSizes = options.sizing !== undefined || options.width !== undefined || options.height !== undefined;
554
596
  if (callerSizes) {
555
597
  resolvedOptions = options;
598
+ } else if (applyTransform) {
599
+ resolvedOptions = {
600
+ sizing: { horizontal: "frame", vertical: "frame" },
601
+ width: xf.layoutBox.width,
602
+ height: xf.layoutBox.height,
603
+ ...options
604
+ };
556
605
  } else {
557
606
  const fw = input.fit.horizontal === "frame" && input.frame.width != null;
558
607
  const fh = input.fit.vertical === "frame" && input.frame.height != null;
@@ -582,13 +631,21 @@ function renderToSVG(input, options = {}) {
582
631
  builder.addBackgroundRect(rx, bg.y, bg.w, bg.h, bg.fill);
583
632
  }
584
633
  }
634
+ for (const span of line.spans) {
635
+ const iw = span.inlineWidget;
636
+ if (!iw)
637
+ continue;
638
+ const ibx = line.x + span.x - bgFirstTextX;
639
+ const iby = baselineY - iw.height + (iw.baselineOffset || 0);
640
+ builder.addInlineBox(ibx, iby, iw.width, iw.height, iw.id ? opts.inlineBoxes?.[iw.id] : undefined);
641
+ }
585
642
  if (opts.structure === "glyph") {
586
643
  const glyphFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
587
644
  const glyphSpanDX = line.x - glyphFirstTextX;
588
645
  let currentRunIdx = -1;
589
646
  let currentSig = "";
590
647
  for (const span of line.spans) {
591
- if (!span.text)
648
+ if (!span.text || span.inlineWidget)
592
649
  continue;
593
650
  const runIdx = span.itemIndex;
594
651
  const sig = styleSignature(span);
@@ -621,7 +678,7 @@ function renderToSVG(input, options = {}) {
621
678
  cur = null;
622
679
  };
623
680
  for (const span of line.spans) {
624
- if (!span.text)
681
+ if (!span.text || span.inlineWidget)
625
682
  continue;
626
683
  const u = !!span.style.underline;
627
684
  const s = !!span.style.strikethrough;
@@ -644,7 +701,7 @@ function renderToSVG(input, options = {}) {
644
701
  } else if (opts.structure === "flat") {
645
702
  const groups = [];
646
703
  for (const span of line.spans) {
647
- if (!span.text)
704
+ if (!span.text || span.inlineWidget)
648
705
  continue;
649
706
  const offset = span.fontMetrics.baselineOffset || 0;
650
707
  const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
@@ -688,7 +745,7 @@ function renderToSVG(input, options = {}) {
688
745
  } else {
689
746
  const groups = [];
690
747
  for (const span of line.spans) {
691
- if (!span.text)
748
+ if (!span.text || span.inlineWidget)
692
749
  continue;
693
750
  const offset = span.fontMetrics.baselineOffset || 0;
694
751
  const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
@@ -704,6 +761,9 @@ function renderToSVG(input, options = {}) {
704
761
  const baseSpan = group.spans.find((f) => f.type === "text" && f.text.length > 0) || group.spans[0];
705
762
  if (!baseSpan)
706
763
  continue;
764
+ const href = hrefOf(baseSpan);
765
+ if (href)
766
+ builder.openLink(href);
707
767
  const lineBaseY = Math.round((line.y + line.baseline) * 100) / 100;
708
768
  const needsOffset = group.targetY !== lineBaseY;
709
769
  if (needsOffset) {
@@ -734,7 +794,7 @@ function renderToSVG(input, options = {}) {
734
794
  const spanDX = line.x - lineFirstTextX;
735
795
  let currentStyle = null;
736
796
  for (const span of group.spans) {
737
- if (!span.text)
797
+ if (!span.text || span.inlineWidget)
738
798
  continue;
739
799
  const x = span.x + spanDX;
740
800
  const shouldRender = span.type !== "space" || opts.spacing === "preserve";
@@ -746,6 +806,8 @@ function renderToSVG(input, options = {}) {
746
806
  }
747
807
  }
748
808
  builder.closeText();
809
+ if (href)
810
+ builder.closeLink();
749
811
  }
750
812
  }
751
813
  }
@@ -756,6 +818,14 @@ function renderToSVG(input, options = {}) {
756
818
  const debugSvg = renderDebugToSVG(lines, opts.debug, frameSize, contentSize, options.columns, options.paddingLeft, 0);
757
819
  builder.addDebug(debugSvg);
758
820
  }
821
+ if (applyTransform) {
822
+ const { transform, width: visW, height: visH } = rotationTransform(rotate, xf.layoutBox.width, xf.layoutBox.height);
823
+ const kids = builder.root.children.splice(0, builder.root.children.length);
824
+ builder.root.children.push(el("g", { transform }, kids));
825
+ builder.root.attrs.width = fmt(visW);
826
+ builder.root.attrs.height = fmt(visH);
827
+ builder.root.attrs.viewBox = `0 0 ${fmt(visW)} ${fmt(visH)}`;
828
+ }
759
829
  return serializeSvg(builder.root);
760
830
  }
761
831
  function renderParagraphToSVG(lines, paragraphWidth, paragraphHeight, options) {
@@ -772,6 +842,147 @@ function renderResultToSVG(result, options) {
772
842
  ...options
773
843
  });
774
844
  }
845
+ // src/TableRenderer.ts
846
+ function withOverflowVisible(svg) {
847
+ return svg.replace(/^<svg /, '<svg style="overflow:visible" ');
848
+ }
849
+ function rect(x, y, w, h, attrs) {
850
+ const a = Object.entries(attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
851
+ return ` <rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(Math.max(0, w))}" height="${fmt(Math.max(0, h))}" ${a} />
852
+ `;
853
+ }
854
+ function line(x1, y1, x2, y2, color, width, extra = {}) {
855
+ const extraAttrs = Object.entries(extra).map(([k, v]) => ` ${k}="${v}"`).join("");
856
+ return ` <line x1="${fmt(x1)}" y1="${fmt(y1)}" x2="${fmt(x2)}" y2="${fmt(y2)}" stroke="${color}" stroke-width="${fmt(width)}"${extraAttrs} />
857
+ `;
858
+ }
859
+ function dashAttrs(b, side) {
860
+ const attrs = {};
861
+ const pattern = b.patterns?.[side];
862
+ if (pattern?.length)
863
+ attrs["stroke-dasharray"] = pattern.join(",");
864
+ const shape = b.shapes?.[side];
865
+ if (shape && shape !== "butt")
866
+ attrs["stroke-linecap"] = shape;
867
+ return attrs;
868
+ }
869
+ function dashUniform(b) {
870
+ const eq = (a, c) => (a?.join(",") ?? "") === (c?.join(",") ?? "");
871
+ const p = b.patterns;
872
+ const patternsUniform = !p || eq(p.top, p.right) && eq(p.right, p.bottom) && eq(p.bottom, p.left);
873
+ const s = b.shapes;
874
+ const shapesUniform = !s || s.top === s.right && s.right === s.bottom && s.bottom === s.left;
875
+ return patternsUniform && shapesUniform;
876
+ }
877
+ function borderMarkup(x, y, w, h, b) {
878
+ const { widths: wd, colors: co } = b;
879
+ 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);
880
+ if (uniform) {
881
+ if (wd.top <= 0)
882
+ return "";
883
+ const inset = wd.top / 2;
884
+ const attrs = { fill: "none", stroke: co.top, "stroke-width": fmt(wd.top), ...dashAttrs(b, "top") };
885
+ if (b.rx !== undefined)
886
+ attrs.rx = fmt(b.rx);
887
+ if (b.ry !== undefined)
888
+ attrs.ry = fmt(b.ry);
889
+ return rect(x + inset, y + inset, w - wd.top, h - wd.top, attrs);
890
+ }
891
+ let out = "";
892
+ if (wd.top > 0)
893
+ out += line(x, y + wd.top / 2, x + w, y + wd.top / 2, co.top, wd.top, dashAttrs(b, "top"));
894
+ if (wd.right > 0)
895
+ out += line(x + w - wd.right / 2, y, x + w - wd.right / 2, y + h, co.right, wd.right, dashAttrs(b, "right"));
896
+ if (wd.bottom > 0)
897
+ out += line(x, y + h - wd.bottom / 2, x + w, y + h - wd.bottom / 2, co.bottom, wd.bottom, dashAttrs(b, "bottom"));
898
+ if (wd.left > 0)
899
+ out += line(x + wd.left / 2, y, x + wd.left / 2, y + h, co.left, wd.left, dashAttrs(b, "left"));
900
+ return out;
901
+ }
902
+ function contentWidthOf(cell) {
903
+ const b = cell.border?.widths;
904
+ return Math.max(0, cell.width - cell.padding.left - cell.padding.right - (b ? b.left + b.right : 0));
905
+ }
906
+ function paintSlot(slot, preset, style) {
907
+ const { content, x, y } = slot;
908
+ if (content.content.width <= 0 || content.content.height <= 0 || content.lines.length === 0)
909
+ return "";
910
+ const svg = renderToSVG(content, { preset, style, sizing: "frame", width: content.content.width, height: content.content.height });
911
+ return ` <g transform="translate(${fmt(x)} ${fmt(y)})">${svg}</g>
912
+ `;
913
+ }
914
+ function renderTableToSVG(result, opts = {}) {
915
+ const preset = opts.preset ?? "browser";
916
+ const style = opts.style;
917
+ const parts = [];
918
+ if (result.bgColor)
919
+ parts.push(rect(0, 0, result.width, result.height, { fill: result.bgColor }));
920
+ for (const row of result.rows) {
921
+ if (row.bgColor)
922
+ parts.push(rect(result.contentBox.x, row.y, result.contentBox.width, row.height, { fill: row.bgColor }));
923
+ }
924
+ for (const row of result.rows) {
925
+ for (const cell of row.cells) {
926
+ if (cell.bgColor)
927
+ parts.push(rect(cell.x, cell.y, cell.width, cell.height, { fill: cell.bgColor }));
928
+ }
929
+ }
930
+ for (const row of result.rows) {
931
+ if (row.border)
932
+ parts.push(borderMarkup(result.contentBox.x, row.y, result.contentBox.width, row.height, row.border));
933
+ }
934
+ for (const row of result.rows) {
935
+ for (const cell of row.cells) {
936
+ if (cell.border)
937
+ parts.push(borderMarkup(cell.x, cell.y, cell.width, cell.height, cell.border));
938
+ }
939
+ }
940
+ if (result.border) {
941
+ parts.push(borderMarkup(result.contentBox.x, result.contentBox.y, result.contentBox.width, result.contentBox.height, result.border));
942
+ }
943
+ for (const row of result.rows) {
944
+ for (const cell of row.cells) {
945
+ if (cell.before)
946
+ parts.push(paintSlot(cell.before, preset, style));
947
+ const originX = cell.x + cell.padding.left + (cell.border?.widths.left ?? 0) + cell.cx;
948
+ const originY = cell.y + cell.padding.top + (cell.border?.widths.top ?? 0) + cell.verticalOffset + cell.cy;
949
+ if (cell.nestedTable) {
950
+ const nestedSvg = renderTableToSVG(cell.nestedTable, { preset, style, fragment: true });
951
+ parts.push(` <g transform="translate(${fmt(originX)} ${fmt(originY)})">${nestedSvg}</g>
952
+ `);
953
+ } else {
954
+ const cw = contentWidthOf(cell);
955
+ const ch = cell.content.content.height;
956
+ if (cw > 0 && ch > 0 && cell.content.lines.length > 0) {
957
+ let svg = renderToSVG(cell.content, { preset, style, sizing: "frame", width: cw, height: ch });
958
+ if (cell.allowOverflow)
959
+ svg = withOverflowVisible(svg);
960
+ parts.push(` <g transform="translate(${fmt(originX)} ${fmt(originY)})">${svg}</g>
961
+ `);
962
+ }
963
+ }
964
+ if (cell.after)
965
+ parts.push(paintSlot(cell.after, preset, style));
966
+ }
967
+ }
968
+ if (opts.fragment) {
969
+ const gAttrs = opts.className ? ` class="${opts.className}"` : "";
970
+ return `<g${gAttrs}>
971
+ ${parts.join("")}</g>
972
+ `;
973
+ }
974
+ const attrs = [
975
+ 'xmlns="http://www.w3.org/2000/svg"',
976
+ `width="${fmt(result.width)}"`,
977
+ `height="${fmt(result.height)}"`,
978
+ `viewBox="0 0 ${fmt(result.width)} ${fmt(result.height)}"`
979
+ ];
980
+ if (opts.className)
981
+ attrs.push(`class="${opts.className}"`);
982
+ return `<svg ${attrs.join(" ")}>
983
+ ${parts.join("")}</svg>
984
+ `;
985
+ }
775
986
  // src/CanvasRenderer.ts
776
987
  function fontWeightNumeric2(weight) {
777
988
  if (weight === "bold")
@@ -792,13 +1003,13 @@ function buildFontString(span) {
792
1003
  const family = span.style.fontFamily;
793
1004
  return `${style} ${weight} ${size}px ${family}`;
794
1005
  }
795
- function groupSpansByBaseline(line, spans) {
1006
+ function groupSpansByBaseline(line2, spans) {
796
1007
  const groups = [];
797
1008
  for (const span of spans) {
798
1009
  if (!span.text)
799
1010
  continue;
800
1011
  const offset = span.fontMetrics.baselineOffset || 0;
801
- const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
1012
+ const targetY = Math.round((line2.y + line2.baseline + offset) * 100) / 100;
802
1013
  const last = groups[groups.length - 1];
803
1014
  if (last && last.targetY === targetY) {
804
1015
  last.spans.push(span);
@@ -808,9 +1019,9 @@ function groupSpansByBaseline(line, spans) {
808
1019
  }
809
1020
  return groups;
810
1021
  }
811
- function drawSpanBackground(ctx, line, span) {
812
- const baselineY = line.y + line.baseline;
813
- const x = line.x + span.x;
1022
+ function drawSpanBackground(ctx, line2, span) {
1023
+ const baselineY = line2.y + line2.baseline;
1024
+ const x = line2.x + span.x;
814
1025
  const y = baselineY - span.fontMetrics.ascent;
815
1026
  const w = span.width;
816
1027
  const h = span.fontMetrics.ascent + span.fontMetrics.descent;
@@ -819,9 +1030,9 @@ function drawSpanBackground(ctx, line, span) {
819
1030
  ctx.fillRect(x, y, w, h);
820
1031
  }
821
1032
  }
822
- function renderSpan(ctx, line, span, baselineY, preserveSpaces) {
823
- const x = line.x + span.x;
824
- drawSpanBackground(ctx, line, span);
1033
+ function renderSpan(ctx, line2, span, baselineY, preserveSpaces) {
1034
+ const x = line2.x + span.x;
1035
+ drawSpanBackground(ctx, line2, span);
825
1036
  ctx.font = buildFontString(span);
826
1037
  ctx.fillStyle = span.style.color || "#000000";
827
1038
  ctx.textBaseline = "alphabetic";
@@ -881,14 +1092,14 @@ function renderToCanvas(ctx, lines, options = {}) {
881
1092
  } else {
882
1093
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
883
1094
  }
884
- for (const line of lines) {
885
- const drawableSpans = line.spans.filter((s) => preserveSpaces || s.type !== "space");
1095
+ for (const line2 of lines) {
1096
+ const drawableSpans = line2.spans.filter((s) => preserveSpaces || s.type !== "space");
886
1097
  if (drawableSpans.length === 0)
887
1098
  continue;
888
- const groups = groupSpansByBaseline(line, drawableSpans);
1099
+ const groups = groupSpansByBaseline(line2, drawableSpans);
889
1100
  for (const group of groups) {
890
1101
  for (const span of group.spans) {
891
- renderSpan(ctx, line, span, group.targetY, preserveSpaces);
1102
+ renderSpan(ctx, line2, span, group.targetY, preserveSpaces);
892
1103
  }
893
1104
  }
894
1105
  }
@@ -950,12 +1161,12 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
950
1161
  ctx.fillText(`⚠ content overflow: ${warnParts.join(" ")}`, 4, _height + 4);
951
1162
  }
952
1163
  }
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;
1164
+ for (const line2 of lines) {
1165
+ const bx = line2.x;
1166
+ const by = line2.y;
1167
+ const bw = line2.width;
1168
+ const bh = line2.height;
1169
+ const baselineY = line2.y + line2.baseline;
959
1170
  if (flags.lineGap) {
960
1171
  ctx.fillStyle = "rgba(0,150,255,0.10)";
961
1172
  ctx.fillRect(bx, by, bw, bh);
@@ -974,8 +1185,8 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
974
1185
  ctx.stroke();
975
1186
  }
976
1187
  if (flags.ascentDescent) {
977
- const ascentY = baselineY - line.ascent;
978
- const descentY = baselineY + line.descent;
1188
+ const ascentY = baselineY - line2.ascent;
1189
+ const descentY = baselineY + line2.descent;
979
1190
  ctx.strokeStyle = "rgba(100,255,100,0.4)";
980
1191
  ctx.lineWidth = sw;
981
1192
  ctx.setLineDash([3, 2]);
@@ -998,10 +1209,10 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
998
1209
  ctx.fillText(label, bx, labelY);
999
1210
  }
1000
1211
  if (flags.runs) {
1001
- for (const span of line.spans) {
1212
+ for (const span of line2.spans) {
1002
1213
  if (span.width <= 0)
1003
1214
  continue;
1004
- const rx = line.x + span.x;
1215
+ const rx = line2.x + span.x;
1005
1216
  const ry = baselineY - span.fontMetrics.ascent;
1006
1217
  const rw = span.width;
1007
1218
  const rh = span.fontMetrics.ascent + span.fontMetrics.descent;
@@ -1019,33 +1230,33 @@ function renderSelection(ctx, lines, start, end, color = "rgba(100, 150, 255, 0.
1019
1230
  const liMin = Math.min(start.lineIndex, end.lineIndex);
1020
1231
  const liMax = Math.max(start.lineIndex, end.lineIndex);
1021
1232
  for (let li = liMin;li <= liMax; li++) {
1022
- const line = lines[li];
1023
- if (!line)
1233
+ const line2 = lines[li];
1234
+ if (!line2)
1024
1235
  continue;
1025
- const baselineY = line.y + line.baseline;
1236
+ const baselineY = line2.y + line2.baseline;
1026
1237
  let xStart;
1027
1238
  let xEnd;
1028
1239
  if (li === liMin && li === liMax) {
1029
1240
  xStart = Math.min(start.x, end.x);
1030
1241
  xEnd = Math.max(start.x, end.x);
1031
1242
  if (xEnd === xStart) {
1032
- const spansEnd = line.x + line.spans[line.spans.length - 1].x + line.spans[line.spans.length - 1].width;
1243
+ const spansEnd = line2.x + line2.spans[line2.spans.length - 1].x + line2.spans[line2.spans.length - 1].width;
1033
1244
  xEnd = spansEnd;
1034
1245
  }
1035
1246
  } else if (li === liMin) {
1036
1247
  xStart = start.x;
1037
- const lastSpan = line.spans[line.spans.length - 1];
1038
- xEnd = line.x + lastSpan.x + lastSpan.width;
1248
+ const lastSpan = line2.spans[line2.spans.length - 1];
1249
+ xEnd = line2.x + lastSpan.x + lastSpan.width;
1039
1250
  } else if (li === liMax) {
1040
- xStart = line.x;
1251
+ xStart = line2.x;
1041
1252
  xEnd = end.x;
1042
1253
  } else {
1043
- xStart = line.x;
1044
- const lastSpan = line.spans[line.spans.length - 1];
1045
- xEnd = line.x + lastSpan.x + lastSpan.width;
1254
+ xStart = line2.x;
1255
+ const lastSpan = line2.spans[line2.spans.length - 1];
1256
+ xEnd = line2.x + lastSpan.x + lastSpan.width;
1046
1257
  }
1047
- const y = baselineY - line.ascent;
1048
- const h = line.ascent + line.descent;
1258
+ const y = baselineY - line2.ascent;
1259
+ const h = line2.ascent + line2.descent;
1049
1260
  const w = xEnd - xStart;
1050
1261
  if (w > 0) {
1051
1262
  ctx.fillRect(xStart, y, w, h);
@@ -1061,8 +1272,8 @@ function renderCursor(ctx, lines, pos, options = {}) {
1061
1272
  if (options.height !== undefined) {
1062
1273
  cursorHeight = options.height;
1063
1274
  } else {
1064
- const line = lines[pos.lineIndex];
1065
- cursorHeight = line.ascent + line.descent;
1275
+ const line2 = lines[pos.lineIndex];
1276
+ cursorHeight = line2.ascent + line2.descent;
1066
1277
  }
1067
1278
  const topY = pos.y - (options.height !== undefined ? options.height : lines[pos.lineIndex].ascent);
1068
1279
  ctx.strokeStyle = color;
@@ -1087,13 +1298,13 @@ function getCharAdvances(span) {
1087
1298
  function buildCharSegments(lines) {
1088
1299
  const segments = [];
1089
1300
  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];
1301
+ const line2 = lines[li];
1302
+ const baselineY = line2.y + line2.baseline;
1303
+ for (let si = 0;si < line2.spans.length; si++) {
1304
+ const span = line2.spans[si];
1094
1305
  const advances = getCharAdvances(span);
1095
1306
  const text = span.text;
1096
- let charX = line.x + span.x;
1307
+ let charX = line2.x + span.x;
1097
1308
  for (let ci = 0;ci < text.length; ci++) {
1098
1309
  const advance = ci < advances.length ? advances[ci] : 0;
1099
1310
  segments.push({
@@ -1105,7 +1316,7 @@ function buildCharSegments(lines) {
1105
1316
  y: baselineY,
1106
1317
  width: advance,
1107
1318
  char: text[ci],
1108
- line,
1319
+ line: line2,
1109
1320
  span
1110
1321
  });
1111
1322
  charX += advance;
@@ -1120,29 +1331,29 @@ function charAtPoint(lines, px, py) {
1120
1331
  let nearestLineIdx = 0;
1121
1332
  let minYDist = Infinity;
1122
1333
  for (let li = 0;li < lines.length; li++) {
1123
- const line2 = lines[li];
1124
- const baselineY2 = line2.y + line2.baseline;
1334
+ const line3 = lines[li];
1335
+ const baselineY2 = line3.y + line3.baseline;
1125
1336
  const yDist = Math.abs(py - baselineY2);
1126
- const inLineBox = py >= line2.y && py <= line2.y + line2.height;
1337
+ const inLineBox = py >= line3.y && py <= line3.y + line3.height;
1127
1338
  const distance = inLineBox ? 0 : yDist;
1128
1339
  if (distance < minYDist) {
1129
1340
  minYDist = distance;
1130
1341
  nearestLineIdx = li;
1131
1342
  }
1132
1343
  }
1133
- const line = lines[nearestLineIdx];
1134
- const baselineY = line.y + line.baseline;
1344
+ const line2 = lines[nearestLineIdx];
1345
+ const baselineY = line2.y + line2.baseline;
1135
1346
  let nearestSpanIdx = 0;
1136
1347
  let nearestCharIdx = 0;
1137
- let nearestX = line.x;
1348
+ let nearestX = line2.x;
1138
1349
  let nearestWidth = 0;
1139
- let nearestSpan = line.spans[0];
1350
+ let nearestSpan = line2.spans[0];
1140
1351
  let minXDist = Infinity;
1141
- for (let si = 0;si < line.spans.length; si++) {
1142
- const span = line.spans[si];
1352
+ for (let si = 0;si < line2.spans.length; si++) {
1353
+ const span = line2.spans[si];
1143
1354
  const advances = getCharAdvances(span);
1144
1355
  const text = span.text;
1145
- let charX = line.x + span.x;
1356
+ let charX = line2.x + span.x;
1146
1357
  for (let ci = 0;ci < text.length; ci++) {
1147
1358
  const advance = ci < advances.length ? advances[ci] : 0;
1148
1359
  const charCenter = charX + advance / 2;
@@ -1166,7 +1377,7 @@ function charAtPoint(lines, px, py) {
1166
1377
  x: nearestX,
1167
1378
  y: baselineY,
1168
1379
  width: nearestWidth,
1169
- line,
1380
+ line: line2,
1170
1381
  span: nearestSpan
1171
1382
  };
1172
1383
  }
@@ -1208,26 +1419,62 @@ function charIndexToPos(lines, charIndex) {
1208
1419
  function posToCharIndex(lines, pos) {
1209
1420
  let index = 0;
1210
1421
  for (let li = 0;li < pos.lineIndex; li++) {
1211
- const line2 = lines[li];
1212
- for (const span of line2.spans) {
1422
+ const line3 = lines[li];
1423
+ for (const span of line3.spans) {
1213
1424
  index += span.text.length;
1214
1425
  }
1215
1426
  }
1216
- const line = lines[pos.lineIndex];
1427
+ const line2 = lines[pos.lineIndex];
1217
1428
  for (let si = 0;si < pos.spanIndex; si++) {
1218
- index += line.spans[si].text.length;
1429
+ index += line2.spans[si].text.length;
1219
1430
  }
1220
1431
  index += pos.charIndex;
1221
1432
  return index;
1222
1433
  }
1434
+ // src/register-font.ts
1435
+ import { fontMetricsProvider } from "@vyaz/core";
1436
+ var CSS_GENERIC = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-serif|ui-sans-serif|ui-monospace|ui-rounded|math|emoji|fangsong)$/i;
1437
+ function normWeight(w) {
1438
+ if (w == null)
1439
+ return "normal";
1440
+ return typeof w === "number" ? String(w) : w;
1441
+ }
1442
+ async function registerFont(family, source, opts = {}) {
1443
+ const weight = normWeight(opts.weight);
1444
+ const style = opts.style ?? "normal";
1445
+ if (CSS_GENERIC.test(family)) {
1446
+ const proc = typeof globalThis !== "undefined" ? globalThis.process : undefined;
1447
+ if (proc?.env?.NODE_ENV !== "production") {
1448
+ 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.");
1449
+ }
1450
+ }
1451
+ const bytes = typeof source === "string" ? new Uint8Array(await (await fetch(source)).arrayBuffer()) : source instanceof Uint8Array ? source : new Uint8Array(source);
1452
+ await fontMetricsProvider.registerFont(family, { weight, style, ...opts.variation ? { variation: opts.variation } : {} }, bytes);
1453
+ let browser = false;
1454
+ if (!opts.engineOnly && typeof document !== "undefined" && document.fonts && typeof FontFace !== "undefined") {
1455
+ try {
1456
+ const face = new FontFace(family, bytes, {
1457
+ weight: opts.variation ? "1 1000" : weight === "normal" ? "400" : weight,
1458
+ style,
1459
+ display: "swap"
1460
+ });
1461
+ await face.load();
1462
+ document.fonts.add(face);
1463
+ browser = true;
1464
+ } catch {}
1465
+ }
1466
+ return { family, engine: true, browser };
1467
+ }
1223
1468
  export {
1224
1469
  renderToSVG,
1225
1470
  renderToCanvas,
1471
+ renderTableToSVG,
1226
1472
  renderSelection,
1227
1473
  renderResultToSVG,
1228
1474
  renderParagraphToSVG,
1229
1475
  renderDebugToCanvas,
1230
1476
  renderCursor,
1477
+ registerFont,
1231
1478
  posToCharIndex,
1232
1479
  charIndexToPos,
1233
1480
  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"}