@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.
- package/README.md +121 -0
- package/dist/SVGRenderer.d.ts +20 -1
- package/dist/SVGRenderer.d.ts.map +1 -1
- package/dist/TableRenderer.d.ts +59 -0
- package/dist/TableRenderer.d.ts.map +1 -0
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +296 -63
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +141788 -63
- package/dist/register-font.d.ts +28 -0
- package/dist/register-font.d.ts.map +1 -0
- package/package.json +11 -2
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/index.browser.js
CHANGED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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(
|
|
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((
|
|
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,
|
|
812
|
-
const baselineY =
|
|
813
|
-
const 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,
|
|
823
|
-
const x =
|
|
824
|
-
drawSpanBackground(ctx,
|
|
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
|
|
885
|
-
const drawableSpans =
|
|
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(
|
|
1085
|
+
const groups = groupSpansByBaseline(line2, drawableSpans);
|
|
889
1086
|
for (const group of groups) {
|
|
890
1087
|
for (const span of group.spans) {
|
|
891
|
-
renderSpan(ctx,
|
|
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
|
|
954
|
-
const bx =
|
|
955
|
-
const by =
|
|
956
|
-
const bw =
|
|
957
|
-
const bh =
|
|
958
|
-
const baselineY =
|
|
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 -
|
|
978
|
-
const descentY = baselineY +
|
|
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
|
|
1198
|
+
for (const span of line2.spans) {
|
|
1002
1199
|
if (span.width <= 0)
|
|
1003
1200
|
continue;
|
|
1004
|
-
const rx =
|
|
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
|
|
1023
|
-
if (!
|
|
1219
|
+
const line2 = lines[li];
|
|
1220
|
+
if (!line2)
|
|
1024
1221
|
continue;
|
|
1025
|
-
const baselineY =
|
|
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 =
|
|
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 =
|
|
1038
|
-
xEnd =
|
|
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 =
|
|
1237
|
+
xStart = line2.x;
|
|
1041
1238
|
xEnd = end.x;
|
|
1042
1239
|
} else {
|
|
1043
|
-
xStart =
|
|
1044
|
-
const lastSpan =
|
|
1045
|
-
xEnd =
|
|
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 -
|
|
1048
|
-
const h =
|
|
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
|
|
1065
|
-
cursorHeight =
|
|
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
|
|
1091
|
-
const baselineY =
|
|
1092
|
-
for (let si = 0;si <
|
|
1093
|
-
const span =
|
|
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 =
|
|
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
|
|
1124
|
-
const baselineY2 =
|
|
1320
|
+
const line3 = lines[li];
|
|
1321
|
+
const baselineY2 = line3.y + line3.baseline;
|
|
1125
1322
|
const yDist = Math.abs(py - baselineY2);
|
|
1126
|
-
const inLineBox = py >=
|
|
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
|
|
1134
|
-
const baselineY =
|
|
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 =
|
|
1334
|
+
let nearestX = line2.x;
|
|
1138
1335
|
let nearestWidth = 0;
|
|
1139
|
-
let nearestSpan =
|
|
1336
|
+
let nearestSpan = line2.spans[0];
|
|
1140
1337
|
let minXDist = Infinity;
|
|
1141
|
-
for (let si = 0;si <
|
|
1142
|
-
const span =
|
|
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 =
|
|
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
|
|
1212
|
-
for (const span of
|
|
1408
|
+
const line3 = lines[li];
|
|
1409
|
+
for (const span of line3.spans) {
|
|
1213
1410
|
index += span.text.length;
|
|
1214
1411
|
}
|
|
1215
1412
|
}
|
|
1216
|
-
const
|
|
1413
|
+
const line2 = lines[pos.lineIndex];
|
|
1217
1414
|
for (let si = 0;si < pos.spanIndex; si++) {
|
|
1218
|
-
index +=
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|