@vyaz/core 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -0
- package/package.json +22 -5
- package/src/compile/DocumentCompiler.ts +11 -3
- package/src/index.browser.ts +90 -0
- package/src/index.ts +17 -0
- package/src/layout/ParagraphLayoutEngine.ts +106 -44
- package/src/layout/PositioningEngine.ts +249 -35
- package/src/layout/TextFrameLayoutEngine.ts +293 -21
- package/src/layout/estimateWidth.ts +74 -0
- package/src/measure/FontEngine.ts +144 -0
- package/src/measure/FontMetricsProvider.ts +74 -76
- package/src/measure/SystemFontRegistry.ts +15 -14
- package/src/measure/canvas-polyfill.ts +17 -9
- package/src/types/Document.ts +129 -3
- package/src/types/LayoutTypes.ts +24 -7
- package/src/utils/env.ts +18 -0
- package/src/utils/font.ts +28 -0
- package/src/utils/groupLinesByParagraph.ts +74 -0
- package/src/utils/list.ts +107 -0
- package/src/utils/textTransform.ts +96 -0
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
* without mutating ClusterData.advance)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { ParagraphStyle } from '../types/Document.js';
|
|
19
|
+
import type { ParagraphStyle, ListStyle, NumberFormat } from '../types/Document.js';
|
|
20
20
|
import type { FontMetrics } from '../types/FontTypes.js';
|
|
21
21
|
import type { Line, Span, SpanFontMetrics } from '../types/LayoutTypes.js';
|
|
22
22
|
import type { PreparedRichInlineItem } from '../compile/DocumentCompiler.js';
|
|
23
|
+
import type { MeasureFn } from './estimateWidth.js';
|
|
24
|
+
import { resolveFragmentWidths } from './estimateWidth.js';
|
|
25
|
+
import { formatListNumber, defaultBulletChar } from '../utils/list.js';
|
|
23
26
|
|
|
24
27
|
// ── Helper types for pretext ───────────────────────────────────────────
|
|
25
28
|
|
|
@@ -38,6 +41,48 @@ interface PretextLine {
|
|
|
38
41
|
end: { segmentIndex: number; graphemeIndex: number };
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// ── List marker helpers ─────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the marker text for a list item.
|
|
48
|
+
*/
|
|
49
|
+
function getMarkerText(listStyle: ListStyle, listIndex: number): string {
|
|
50
|
+
if (listStyle.type === 'bullet') {
|
|
51
|
+
return listStyle.bulletChar ?? defaultBulletChar(listStyle.level ?? 0);
|
|
52
|
+
}
|
|
53
|
+
if (listStyle.type === 'number') {
|
|
54
|
+
const fmt = listStyle.numberFormat ?? 'decimal';
|
|
55
|
+
return formatListNumber(listIndex, fmt) + '.';
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the paragraph-level font size for marker sizing.
|
|
62
|
+
* Uses the first child run's fontSize, or DEFAULT_TEXT_STYLE.fontSize.
|
|
63
|
+
*/
|
|
64
|
+
function getParagraphFontSize(items: PreparedRichInlineItem[]): number {
|
|
65
|
+
if (items.length === 0) return 12;
|
|
66
|
+
return items[0].metadata.style.fontSize ?? 12;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the effective bulletIndent for a paragraph.
|
|
71
|
+
* Uses listStyle.indents[level] if available, otherwise default * (level + 1).
|
|
72
|
+
*/
|
|
73
|
+
function resolveBulletIndent(
|
|
74
|
+
listStyle: ListStyle,
|
|
75
|
+
paraFontSize: number,
|
|
76
|
+
): number {
|
|
77
|
+
const level = listStyle.level ?? 0;
|
|
78
|
+
const indents = listStyle.indents;
|
|
79
|
+
if (indents && indents[level] !== undefined) {
|
|
80
|
+
return indents[level];
|
|
81
|
+
}
|
|
82
|
+
const defaultIndent = listStyle.bulletIndent ?? (paraFontSize * 1.5);
|
|
83
|
+
return defaultIndent * (level + 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
41
86
|
// ── PositioningEngine ─────────────────────────────────────────────────
|
|
42
87
|
|
|
43
88
|
/**
|
|
@@ -50,6 +95,14 @@ interface PretextLine {
|
|
|
50
95
|
* @param maxWidth — available container width
|
|
51
96
|
* @param startY — initial Y position
|
|
52
97
|
* @param mode — metric mode ('browser' | 'office'), affects line height calculation
|
|
98
|
+
* @param tag — optional tag for debugging
|
|
99
|
+
* @param measureText — function to measure text width accurately via fontkit.
|
|
100
|
+
* The function accepts (text, fontSize, fontFamily, fontWeight, fontStyle)
|
|
101
|
+
* and returns width in px. Throws FontNotFoundError if font not registered.
|
|
102
|
+
* @param listStyle — optional list configuration (bullet / numbered)
|
|
103
|
+
* @param listIndex — current index in the list (for numbered lists). 1-based.
|
|
104
|
+
* @param listMarkerWidth — pre-computed width of the widest marker in the list group.
|
|
105
|
+
* When provided, bulletIndent is expanded to this value if needed.
|
|
53
106
|
* @returns { lines: Line[], contentWidth: number }
|
|
54
107
|
*/
|
|
55
108
|
export function positionLines(
|
|
@@ -60,7 +113,11 @@ export function positionLines(
|
|
|
60
113
|
maxWidth: number,
|
|
61
114
|
startY: number = 0,
|
|
62
115
|
mode: 'browser' | 'office' = 'browser',
|
|
63
|
-
|
|
116
|
+
measureText: (text: string, fontSize: number, fontFamily?: string, fontWeight?: string, fontStyle?: string) => number,
|
|
117
|
+
tag?: string,
|
|
118
|
+
listStyle?: ListStyle,
|
|
119
|
+
listIndex?: number,
|
|
120
|
+
listMarkerWidth?: number,
|
|
64
121
|
): { lines: Line[]; contentWidth: number } {
|
|
65
122
|
const lines: Line[] = [];
|
|
66
123
|
let currentY = startY + style.spaceBefore;
|
|
@@ -68,6 +125,41 @@ export function positionLines(
|
|
|
68
125
|
let isFirstLine = true;
|
|
69
126
|
let contentWidth = 0;
|
|
70
127
|
|
|
128
|
+
// ── Pre-compute marker-related values ──────────────────────────
|
|
129
|
+
const isListItem = listStyle && listStyle.type !== 'none' && listIndex !== undefined;
|
|
130
|
+
let markerText = '';
|
|
131
|
+
let markerWidth = 0;
|
|
132
|
+
let bulletZoneIndent = 0;
|
|
133
|
+
let effectiveLeftIndent = style.leftIndent ?? 0;
|
|
134
|
+
|
|
135
|
+
if (isListItem) {
|
|
136
|
+
markerText = getMarkerText(listStyle!, listIndex!);
|
|
137
|
+
const paraFontSize = getParagraphFontSize(items);
|
|
138
|
+
bulletZoneIndent = resolveBulletIndent(listStyle!, paraFontSize);
|
|
139
|
+
|
|
140
|
+
// Expand bulletIndent to fit the widest marker in the list group
|
|
141
|
+
if (listMarkerWidth !== undefined && listMarkerWidth > bulletZoneIndent) {
|
|
142
|
+
bulletZoneIndent = listMarkerWidth;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Measure marker width using paragraph's first run font
|
|
146
|
+
const firstRunFontFamily = items[0]?.metadata.style.fontFamily ?? 'Arial';
|
|
147
|
+
const firstRunFontWeight = String(items[0]?.metadata.style.fontWeight ?? 400);
|
|
148
|
+
const firstRunFontStyle = items[0]?.metadata.style.fontStyle ?? 'normal';
|
|
149
|
+
|
|
150
|
+
// Marker font-size: same as paragraph font size for numbered,
|
|
151
|
+
// slightly smaller (0.9x) for bullets (PowerPoint convention)
|
|
152
|
+
const markerFontSize = listStyle!.type === 'bullet'
|
|
153
|
+
? paraFontSize * 0.9
|
|
154
|
+
: paraFontSize;
|
|
155
|
+
markerWidth = measureText(markerText, markerFontSize, firstRunFontFamily, firstRunFontWeight, firstRunFontStyle);
|
|
156
|
+
|
|
157
|
+
// For 'outside': text block is shifted right by bulletZoneIndent
|
|
158
|
+
if (listStyle!.position !== 'inside') {
|
|
159
|
+
effectiveLeftIndent += bulletZoneIndent;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
71
163
|
for (let lineIdx = 0; lineIdx < pretextLines.length; lineIdx++) {
|
|
72
164
|
const ptLine = pretextLines[lineIdx];
|
|
73
165
|
|
|
@@ -82,8 +174,12 @@ export function positionLines(
|
|
|
82
174
|
if (!item) continue;
|
|
83
175
|
|
|
84
176
|
const metrics = fontMetricsFn(item);
|
|
85
|
-
|
|
86
|
-
|
|
177
|
+
// baselineOffset: superscript → negative (glyph moves up).
|
|
178
|
+
// ascent must increase (glyph above baseline), descent must decrease (less overhang below).
|
|
179
|
+
// subscript → positive (glyph moves down).
|
|
180
|
+
// ascent must decrease, descent must increase.
|
|
181
|
+
const effectiveAscent = metrics.ascent - (item.metadata.baselineOffset || 0);
|
|
182
|
+
const effectiveDescent = metrics.descent + (item.metadata.baselineOffset || 0);
|
|
87
183
|
|
|
88
184
|
maxAscent = Math.max(maxAscent, effectiveAscent);
|
|
89
185
|
maxDescent = Math.max(maxDescent, effectiveDescent);
|
|
@@ -109,7 +205,8 @@ export function positionLines(
|
|
|
109
205
|
width: gapWidth,
|
|
110
206
|
text: ' ',
|
|
111
207
|
itemIndex: frag.itemIndex,
|
|
112
|
-
|
|
208
|
+
pIdx: 0,
|
|
209
|
+
tag,
|
|
113
210
|
fontMetrics: baseFontMetrics,
|
|
114
211
|
style: item.metadata.style,
|
|
115
212
|
inlineWidget: item.metadata.inlineWidget,
|
|
@@ -122,7 +219,6 @@ export function positionLines(
|
|
|
122
219
|
const text = frag.text;
|
|
123
220
|
const leadingMatch = text.match(/^(\s+)/);
|
|
124
221
|
|
|
125
|
-
const totalChars = text.length;
|
|
126
222
|
let remainingText = text;
|
|
127
223
|
let leadingSpaceChars = 0;
|
|
128
224
|
let trailingSpaceChars = 0;
|
|
@@ -139,18 +235,37 @@ export function positionLines(
|
|
|
139
235
|
}
|
|
140
236
|
}
|
|
141
237
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
238
|
+
// Resolve widths via resolveFragmentWidths:
|
|
239
|
+
// exact measurement (measureText) when available,
|
|
240
|
+
// weight-based fallback otherwise,
|
|
241
|
+
// then correctToSumInvariant to preserve line-breaking invariant.
|
|
242
|
+
const fragments: string[] = [];
|
|
243
|
+
if (leadingSpaceChars > 0) fragments.push(text.slice(0, leadingSpaceChars));
|
|
244
|
+
if (remainingText.length > 0) fragments.push(remainingText);
|
|
245
|
+
if (trailingSpaceChars > 0) fragments.push(text.slice(leadingSpaceChars + remainingText.length));
|
|
246
|
+
|
|
247
|
+
// Build measure function with font parameters baked in
|
|
248
|
+
const { fontFamily, fontWeight, fontStyle } = item.metadata.style;
|
|
249
|
+
const fsWeight = String(fontWeight || 400);
|
|
250
|
+
const fsStyle = fontStyle || 'normal';
|
|
251
|
+
const fragmentMeasureFn: MeasureFn = (t: string) => measureText(t, baseFontMetrics.fontSize, fontFamily, fsWeight, fsStyle);
|
|
252
|
+
|
|
253
|
+
const resolvedWidths = resolveFragmentWidths(fragments, text, textWidth, fragmentMeasureFn);
|
|
254
|
+
let resolvedIdx = 0;
|
|
255
|
+
const leadingWidth = leadingSpaceChars > 0 ? resolvedWidths[resolvedIdx++] : 0;
|
|
256
|
+
const trimmedWidth = remainingText.length > 0 ? resolvedWidths[resolvedIdx++] : 0;
|
|
257
|
+
const trailingWidthVal = trailingSpaceChars > 0 ? resolvedWidths[resolvedIdx++] : 0;
|
|
145
258
|
|
|
146
259
|
// Leading space span
|
|
147
260
|
if (leadingSpaceChars > 0) {
|
|
261
|
+
const leadingText = text.slice(0, leadingSpaceChars);
|
|
148
262
|
spans.push({
|
|
149
263
|
x: 0,
|
|
150
|
-
width:
|
|
151
|
-
text:
|
|
264
|
+
width: leadingWidth,
|
|
265
|
+
text: leadingText,
|
|
152
266
|
itemIndex: frag.itemIndex,
|
|
153
|
-
|
|
267
|
+
pIdx: 0,
|
|
268
|
+
tag,
|
|
154
269
|
fontMetrics: baseFontMetrics,
|
|
155
270
|
style: item.metadata.style,
|
|
156
271
|
inlineWidget: item.metadata.inlineWidget,
|
|
@@ -160,40 +275,32 @@ export function positionLines(
|
|
|
160
275
|
|
|
161
276
|
// Text span (trimmed)
|
|
162
277
|
if (remainingText.length > 0) {
|
|
163
|
-
const
|
|
164
|
-
// Compute per-glyph advances by distributing textWidth equally across glyphs.
|
|
165
|
-
// For monospace fonts this is exact; for proportional it's a linear approximation.
|
|
166
|
-
// The FontMetricsProvider can later supply real glyph advances when available.
|
|
167
|
-
const glyphAdvances: number[] = [];
|
|
168
|
-
if (remainingText.length > 1) {
|
|
169
|
-
const perGlyph = textWidth / remainingText.length;
|
|
170
|
-
for (let i = 0; i < remainingText.length; i++) {
|
|
171
|
-
glyphAdvances.push(perGlyph);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
278
|
+
const actualTextWidth = trimmedWidth;
|
|
174
279
|
spans.push({
|
|
175
280
|
x: 0,
|
|
176
|
-
width:
|
|
281
|
+
width: actualTextWidth,
|
|
177
282
|
text: remainingText,
|
|
178
283
|
itemIndex: frag.itemIndex,
|
|
179
|
-
|
|
284
|
+
pIdx: 0,
|
|
285
|
+
tag,
|
|
180
286
|
fontMetrics: baseFontMetrics,
|
|
181
287
|
style: item.metadata.style,
|
|
182
288
|
inlineWidget: item.metadata.inlineWidget,
|
|
183
289
|
type: 'text',
|
|
184
|
-
glyphAdvances: glyphAdvances.length > 0 ? glyphAdvances : undefined,
|
|
185
290
|
});
|
|
186
291
|
}
|
|
187
292
|
|
|
188
293
|
// Trailing space span
|
|
189
294
|
if (trailingSpaceChars > 0) {
|
|
190
295
|
const trailingStart = leadingSpaceChars + remainingText.length;
|
|
296
|
+
const trailingText = text.slice(trailingStart, trailingStart + trailingSpaceChars);
|
|
191
297
|
spans.push({
|
|
192
298
|
x: 0,
|
|
193
|
-
width:
|
|
194
|
-
text:
|
|
299
|
+
width: trailingWidthVal,
|
|
300
|
+
text: trailingText,
|
|
195
301
|
itemIndex: frag.itemIndex,
|
|
196
|
-
|
|
302
|
+
pIdx: 0,
|
|
303
|
+
tag,
|
|
197
304
|
fontMetrics: baseFontMetrics,
|
|
198
305
|
style: item.metadata.style,
|
|
199
306
|
inlineWidget: item.metadata.inlineWidget,
|
|
@@ -211,6 +318,80 @@ export function positionLines(
|
|
|
211
318
|
}
|
|
212
319
|
}
|
|
213
320
|
|
|
321
|
+
// ── Prepare marker data (only on first line) ──────────────────
|
|
322
|
+
// For 'inside': marker is inserted into the span flow before X positioning.
|
|
323
|
+
// For 'outside': marker is added after X positioning with a custom x in the bullet zone.
|
|
324
|
+
let outsideMarkerSpan: Span | null = null;
|
|
325
|
+
|
|
326
|
+
if (isListItem && isFirstLine) {
|
|
327
|
+
const paraFontSize = getParagraphFontSize(items);
|
|
328
|
+
const firstRun = items[0];
|
|
329
|
+
const markerFontSize = listStyle!.type === 'bullet' ? paraFontSize * 0.9 : paraFontSize;
|
|
330
|
+
const markerAscent = markerFontSize * 0.8; // approximate ascent for marker
|
|
331
|
+
const markerDescent = markerFontSize * 0.2;
|
|
332
|
+
|
|
333
|
+
maxAscent = Math.max(maxAscent, markerAscent);
|
|
334
|
+
maxDescent = Math.max(maxDescent, markerDescent);
|
|
335
|
+
maxLineHeightBase = Math.max(maxLineHeightBase, markerAscent + markerDescent);
|
|
336
|
+
|
|
337
|
+
// Create a style object for the marker span
|
|
338
|
+
const markerStyle = {
|
|
339
|
+
...(firstRun?.metadata.style ?? {
|
|
340
|
+
fontFamily: 'Arial',
|
|
341
|
+
fontSize: markerFontSize,
|
|
342
|
+
fontWeight: 400,
|
|
343
|
+
fontStyle: 'normal' as const,
|
|
344
|
+
color: '#000000',
|
|
345
|
+
type: 'text' as const,
|
|
346
|
+
text: markerText,
|
|
347
|
+
}),
|
|
348
|
+
fontSize: markerFontSize,
|
|
349
|
+
text: markerText,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const markerSpanBase: Span = {
|
|
353
|
+
x: 0,
|
|
354
|
+
width: markerWidth,
|
|
355
|
+
text: markerText,
|
|
356
|
+
itemIndex: 0,
|
|
357
|
+
pIdx: 0,
|
|
358
|
+
tag,
|
|
359
|
+
fontMetrics: {
|
|
360
|
+
ascent: markerAscent,
|
|
361
|
+
descent: markerDescent,
|
|
362
|
+
fontSize: markerFontSize,
|
|
363
|
+
},
|
|
364
|
+
style: markerStyle,
|
|
365
|
+
type: 'marker',
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// For 'inside': marker is part of text flow — insert at beginning of spans
|
|
369
|
+
// For 'outside': store for later (after X positioning)
|
|
370
|
+
if (listStyle!.position === 'inside') {
|
|
371
|
+
spans.unshift(markerSpanBase);
|
|
372
|
+
// Add a small gap after the marker (0.5em)
|
|
373
|
+
const gapWidth = markerFontSize * 0.5;
|
|
374
|
+
spans.splice(1, 0, {
|
|
375
|
+
x: 0,
|
|
376
|
+
width: gapWidth,
|
|
377
|
+
text: ' ',
|
|
378
|
+
itemIndex: 0,
|
|
379
|
+
pIdx: 0,
|
|
380
|
+
tag,
|
|
381
|
+
fontMetrics: {
|
|
382
|
+
ascent: 0,
|
|
383
|
+
descent: 0,
|
|
384
|
+
fontSize: markerFontSize,
|
|
385
|
+
},
|
|
386
|
+
style: markerStyle,
|
|
387
|
+
type: 'space',
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
// 'outside' (default): marker sits in the bullet zone, left of text
|
|
391
|
+
outsideMarkerSpan = markerSpanBase;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
214
395
|
// ── Mark trailing whitespace spans ───────────────────
|
|
215
396
|
// CSS Text §4.1.3: end-of-line spaces have zero measure for line-advance calculations.
|
|
216
397
|
// Parley: LineItemData.has_trailing_whitespace → trailing whitespace is excluded from advance.
|
|
@@ -242,8 +423,8 @@ export function positionLines(
|
|
|
242
423
|
|
|
243
424
|
// ── X positioning ───────────────────────────────
|
|
244
425
|
const indent = isFirstLine
|
|
245
|
-
?
|
|
246
|
-
:
|
|
426
|
+
? effectiveLeftIndent + (style.indent || 0)
|
|
427
|
+
: effectiveLeftIndent;
|
|
247
428
|
const rightIndent = style.rightIndent || 0;
|
|
248
429
|
const availableWidth = maxWidth - indent - rightIndent;
|
|
249
430
|
|
|
@@ -293,8 +474,37 @@ export function positionLines(
|
|
|
293
474
|
runX += frag.width;
|
|
294
475
|
}
|
|
295
476
|
|
|
296
|
-
|
|
297
|
-
|
|
477
|
+
// ── Outside marker: place in the bullet zone (left of text) ──
|
|
478
|
+
// Marker is right-aligned within the bullet zone so multi-digit
|
|
479
|
+
// numbers ("9." vs "10.") share the same right edge.
|
|
480
|
+
// CSS list-style-position: outside — marker is outside the principal box.
|
|
481
|
+
// OOXML a:buFont / a:buChar — bullet sits in the indent zone.
|
|
482
|
+
if (outsideMarkerSpan) {
|
|
483
|
+
// Right edge of the bullet zone = xOffset (start of text)
|
|
484
|
+
// Marker right-aligned: x = xOffset - markerWidth - small gap
|
|
485
|
+
const gap = outsideMarkerSpan.fontMetrics.fontSize * 0.25; // 0.25em gap
|
|
486
|
+
outsideMarkerSpan.x = Math.round((xOffset - outsideMarkerSpan.width - gap) * 100) / 100;
|
|
487
|
+
// Prepend so it appears first in the spans array
|
|
488
|
+
spans.unshift(outsideMarkerSpan);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Line box must cover ALL spans, including outside markers that sit
|
|
492
|
+
// left of the principal text box. Otherwise contentWidth / computeBBox
|
|
493
|
+
// miss the marker and content sizing clips it.
|
|
494
|
+
let lineX = xOffset;
|
|
495
|
+
let lineWidth = runX - xOffset;
|
|
496
|
+
if (spans.length > 0) {
|
|
497
|
+
const minSpanX = Math.min(...spans.map(s => s.x));
|
|
498
|
+
const maxSpanRight = Math.max(...spans.map(s => s.x + s.width));
|
|
499
|
+
lineX = minSpanX;
|
|
500
|
+
lineWidth = maxSpanRight - minSpanX;
|
|
501
|
+
}
|
|
502
|
+
// contentWidth is the absolute right edge of content (not just line.width).
|
|
503
|
+
// When line.x > 0 (outside marker, indent, center/right), consumers that
|
|
504
|
+
// size the canvas as contentWidth with viewBox origin 0 need this value.
|
|
505
|
+
contentWidth = Math.max(contentWidth, lineX + lineWidth);
|
|
506
|
+
|
|
507
|
+
|
|
298
508
|
|
|
299
509
|
// ── Y positioning ───────────────────────────────
|
|
300
510
|
// Line height algorithm depends on mode:
|
|
@@ -356,7 +566,10 @@ export function positionLines(
|
|
|
356
566
|
// Count characters in line (for INDEX_CONSIST)
|
|
357
567
|
let lineCharCount = 0;
|
|
358
568
|
for (const frag of spans) {
|
|
359
|
-
|
|
569
|
+
// Marker spans don't count toward the paragraph's character index
|
|
570
|
+
if (frag.type !== 'marker') {
|
|
571
|
+
lineCharCount += frag.text.length;
|
|
572
|
+
}
|
|
360
573
|
}
|
|
361
574
|
const endIdx = startIdx + lineCharCount;
|
|
362
575
|
charIndex = endIdx;
|
|
@@ -377,7 +590,7 @@ export function positionLines(
|
|
|
377
590
|
}
|
|
378
591
|
|
|
379
592
|
lines.push({
|
|
380
|
-
x: Math.round(
|
|
593
|
+
x: Math.round(lineX * 100) / 100,
|
|
381
594
|
y: Math.round(currentY * 100) / 100,
|
|
382
595
|
width: Math.round(lineWidth * 100) / 100,
|
|
383
596
|
height: Math.round(lineBoxHeight * 100) / 100,
|
|
@@ -390,6 +603,7 @@ export function positionLines(
|
|
|
390
603
|
spans,
|
|
391
604
|
});
|
|
392
605
|
|
|
606
|
+
|
|
393
607
|
currentY += lineBoxHeight;
|
|
394
608
|
isFirstLine = false;
|
|
395
609
|
}
|