@vyaz/renderer 0.0.11 → 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/CanvasRenderer.d.ts +106 -0
- package/dist/CanvasRenderer.d.ts.map +1 -0
- package/dist/SVGRenderer.d.ts +110 -0
- package/dist/SVGRenderer.d.ts.map +1 -0
- package/dist/index.browser.d.ts +17 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +95 -12
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +99 -16
- package/dist/interactive.d.ts +64 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/src/CanvasRenderer.d.ts +1 -0
- package/dist/src/CanvasRenderer.d.ts.map +1 -0
- package/dist/src/SVGRenderer.d.ts +1 -0
- package/dist/src/SVGRenderer.d.ts.map +1 -0
- package/dist/src/index.browser.d.ts +1 -0
- package/dist/src/index.browser.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/interactive.d.ts +1 -0
- package/dist/src/interactive.d.ts.map +1 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +22 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +8 -5
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasRenderer.ts — Layered Canvas rendering for Line[].
|
|
3
|
+
*
|
|
4
|
+
* Takes ready Line[] with absolute coordinates and provides
|
|
5
|
+
* rendering functions for each visual layer:
|
|
6
|
+
*
|
|
7
|
+
* 1. Background layer — clearRect / fillRect
|
|
8
|
+
* 2. Text layer — text spans (dumb drawer)
|
|
9
|
+
* 3. Selection layer — highlighted selection range
|
|
10
|
+
* 4. Cursor layer — blinking caret
|
|
11
|
+
* 5. Debug overlay layer — bounding boxes, baselines, labels
|
|
12
|
+
*
|
|
13
|
+
* Each function is standalone so consumers (e.g. fabric.js adapter)
|
|
14
|
+
* can compose layers in any order or skip layers as needed.
|
|
15
|
+
*
|
|
16
|
+
* Does NOT compute anything — only draws (dumb drawer principle).
|
|
17
|
+
*
|
|
18
|
+
* @see SVGRenderer for reference SVG implementation.
|
|
19
|
+
*/
|
|
20
|
+
import type { Line } from '@vyaz/core';
|
|
21
|
+
import type { DebugFlags } from './types.js';
|
|
22
|
+
import type { CharPos } from './interactive.js';
|
|
23
|
+
export interface CanvasRenderOptions {
|
|
24
|
+
/**
|
|
25
|
+
* How the canvas size is determined:
|
|
26
|
+
* 'frame' — use current ctx.canvas.width/height (default)
|
|
27
|
+
* 'content' — compute bounding box from lines, resize canvas to fit
|
|
28
|
+
*/
|
|
29
|
+
sizing?: 'frame' | 'content';
|
|
30
|
+
/**
|
|
31
|
+
* When true: render space spans with a space character.
|
|
32
|
+
* When false (default): skip space spans (position is already accounted for in x).
|
|
33
|
+
*/
|
|
34
|
+
preserveSpaces?: boolean;
|
|
35
|
+
/** Background color for clearing. If omitted, canvas is cleared transparent. */
|
|
36
|
+
backgroundColor?: string;
|
|
37
|
+
/** Debug overlay flags. */
|
|
38
|
+
debug?: DebugFlags;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Options for cursor rendering.
|
|
42
|
+
*/
|
|
43
|
+
export interface CursorOptions {
|
|
44
|
+
/** Cursor color. Default: '#000'. */
|
|
45
|
+
color?: string;
|
|
46
|
+
/** Cursor width in px. Default: 1. */
|
|
47
|
+
width?: number;
|
|
48
|
+
/** Cursor height relative to baseline. If undefined, uses the line's ascent + descent. */
|
|
49
|
+
height?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Render Line[] array to Canvas.
|
|
53
|
+
*
|
|
54
|
+
* @param ctx — Canvas 2D rendering context
|
|
55
|
+
* @param lines — ready Line[] with absolute coordinates
|
|
56
|
+
* @param options — rendering options
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderToCanvas(ctx: CanvasRenderingContext2D | any, lines: Line[], options?: CanvasRenderOptions): void;
|
|
59
|
+
/**
|
|
60
|
+
* Draw debug overlays on Canvas.
|
|
61
|
+
*
|
|
62
|
+
* Mirrors SVGRenderer's renderDebugToSVG (lines 658-814) but draws
|
|
63
|
+
* directly on Canvas 2D instead of generating SVG markup.
|
|
64
|
+
*
|
|
65
|
+
* Supported flags:
|
|
66
|
+
* frameBox / frame — frame container bounding box (blue dashed)
|
|
67
|
+
* contentBox — content bounding box (pink dotted)
|
|
68
|
+
* paragraphBox — per-paragraph colored boxes (requires groupLinesByParagraph)
|
|
69
|
+
* columnBox — column separators for multi-column layout
|
|
70
|
+
* box — line box outlines (red)
|
|
71
|
+
* baseline — baseline line (blue)
|
|
72
|
+
* ascentDescent — ascent/descent lines (green dashed)
|
|
73
|
+
* lineGap — line height fill (blue transparent)
|
|
74
|
+
* labels — coordinate labels
|
|
75
|
+
* runs — span bounding boxes (purple)
|
|
76
|
+
*/
|
|
77
|
+
export declare function renderDebugToCanvas(ctx: CanvasRenderingContext2D, lines: Line[], _width: number, _height: number, flags: DebugFlags): void;
|
|
78
|
+
/**
|
|
79
|
+
* Render a text selection highlight overlay.
|
|
80
|
+
*
|
|
81
|
+
* Draws a semi-transparent blue rectangle for each character
|
|
82
|
+
* in the range [start, end). Supports cross-line selections.
|
|
83
|
+
*
|
|
84
|
+
* If `start` and `end` are in different lines, the full width
|
|
85
|
+
* of intermediate lines is highlighted.
|
|
86
|
+
*
|
|
87
|
+
* @param ctx — Canvas 2D rendering context
|
|
88
|
+
* @param lines — layout lines
|
|
89
|
+
* @param start — selection start position (inclusive)
|
|
90
|
+
* @param end — selection end position (exclusive)
|
|
91
|
+
* @param color — highlight color. Default: 'rgba(100, 150, 255, 0.3)'
|
|
92
|
+
*/
|
|
93
|
+
export declare function renderSelection(ctx: CanvasRenderingContext2D, lines: Line[], start: CharPos, end: CharPos, color?: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Render a text cursor (caret) at the given character position.
|
|
96
|
+
*
|
|
97
|
+
* Draws a vertical line at the character's left edge.
|
|
98
|
+
* The caller is responsible for cursor blink timing.
|
|
99
|
+
*
|
|
100
|
+
* @param ctx — Canvas 2D rendering context
|
|
101
|
+
* @param lines — layout lines
|
|
102
|
+
* @param pos — cursor position (character left edge)
|
|
103
|
+
* @param options — cursor visual options
|
|
104
|
+
*/
|
|
105
|
+
export declare function renderCursor(ctx: CanvasRenderingContext2D, lines: Line[], pos: CharPos, options?: CursorOptions): void;
|
|
106
|
+
//# sourceMappingURL=CanvasRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CanvasRenderer.d.ts","sourceRoot":"","sources":["../src/CanvasRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAQ,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGhD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuJD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,wBAAwB,GAAG,GAAG,EACnC,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,GAAE,mBAAwB,GAChC,IAAI,CAmDN;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,wBAAwB,EAC7B,KAAK,EAAE,IAAI,EAAE,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,UAAU,GAChB,IAAI,CA0IN;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,wBAAwB,EAC7B,KAAK,EAAE,IAAI,EAAE,EACb,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,KAAK,GAAE,MAAmC,GACzC,IAAI,CAuDN;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,wBAAwB,EAC7B,KAAK,EAAE,IAAI,EAAE,EACb,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,aAAkB,GAC1B,IAAI,CA0BN"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVGRenderer.ts — SVG text builder.
|
|
3
|
+
*
|
|
4
|
+
* Converts Line[] into SVG markup using an AST-first approach.
|
|
5
|
+
* Builds a tree of SvgNode, then serializes to string in one pass.
|
|
6
|
+
*
|
|
7
|
+
* Four presets:
|
|
8
|
+
* flat — all text in one <text> element, xml:space="preserve", no <tspan>
|
|
9
|
+
* browser — expanded <tspan> per run, xml:space="preserve", diff attributes, no textLength
|
|
10
|
+
* preserve — expanded <tspan> per run, xml:space="preserve", diff attributes, textLength
|
|
11
|
+
* glyph — <tspan> per glyph with per-character x positions, xml:space="preserve"
|
|
12
|
+
*
|
|
13
|
+
* All presets preserve whitespace via xml:space="preserve". Space spans (type: 'space')
|
|
14
|
+
* are rendered as separate <tspan> elements with explicit x coordinates.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const svg = renderToSVG(lines, { preset: 'browser' })
|
|
18
|
+
* const svg = renderToSVG(lines, { preset: 'preserve', style: 'css', fit: 'frag' })
|
|
19
|
+
*/
|
|
20
|
+
import type { Line, ParagraphLayoutResult, TextFrameLayoutResult, MultiColumnConfig } from '@vyaz/core';
|
|
21
|
+
import type { DebugFlags } from './types.js';
|
|
22
|
+
export type SvgPreset = 'flat' | 'browser' | 'preserve' | 'glyph';
|
|
23
|
+
export type SvgStyle = 'css' | 'xml';
|
|
24
|
+
export type SvgFit = 'none' | 'text' | 'frag';
|
|
25
|
+
export type SvgSizing = 'frame' | 'content';
|
|
26
|
+
export type PerAxisSizing = {
|
|
27
|
+
horizontal: SvgSizing;
|
|
28
|
+
vertical: SvgSizing;
|
|
29
|
+
};
|
|
30
|
+
export interface SVGRenderOptions {
|
|
31
|
+
/** Shorthand that sets structure + spacing at once. */
|
|
32
|
+
preset?: SvgPreset;
|
|
33
|
+
/** How style properties are expressed: as CSS `style` attribute or as XML presentation attributes. */
|
|
34
|
+
style?: SvgStyle;
|
|
35
|
+
/** How `textLength` is applied. */
|
|
36
|
+
fit?: SvgFit;
|
|
37
|
+
/**
|
|
38
|
+
* How SVG determines its canvas size.
|
|
39
|
+
* Single string: applies to both axes. Object: per-axis control.
|
|
40
|
+
* 'frame' — use explicit width/height from options.
|
|
41
|
+
* 'content' — compute from lines bounding box.
|
|
42
|
+
*/
|
|
43
|
+
sizing?: SvgSizing | PerAxisSizing;
|
|
44
|
+
/** SVG canvas width (px). Used when horizontal sizing='frame' or as fallback. */
|
|
45
|
+
width?: number;
|
|
46
|
+
/** SVG canvas height (px). Used when vertical sizing='frame' or as fallback. */
|
|
47
|
+
height?: number;
|
|
48
|
+
/** CSS class for `<svg>`. */
|
|
49
|
+
className?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Extra padding added around the SVG canvas. Content coordinates stay unchanged;
|
|
52
|
+
* the SVG viewBox is shifted and canvas is enlarged so debug overlays
|
|
53
|
+
* (frameBox / contentBox) are visible with a gap from the edge.
|
|
54
|
+
* Useful for snapshot tests to clearly show frame vs content boundaries.
|
|
55
|
+
*/
|
|
56
|
+
contentPadding?: number;
|
|
57
|
+
/** Debug overlays. */
|
|
58
|
+
debug?: DebugFlags;
|
|
59
|
+
/**
|
|
60
|
+
* Multi-column layout configuration for debug overlays.
|
|
61
|
+
* When set, paragraph and column boxes are rendered per-column.
|
|
62
|
+
*/
|
|
63
|
+
columns?: MultiColumnConfig;
|
|
64
|
+
/** Left padding from frame (needed for column debug rendering). */
|
|
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;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
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.
|
|
81
|
+
*
|
|
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
|
|
85
|
+
* @returns SVG string
|
|
86
|
+
*/
|
|
87
|
+
export declare function renderToSVG(input: Line[] | TextFrameLayoutResult, options?: SVGRenderOptions): string;
|
|
88
|
+
/**
|
|
89
|
+
* Render one ParagraphLayoutResult to SVG (convenience wrapper).
|
|
90
|
+
*/
|
|
91
|
+
export declare function renderParagraphToSVG(lines: Line[], paragraphWidth: number, paragraphHeight: number, options?: SVGRenderOptions): string;
|
|
92
|
+
/**
|
|
93
|
+
* Render a ParagraphLayoutResult to SVG, auto-passing dimensions.
|
|
94
|
+
*
|
|
95
|
+
* Uses `result.width` and `result.height` as the SVG canvas size.
|
|
96
|
+
* This is the recommended way to render when you have a layout result
|
|
97
|
+
* and want `sizing: 'frame'` with correct dimensions.
|
|
98
|
+
*
|
|
99
|
+
* @param result — layout result from ParagraphLayoutEngine.layout()
|
|
100
|
+
* @param options — rendering options (preset, style, fit, etc.)
|
|
101
|
+
* @returns SVG string
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const result = paragraphLayoutEngine.layout(paragraph, 300);
|
|
106
|
+
* const svg = renderResultToSVG(result, { preset: 'preserve' });
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function renderResultToSVG(result: ParagraphLayoutResult, options?: SVGRenderOptions): string;
|
|
110
|
+
//# sourceMappingURL=SVGRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vyaz/renderer — Browser entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports everything from the main index.
|
|
5
|
+
* In browser environments @vyaz/core resolves to its browser entry
|
|
6
|
+
* (dist/index.browser.js) via package.json exports.
|
|
7
|
+
*
|
|
8
|
+
* ✅ Safe for Vite / webpack / Rollup / esbuild browser builds.
|
|
9
|
+
*/
|
|
10
|
+
export { renderToSVG, renderParagraphToSVG, renderResultToSVG } from './SVGRenderer.js';
|
|
11
|
+
export type { SVGRenderOptions, SvgPreset, SvgStyle, SvgFit, SvgSizing } from './SVGRenderer.js';
|
|
12
|
+
export { renderToCanvas, renderDebugToCanvas, renderSelection, renderCursor } from './CanvasRenderer.js';
|
|
13
|
+
export type { CanvasRenderOptions, CursorOptions } from './CanvasRenderer.js';
|
|
14
|
+
export { charAtPoint, charIndexToPos, posToCharIndex } from './interactive.js';
|
|
15
|
+
export type { CharPos } from './interactive.js';
|
|
16
|
+
export type { DebugFlags } from './types.js';
|
|
17
|
+
//# sourceMappingURL=index.browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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"}
|
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.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vyaz/renderer — SVG and Canvas renderers.
|
|
3
|
+
*
|
|
4
|
+
* Converts Line[] (from @vyaz/core) into SVG strings or Canvas drawings.
|
|
5
|
+
*/
|
|
6
|
+
export { renderToSVG, renderParagraphToSVG, renderResultToSVG } from './SVGRenderer.js';
|
|
7
|
+
export type { SVGRenderOptions, SvgPreset, SvgStyle, SvgFit, SvgSizing } from './SVGRenderer.js';
|
|
8
|
+
export { renderToCanvas, renderDebugToCanvas, renderSelection, renderCursor } from './CanvasRenderer.js';
|
|
9
|
+
export type { CanvasRenderOptions, CursorOptions } from './CanvasRenderer.js';
|
|
10
|
+
export { charAtPoint, charIndexToPos, posToCharIndex } from './interactive.js';
|
|
11
|
+
export type { CharPos } from './interactive.js';
|
|
12
|
+
export type { DebugFlags } from './types.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
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);
|