@vyaz/renderer 0.0.2 → 0.0.3
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/index.js +789 -220
- package/package.json +1 -1
- package/src/CanvasRenderer.ts +388 -78
- package/src/SVGRenderer.ts +694 -166
- package/src/index.ts +6 -3
- package/src/interactive.ts +284 -0
- package/src/types.ts +59 -4
- package/src/utils.ts +19 -6
package/dist/index.js
CHANGED
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../core/src/utils/groupLinesByParagraph.ts
|
|
3
|
+
function groupLinesByParagraph(lines) {
|
|
4
|
+
const map = new Map;
|
|
5
|
+
for (const line of lines) {
|
|
6
|
+
const idx = line.spans[0]?.pIdx ?? -1;
|
|
7
|
+
let entry = map.get(idx);
|
|
8
|
+
if (!entry) {
|
|
9
|
+
entry = { lines: [] };
|
|
10
|
+
map.set(idx, entry);
|
|
11
|
+
}
|
|
12
|
+
entry.lines.push(line);
|
|
13
|
+
if (!entry.tag) {
|
|
14
|
+
entry.tag = line.spans[0]?.tag;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const groups = [];
|
|
18
|
+
const sortedKeys = Array.from(map.keys()).sort((a, b) => a - b);
|
|
19
|
+
for (const key of sortedKeys) {
|
|
20
|
+
if (key === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const entry = map.get(key);
|
|
23
|
+
groups.push({ lines: entry.lines, pIdx: key, tag: entry.tag });
|
|
24
|
+
}
|
|
25
|
+
return groups;
|
|
26
|
+
}
|
|
2
27
|
// src/utils.ts
|
|
28
|
+
function fmt(n, precision = 2) {
|
|
29
|
+
return (Math.round(n * 10 ** precision) / 10 ** precision).toString();
|
|
30
|
+
}
|
|
3
31
|
function computeBBox(lines) {
|
|
4
32
|
if (lines.length === 0)
|
|
5
|
-
return { width: 0, height: 0 };
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
33
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
34
|
+
const minX = Math.min(...lines.map((l) => l.x));
|
|
35
|
+
const maxX = Math.max(...lines.map((l) => l.x + l.width));
|
|
36
|
+
const minY = Math.min(...lines.map((l) => l.y));
|
|
37
|
+
const maxY = lines[lines.length - 1].y + lines[lines.length - 1].height;
|
|
38
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
9
39
|
}
|
|
10
40
|
|
|
11
41
|
// src/SVGRenderer.ts
|
|
12
42
|
var PRESETS = {
|
|
13
43
|
flat: { structure: "flat", spacing: "preserve", defaultFit: "none" },
|
|
14
44
|
browser: { structure: "expanded", spacing: "preserve", defaultFit: "none" },
|
|
15
|
-
preserve: { structure: "expanded", spacing: "preserve", defaultFit: "
|
|
45
|
+
preserve: { structure: "expanded", spacing: "preserve", defaultFit: "frag" },
|
|
16
46
|
glyph: { structure: "glyph", spacing: "preserve", defaultFit: "none" }
|
|
17
47
|
};
|
|
18
48
|
function escapeXml(text) {
|
|
@@ -30,14 +60,20 @@ function fontWeightNumeric(weight) {
|
|
|
30
60
|
function colorToRGB(color) {
|
|
31
61
|
if (!color)
|
|
32
62
|
return "rgb(0, 0, 0)";
|
|
63
|
+
if (color[0] !== "#")
|
|
64
|
+
return color;
|
|
33
65
|
let hex = color;
|
|
34
|
-
if (hex.length === 4
|
|
66
|
+
if (hex.length === 4) {
|
|
35
67
|
hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
|
|
36
68
|
}
|
|
37
|
-
if (hex.length === 7
|
|
69
|
+
if (hex.length === 7) {
|
|
38
70
|
return `rgb(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)})`;
|
|
39
71
|
}
|
|
40
|
-
|
|
72
|
+
if (hex.length === 9) {
|
|
73
|
+
const a = parseInt(hex.slice(7, 9), 16) / 255;
|
|
74
|
+
return `rgba(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)}, ${a.toFixed(3)})`;
|
|
75
|
+
}
|
|
76
|
+
return color;
|
|
41
77
|
}
|
|
42
78
|
function resolveOptions(opts) {
|
|
43
79
|
let structure;
|
|
@@ -62,7 +98,16 @@ function resolveOptions(opts) {
|
|
|
62
98
|
}
|
|
63
99
|
const style = opts.style ?? "xml";
|
|
64
100
|
let fit = opts.fit ?? defaultFit;
|
|
65
|
-
|
|
101
|
+
let sizingHorizontal;
|
|
102
|
+
let sizingVertical;
|
|
103
|
+
if (typeof opts.sizing === "object" && opts.sizing !== null) {
|
|
104
|
+
sizingHorizontal = opts.sizing.horizontal ?? "frame";
|
|
105
|
+
sizingVertical = opts.sizing.vertical ?? "frame";
|
|
106
|
+
} else {
|
|
107
|
+
const s = opts.sizing ?? "frame";
|
|
108
|
+
sizingHorizontal = s;
|
|
109
|
+
sizingVertical = s;
|
|
110
|
+
}
|
|
66
111
|
if (structure === "glyph" && fit !== "none") {
|
|
67
112
|
console.warn(`SVGRenderer: fit="${fit}" is ignored when structure="glyph"`);
|
|
68
113
|
fit = "none";
|
|
@@ -71,85 +116,132 @@ function resolveOptions(opts) {
|
|
|
71
116
|
console.warn(`SVGRenderer: fit="frag" downgraded to "text" when structure="flat"`);
|
|
72
117
|
fit = "text";
|
|
73
118
|
}
|
|
74
|
-
return { structure, spacing, style, fit,
|
|
119
|
+
return { structure, spacing, style, fit, sizingHorizontal, sizingVertical, width: opts.width, height: opts.height, className: opts.className, contentPadding: opts.contentPadding ?? 0, debug: opts.debug };
|
|
120
|
+
}
|
|
121
|
+
function resolveSize(lines, opts) {
|
|
122
|
+
const needsBBox = opts.sizingHorizontal === "content" || opts.sizingVertical === "content";
|
|
123
|
+
const bbox = needsBBox ? computeBBox(lines) : null;
|
|
124
|
+
const frameWidth = opts.width;
|
|
125
|
+
const frameHeight = opts.height;
|
|
126
|
+
let width;
|
|
127
|
+
let height;
|
|
128
|
+
if (opts.sizingHorizontal === "content") {
|
|
129
|
+
width = bbox.width;
|
|
130
|
+
} else {
|
|
131
|
+
if (opts.width === undefined) {
|
|
132
|
+
throw new Error(`renderToSVG: horizontal sizing="frame" requires explicit width. ` + `Got width=${opts.width}.`);
|
|
133
|
+
}
|
|
134
|
+
width = opts.width;
|
|
135
|
+
}
|
|
136
|
+
if (opts.sizingVertical === "content") {
|
|
137
|
+
height = bbox.height;
|
|
138
|
+
} else {
|
|
139
|
+
if (opts.height === undefined) {
|
|
140
|
+
throw new Error(`renderToSVG: vertical sizing="frame" requires explicit height. ` + `Got height=${opts.height}.`);
|
|
141
|
+
}
|
|
142
|
+
height = opts.height;
|
|
143
|
+
}
|
|
144
|
+
let viewBox;
|
|
145
|
+
if (bbox) {
|
|
146
|
+
viewBox = {
|
|
147
|
+
x: opts.sizingHorizontal === "content" ? bbox.x : 0,
|
|
148
|
+
y: opts.sizingVertical === "content" ? bbox.y : 0,
|
|
149
|
+
w: opts.sizingHorizontal === "content" ? bbox.width : width,
|
|
150
|
+
h: opts.sizingVertical === "content" ? bbox.height : height
|
|
151
|
+
};
|
|
152
|
+
} else {
|
|
153
|
+
viewBox = { x: 0, y: 0, w: width, h: height };
|
|
154
|
+
}
|
|
155
|
+
const pad = opts.contentPadding || 0;
|
|
156
|
+
if (pad > 0) {
|
|
157
|
+
width += pad * 2;
|
|
158
|
+
height += pad * 2;
|
|
159
|
+
viewBox = {
|
|
160
|
+
x: viewBox.x - pad,
|
|
161
|
+
y: viewBox.y - pad,
|
|
162
|
+
w: viewBox.w + pad * 2,
|
|
163
|
+
h: viewBox.h + pad * 2
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { width, height, viewBox, frameWidth, frameHeight };
|
|
75
167
|
}
|
|
76
168
|
function defaultStyleState(span) {
|
|
169
|
+
const decorations = [];
|
|
170
|
+
if (span.style.underline)
|
|
171
|
+
decorations.push("underline");
|
|
172
|
+
if (span.style.strikethrough)
|
|
173
|
+
decorations.push("line-through");
|
|
77
174
|
return {
|
|
78
175
|
fontFamily: span.style.fontFamily || "Arial",
|
|
79
176
|
fontSize: span.fontMetrics.fontSize || 16,
|
|
80
177
|
fontWeight: fontWeightNumeric(span.style.fontWeight),
|
|
81
178
|
color: span.style.color || "#000000",
|
|
82
179
|
fontStyle: span.style.fontStyle || "normal",
|
|
83
|
-
decoration:
|
|
180
|
+
decoration: decorations.join(" "),
|
|
181
|
+
letterSpacing: span.style.letterSpacing
|
|
84
182
|
};
|
|
85
183
|
}
|
|
86
184
|
function equalStyle(a, b) {
|
|
87
|
-
return a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && a.fontWeight === b.fontWeight && a.color === b.color && a.fontStyle === b.fontStyle && a.decoration === b.decoration;
|
|
185
|
+
return a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && a.fontWeight === b.fontWeight && a.color === b.color && a.fontStyle === b.fontStyle && a.decoration === b.decoration && a.letterSpacing === b.letterSpacing;
|
|
88
186
|
}
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
parts.push(`font-size: ${s.fontSize}px`);
|
|
93
|
-
parts.push(`fill: ${colorToRGB(s.color)}`);
|
|
94
|
-
if (s.fontWeight !== 400)
|
|
95
|
-
parts.push(`font-weight: ${s.fontWeight}`);
|
|
96
|
-
if (s.fontStyle === "italic")
|
|
97
|
-
parts.push(`font-style: italic`);
|
|
98
|
-
if (s.decoration)
|
|
99
|
-
parts.push(`text-decoration: ${s.decoration}`);
|
|
100
|
-
return parts.join("; ");
|
|
101
|
-
}
|
|
102
|
-
function xmlStyleAttrs(s) {
|
|
103
|
-
let attrs = `font-family="${s.fontFamily}" font-size="${s.fontSize}" fill="${s.color}" font-weight="${s.fontWeight}"`;
|
|
104
|
-
if (s.fontStyle === "italic")
|
|
105
|
-
attrs += ' font-style="italic"';
|
|
106
|
-
if (s.decoration)
|
|
107
|
-
attrs += ` text-decoration="${s.decoration}"`;
|
|
108
|
-
return attrs;
|
|
187
|
+
function styleSignature(span) {
|
|
188
|
+
const s = defaultStyleState(span);
|
|
189
|
+
return `${s.fontFamily}|${s.fontSize}|${s.fontWeight}|${s.color}|${s.fontStyle}|${s.decoration}|${s.letterSpacing ?? ""}`;
|
|
109
190
|
}
|
|
110
191
|
function buildTextAttrs(line, span, opts, runId) {
|
|
111
192
|
const x = line.x;
|
|
112
193
|
const y = line.y + line.baseline;
|
|
113
194
|
const s = defaultStyleState(span);
|
|
114
|
-
|
|
195
|
+
const attrs = {
|
|
196
|
+
x: fmt(x),
|
|
197
|
+
y: fmt(y)
|
|
198
|
+
};
|
|
115
199
|
if (runId)
|
|
116
|
-
attrs
|
|
200
|
+
attrs.id = runId;
|
|
117
201
|
if (opts.style === "css") {
|
|
118
|
-
let css =
|
|
202
|
+
let css = `font-family: '${s.fontFamily}', sans-serif; font-size: ${fmt(s.fontSize)}px; fill: ${colorToRGB(s.color)}; font-weight: ${s.fontWeight}`;
|
|
203
|
+
if (s.fontStyle === "italic")
|
|
204
|
+
css += `; font-style: italic`;
|
|
119
205
|
if (opts.spacing === "preserve")
|
|
120
206
|
css += "; white-space: pre";
|
|
121
|
-
attrs
|
|
207
|
+
attrs.style = css;
|
|
122
208
|
} else {
|
|
123
|
-
attrs
|
|
209
|
+
attrs["font-family"] = s.fontFamily;
|
|
210
|
+
attrs["font-size"] = fmt(s.fontSize);
|
|
211
|
+
attrs.fill = s.color;
|
|
212
|
+
attrs["font-weight"] = s.fontWeight;
|
|
213
|
+
if (s.fontStyle === "italic")
|
|
214
|
+
attrs["font-style"] = "italic";
|
|
124
215
|
if (opts.spacing === "preserve")
|
|
125
|
-
attrs
|
|
126
|
-
}
|
|
127
|
-
if (opts.structure === "flat") {
|
|
128
|
-
const anchor = line.alignment === "center" ? "middle" : line.alignment === "right" ? "end" : "start";
|
|
129
|
-
if (anchor !== "start")
|
|
130
|
-
attrs += ` text-anchor="${anchor}"`;
|
|
216
|
+
attrs["xml:space"] = "preserve";
|
|
131
217
|
}
|
|
132
218
|
return attrs;
|
|
133
219
|
}
|
|
134
220
|
function buildTspanAttrs(span, x, currentStyle) {
|
|
135
221
|
const s = defaultStyleState(span);
|
|
136
|
-
|
|
222
|
+
const attrs = {
|
|
223
|
+
x: fmt(x)
|
|
224
|
+
};
|
|
137
225
|
if (currentStyle && equalStyle(s, currentStyle)) {
|
|
138
226
|
return { attrs, newStyle: s };
|
|
139
227
|
}
|
|
140
228
|
if (!currentStyle || s.fontWeight !== currentStyle.fontWeight)
|
|
141
|
-
attrs
|
|
229
|
+
attrs["font-weight"] = s.fontWeight;
|
|
142
230
|
if (!currentStyle || s.fontStyle !== currentStyle.fontStyle)
|
|
143
|
-
attrs
|
|
231
|
+
attrs["font-style"] = s.fontStyle;
|
|
144
232
|
if (!currentStyle || s.fontFamily !== currentStyle.fontFamily)
|
|
145
|
-
attrs
|
|
233
|
+
attrs["font-family"] = s.fontFamily;
|
|
146
234
|
if (!currentStyle || s.fontSize !== currentStyle.fontSize)
|
|
147
|
-
attrs
|
|
235
|
+
attrs["font-size"] = fmt(s.fontSize);
|
|
148
236
|
if (!currentStyle || s.color !== currentStyle.color)
|
|
149
|
-
attrs
|
|
237
|
+
attrs.fill = s.color;
|
|
150
238
|
if (!currentStyle || s.decoration !== currentStyle.decoration) {
|
|
151
239
|
if (s.decoration)
|
|
152
|
-
attrs
|
|
240
|
+
attrs["text-decoration"] = s.decoration;
|
|
241
|
+
}
|
|
242
|
+
if (!currentStyle || s.letterSpacing !== currentStyle.letterSpacing) {
|
|
243
|
+
if (s.letterSpacing !== undefined && s.letterSpacing !== 0)
|
|
244
|
+
attrs["letter-spacing"] = fmt(s.letterSpacing);
|
|
153
245
|
}
|
|
154
246
|
return { attrs, newStyle: s };
|
|
155
247
|
}
|
|
@@ -157,135 +249,275 @@ function buildGlyphPositions(span, _lineX) {
|
|
|
157
249
|
if (!span.glyphAdvances || span.glyphAdvances.length === 0) {
|
|
158
250
|
return "";
|
|
159
251
|
}
|
|
252
|
+
const ls = span.style.letterSpacing || 0;
|
|
160
253
|
const spanX = span.x;
|
|
161
254
|
let xPos = spanX;
|
|
162
|
-
const positions = [xPos
|
|
255
|
+
const positions = [fmt(xPos, 1)];
|
|
163
256
|
for (let i = 0;i < span.glyphAdvances.length - 1; i++) {
|
|
164
|
-
xPos += span.glyphAdvances[i];
|
|
165
|
-
positions.push(xPos
|
|
257
|
+
xPos += span.glyphAdvances[i] + ls;
|
|
258
|
+
positions.push(fmt(xPos, 1));
|
|
166
259
|
}
|
|
167
260
|
return positions.join(" ");
|
|
168
261
|
}
|
|
169
262
|
function buildFitAttr(line, opts) {
|
|
170
263
|
if (opts.fit === "text") {
|
|
171
|
-
return
|
|
264
|
+
return { textLength: fmt(line.width), lengthAdjust: "spacing" };
|
|
172
265
|
}
|
|
173
|
-
return
|
|
266
|
+
return;
|
|
174
267
|
}
|
|
175
268
|
function buildSpanFitAttr(span, opts) {
|
|
176
269
|
if (opts.fit === "frag") {
|
|
177
|
-
return
|
|
270
|
+
return { textLength: fmt(span.width) };
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
function el(tag, attrs = {}, children = []) {
|
|
275
|
+
const cleanAttrs = {};
|
|
276
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
277
|
+
if (v !== undefined) {
|
|
278
|
+
cleanAttrs[k] = v;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { type: "element", tag, attrs: cleanAttrs, children };
|
|
282
|
+
}
|
|
283
|
+
function textNode(value) {
|
|
284
|
+
return { type: "text", value };
|
|
285
|
+
}
|
|
286
|
+
function rawNode(value) {
|
|
287
|
+
return { type: "raw", value };
|
|
288
|
+
}
|
|
289
|
+
function serializeSvg(node, indent = 0) {
|
|
290
|
+
const pad = " ".repeat(indent);
|
|
291
|
+
switch (node.type) {
|
|
292
|
+
case "text":
|
|
293
|
+
return escapeXml(node.value);
|
|
294
|
+
case "raw":
|
|
295
|
+
return node.value;
|
|
296
|
+
case "comment":
|
|
297
|
+
return `${pad}<!-- ${node.value} -->
|
|
298
|
+
`;
|
|
299
|
+
case "element": {
|
|
300
|
+
const tag = node.tag;
|
|
301
|
+
const attrsStr = Object.entries(node.attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
302
|
+
if (node.children.length === 0) {
|
|
303
|
+
return `${pad}<${tag}${attrsStr ? " " + attrsStr : ""} />
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
const allTextChildren = node.children.every((c) => c.type === "text");
|
|
307
|
+
if (allTextChildren) {
|
|
308
|
+
const text = node.children.map((c) => c.value).join("");
|
|
309
|
+
return `${pad}<${tag}${attrsStr ? " " + attrsStr : ""}>${escapeXml(text)}</${tag}>
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
const openTag = `${pad}<${tag}${attrsStr ? " " + attrsStr : ""}>
|
|
313
|
+
`;
|
|
314
|
+
const childrenStr = node.children.map((c) => serializeSvg(c, indent + 1)).join("");
|
|
315
|
+
const closeTag = `${pad}</${tag}>
|
|
316
|
+
`;
|
|
317
|
+
return openTag + childrenStr + closeTag;
|
|
318
|
+
}
|
|
178
319
|
}
|
|
179
|
-
return "";
|
|
180
320
|
}
|
|
181
321
|
|
|
182
|
-
class
|
|
183
|
-
|
|
322
|
+
class SvgAstBuilder {
|
|
323
|
+
root;
|
|
324
|
+
currentText = null;
|
|
184
325
|
opts;
|
|
185
|
-
constructor(width, height, opts) {
|
|
326
|
+
constructor(width, height, opts, viewBox) {
|
|
186
327
|
this.opts = opts;
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
328
|
+
const svgAttrs = {
|
|
329
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
330
|
+
width: fmt(width),
|
|
331
|
+
height: fmt(height),
|
|
332
|
+
viewBox: viewBox ? `${fmt(viewBox.x)} ${fmt(viewBox.y)} ${fmt(viewBox.w)} ${fmt(viewBox.h)}` : `0 0 ${fmt(width)} ${fmt(height)}`
|
|
333
|
+
};
|
|
190
334
|
if (opts.className) {
|
|
191
|
-
|
|
192
|
-
`;
|
|
335
|
+
svgAttrs.class = opts.className;
|
|
193
336
|
}
|
|
337
|
+
this.root = el("svg", svgAttrs);
|
|
194
338
|
}
|
|
195
|
-
|
|
196
|
-
|
|
339
|
+
openText(line, baseSpan, runId, yOverride, fontSizeOverride) {
|
|
340
|
+
this.closeText();
|
|
341
|
+
let textAttrs;
|
|
197
342
|
if (yOverride !== undefined && fontSizeOverride !== undefined) {
|
|
198
343
|
const s = defaultStyleState(baseSpan);
|
|
199
344
|
const x = line.x;
|
|
200
|
-
attrs =
|
|
345
|
+
const attrs = {
|
|
346
|
+
x: fmt(x),
|
|
347
|
+
y: fmt(yOverride)
|
|
348
|
+
};
|
|
201
349
|
if (this.opts.style === "css") {
|
|
202
|
-
let css =
|
|
350
|
+
let css = `font-family: '${s.fontFamily}', sans-serif; font-size: ${fmt(fontSizeOverride)}px; fill: ${colorToRGB(s.color)}; font-weight: ${s.fontWeight}`;
|
|
351
|
+
if (s.fontStyle === "italic")
|
|
352
|
+
css += `; font-style: italic`;
|
|
203
353
|
if (this.opts.spacing === "preserve")
|
|
204
354
|
css += "; white-space: pre";
|
|
205
|
-
attrs
|
|
355
|
+
attrs.style = css;
|
|
206
356
|
} else {
|
|
207
|
-
|
|
357
|
+
attrs["font-family"] = s.fontFamily;
|
|
358
|
+
attrs["font-size"] = fmt(fontSizeOverride);
|
|
359
|
+
attrs.fill = s.color;
|
|
360
|
+
attrs["font-weight"] = s.fontWeight;
|
|
208
361
|
if (s.fontStyle === "italic")
|
|
209
|
-
|
|
210
|
-
if (s.decoration)
|
|
211
|
-
xml += ` text-decoration="${s.decoration}"`;
|
|
362
|
+
attrs["font-style"] = "italic";
|
|
212
363
|
if (this.opts.spacing === "preserve")
|
|
213
|
-
xml
|
|
214
|
-
attrs += xml;
|
|
364
|
+
attrs["xml:space"] = "preserve";
|
|
215
365
|
}
|
|
366
|
+
textAttrs = attrs;
|
|
216
367
|
} else {
|
|
217
|
-
|
|
368
|
+
textAttrs = buildTextAttrs(line, baseSpan, this.opts, runId);
|
|
218
369
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.
|
|
370
|
+
const fitAttrs = buildFitAttr(line, this.opts);
|
|
371
|
+
if (fitAttrs) {
|
|
372
|
+
Object.assign(textAttrs, fitAttrs);
|
|
373
|
+
}
|
|
374
|
+
const textEl = el("text", textAttrs);
|
|
375
|
+
this.root.children.push(textEl);
|
|
376
|
+
this.currentText = textEl;
|
|
225
377
|
}
|
|
226
|
-
|
|
227
|
-
|
|
378
|
+
addDebug(debugMarkup) {
|
|
379
|
+
if (debugMarkup) {
|
|
380
|
+
this.root.children.push(rawNode(`<!-- debug overlay -->
|
|
381
|
+
${debugMarkup}
|
|
382
|
+
`));
|
|
383
|
+
}
|
|
228
384
|
}
|
|
229
|
-
|
|
230
|
-
this.
|
|
231
|
-
|
|
385
|
+
addTextContent(text) {
|
|
386
|
+
if (this.currentText) {
|
|
387
|
+
this.currentText.children.push(textNode(text));
|
|
388
|
+
}
|
|
232
389
|
}
|
|
233
|
-
|
|
390
|
+
addTspan(span, x, style) {
|
|
234
391
|
const { attrs, newStyle } = buildTspanAttrs(span, x, style);
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
392
|
+
const fitAttrs = buildSpanFitAttr(span, this.opts);
|
|
393
|
+
if (fitAttrs) {
|
|
394
|
+
Object.assign(attrs, fitAttrs);
|
|
395
|
+
}
|
|
396
|
+
const tspan = el("tspan", attrs, [textNode(span.text)]);
|
|
397
|
+
if (this.currentText) {
|
|
398
|
+
this.currentText.children.push(tspan);
|
|
399
|
+
}
|
|
238
400
|
return newStyle;
|
|
239
401
|
}
|
|
240
|
-
|
|
402
|
+
addGlyphTspan(span, lineX) {
|
|
241
403
|
const positions = buildGlyphPositions(span, lineX);
|
|
404
|
+
const attrs = {};
|
|
242
405
|
if (positions) {
|
|
243
|
-
|
|
244
|
-
`);
|
|
245
|
-
} else {
|
|
246
|
-
this.parts.push(` <tspan>${escapeXml(span.text)}</tspan>
|
|
247
|
-
`);
|
|
406
|
+
attrs.x = positions;
|
|
248
407
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.parts.push(`<!-- debug overlay -->
|
|
253
|
-
${debugOverlay}
|
|
254
|
-
`);
|
|
408
|
+
const tspan = el("tspan", attrs, [textNode(span.text)]);
|
|
409
|
+
if (this.currentText) {
|
|
410
|
+
this.currentText.children.push(tspan);
|
|
255
411
|
}
|
|
256
412
|
}
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
|
|
413
|
+
addRawLine(lineStr) {
|
|
414
|
+
this.closeText();
|
|
415
|
+
this.root.children.push(rawNode(lineStr));
|
|
416
|
+
}
|
|
417
|
+
closeText() {
|
|
418
|
+
this.currentText = null;
|
|
261
419
|
}
|
|
262
420
|
}
|
|
263
|
-
function renderDebugToSVG(lines,
|
|
421
|
+
function renderDebugToSVG(lines, flags, frameSize, contentSize, columns, leftPad, rightPad) {
|
|
264
422
|
const parts = [];
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
423
|
+
const sw = flags.widthBorder ?? 1;
|
|
424
|
+
if ((flags.frameBox || flags.frame) && frameSize) {
|
|
425
|
+
parts.push(` <rect x="0" y="0" width="${fmt(frameSize.width)}" height="${fmt(frameSize.height)}"` + ` fill="none" stroke="rgba(0,140,255,0.8)" stroke-width="${fmt(sw)}" stroke-dasharray="4,3" />`);
|
|
426
|
+
if (flags.labels) {
|
|
427
|
+
parts.push(` <text x="4" y="14" font-size="10" fill="rgba(0,140,255,0.9)" font-family="monospace">frame ${fmt(frameSize.width)}\xD7${fmt(frameSize.height)}</text>`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (flags.contentBox) {
|
|
431
|
+
const bbox = computeBBox(lines);
|
|
432
|
+
parts.push(` <rect x="${fmt(bbox.x)}" y="${fmt(bbox.y)}" width="${fmt(bbox.width)}" height="${fmt(bbox.height)}"` + ` fill="none" stroke="rgba(255,60,140,0.8)" stroke-width="${fmt(sw)}" stroke-dasharray="1,2" />`);
|
|
433
|
+
if (flags.labels) {
|
|
434
|
+
const labelY = bbox.y + bbox.height + 14;
|
|
435
|
+
parts.push(` <text x="${fmt(bbox.x)}" y="${fmt(labelY)}" font-size="10" fill="rgba(255,60,140,0.9)" font-family="monospace">content ${fmt(bbox.width)}\xD7${fmt(bbox.height)}</text>`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (flags.contentBox && frameSize && contentSize && flags.labels) {
|
|
439
|
+
const overflowX = contentSize.width > frameSize.width;
|
|
440
|
+
const overflowY = contentSize.height > frameSize.height;
|
|
441
|
+
if (overflowX || overflowY) {
|
|
442
|
+
parts.push(` <text x="4" y="${fmt(frameSize.height + 14)}" font-size="10" fill="rgba(220,0,0,0.9)" font-family="monospace">\u26A0 content overflow: ${overflowX ? `\u0394x=${fmt(contentSize.width - frameSize.width)} ` : ""}${overflowY ? `\u0394y=${fmt(contentSize.height - frameSize.height)}` : ""}</text>`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (columns && columns.count > 1 && (flags.paragraphBox || flags.columnBox)) {
|
|
446
|
+
const colCount = columns.count;
|
|
447
|
+
const colGap = columns.gap;
|
|
448
|
+
const lp = leftPad ?? 0;
|
|
449
|
+
const totalHorizontalSpace = frameSize?.width ?? (lines.length > 0 ? Math.max(...lines.map((l) => l.x + l.width)) : 0);
|
|
450
|
+
const usableWidth = totalHorizontalSpace - lp - (rightPad ?? 0);
|
|
451
|
+
const colWidth = (usableWidth - (colCount - 1) * colGap) / colCount;
|
|
452
|
+
for (let c = 1;c < colCount; c++) {
|
|
453
|
+
const sepX = lp + c * (colWidth + colGap) - colGap / 2;
|
|
454
|
+
parts.push(` <line x1="${fmt(sepX)}" y1="0" x2="${fmt(sepX)}" y2="${fmt(frameSize?.height ?? 9999)}"` + ` stroke="rgba(100,100,100,0.15)" stroke-width="1" stroke-dasharray="2,2" />`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (flags.paragraphBox) {
|
|
458
|
+
const paraGroups = groupLinesByParagraph(lines);
|
|
459
|
+
const paraColors = [
|
|
460
|
+
"rgba(0,180,80,0.25)",
|
|
461
|
+
"rgba(180,0,80,0.25)",
|
|
462
|
+
"rgba(80,0,180,0.25)",
|
|
463
|
+
"rgba(180,180,0,0.25)"
|
|
464
|
+
];
|
|
465
|
+
for (let i = 0;i < paraGroups.length; i++) {
|
|
466
|
+
const group = paraGroups[i];
|
|
467
|
+
if (group.lines.length === 0)
|
|
468
|
+
continue;
|
|
469
|
+
const colMap = new Map;
|
|
470
|
+
for (const line of group.lines) {
|
|
471
|
+
const ci = line.columnIndex ?? 0;
|
|
472
|
+
const existing = colMap.get(ci);
|
|
473
|
+
const lineTop = line.y;
|
|
474
|
+
const lineBottom = line.y + line.height;
|
|
475
|
+
if (existing) {
|
|
476
|
+
existing.top = Math.min(existing.top, lineTop);
|
|
477
|
+
existing.bottom = Math.max(existing.bottom, lineBottom);
|
|
478
|
+
} else {
|
|
479
|
+
colMap.set(ci, { top: lineTop, bottom: lineBottom });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const lp = leftPad ?? 0;
|
|
483
|
+
const rp = rightPad ?? 0;
|
|
484
|
+
const totalW = frameSize?.width ?? (lines.length > 0 ? Math.max(...lines.map((l) => l.x + l.width)) : 0);
|
|
485
|
+
const usableW = totalW - lp - rp;
|
|
486
|
+
const colCount = columns?.count ?? 1;
|
|
487
|
+
const colGap = columns?.gap ?? 0;
|
|
488
|
+
const colW = (usableW - (colCount - 1) * colGap) / colCount;
|
|
489
|
+
const color = paraColors[i % paraColors.length];
|
|
490
|
+
for (const [ci, rect] of colMap) {
|
|
491
|
+
const colX = lp + ci * (colW + colGap);
|
|
492
|
+
parts.push(` <rect x="${fmt(colX)}" y="${fmt(rect.top)}" width="${fmt(colW)}" height="${fmt(rect.bottom - rect.top)}" fill="none" stroke="${color}" stroke-width="${fmt(sw)}" />`);
|
|
493
|
+
}
|
|
494
|
+
const label = group.tag ? `#${group.pIdx} ${group.tag}` : `#${group.pIdx}`;
|
|
495
|
+
if (flags.labels) {
|
|
496
|
+
const firstColIdx = Math.min(...Array.from(colMap.keys()));
|
|
497
|
+
const firstColX = lp + firstColIdx * (colW + colGap);
|
|
498
|
+
const firstTop = colMap.get(firstColIdx).top;
|
|
499
|
+
parts.push(` <text x="${fmt(firstColX + 4)}" y="${fmt(firstTop - 2)}" font-size="9" fill="rgba(0,0,0,0.6)" font-family="monospace">\xB6 ${label}</text>`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
270
502
|
}
|
|
271
503
|
for (const line of lines) {
|
|
272
504
|
const { x: bx, y: by, width: bw, height: bh } = line;
|
|
273
505
|
const baselineY = line.y + line.baseline;
|
|
274
506
|
if (flags.lineGap) {
|
|
275
|
-
parts.push(` <rect x="${bx}" y="${by}" width="${bw}" height="${bh}" fill="rgba(0,150,255,0.10)" stroke="none" />`);
|
|
507
|
+
parts.push(` <rect x="${fmt(bx)}" y="${fmt(by)}" width="${fmt(bw)}" height="${fmt(bh)}" fill="rgba(0,150,255,0.10)" stroke="none" />`);
|
|
276
508
|
}
|
|
277
509
|
if (flags.box) {
|
|
278
|
-
parts.push(` <rect x="${bx}" y="${by}" width="${bw}" height="${bh}" fill="none" stroke="rgba(255,100,100,0.5)" stroke-width="
|
|
510
|
+
parts.push(` <rect x="${fmt(bx)}" y="${fmt(by)}" width="${fmt(bw)}" height="${fmt(bh)}" fill="none" stroke="rgba(255,100,100,0.5)" stroke-width="${fmt(sw)}" />`);
|
|
279
511
|
}
|
|
280
512
|
if (flags.baseline) {
|
|
281
|
-
parts.push(` <line x1="${bx}" y1="${baselineY}" x2="${bx + bw}" y2="${baselineY}" stroke="rgba(100,100,255,0.5)" stroke-width="
|
|
513
|
+
parts.push(` <line x1="${fmt(bx)}" y1="${fmt(baselineY)}" x2="${fmt(bx + bw)}" y2="${fmt(baselineY)}" stroke="rgba(100,100,255,0.5)" stroke-width="${fmt(sw)}" />`);
|
|
282
514
|
}
|
|
283
515
|
if (flags.ascentDescent) {
|
|
284
|
-
parts.push(` <line x1="${bx}" y1="${baselineY - line.ascent}" x2="${bx + bw}" y2="${baselineY - line.ascent}" stroke="rgba(100,255,100,0.4)" stroke-width="
|
|
285
|
-
parts.push(` <line x1="${bx}" y1="${baselineY + line.descent}" x2="${bx + bw}" y2="${baselineY + line.descent}" stroke="rgba(100,255,100,0.4)" stroke-width="
|
|
516
|
+
parts.push(` <line x1="${fmt(bx)}" y1="${fmt(baselineY - line.ascent)}" x2="${fmt(bx + bw)}" y2="${fmt(baselineY - line.ascent)}" stroke="rgba(100,255,100,0.4)" stroke-width="${fmt(sw)}" stroke-dasharray="3,2" />`);
|
|
517
|
+
parts.push(` <line x1="${fmt(bx)}" y1="${fmt(baselineY + line.descent)}" x2="${fmt(bx + bw)}" y2="${fmt(baselineY + line.descent)}" stroke="rgba(100,255,100,0.4)" stroke-width="${fmt(sw)}" stroke-dasharray="3,2" />`);
|
|
286
518
|
}
|
|
287
519
|
if (flags.labels) {
|
|
288
|
-
parts.push(` <text x="${bx}" y="${by - 2}" font-size="9" fill="rgba(0,0,0,0.55)" font-family="monospace">y=${by
|
|
520
|
+
parts.push(` <text x="${fmt(bx)}" y="${fmt(by - 2)}" font-size="9" fill="rgba(0,0,0,0.55)" font-family="monospace">y=${fmt(by)} x=${fmt(bx)} w=${fmt(bw)} h=${fmt(bh)} bl=${fmt(baselineY)}</text>`);
|
|
289
521
|
}
|
|
290
522
|
if (flags.runs) {
|
|
291
523
|
for (const span of line.spans) {
|
|
@@ -293,7 +525,7 @@ function renderDebugToSVG(lines, width, height, flags) {
|
|
|
293
525
|
continue;
|
|
294
526
|
const rx = line.x + span.x;
|
|
295
527
|
const ry = baselineY - span.fontMetrics.ascent;
|
|
296
|
-
parts.push(` <rect x="${rx}" y="${ry}" width="${span.width}" height="${span.fontMetrics.ascent + span.fontMetrics.descent}" fill="none" stroke="rgba(200,100,255,0.4)" stroke-width="
|
|
528
|
+
parts.push(` <rect x="${fmt(rx)}" y="${fmt(ry)}" width="${fmt(span.width)}" height="${fmt(span.fontMetrics.ascent + span.fontMetrics.descent)}" fill="none" stroke="rgba(200,100,255,0.4)" stroke-width="${fmt(sw)}" />`);
|
|
297
529
|
}
|
|
298
530
|
}
|
|
299
531
|
}
|
|
@@ -302,20 +534,8 @@ function renderDebugToSVG(lines, width, height, flags) {
|
|
|
302
534
|
}
|
|
303
535
|
function renderToSVG(lines, options = {}) {
|
|
304
536
|
const opts = resolveOptions(options);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (opts.sizing === "content") {
|
|
308
|
-
const bbox = computeBBox(lines);
|
|
309
|
-
svgWidth = bbox.width;
|
|
310
|
-
svgHeight = bbox.height;
|
|
311
|
-
} else {
|
|
312
|
-
if (opts.width === undefined || opts.height === undefined) {
|
|
313
|
-
throw new Error(`renderToSVG: sizing="frame" requires explicit width and height. ` + `Got width=${opts.width}, height=${opts.height}. ` + `Use renderResultToSVG(result, options) to auto-pass dimensions.`);
|
|
314
|
-
}
|
|
315
|
-
svgWidth = opts.width;
|
|
316
|
-
svgHeight = opts.height;
|
|
317
|
-
}
|
|
318
|
-
const builder = new SvgBuilder(svgWidth, svgHeight, opts);
|
|
537
|
+
const { width: svgWidth, height: svgHeight, viewBox, frameWidth, frameHeight } = resolveSize(lines, opts);
|
|
538
|
+
const builder = new SvgAstBuilder(svgWidth, svgHeight, opts, viewBox);
|
|
319
539
|
for (const line of lines) {
|
|
320
540
|
if (opts.structure === "glyph") {
|
|
321
541
|
let currentRunIdx = -1;
|
|
@@ -324,18 +544,14 @@ function renderToSVG(lines, options = {}) {
|
|
|
324
544
|
continue;
|
|
325
545
|
const runIdx = span.itemIndex;
|
|
326
546
|
if (runIdx !== currentRunIdx) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const runId = span.paragraphId ? `${span.paragraphId}-${runIdx}` : undefined;
|
|
331
|
-
builder.addText(line, span, runId);
|
|
547
|
+
builder.closeText();
|
|
548
|
+
const runId = span.tag ? `${span.tag}-${runIdx}` : undefined;
|
|
549
|
+
builder.openText(line, span, runId);
|
|
332
550
|
currentRunIdx = runIdx;
|
|
333
551
|
}
|
|
334
|
-
builder.
|
|
335
|
-
}
|
|
336
|
-
if (currentRunIdx !== -1) {
|
|
337
|
-
builder.closeText();
|
|
552
|
+
builder.addGlyphTspan(span, line.x);
|
|
338
553
|
}
|
|
554
|
+
builder.closeText();
|
|
339
555
|
} else if (opts.structure === "flat") {
|
|
340
556
|
const groups = [];
|
|
341
557
|
for (const span of line.spans) {
|
|
@@ -343,56 +559,113 @@ function renderToSVG(lines, options = {}) {
|
|
|
343
559
|
continue;
|
|
344
560
|
const offset = span.fontMetrics.baselineOffset || 0;
|
|
345
561
|
const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
|
|
562
|
+
const sig = styleSignature(span);
|
|
346
563
|
const fontSize = span.fontMetrics.fontSize;
|
|
347
564
|
const last = groups[groups.length - 1];
|
|
348
|
-
if (last && last.targetY === targetY && last.
|
|
565
|
+
if (last && last.targetY === targetY && last.signature === sig) {
|
|
349
566
|
last.spans.push(span);
|
|
350
567
|
} else {
|
|
351
|
-
groups.push({ spans: [span], targetY, fontSize });
|
|
568
|
+
groups.push({ spans: [span], targetY, signature: sig, fontSize });
|
|
352
569
|
}
|
|
353
570
|
}
|
|
571
|
+
const firstTextX = line.spans.find((s) => s.type === "text" || s.type === "marker")?.x ?? 0;
|
|
572
|
+
const fitAttr = buildFitAttr(line, opts);
|
|
354
573
|
for (const group of groups) {
|
|
355
574
|
const s = defaultStyleState(group.spans[0]);
|
|
356
575
|
const text = group.spans.map((sp) => escapeXml(sp.text)).join("");
|
|
357
|
-
|
|
576
|
+
const groupX = line.x + (group.spans[0].x - firstTextX);
|
|
577
|
+
const textAttrs = {
|
|
578
|
+
x: fmt(groupX),
|
|
579
|
+
y: fmt(group.targetY),
|
|
580
|
+
"font-family": s.fontFamily,
|
|
581
|
+
"font-size": fmt(group.fontSize),
|
|
582
|
+
fill: s.color,
|
|
583
|
+
"font-weight": s.fontWeight
|
|
584
|
+
};
|
|
358
585
|
if (s.fontStyle === "italic")
|
|
359
|
-
|
|
586
|
+
textAttrs["font-style"] = "italic";
|
|
360
587
|
if (s.decoration)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
588
|
+
textAttrs["text-decoration"] = s.decoration;
|
|
589
|
+
if (s.letterSpacing !== undefined && s.letterSpacing !== 0)
|
|
590
|
+
textAttrs["letter-spacing"] = fmt(s.letterSpacing);
|
|
591
|
+
textAttrs["xml:space"] = "preserve";
|
|
592
|
+
if (fitAttr) {
|
|
593
|
+
Object.assign(textAttrs, fitAttr);
|
|
594
|
+
}
|
|
595
|
+
const attrsStr = Object.entries(textAttrs).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
596
|
+
builder.addRawLine(` <text ${attrsStr}>${text}</text>
|
|
368
597
|
`);
|
|
369
598
|
}
|
|
370
599
|
} else {
|
|
371
|
-
const
|
|
372
|
-
if (!baseSpan)
|
|
373
|
-
continue;
|
|
374
|
-
builder.addText(line, baseSpan);
|
|
375
|
-
let currentStyle = null;
|
|
600
|
+
const groups = [];
|
|
376
601
|
for (const span of line.spans) {
|
|
377
602
|
if (!span.text)
|
|
378
603
|
continue;
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
604
|
+
const offset = span.fontMetrics.baselineOffset || 0;
|
|
605
|
+
const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
|
|
606
|
+
const sig = styleSignature(span);
|
|
607
|
+
const last = groups[groups.length - 1];
|
|
608
|
+
if (last && last.targetY === targetY && last.signature === sig) {
|
|
609
|
+
last.spans.push(span);
|
|
610
|
+
} else {
|
|
611
|
+
groups.push({ targetY, spans: [span], signature: sig });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
for (const group of groups) {
|
|
615
|
+
const baseSpan = group.spans.find((f) => f.type === "text" && f.text.length > 0) || group.spans[0];
|
|
616
|
+
if (!baseSpan)
|
|
617
|
+
continue;
|
|
618
|
+
const lineBaseY = Math.round((line.y + line.baseline) * 100) / 100;
|
|
619
|
+
const needsOffset = group.targetY !== lineBaseY;
|
|
620
|
+
if (needsOffset) {
|
|
621
|
+
const s = defaultStyleState(baseSpan);
|
|
622
|
+
const firstTextX = line.spans.find((s2) => s2.type === "text")?.x ?? 0;
|
|
623
|
+
const groupX = line.x + (group.spans[0].x - firstTextX);
|
|
624
|
+
const fontSize = baseSpan.fontMetrics.fontSize;
|
|
625
|
+
const textAttrs = {
|
|
626
|
+
x: fmt(groupX),
|
|
627
|
+
y: fmt(group.targetY),
|
|
628
|
+
"font-family": s.fontFamily,
|
|
629
|
+
"font-size": fmt(fontSize),
|
|
630
|
+
fill: s.color,
|
|
631
|
+
"font-weight": s.fontWeight
|
|
632
|
+
};
|
|
633
|
+
if (s.fontStyle === "italic")
|
|
634
|
+
textAttrs["font-style"] = "italic";
|
|
635
|
+
textAttrs["xml:space"] = "preserve";
|
|
636
|
+
const fit = buildFitAttr(line, opts);
|
|
637
|
+
if (fit) {
|
|
638
|
+
Object.assign(textAttrs, fit);
|
|
639
|
+
}
|
|
640
|
+
builder.openText(line, baseSpan, undefined, group.targetY, fontSize);
|
|
641
|
+
} else {
|
|
642
|
+
builder.openText(line, baseSpan);
|
|
643
|
+
}
|
|
644
|
+
let currentStyle = null;
|
|
645
|
+
for (const span of group.spans) {
|
|
646
|
+
if (!span.text)
|
|
647
|
+
continue;
|
|
648
|
+
const x = span.x;
|
|
649
|
+
const shouldRender = span.type !== "space" || opts.spacing === "preserve";
|
|
650
|
+
if (shouldRender) {
|
|
651
|
+
const newStyle = builder.addTspan(span, x, currentStyle);
|
|
652
|
+
if (span.type !== "space") {
|
|
653
|
+
currentStyle = newStyle;
|
|
654
|
+
}
|
|
385
655
|
}
|
|
386
656
|
}
|
|
657
|
+
builder.closeText();
|
|
387
658
|
}
|
|
388
|
-
builder.closeText();
|
|
389
659
|
}
|
|
390
660
|
}
|
|
391
661
|
if (opts.debug) {
|
|
392
|
-
const
|
|
662
|
+
const frameSize = frameWidth !== undefined && frameHeight !== undefined ? { width: frameWidth, height: frameHeight } : undefined;
|
|
663
|
+
const contentBbox = computeBBox(lines);
|
|
664
|
+
const contentSize = { width: contentBbox.width, height: contentBbox.height };
|
|
665
|
+
const debugSvg = renderDebugToSVG(lines, opts.debug, frameSize, contentSize, options.columns, options.paddingLeft, 0);
|
|
393
666
|
builder.addDebug(debugSvg);
|
|
394
667
|
}
|
|
395
|
-
return builder.
|
|
668
|
+
return serializeSvg(builder.root);
|
|
396
669
|
}
|
|
397
670
|
function renderParagraphToSVG(lines, paragraphWidth, paragraphHeight, options) {
|
|
398
671
|
return renderToSVG(lines, {
|
|
@@ -409,18 +682,93 @@ function renderResultToSVG(result, options) {
|
|
|
409
682
|
});
|
|
410
683
|
}
|
|
411
684
|
// src/CanvasRenderer.ts
|
|
412
|
-
function
|
|
685
|
+
function fontWeightNumeric2(weight) {
|
|
413
686
|
if (weight === "bold")
|
|
414
|
-
return
|
|
687
|
+
return 700;
|
|
415
688
|
if (weight === "normal")
|
|
416
|
-
return
|
|
689
|
+
return 400;
|
|
417
690
|
if (typeof weight === "number")
|
|
418
|
-
return
|
|
419
|
-
return
|
|
691
|
+
return weight;
|
|
692
|
+
return 400;
|
|
420
693
|
}
|
|
421
694
|
function fontStyleCSS(style) {
|
|
422
695
|
return style === "italic" ? "italic" : "normal";
|
|
423
696
|
}
|
|
697
|
+
function buildFontString(span) {
|
|
698
|
+
const style = fontStyleCSS(span.style.fontStyle);
|
|
699
|
+
const weight = fontWeightNumeric2(span.style.fontWeight);
|
|
700
|
+
const size = span.fontMetrics.fontSize;
|
|
701
|
+
const family = span.style.fontFamily;
|
|
702
|
+
return `${style} ${weight} ${size}px ${family}`;
|
|
703
|
+
}
|
|
704
|
+
function groupSpansByBaseline(line, spans) {
|
|
705
|
+
const groups = [];
|
|
706
|
+
for (const span of spans) {
|
|
707
|
+
if (!span.text)
|
|
708
|
+
continue;
|
|
709
|
+
const offset = span.fontMetrics.baselineOffset || 0;
|
|
710
|
+
const targetY = Math.round((line.y + line.baseline + offset) * 100) / 100;
|
|
711
|
+
const last = groups[groups.length - 1];
|
|
712
|
+
if (last && last.targetY === targetY) {
|
|
713
|
+
last.spans.push(span);
|
|
714
|
+
} else {
|
|
715
|
+
groups.push({ targetY, spans: [span] });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return groups;
|
|
719
|
+
}
|
|
720
|
+
function drawSpanBackground(ctx, line, span) {
|
|
721
|
+
const baselineY = line.y + line.baseline;
|
|
722
|
+
const x = line.x + span.x;
|
|
723
|
+
const y = baselineY - span.fontMetrics.ascent;
|
|
724
|
+
const w = span.width;
|
|
725
|
+
const h = span.fontMetrics.ascent + span.fontMetrics.descent;
|
|
726
|
+
ctx.fillStyle = span.style.backgroundColor || "transparent";
|
|
727
|
+
if (span.style.backgroundColor) {
|
|
728
|
+
ctx.fillRect(x, y, w, h);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function renderSpan(ctx, line, span, baselineY, preserveSpaces) {
|
|
732
|
+
const x = line.x + span.x;
|
|
733
|
+
drawSpanBackground(ctx, line, span);
|
|
734
|
+
ctx.font = buildFontString(span);
|
|
735
|
+
ctx.fillStyle = span.style.color || "#000000";
|
|
736
|
+
ctx.textBaseline = "alphabetic";
|
|
737
|
+
const ls = span.style.letterSpacing;
|
|
738
|
+
if (ls !== undefined && ls !== 0) {
|
|
739
|
+
ctx.letterSpacing = `${ls}px`;
|
|
740
|
+
} else {
|
|
741
|
+
ctx.letterSpacing = "normal";
|
|
742
|
+
}
|
|
743
|
+
if (preserveSpaces || span.type !== "space") {
|
|
744
|
+
ctx.fillText(span.text, x, baselineY);
|
|
745
|
+
}
|
|
746
|
+
ctx.letterSpacing = "normal";
|
|
747
|
+
if (span.style.underline) {
|
|
748
|
+
const ulY = baselineY + 2;
|
|
749
|
+
ctx.strokeStyle = span.style.color || "#000000";
|
|
750
|
+
ctx.lineWidth = 1;
|
|
751
|
+
ctx.beginPath();
|
|
752
|
+
ctx.moveTo(x, ulY);
|
|
753
|
+
ctx.lineTo(x + span.width, ulY);
|
|
754
|
+
ctx.stroke();
|
|
755
|
+
}
|
|
756
|
+
if (span.style.strikethrough) {
|
|
757
|
+
const stY = baselineY - span.fontMetrics.ascent * 0.4;
|
|
758
|
+
ctx.strokeStyle = span.style.color || "#000000";
|
|
759
|
+
ctx.lineWidth = 1;
|
|
760
|
+
ctx.beginPath();
|
|
761
|
+
ctx.moveTo(x, stY);
|
|
762
|
+
ctx.lineTo(x + span.width, stY);
|
|
763
|
+
ctx.stroke();
|
|
764
|
+
}
|
|
765
|
+
if (span.inlineWidget) {
|
|
766
|
+
const iw = span.inlineWidget;
|
|
767
|
+
const iwY = baselineY - (iw.height || span.fontMetrics.ascent) + (iw.baselineOffset || 0);
|
|
768
|
+
ctx.fillStyle = "#cccccc";
|
|
769
|
+
ctx.fillRect(x, iwY, iw.width, iw.height);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
424
772
|
function renderToCanvas(ctx, lines, options = {}) {
|
|
425
773
|
const sizing = options.sizing ?? "frame";
|
|
426
774
|
const preserveSpaces = options.preserveSpaces ?? false;
|
|
@@ -443,42 +791,13 @@ function renderToCanvas(ctx, lines, options = {}) {
|
|
|
443
791
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
444
792
|
}
|
|
445
793
|
for (const line of lines) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
ctx.font = `${style} ${weight} ${size}px ${family}`;
|
|
454
|
-
ctx.fillStyle = span.style.color || "#000000";
|
|
455
|
-
ctx.textBaseline = "alphabetic";
|
|
456
|
-
if (preserveSpaces || span.type !== "space") {
|
|
457
|
-
ctx.fillText(span.text, x, baselineY);
|
|
458
|
-
}
|
|
459
|
-
if (span.style.underline) {
|
|
460
|
-
const ulY = baselineY + 2;
|
|
461
|
-
ctx.strokeStyle = span.style.color || "#000000";
|
|
462
|
-
ctx.lineWidth = 1;
|
|
463
|
-
ctx.beginPath();
|
|
464
|
-
ctx.moveTo(x, ulY);
|
|
465
|
-
ctx.lineTo(x + span.width, ulY);
|
|
466
|
-
ctx.stroke();
|
|
467
|
-
}
|
|
468
|
-
if (span.style.strikethrough) {
|
|
469
|
-
const stY = baselineY - span.fontMetrics.ascent * 0.4;
|
|
470
|
-
ctx.strokeStyle = span.style.color || "#000000";
|
|
471
|
-
ctx.lineWidth = 1;
|
|
472
|
-
ctx.beginPath();
|
|
473
|
-
ctx.moveTo(x, stY);
|
|
474
|
-
ctx.lineTo(x + span.width, stY);
|
|
475
|
-
ctx.stroke();
|
|
476
|
-
}
|
|
477
|
-
if (span.inlineWidget) {
|
|
478
|
-
const iw = span.inlineWidget;
|
|
479
|
-
const iwY = baselineY - (iw.height || span.fontMetrics.ascent) + (iw.baselineOffset || 0);
|
|
480
|
-
ctx.fillStyle = "#cccccc";
|
|
481
|
-
ctx.fillRect(x, iwY, iw.width, iw.height);
|
|
794
|
+
const drawableSpans = line.spans.filter((s) => preserveSpaces || s.type !== "space");
|
|
795
|
+
if (drawableSpans.length === 0)
|
|
796
|
+
continue;
|
|
797
|
+
const groups = groupSpansByBaseline(line, drawableSpans);
|
|
798
|
+
for (const group of groups) {
|
|
799
|
+
for (const span of group.spans) {
|
|
800
|
+
renderSpan(ctx, line, span, group.targetY, preserveSpaces);
|
|
482
801
|
}
|
|
483
802
|
}
|
|
484
803
|
}
|
|
@@ -488,7 +807,10 @@ function renderToCanvas(ctx, lines, options = {}) {
|
|
|
488
807
|
}
|
|
489
808
|
function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
|
|
490
809
|
ctx.save();
|
|
491
|
-
|
|
810
|
+
const sw = flags.widthBorder ?? 1;
|
|
811
|
+
const hasBBox = lines.length > 0;
|
|
812
|
+
const bbox = hasBBox ? computeBBox(lines) : null;
|
|
813
|
+
if ((flags.frameBox || flags.frame) && hasBBox) {
|
|
492
814
|
const first = lines[0];
|
|
493
815
|
const last = lines[lines.length - 1];
|
|
494
816
|
const maxW = Math.max(...lines.map((l) => l.x + l.width));
|
|
@@ -496,12 +818,47 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
|
|
|
496
818
|
const frameY = first.y;
|
|
497
819
|
const frameW = maxW - frameX;
|
|
498
820
|
const frameH = last.y + last.height - first.y;
|
|
499
|
-
ctx.strokeStyle = "rgba(
|
|
500
|
-
ctx.lineWidth =
|
|
501
|
-
ctx.setLineDash([
|
|
821
|
+
ctx.strokeStyle = "rgba(0,140,255,0.8)";
|
|
822
|
+
ctx.lineWidth = sw;
|
|
823
|
+
ctx.setLineDash([4, 3]);
|
|
502
824
|
ctx.strokeRect(frameX, frameY, frameW, frameH);
|
|
825
|
+
ctx.setLineDash([]);
|
|
826
|
+
if (flags.labels && bbox) {
|
|
827
|
+
ctx.font = "10px monospace";
|
|
828
|
+
ctx.fillStyle = "rgba(0,140,255,0.9)";
|
|
829
|
+
ctx.textBaseline = "top";
|
|
830
|
+
ctx.fillText(`frame ${frameW.toFixed(0)}\xD7${frameH.toFixed(0)}`, 4, 4);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (flags.contentBox && bbox) {
|
|
834
|
+
ctx.strokeStyle = "rgba(255,60,140,0.8)";
|
|
835
|
+
ctx.lineWidth = sw;
|
|
836
|
+
ctx.setLineDash([1, 2]);
|
|
837
|
+
ctx.strokeRect(bbox.x, bbox.y, bbox.width, bbox.height);
|
|
838
|
+
ctx.setLineDash([]);
|
|
839
|
+
if (flags.labels) {
|
|
840
|
+
const labelY = bbox.y + bbox.height + 4;
|
|
841
|
+
ctx.font = "10px monospace";
|
|
842
|
+
ctx.fillStyle = "rgba(255,60,140,0.9)";
|
|
843
|
+
ctx.textBaseline = "top";
|
|
844
|
+
ctx.fillText(`content ${bbox.width.toFixed(0)}\xD7${bbox.height.toFixed(0)}`, bbox.x, labelY);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (flags.contentBox && bbox && _width > 0 && _height > 0) {
|
|
848
|
+
const overflowX = bbox.width > _width;
|
|
849
|
+
const overflowY = bbox.height > _height;
|
|
850
|
+
if (overflowX || overflowY) {
|
|
851
|
+
ctx.font = "10px monospace";
|
|
852
|
+
ctx.fillStyle = "rgba(220,0,0,0.9)";
|
|
853
|
+
ctx.textBaseline = "top";
|
|
854
|
+
const warnParts = [];
|
|
855
|
+
if (overflowX)
|
|
856
|
+
warnParts.push(`\u0394x=${(bbox.width - _width).toFixed(0)}`);
|
|
857
|
+
if (overflowY)
|
|
858
|
+
warnParts.push(`\u0394y=${(bbox.height - _height).toFixed(0)}`);
|
|
859
|
+
ctx.fillText(`\u26A0 content overflow: ${warnParts.join(" ")}`, 4, _height + 4);
|
|
860
|
+
}
|
|
503
861
|
}
|
|
504
|
-
ctx.setLineDash([]);
|
|
505
862
|
for (const line of lines) {
|
|
506
863
|
const bx = line.x;
|
|
507
864
|
const by = line.y;
|
|
@@ -514,12 +871,12 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
|
|
|
514
871
|
}
|
|
515
872
|
if (flags.box) {
|
|
516
873
|
ctx.strokeStyle = "rgba(255,100,100,0.5)";
|
|
517
|
-
ctx.lineWidth =
|
|
874
|
+
ctx.lineWidth = sw;
|
|
518
875
|
ctx.strokeRect(bx, by, bw, bh);
|
|
519
876
|
}
|
|
520
877
|
if (flags.baseline) {
|
|
521
878
|
ctx.strokeStyle = "rgba(100,100,255,0.5)";
|
|
522
|
-
ctx.lineWidth =
|
|
879
|
+
ctx.lineWidth = sw;
|
|
523
880
|
ctx.beginPath();
|
|
524
881
|
ctx.moveTo(bx, baselineY);
|
|
525
882
|
ctx.lineTo(bx + bw, baselineY);
|
|
@@ -529,7 +886,7 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
|
|
|
529
886
|
const ascentY = baselineY - line.ascent;
|
|
530
887
|
const descentY = baselineY + line.descent;
|
|
531
888
|
ctx.strokeStyle = "rgba(100,255,100,0.4)";
|
|
532
|
-
ctx.lineWidth =
|
|
889
|
+
ctx.lineWidth = sw;
|
|
533
890
|
ctx.setLineDash([3, 2]);
|
|
534
891
|
ctx.beginPath();
|
|
535
892
|
ctx.moveTo(bx, ascentY);
|
|
@@ -558,17 +915,229 @@ function renderDebugToCanvas(ctx, lines, _width, _height, flags) {
|
|
|
558
915
|
const rw = span.width;
|
|
559
916
|
const rh = span.fontMetrics.ascent + span.fontMetrics.descent;
|
|
560
917
|
ctx.strokeStyle = "rgba(200,100,255,0.4)";
|
|
561
|
-
ctx.lineWidth =
|
|
918
|
+
ctx.lineWidth = sw;
|
|
562
919
|
ctx.strokeRect(rx, ry, rw, rh);
|
|
563
920
|
}
|
|
564
921
|
}
|
|
565
922
|
}
|
|
566
923
|
ctx.restore();
|
|
567
924
|
}
|
|
925
|
+
function renderSelection(ctx, lines, start, end, color = "rgba(100, 150, 255, 0.3)") {
|
|
926
|
+
ctx.save();
|
|
927
|
+
ctx.fillStyle = color;
|
|
928
|
+
const liMin = Math.min(start.lineIndex, end.lineIndex);
|
|
929
|
+
const liMax = Math.max(start.lineIndex, end.lineIndex);
|
|
930
|
+
for (let li = liMin;li <= liMax; li++) {
|
|
931
|
+
const line = lines[li];
|
|
932
|
+
if (!line)
|
|
933
|
+
continue;
|
|
934
|
+
const baselineY = line.y + line.baseline;
|
|
935
|
+
let xStart;
|
|
936
|
+
let xEnd;
|
|
937
|
+
if (li === liMin && li === liMax) {
|
|
938
|
+
xStart = Math.min(start.x, end.x);
|
|
939
|
+
xEnd = Math.max(start.x, end.x);
|
|
940
|
+
if (xEnd === xStart) {
|
|
941
|
+
const spansEnd = line.x + line.spans[line.spans.length - 1].x + line.spans[line.spans.length - 1].width;
|
|
942
|
+
xEnd = spansEnd;
|
|
943
|
+
}
|
|
944
|
+
} else if (li === liMin) {
|
|
945
|
+
xStart = start.x;
|
|
946
|
+
const lastSpan = line.spans[line.spans.length - 1];
|
|
947
|
+
xEnd = line.x + lastSpan.x + lastSpan.width;
|
|
948
|
+
} else if (li === liMax) {
|
|
949
|
+
xStart = line.x;
|
|
950
|
+
xEnd = end.x;
|
|
951
|
+
} else {
|
|
952
|
+
xStart = line.x;
|
|
953
|
+
const lastSpan = line.spans[line.spans.length - 1];
|
|
954
|
+
xEnd = line.x + lastSpan.x + lastSpan.width;
|
|
955
|
+
}
|
|
956
|
+
const y = baselineY - line.ascent;
|
|
957
|
+
const h = line.ascent + line.descent;
|
|
958
|
+
const w = xEnd - xStart;
|
|
959
|
+
if (w > 0) {
|
|
960
|
+
ctx.fillRect(xStart, y, w, h);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
ctx.restore();
|
|
964
|
+
}
|
|
965
|
+
function renderCursor(ctx, lines, pos, options = {}) {
|
|
966
|
+
const color = options.color ?? "#000";
|
|
967
|
+
const width = options.width ?? 1;
|
|
968
|
+
ctx.save();
|
|
969
|
+
let cursorHeight;
|
|
970
|
+
if (options.height !== undefined) {
|
|
971
|
+
cursorHeight = options.height;
|
|
972
|
+
} else {
|
|
973
|
+
const line = lines[pos.lineIndex];
|
|
974
|
+
cursorHeight = line.ascent + line.descent;
|
|
975
|
+
}
|
|
976
|
+
const topY = pos.y - (options.height !== undefined ? options.height : lines[pos.lineIndex].ascent);
|
|
977
|
+
ctx.strokeStyle = color;
|
|
978
|
+
ctx.lineWidth = width;
|
|
979
|
+
ctx.beginPath();
|
|
980
|
+
ctx.moveTo(pos.x, topY);
|
|
981
|
+
ctx.lineTo(pos.x, topY + cursorHeight);
|
|
982
|
+
ctx.stroke();
|
|
983
|
+
ctx.restore();
|
|
984
|
+
}
|
|
985
|
+
// src/interactive.ts
|
|
986
|
+
function getCharAdvances(span) {
|
|
987
|
+
if (span.glyphAdvances && span.glyphAdvances.length > 0) {
|
|
988
|
+
return Array.from(span.glyphAdvances);
|
|
989
|
+
}
|
|
990
|
+
const len = span.text.length;
|
|
991
|
+
if (len === 0)
|
|
992
|
+
return [];
|
|
993
|
+
const avgWidth = span.width / len;
|
|
994
|
+
return new Array(len).fill(avgWidth);
|
|
995
|
+
}
|
|
996
|
+
function buildCharSegments(lines) {
|
|
997
|
+
const segments = [];
|
|
998
|
+
for (let li = 0;li < lines.length; li++) {
|
|
999
|
+
const line = lines[li];
|
|
1000
|
+
const baselineY = line.y + line.baseline;
|
|
1001
|
+
for (let si = 0;si < line.spans.length; si++) {
|
|
1002
|
+
const span = line.spans[si];
|
|
1003
|
+
const advances = getCharAdvances(span);
|
|
1004
|
+
const text = span.text;
|
|
1005
|
+
let charX = line.x + span.x;
|
|
1006
|
+
for (let ci = 0;ci < text.length; ci++) {
|
|
1007
|
+
const advance = ci < advances.length ? advances[ci] : 0;
|
|
1008
|
+
segments.push({
|
|
1009
|
+
lineIndex: li,
|
|
1010
|
+
spanIndex: si,
|
|
1011
|
+
charIndex: ci,
|
|
1012
|
+
pIdx: span.pIdx,
|
|
1013
|
+
x: charX,
|
|
1014
|
+
y: baselineY,
|
|
1015
|
+
width: advance,
|
|
1016
|
+
char: text[ci],
|
|
1017
|
+
line,
|
|
1018
|
+
span
|
|
1019
|
+
});
|
|
1020
|
+
charX += advance;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return segments;
|
|
1025
|
+
}
|
|
1026
|
+
function charAtPoint(lines, px, py) {
|
|
1027
|
+
if (lines.length === 0)
|
|
1028
|
+
return null;
|
|
1029
|
+
let nearestLineIdx = 0;
|
|
1030
|
+
let minYDist = Infinity;
|
|
1031
|
+
for (let li = 0;li < lines.length; li++) {
|
|
1032
|
+
const line2 = lines[li];
|
|
1033
|
+
const baselineY2 = line2.y + line2.baseline;
|
|
1034
|
+
const yDist = Math.abs(py - baselineY2);
|
|
1035
|
+
const inLineBox = py >= line2.y && py <= line2.y + line2.height;
|
|
1036
|
+
const distance = inLineBox ? 0 : yDist;
|
|
1037
|
+
if (distance < minYDist) {
|
|
1038
|
+
minYDist = distance;
|
|
1039
|
+
nearestLineIdx = li;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const line = lines[nearestLineIdx];
|
|
1043
|
+
const baselineY = line.y + line.baseline;
|
|
1044
|
+
let nearestSpanIdx = 0;
|
|
1045
|
+
let nearestCharIdx = 0;
|
|
1046
|
+
let nearestX = line.x;
|
|
1047
|
+
let nearestWidth = 0;
|
|
1048
|
+
let nearestSpan = line.spans[0];
|
|
1049
|
+
let minXDist = Infinity;
|
|
1050
|
+
for (let si = 0;si < line.spans.length; si++) {
|
|
1051
|
+
const span = line.spans[si];
|
|
1052
|
+
const advances = getCharAdvances(span);
|
|
1053
|
+
const text = span.text;
|
|
1054
|
+
let charX = line.x + span.x;
|
|
1055
|
+
for (let ci = 0;ci < text.length; ci++) {
|
|
1056
|
+
const advance = ci < advances.length ? advances[ci] : 0;
|
|
1057
|
+
const charCenter = charX + advance / 2;
|
|
1058
|
+
const xDist = Math.abs(px - charCenter);
|
|
1059
|
+
if (xDist < minXDist) {
|
|
1060
|
+
minXDist = xDist;
|
|
1061
|
+
nearestSpanIdx = si;
|
|
1062
|
+
nearestCharIdx = ci;
|
|
1063
|
+
nearestX = charX;
|
|
1064
|
+
nearestWidth = advance;
|
|
1065
|
+
nearestSpan = span;
|
|
1066
|
+
}
|
|
1067
|
+
charX += advance;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
lineIndex: nearestLineIdx,
|
|
1072
|
+
spanIndex: nearestSpanIdx,
|
|
1073
|
+
charIndex: nearestCharIdx,
|
|
1074
|
+
pIdx: nearestSpan.pIdx,
|
|
1075
|
+
x: nearestX,
|
|
1076
|
+
y: baselineY,
|
|
1077
|
+
width: nearestWidth,
|
|
1078
|
+
line,
|
|
1079
|
+
span: nearestSpan
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function charIndexToPos(lines, charIndex) {
|
|
1083
|
+
const segments = buildCharSegments(lines);
|
|
1084
|
+
let globalIdx = 0;
|
|
1085
|
+
for (const seg of segments) {
|
|
1086
|
+
if (globalIdx === charIndex) {
|
|
1087
|
+
return {
|
|
1088
|
+
lineIndex: seg.lineIndex,
|
|
1089
|
+
spanIndex: seg.spanIndex,
|
|
1090
|
+
charIndex: seg.charIndex,
|
|
1091
|
+
pIdx: seg.pIdx,
|
|
1092
|
+
x: seg.x,
|
|
1093
|
+
y: seg.y,
|
|
1094
|
+
width: seg.width,
|
|
1095
|
+
line: seg.line,
|
|
1096
|
+
span: seg.span
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
globalIdx++;
|
|
1100
|
+
}
|
|
1101
|
+
if (charIndex >= globalIdx && segments.length > 0) {
|
|
1102
|
+
const last = segments[segments.length - 1];
|
|
1103
|
+
return {
|
|
1104
|
+
lineIndex: last.lineIndex,
|
|
1105
|
+
spanIndex: last.spanIndex,
|
|
1106
|
+
charIndex: last.charIndex + 1,
|
|
1107
|
+
pIdx: last.pIdx,
|
|
1108
|
+
x: last.x + last.width,
|
|
1109
|
+
y: last.y,
|
|
1110
|
+
width: 0,
|
|
1111
|
+
line: last.line,
|
|
1112
|
+
span: last.span
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
function posToCharIndex(lines, pos) {
|
|
1118
|
+
let index = 0;
|
|
1119
|
+
for (let li = 0;li < pos.lineIndex; li++) {
|
|
1120
|
+
const line2 = lines[li];
|
|
1121
|
+
for (const span of line2.spans) {
|
|
1122
|
+
index += span.text.length;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const line = lines[pos.lineIndex];
|
|
1126
|
+
for (let si = 0;si < pos.spanIndex; si++) {
|
|
1127
|
+
index += line.spans[si].text.length;
|
|
1128
|
+
}
|
|
1129
|
+
index += pos.charIndex;
|
|
1130
|
+
return index;
|
|
1131
|
+
}
|
|
568
1132
|
export {
|
|
569
1133
|
renderToSVG,
|
|
570
1134
|
renderToCanvas,
|
|
1135
|
+
renderSelection,
|
|
571
1136
|
renderResultToSVG,
|
|
572
1137
|
renderParagraphToSVG,
|
|
573
|
-
renderDebugToCanvas
|
|
1138
|
+
renderDebugToCanvas,
|
|
1139
|
+
renderCursor,
|
|
1140
|
+
posToCharIndex,
|
|
1141
|
+
charIndexToPos,
|
|
1142
|
+
charAtPoint
|
|
574
1143
|
};
|