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