@vyaz/renderer 0.0.14 → 0.1.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/dist/SVGRenderer.d.ts +18 -5
- package/dist/SVGRenderer.d.ts.map +1 -1
- package/dist/index.browser.js +95 -12
- package/dist/index.js +99 -16
- package/package.json +2 -2
package/dist/SVGRenderer.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* const svg = renderToSVG(lines, { preset: 'browser' })
|
|
18
18
|
* const svg = renderToSVG(lines, { preset: 'preserve', style: 'css', fit: 'frag' })
|
|
19
19
|
*/
|
|
20
|
-
import type { Line, ParagraphLayoutResult, MultiColumnConfig } from '@vyaz/core';
|
|
20
|
+
import type { Line, ParagraphLayoutResult, TextFrameLayoutResult, MultiColumnConfig } from '@vyaz/core';
|
|
21
21
|
import type { DebugFlags } from './types.js';
|
|
22
22
|
export type SvgPreset = 'flat' | 'browser' | 'preserve' | 'glyph';
|
|
23
23
|
export type SvgStyle = 'css' | 'xml';
|
|
@@ -63,15 +63,28 @@ export interface SVGRenderOptions {
|
|
|
63
63
|
columns?: MultiColumnConfig;
|
|
64
64
|
/** Left padding from frame (needed for column debug rendering). */
|
|
65
65
|
paddingLeft?: number;
|
|
66
|
+
/**
|
|
67
|
+
* glyph preset only: draw `underline` / `strikethrough` as explicit `<line>`
|
|
68
|
+
* geometry. The glyph path positions each character with its own `x`, so it
|
|
69
|
+
* cannot rely on SVG `text-decoration` (which the flat/expanded paths use).
|
|
70
|
+
* Ignored by every other preset. Default `true`.
|
|
71
|
+
*/
|
|
72
|
+
glyphDecorations?: boolean;
|
|
66
73
|
}
|
|
67
74
|
/**
|
|
68
|
-
* Render
|
|
75
|
+
* Render a layout to an SVG string.
|
|
76
|
+
*
|
|
77
|
+
* Pass a full {@link TextFrameLayoutResult} (recommended) and the canvas size /
|
|
78
|
+
* `sizing` are derived from its `frame*` / `content*` / `fit*` fields — you only
|
|
79
|
+
* add render options (preset, style, debug). Passing a bare `Line[]` is the
|
|
80
|
+
* low-level form: you supply `width` / `height` / `sizing` yourself.
|
|
69
81
|
*
|
|
70
|
-
* @param
|
|
71
|
-
* @param options — rendering options (preset + style/fit/sizing modifiers)
|
|
82
|
+
* @param input — a layout result, or bare layout lines
|
|
83
|
+
* @param options — rendering options (preset + style/fit/sizing modifiers); any
|
|
84
|
+
* field here overrides the value derived from a result
|
|
72
85
|
* @returns SVG string
|
|
73
86
|
*/
|
|
74
|
-
export declare function renderToSVG(
|
|
87
|
+
export declare function renderToSVG(input: Line[] | TextFrameLayoutResult, options?: SVGRenderOptions): string;
|
|
75
88
|
/**
|
|
76
89
|
* Render one ParagraphLayoutResult to SVG (convenience wrapper).
|
|
77
90
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SVGRenderer.d.ts","sourceRoot":"","sources":["../src/SVGRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAQ,qBAAqB,EAAkB,iBAAiB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"SVGRenderer.d.ts","sourceRoot":"","sources":["../src/SVGRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAQ,qBAAqB,EAAE,qBAAqB,EAAkB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE9H,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,YAAY,CAAC;AAKlE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;AAElE,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC;AAErC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5C,MAAM,MAAM,aAAa,GAAG;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC;AAE3E,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,sGAAsG;IACtG,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC;IACnC,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB;IACtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA8zBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,IAAI,EAAE,GAAG,qBAAqB,EACrC,OAAO,GAAE,gBAAqB,GAC7B,MAAM,CA6QR;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,IAAI,EAAE,EACb,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CAMR;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,qBAAqB,EAC7B,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CAMR"}
|
package/dist/index.browser.js
CHANGED
|
@@ -93,7 +93,7 @@ function resolveOptions(opts) {
|
|
|
93
93
|
console.warn(`SVGRenderer: fit="frag" downgraded to "text" when structure="flat"`);
|
|
94
94
|
fit = "text";
|
|
95
95
|
}
|
|
96
|
-
return { structure, spacing, style, fit, sizingHorizontal, sizingVertical, width: opts.width, height: opts.height, className: opts.className, contentPadding: opts.contentPadding ?? 0, debug: opts.debug };
|
|
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 };
|
|
97
97
|
}
|
|
98
98
|
function resolveSize(lines, opts) {
|
|
99
99
|
const needsBBox = opts.sizingHorizontal === "content" || opts.sizingVertical === "content";
|
|
@@ -223,13 +223,12 @@ function buildTspanAttrs(span, x, currentStyle) {
|
|
|
223
223
|
}
|
|
224
224
|
return { attrs, newStyle: s };
|
|
225
225
|
}
|
|
226
|
-
function buildGlyphPositions(span,
|
|
226
|
+
function buildGlyphPositions(span, spanDX) {
|
|
227
227
|
if (!span.glyphAdvances || span.glyphAdvances.length === 0) {
|
|
228
|
-
return "";
|
|
228
|
+
return (span.type === "marker" || span.type === "space") && span.text ? fmt(span.x + spanDX, 1) : "";
|
|
229
229
|
}
|
|
230
230
|
const ls = span.style.letterSpacing || 0;
|
|
231
|
-
|
|
232
|
-
let xPos = spanX;
|
|
231
|
+
let xPos = span.x + spanDX;
|
|
233
232
|
const positions = [fmt(xPos, 1)];
|
|
234
233
|
for (let i = 0;i < span.glyphAdvances.length - 1; i++) {
|
|
235
234
|
xPos += span.glyphAdvances[i] + ls;
|
|
@@ -324,6 +323,8 @@ class SvgAstBuilder {
|
|
|
324
323
|
x: fmt(x),
|
|
325
324
|
y: fmt(yOverride)
|
|
326
325
|
};
|
|
326
|
+
if (runId)
|
|
327
|
+
attrs.id = runId;
|
|
327
328
|
if (this.opts.style === "css") {
|
|
328
329
|
let css = `font-family: '${s.fontFamily}', sans-serif; font-size: ${fmt(fontSizeOverride)}px; fill: ${colorToRGB(s.color)}; font-weight: ${s.fontWeight}`;
|
|
329
330
|
if (s.fontStyle === "italic")
|
|
@@ -399,6 +400,17 @@ ${debugMarkup}
|
|
|
399
400
|
});
|
|
400
401
|
this.root.children.push(rect);
|
|
401
402
|
}
|
|
403
|
+
addDecorationLine(x, width, y, color, thickness) {
|
|
404
|
+
this.closeText();
|
|
405
|
+
this.root.children.push(el("line", {
|
|
406
|
+
x1: fmt(x),
|
|
407
|
+
y1: fmt(y),
|
|
408
|
+
x2: fmt(x + width),
|
|
409
|
+
y2: fmt(y),
|
|
410
|
+
stroke: color,
|
|
411
|
+
"stroke-width": fmt(thickness)
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
402
414
|
addRawLine(lineStr) {
|
|
403
415
|
this.closeText();
|
|
404
416
|
this.root.children.push(rawNode(lineStr));
|
|
@@ -530,36 +542,105 @@ function getSpanBackgroundAttrs(span, baselineY) {
|
|
|
530
542
|
const h = span.fontMetrics.ascent + span.fontMetrics.descent;
|
|
531
543
|
return { x, y, w, h, fill: span.style.backgroundColor };
|
|
532
544
|
}
|
|
533
|
-
function renderToSVG(
|
|
534
|
-
|
|
545
|
+
function renderToSVG(input, options = {}) {
|
|
546
|
+
let lines;
|
|
547
|
+
let resolvedOptions;
|
|
548
|
+
if (Array.isArray(input)) {
|
|
549
|
+
lines = input;
|
|
550
|
+
resolvedOptions = options;
|
|
551
|
+
} else {
|
|
552
|
+
lines = input.lines;
|
|
553
|
+
const callerSizes = options.sizing !== undefined || options.width !== undefined || options.height !== undefined;
|
|
554
|
+
if (callerSizes) {
|
|
555
|
+
resolvedOptions = options;
|
|
556
|
+
} else {
|
|
557
|
+
const fw = input.fit.horizontal === "frame" && input.frame.width != null;
|
|
558
|
+
const fh = input.fit.vertical === "frame" && input.frame.height != null;
|
|
559
|
+
resolvedOptions = {
|
|
560
|
+
sizing: {
|
|
561
|
+
horizontal: fw ? "frame" : "content",
|
|
562
|
+
vertical: fh ? "frame" : "content"
|
|
563
|
+
},
|
|
564
|
+
width: fw ? input.frame.width : input.content.width,
|
|
565
|
+
height: fh ? input.frame.height : input.content.height,
|
|
566
|
+
...options
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const opts = resolveOptions(resolvedOptions);
|
|
535
571
|
const { width: svgWidth, height: svgHeight, viewBox, frameWidth, frameHeight } = resolveSize(lines, opts);
|
|
536
572
|
const builder = new SvgAstBuilder(svgWidth, svgHeight, opts, viewBox);
|
|
537
573
|
for (const line of lines) {
|
|
538
574
|
const baselineY = line.y + line.baseline;
|
|
575
|
+
const bgFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
539
576
|
for (const span of line.spans) {
|
|
540
577
|
if (!span.text || !span.style.backgroundColor)
|
|
541
578
|
continue;
|
|
542
579
|
const bg = getSpanBackgroundAttrs(span, baselineY);
|
|
543
580
|
if (bg) {
|
|
544
|
-
const rx = line.x + bg.x;
|
|
581
|
+
const rx = line.x + bg.x - bgFirstTextX;
|
|
545
582
|
builder.addBackgroundRect(rx, bg.y, bg.w, bg.h, bg.fill);
|
|
546
583
|
}
|
|
547
584
|
}
|
|
548
585
|
if (opts.structure === "glyph") {
|
|
586
|
+
const glyphFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
587
|
+
const glyphSpanDX = line.x - glyphFirstTextX;
|
|
549
588
|
let currentRunIdx = -1;
|
|
589
|
+
let currentSig = "";
|
|
550
590
|
for (const span of line.spans) {
|
|
551
591
|
if (!span.text)
|
|
552
592
|
continue;
|
|
553
593
|
const runIdx = span.itemIndex;
|
|
554
|
-
|
|
594
|
+
const sig = styleSignature(span);
|
|
595
|
+
if (runIdx !== currentRunIdx || sig !== currentSig) {
|
|
555
596
|
builder.closeText();
|
|
556
597
|
const runId = span.tag ? `${span.tag}-${runIdx}` : undefined;
|
|
557
|
-
|
|
598
|
+
const offset = span.fontMetrics.baselineOffset || 0;
|
|
599
|
+
if (offset !== 0) {
|
|
600
|
+
const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
|
|
601
|
+
builder.openText(line, span, runId, targetY, span.fontMetrics.fontSize);
|
|
602
|
+
} else {
|
|
603
|
+
builder.openText(line, span, runId);
|
|
604
|
+
}
|
|
558
605
|
currentRunIdx = runIdx;
|
|
606
|
+
currentSig = sig;
|
|
559
607
|
}
|
|
560
|
-
builder.addGlyphTspan(span,
|
|
608
|
+
builder.addGlyphTspan(span, glyphSpanDX);
|
|
561
609
|
}
|
|
562
610
|
builder.closeText();
|
|
611
|
+
if (opts.glyphDecorations) {
|
|
612
|
+
let cur = null;
|
|
613
|
+
const flushDeco = () => {
|
|
614
|
+
if (!cur)
|
|
615
|
+
return;
|
|
616
|
+
const w = cur.end - cur.x;
|
|
617
|
+
if (cur.u)
|
|
618
|
+
builder.addDecorationLine(cur.x, w, cur.baseY + 2, cur.color, 1);
|
|
619
|
+
if (cur.s)
|
|
620
|
+
builder.addDecorationLine(cur.x, w, cur.baseY - cur.ascent * 0.4, cur.color, 1);
|
|
621
|
+
cur = null;
|
|
622
|
+
};
|
|
623
|
+
for (const span of line.spans) {
|
|
624
|
+
if (!span.text)
|
|
625
|
+
continue;
|
|
626
|
+
const u = !!span.style.underline;
|
|
627
|
+
const s = !!span.style.strikethrough;
|
|
628
|
+
if (!u && !s) {
|
|
629
|
+
flushDeco();
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const color = span.style.color || "#000000";
|
|
633
|
+
const x = span.x + glyphSpanDX;
|
|
634
|
+
const baseY = line.y + line.baseline;
|
|
635
|
+
if (cur && cur.u === u && cur.s === s && cur.color === color && Math.abs(cur.end - x) < 0.01) {
|
|
636
|
+
cur.end = x + span.width;
|
|
637
|
+
} else {
|
|
638
|
+
flushDeco();
|
|
639
|
+
cur = { x, end: x + span.width, baseY, ascent: span.fontMetrics.ascent, color, u, s };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
flushDeco();
|
|
643
|
+
}
|
|
563
644
|
} else if (opts.structure === "flat") {
|
|
564
645
|
const groups = [];
|
|
565
646
|
for (const span of line.spans) {
|
|
@@ -649,11 +730,13 @@ function renderToSVG(lines, options = {}) {
|
|
|
649
730
|
} else {
|
|
650
731
|
builder.openText(line, baseSpan);
|
|
651
732
|
}
|
|
733
|
+
const lineFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
734
|
+
const spanDX = line.x - lineFirstTextX;
|
|
652
735
|
let currentStyle = null;
|
|
653
736
|
for (const span of group.spans) {
|
|
654
737
|
if (!span.text)
|
|
655
738
|
continue;
|
|
656
|
-
const x = span.x;
|
|
739
|
+
const x = span.x + spanDX;
|
|
657
740
|
const shouldRender = span.type !== "space" || opts.spacing === "preserve";
|
|
658
741
|
if (shouldRender) {
|
|
659
742
|
const newStyle = builder.addTspan(span, x, currentStyle);
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../core/src/types/Document.ts
|
|
3
|
-
var DEFAULT_PARAGRAPH_STYLE = {
|
|
3
|
+
var DEFAULT_PARAGRAPH_STYLE = Object.freeze({
|
|
4
4
|
alignment: "left",
|
|
5
5
|
lineHeight: 1.15,
|
|
6
6
|
spaceBefore: 0,
|
|
7
7
|
spaceAfter: 0,
|
|
8
8
|
whiteSpace: "normal"
|
|
9
|
-
};
|
|
10
|
-
var DEFAULT_TEXT_STYLE = {
|
|
9
|
+
});
|
|
10
|
+
var DEFAULT_TEXT_STYLE = Object.freeze({
|
|
11
11
|
fontFamily: "Arial",
|
|
12
12
|
fontSize: 12,
|
|
13
13
|
fontWeight: "normal",
|
|
14
14
|
fontStyle: "normal",
|
|
15
15
|
color: "#000000"
|
|
16
|
-
};
|
|
16
|
+
});
|
|
17
17
|
|
|
18
18
|
// ../core/src/utils/list.ts
|
|
19
19
|
var BULLET_CHARACTERS = {
|
|
@@ -144,7 +144,7 @@ function resolveOptions(opts) {
|
|
|
144
144
|
console.warn(`SVGRenderer: fit="frag" downgraded to "text" when structure="flat"`);
|
|
145
145
|
fit = "text";
|
|
146
146
|
}
|
|
147
|
-
return { structure, spacing, style, fit, sizingHorizontal, sizingVertical, width: opts.width, height: opts.height, className: opts.className, contentPadding: opts.contentPadding ?? 0, debug: opts.debug };
|
|
147
|
+
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 };
|
|
148
148
|
}
|
|
149
149
|
function resolveSize(lines, opts) {
|
|
150
150
|
const needsBBox = opts.sizingHorizontal === "content" || opts.sizingVertical === "content";
|
|
@@ -274,13 +274,12 @@ function buildTspanAttrs(span, x, currentStyle) {
|
|
|
274
274
|
}
|
|
275
275
|
return { attrs, newStyle: s };
|
|
276
276
|
}
|
|
277
|
-
function buildGlyphPositions(span,
|
|
277
|
+
function buildGlyphPositions(span, spanDX) {
|
|
278
278
|
if (!span.glyphAdvances || span.glyphAdvances.length === 0) {
|
|
279
|
-
return "";
|
|
279
|
+
return (span.type === "marker" || span.type === "space") && span.text ? fmt(span.x + spanDX, 1) : "";
|
|
280
280
|
}
|
|
281
281
|
const ls = span.style.letterSpacing || 0;
|
|
282
|
-
|
|
283
|
-
let xPos = spanX;
|
|
282
|
+
let xPos = span.x + spanDX;
|
|
284
283
|
const positions = [fmt(xPos, 1)];
|
|
285
284
|
for (let i = 0;i < span.glyphAdvances.length - 1; i++) {
|
|
286
285
|
xPos += span.glyphAdvances[i] + ls;
|
|
@@ -375,6 +374,8 @@ class SvgAstBuilder {
|
|
|
375
374
|
x: fmt(x),
|
|
376
375
|
y: fmt(yOverride)
|
|
377
376
|
};
|
|
377
|
+
if (runId)
|
|
378
|
+
attrs.id = runId;
|
|
378
379
|
if (this.opts.style === "css") {
|
|
379
380
|
let css = `font-family: '${s.fontFamily}', sans-serif; font-size: ${fmt(fontSizeOverride)}px; fill: ${colorToRGB(s.color)}; font-weight: ${s.fontWeight}`;
|
|
380
381
|
if (s.fontStyle === "italic")
|
|
@@ -450,6 +451,17 @@ ${debugMarkup}
|
|
|
450
451
|
});
|
|
451
452
|
this.root.children.push(rect);
|
|
452
453
|
}
|
|
454
|
+
addDecorationLine(x, width, y, color, thickness) {
|
|
455
|
+
this.closeText();
|
|
456
|
+
this.root.children.push(el("line", {
|
|
457
|
+
x1: fmt(x),
|
|
458
|
+
y1: fmt(y),
|
|
459
|
+
x2: fmt(x + width),
|
|
460
|
+
y2: fmt(y),
|
|
461
|
+
stroke: color,
|
|
462
|
+
"stroke-width": fmt(thickness)
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
453
465
|
addRawLine(lineStr) {
|
|
454
466
|
this.closeText();
|
|
455
467
|
this.root.children.push(rawNode(lineStr));
|
|
@@ -581,36 +593,105 @@ function getSpanBackgroundAttrs(span, baselineY) {
|
|
|
581
593
|
const h = span.fontMetrics.ascent + span.fontMetrics.descent;
|
|
582
594
|
return { x, y, w, h, fill: span.style.backgroundColor };
|
|
583
595
|
}
|
|
584
|
-
function renderToSVG(
|
|
585
|
-
|
|
596
|
+
function renderToSVG(input, options = {}) {
|
|
597
|
+
let lines;
|
|
598
|
+
let resolvedOptions;
|
|
599
|
+
if (Array.isArray(input)) {
|
|
600
|
+
lines = input;
|
|
601
|
+
resolvedOptions = options;
|
|
602
|
+
} else {
|
|
603
|
+
lines = input.lines;
|
|
604
|
+
const callerSizes = options.sizing !== undefined || options.width !== undefined || options.height !== undefined;
|
|
605
|
+
if (callerSizes) {
|
|
606
|
+
resolvedOptions = options;
|
|
607
|
+
} else {
|
|
608
|
+
const fw = input.fit.horizontal === "frame" && input.frame.width != null;
|
|
609
|
+
const fh = input.fit.vertical === "frame" && input.frame.height != null;
|
|
610
|
+
resolvedOptions = {
|
|
611
|
+
sizing: {
|
|
612
|
+
horizontal: fw ? "frame" : "content",
|
|
613
|
+
vertical: fh ? "frame" : "content"
|
|
614
|
+
},
|
|
615
|
+
width: fw ? input.frame.width : input.content.width,
|
|
616
|
+
height: fh ? input.frame.height : input.content.height,
|
|
617
|
+
...options
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const opts = resolveOptions(resolvedOptions);
|
|
586
622
|
const { width: svgWidth, height: svgHeight, viewBox, frameWidth, frameHeight } = resolveSize(lines, opts);
|
|
587
623
|
const builder = new SvgAstBuilder(svgWidth, svgHeight, opts, viewBox);
|
|
588
624
|
for (const line of lines) {
|
|
589
625
|
const baselineY = line.y + line.baseline;
|
|
626
|
+
const bgFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
590
627
|
for (const span of line.spans) {
|
|
591
628
|
if (!span.text || !span.style.backgroundColor)
|
|
592
629
|
continue;
|
|
593
630
|
const bg = getSpanBackgroundAttrs(span, baselineY);
|
|
594
631
|
if (bg) {
|
|
595
|
-
const rx = line.x + bg.x;
|
|
632
|
+
const rx = line.x + bg.x - bgFirstTextX;
|
|
596
633
|
builder.addBackgroundRect(rx, bg.y, bg.w, bg.h, bg.fill);
|
|
597
634
|
}
|
|
598
635
|
}
|
|
599
636
|
if (opts.structure === "glyph") {
|
|
637
|
+
const glyphFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
638
|
+
const glyphSpanDX = line.x - glyphFirstTextX;
|
|
600
639
|
let currentRunIdx = -1;
|
|
640
|
+
let currentSig = "";
|
|
601
641
|
for (const span of line.spans) {
|
|
602
642
|
if (!span.text)
|
|
603
643
|
continue;
|
|
604
644
|
const runIdx = span.itemIndex;
|
|
605
|
-
|
|
645
|
+
const sig = styleSignature(span);
|
|
646
|
+
if (runIdx !== currentRunIdx || sig !== currentSig) {
|
|
606
647
|
builder.closeText();
|
|
607
648
|
const runId = span.tag ? `${span.tag}-${runIdx}` : undefined;
|
|
608
|
-
|
|
649
|
+
const offset = span.fontMetrics.baselineOffset || 0;
|
|
650
|
+
if (offset !== 0) {
|
|
651
|
+
const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
|
|
652
|
+
builder.openText(line, span, runId, targetY, span.fontMetrics.fontSize);
|
|
653
|
+
} else {
|
|
654
|
+
builder.openText(line, span, runId);
|
|
655
|
+
}
|
|
609
656
|
currentRunIdx = runIdx;
|
|
657
|
+
currentSig = sig;
|
|
610
658
|
}
|
|
611
|
-
builder.addGlyphTspan(span,
|
|
659
|
+
builder.addGlyphTspan(span, glyphSpanDX);
|
|
612
660
|
}
|
|
613
661
|
builder.closeText();
|
|
662
|
+
if (opts.glyphDecorations) {
|
|
663
|
+
let cur = null;
|
|
664
|
+
const flushDeco = () => {
|
|
665
|
+
if (!cur)
|
|
666
|
+
return;
|
|
667
|
+
const w = cur.end - cur.x;
|
|
668
|
+
if (cur.u)
|
|
669
|
+
builder.addDecorationLine(cur.x, w, cur.baseY + 2, cur.color, 1);
|
|
670
|
+
if (cur.s)
|
|
671
|
+
builder.addDecorationLine(cur.x, w, cur.baseY - cur.ascent * 0.4, cur.color, 1);
|
|
672
|
+
cur = null;
|
|
673
|
+
};
|
|
674
|
+
for (const span of line.spans) {
|
|
675
|
+
if (!span.text)
|
|
676
|
+
continue;
|
|
677
|
+
const u = !!span.style.underline;
|
|
678
|
+
const s = !!span.style.strikethrough;
|
|
679
|
+
if (!u && !s) {
|
|
680
|
+
flushDeco();
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const color = span.style.color || "#000000";
|
|
684
|
+
const x = span.x + glyphSpanDX;
|
|
685
|
+
const baseY = line.y + line.baseline;
|
|
686
|
+
if (cur && cur.u === u && cur.s === s && cur.color === color && Math.abs(cur.end - x) < 0.01) {
|
|
687
|
+
cur.end = x + span.width;
|
|
688
|
+
} else {
|
|
689
|
+
flushDeco();
|
|
690
|
+
cur = { x, end: x + span.width, baseY, ascent: span.fontMetrics.ascent, color, u, s };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
flushDeco();
|
|
694
|
+
}
|
|
614
695
|
} else if (opts.structure === "flat") {
|
|
615
696
|
const groups = [];
|
|
616
697
|
for (const span of line.spans) {
|
|
@@ -700,11 +781,13 @@ function renderToSVG(lines, options = {}) {
|
|
|
700
781
|
} else {
|
|
701
782
|
builder.openText(line, baseSpan);
|
|
702
783
|
}
|
|
784
|
+
const lineFirstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
785
|
+
const spanDX = line.x - lineFirstTextX;
|
|
703
786
|
let currentStyle = null;
|
|
704
787
|
for (const span of group.spans) {
|
|
705
788
|
if (!span.text)
|
|
706
789
|
continue;
|
|
707
|
-
const x = span.x;
|
|
790
|
+
const x = span.x + spanDX;
|
|
708
791
|
const shouldRender = span.type !== "space" || opts.spacing === "preserve";
|
|
709
792
|
if (shouldRender) {
|
|
710
793
|
const newStyle = builder.addTspan(span, x, currentStyle);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vyaz/renderer",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"prepublishOnly": "bun run build"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@vyaz/core": "^0.0
|
|
27
|
+
"@vyaz/core": "^0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^26.1.1",
|