@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.
- 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 +311 -64
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +141803 -64
- 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";
|
|
@@ -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(
|
|
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((
|
|
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,
|
|
812
|
-
const baselineY =
|
|
813
|
-
const 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,
|
|
823
|
-
const x =
|
|
824
|
-
drawSpanBackground(ctx,
|
|
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
|
|
885
|
-
const drawableSpans =
|
|
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(
|
|
1099
|
+
const groups = groupSpansByBaseline(line2, drawableSpans);
|
|
889
1100
|
for (const group of groups) {
|
|
890
1101
|
for (const span of group.spans) {
|
|
891
|
-
renderSpan(ctx,
|
|
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
|
|
954
|
-
const bx =
|
|
955
|
-
const by =
|
|
956
|
-
const bw =
|
|
957
|
-
const bh =
|
|
958
|
-
const baselineY =
|
|
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 -
|
|
978
|
-
const descentY = baselineY +
|
|
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
|
|
1212
|
+
for (const span of line2.spans) {
|
|
1002
1213
|
if (span.width <= 0)
|
|
1003
1214
|
continue;
|
|
1004
|
-
const rx =
|
|
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
|
|
1023
|
-
if (!
|
|
1233
|
+
const line2 = lines[li];
|
|
1234
|
+
if (!line2)
|
|
1024
1235
|
continue;
|
|
1025
|
-
const baselineY =
|
|
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 =
|
|
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 =
|
|
1038
|
-
xEnd =
|
|
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 =
|
|
1251
|
+
xStart = line2.x;
|
|
1041
1252
|
xEnd = end.x;
|
|
1042
1253
|
} else {
|
|
1043
|
-
xStart =
|
|
1044
|
-
const lastSpan =
|
|
1045
|
-
xEnd =
|
|
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 -
|
|
1048
|
-
const h =
|
|
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
|
|
1065
|
-
cursorHeight =
|
|
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
|
|
1091
|
-
const baselineY =
|
|
1092
|
-
for (let si = 0;si <
|
|
1093
|
-
const span =
|
|
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 =
|
|
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
|
|
1124
|
-
const baselineY2 =
|
|
1334
|
+
const line3 = lines[li];
|
|
1335
|
+
const baselineY2 = line3.y + line3.baseline;
|
|
1125
1336
|
const yDist = Math.abs(py - baselineY2);
|
|
1126
|
-
const inLineBox = py >=
|
|
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
|
|
1134
|
-
const baselineY =
|
|
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 =
|
|
1348
|
+
let nearestX = line2.x;
|
|
1138
1349
|
let nearestWidth = 0;
|
|
1139
|
-
let nearestSpan =
|
|
1350
|
+
let nearestSpan = line2.spans[0];
|
|
1140
1351
|
let minXDist = Infinity;
|
|
1141
|
-
for (let si = 0;si <
|
|
1142
|
-
const span =
|
|
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 =
|
|
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
|
|
1212
|
-
for (const span of
|
|
1422
|
+
const line3 = lines[li];
|
|
1423
|
+
for (const span of line3.spans) {
|
|
1213
1424
|
index += span.text.length;
|
|
1214
1425
|
}
|
|
1215
1426
|
}
|
|
1216
|
-
const
|
|
1427
|
+
const line2 = lines[pos.lineIndex];
|
|
1217
1428
|
for (let si = 0;si < pos.spanIndex; si++) {
|
|
1218
|
-
index +=
|
|
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
|
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"}
|