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