postext 0.3.6 → 0.3.8

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.
Files changed (137) hide show
  1. package/dist/__tests__/exports.test.js +3 -0
  2. package/dist/__tests__/exports.test.js.map +1 -1
  3. package/dist/canvas-backend/blockRender.d.ts +3 -0
  4. package/dist/canvas-backend/blockRender.d.ts.map +1 -0
  5. package/dist/canvas-backend/blockRender.js +176 -0
  6. package/dist/canvas-backend/blockRender.js.map +1 -0
  7. package/dist/canvas-backend/decorations.d.ts +6 -0
  8. package/dist/canvas-backend/decorations.d.ts.map +1 -0
  9. package/dist/canvas-backend/decorations.js +106 -0
  10. package/dist/canvas-backend/decorations.js.map +1 -0
  11. package/dist/{canvas-backend.d.ts → canvas-backend/index.d.ts} +2 -2
  12. package/dist/canvas-backend/index.d.ts.map +1 -0
  13. package/dist/canvas-backend/index.js +66 -0
  14. package/dist/canvas-backend/index.js.map +1 -0
  15. package/dist/defaults/index.d.ts +1 -0
  16. package/dist/defaults/index.d.ts.map +1 -1
  17. package/dist/defaults/index.js +9 -0
  18. package/dist/defaults/index.js.map +1 -1
  19. package/dist/defaults/pdfGeneration.d.ts +5 -0
  20. package/dist/defaults/pdfGeneration.d.ts.map +1 -0
  21. package/dist/defaults/pdfGeneration.js +37 -0
  22. package/dist/defaults/pdfGeneration.js.map +1 -0
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/knuthPlass/breakpoints.d.ts +6 -0
  28. package/dist/knuthPlass/breakpoints.d.ts.map +1 -0
  29. package/dist/knuthPlass/breakpoints.js +284 -0
  30. package/dist/knuthPlass/breakpoints.js.map +1 -0
  31. package/dist/knuthPlass/constants.d.ts +8 -0
  32. package/dist/knuthPlass/constants.d.ts.map +1 -0
  33. package/dist/knuthPlass/constants.js +8 -0
  34. package/dist/knuthPlass/constants.js.map +1 -0
  35. package/dist/knuthPlass/index.d.ts +12 -0
  36. package/dist/knuthPlass/index.d.ts.map +1 -0
  37. package/dist/knuthPlass/index.js +11 -0
  38. package/dist/knuthPlass/index.js.map +1 -0
  39. package/dist/knuthPlass/pretextAdapter.d.ts +12 -0
  40. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -0
  41. package/dist/knuthPlass/pretextAdapter.js +178 -0
  42. package/dist/knuthPlass/pretextAdapter.js.map +1 -0
  43. package/dist/knuthPlass/richAdapter.d.ts +26 -0
  44. package/dist/knuthPlass/richAdapter.d.ts.map +1 -0
  45. package/dist/knuthPlass/richAdapter.js +189 -0
  46. package/dist/knuthPlass/richAdapter.js.map +1 -0
  47. package/dist/{knuthPlass.d.ts → knuthPlass/types.d.ts} +1 -31
  48. package/dist/knuthPlass/types.d.ts.map +1 -0
  49. package/dist/knuthPlass/types.js +5 -0
  50. package/dist/knuthPlass/types.js.map +1 -0
  51. package/dist/knuthPlass/utils.d.ts +2 -0
  52. package/dist/knuthPlass/utils.d.ts.map +1 -0
  53. package/dist/knuthPlass/utils.js +4 -0
  54. package/dist/knuthPlass/utils.js.map +1 -0
  55. package/dist/measure/cache.d.ts +5 -0
  56. package/dist/measure/cache.d.ts.map +1 -0
  57. package/dist/measure/cache.js +38 -0
  58. package/dist/measure/cache.js.map +1 -0
  59. package/dist/measure/canvas.d.ts +8 -0
  60. package/dist/measure/canvas.d.ts.map +1 -0
  61. package/dist/measure/canvas.js +30 -0
  62. package/dist/measure/canvas.js.map +1 -0
  63. package/dist/measure/font.d.ts +19 -0
  64. package/dist/measure/font.d.ts.map +1 -0
  65. package/dist/measure/font.js +34 -0
  66. package/dist/measure/font.js.map +1 -0
  67. package/dist/measure/index.d.ts +7 -0
  68. package/dist/measure/index.d.ts.map +1 -0
  69. package/dist/measure/index.js +6 -0
  70. package/dist/measure/index.js.map +1 -0
  71. package/dist/measure/plain.d.ts +13 -0
  72. package/dist/measure/plain.d.ts.map +1 -0
  73. package/dist/measure/plain.js +167 -0
  74. package/dist/measure/plain.js.map +1 -0
  75. package/dist/measure/rich.d.ts +25 -0
  76. package/dist/measure/rich.d.ts.map +1 -0
  77. package/dist/measure/rich.js +246 -0
  78. package/dist/measure/rich.js.map +1 -0
  79. package/dist/measure/types.d.ts +28 -0
  80. package/dist/measure/types.d.ts.map +1 -0
  81. package/dist/measure/types.js +2 -0
  82. package/dist/measure/types.js.map +1 -0
  83. package/dist/parse/blockParser.d.ts +28 -0
  84. package/dist/parse/blockParser.d.ts.map +1 -0
  85. package/dist/parse/blockParser.js +302 -0
  86. package/dist/parse/blockParser.js.map +1 -0
  87. package/dist/parse/index.d.ts +4 -0
  88. package/dist/parse/index.d.ts.map +1 -0
  89. package/dist/parse/index.js +3 -0
  90. package/dist/parse/index.js.map +1 -0
  91. package/dist/parse/inlineFormatting.d.ts +12 -0
  92. package/dist/parse/inlineFormatting.d.ts.map +1 -0
  93. package/dist/parse/inlineFormatting.js +90 -0
  94. package/dist/parse/inlineFormatting.js.map +1 -0
  95. package/dist/parse/inlineMath.d.ts +29 -0
  96. package/dist/parse/inlineMath.d.ts.map +1 -0
  97. package/dist/parse/inlineMath.js +141 -0
  98. package/dist/parse/inlineMath.js.map +1 -0
  99. package/dist/parse/sourceMapping.d.ts +12 -0
  100. package/dist/parse/sourceMapping.d.ts.map +1 -0
  101. package/dist/parse/sourceMapping.js +130 -0
  102. package/dist/parse/sourceMapping.js.map +1 -0
  103. package/dist/{parse.d.ts → parse/types.d.ts} +2 -25
  104. package/dist/parse/types.d.ts.map +1 -0
  105. package/dist/parse/types.js +2 -0
  106. package/dist/parse/types.js.map +1 -0
  107. package/dist/pipeline/build.d.ts.map +1 -1
  108. package/dist/pipeline/build.js +34 -276
  109. package/dist/pipeline/build.js.map +1 -1
  110. package/dist/pipeline/buildBlockKind.d.ts +33 -0
  111. package/dist/pipeline/buildBlockKind.d.ts.map +1 -0
  112. package/dist/pipeline/buildBlockKind.js +82 -0
  113. package/dist/pipeline/buildBlockKind.js.map +1 -0
  114. package/dist/pipeline/buildHelpers.d.ts +47 -0
  115. package/dist/pipeline/buildHelpers.d.ts.map +1 -0
  116. package/dist/pipeline/buildHelpers.js +169 -0
  117. package/dist/pipeline/buildHelpers.js.map +1 -0
  118. package/dist/pipeline/buildMeasurement.d.ts +27 -0
  119. package/dist/pipeline/buildMeasurement.d.ts.map +1 -0
  120. package/dist/pipeline/buildMeasurement.js +49 -0
  121. package/dist/pipeline/buildMeasurement.js.map +1 -0
  122. package/dist/types.d.ts +15 -0
  123. package/dist/types.d.ts.map +1 -1
  124. package/package.json +1 -1
  125. package/dist/canvas-backend.d.ts.map +0 -1
  126. package/dist/canvas-backend.js +0 -370
  127. package/dist/canvas-backend.js.map +0 -1
  128. package/dist/knuthPlass.d.ts.map +0 -1
  129. package/dist/knuthPlass.js +0 -665
  130. package/dist/knuthPlass.js.map +0 -1
  131. package/dist/measure.d.ts +0 -61
  132. package/dist/measure.d.ts.map +0 -1
  133. package/dist/measure.js +0 -512
  134. package/dist/measure.js.map +0 -1
  135. package/dist/parse.d.ts.map +0 -1
  136. package/dist/parse.js +0 -653
  137. package/dist/parse.js.map +0 -1
@@ -1,665 +0,0 @@
1
- /**
2
- * Knuth-Plass optimal paragraph line-breaking algorithm.
3
- *
4
- * Replaces the greedy first-fit line breaker with a dynamic-programming
5
- * approach that minimizes total demerits across all lines of a paragraph,
6
- * producing more even word-spacing and fewer "loose" lines.
7
- */
8
- import { createBoundingBox } from './vdt';
9
- // ---------------------------------------------------------------------------
10
- // Constants
11
- // ---------------------------------------------------------------------------
12
- const KP_INFINITY = 10000;
13
- const HYPHEN_PENALTY = 50;
14
- const DEFAULT_CONSECUTIVE_HYPHEN_DEMERIT = 3000;
15
- const DEFAULT_FITNESS_CLASS_DEMERIT = 100;
16
- const BADNESS_CAP = 10000;
17
- const MAX_STRETCH = Number.MAX_SAFE_INTEGER;
18
- // ---------------------------------------------------------------------------
19
- // Helpers
20
- // ---------------------------------------------------------------------------
21
- function classifyFitness(r) {
22
- if (r < -0.5)
23
- return 0; // tight
24
- if (r < 0.5)
25
- return 1; // normal
26
- if (r < 1.0)
27
- return 2; // loose
28
- return 3; // very loose
29
- }
30
- function computeBadness(r) {
31
- const absR = Math.abs(r);
32
- const b = Math.round(100 * absR * absR * absR);
33
- return Math.min(b, BADNESS_CAP);
34
- }
35
- // ---------------------------------------------------------------------------
36
- // Core DP: compute optimal breakpoints
37
- // ---------------------------------------------------------------------------
38
- export function computeBreakpoints(items, options) {
39
- const { lineWidth, consecutiveHyphenDemerit = DEFAULT_CONSECUTIVE_HYPHEN_DEMERIT, fitnessClassDemerit = DEFAULT_FITNESS_CLASS_DEMERIT, runtPenalty = 0, runtMinWidth = 0, } = options;
40
- const terminalBreakPosition = items.length - 1;
41
- // Running totals up to current item
42
- let sumWidth = 0;
43
- let sumStretch = 0;
44
- let sumShrink = 0;
45
- // Prefix sums: sumWidthAt[i] = sum of widths of items 0..i-1
46
- // We build these incrementally and store for the range computations.
47
- const sumWidthAt = [0];
48
- const sumStretchAt = [0];
49
- const sumShrinkAt = [0];
50
- for (const item of items) {
51
- if (item.type === 'box') {
52
- sumWidth += item.width;
53
- }
54
- else if (item.type === 'glue') {
55
- sumWidth += item.width;
56
- sumStretch += item.stretch;
57
- sumShrink += item.shrink;
58
- }
59
- // Penalties don't contribute to running width/stretch/shrink
60
- sumWidthAt.push(sumWidth);
61
- sumStretchAt.push(sumStretch);
62
- sumShrinkAt.push(sumShrink);
63
- }
64
- // Seed active node list with a "start of paragraph" sentinel
65
- let activeNodes = [{
66
- position: -1,
67
- line: 0,
68
- fitnessClass: 1,
69
- totalWidth: 0,
70
- totalStretch: 0,
71
- totalShrink: 0,
72
- totalDemerits: 0,
73
- previous: null,
74
- }];
75
- for (let i = 0; i < items.length; i++) {
76
- const item = items[i];
77
- // Determine if this position is a legal breakpoint
78
- let isLegalBreak = false;
79
- if (item.type === 'glue') {
80
- // Glue breaks if preceded by a box
81
- if (i > 0 && items[i - 1].type === 'box') {
82
- isLegalBreak = true;
83
- }
84
- }
85
- else if (item.type === 'penalty') {
86
- if (item.penalty < KP_INFINITY) {
87
- isLegalBreak = true;
88
- }
89
- }
90
- if (!isLegalBreak)
91
- continue;
92
- // Width contribution of breaking at this item
93
- const breakWidth = item.type === 'penalty' ? item.width : 0;
94
- const isFlagged = item.type === 'penalty' && item.flagged;
95
- // For glue breakpoints, the line content goes up to (but excluding) this glue.
96
- // For penalty breakpoints, the line content goes up to this penalty.
97
- // The content width from after active node a to break at i:
98
- // If glue: sumWidthAt[i] - a.totalWidth (excludes glue itself)
99
- // If penalty: sumWidthAt[i] - a.totalWidth (sumWidthAt doesn't include penalty width)
100
- // Plus breakWidth for penalties.
101
- // Compute the content width from the items between a.position and i.
102
- // After the break at active node a.position:
103
- // - If a.position was a glue break, the next line starts at the item AFTER the glue
104
- // - If a.position was a penalty break, the next line starts at the item AFTER the penalty
105
- // - For the sentinel (-1), the line starts at item 0
106
- // Total width from start to just before item i (excluding this glue/penalty's own width):
107
- // For a glue at i: line content = sumWidthAt[i] (which includes all box+glue widths before i)
108
- // but we need to subtract the width of glues/boxes after the previous break.
109
- // Actually sumWidthAt[i] already accounts for that.
110
- // We subtract a.totalWidth which is sumWidthAt at the point right after a's break.
111
- const newNodes = [];
112
- const toDeactivate = new Set();
113
- for (let ai = 0; ai < activeNodes.length; ai++) {
114
- const a = activeNodes[ai];
115
- // Line content width: width of boxes and glues from after a's break to this break.
116
- // For glue break: content is items from (a.position+1) to (i-1) inclusive + breakWidth
117
- // = sumWidthAt[i] - a.totalWidth
118
- // For penalty break: content is items from (a.position+1) to (i-1) inclusive + penalty.width
119
- // = sumWidthAt[i] - a.totalWidth + breakWidth
120
- // Note: sumWidthAt[i] does NOT include item i if it's a penalty.
121
- // sumWidthAt[i] includes item i's width only for box/glue through the push.
122
- // Wait — re-examine: sumWidthAt[i] is the prefix sum BEFORE item i is processed.
123
- // Actually, sumWidthAt has length items.length + 1. sumWidthAt[0] = 0, sumWidthAt[k+1] includes item k.
124
- // So sumWidthAt[i] includes items 0..i-1, and sumWidthAt[i+1] includes items 0..i.
125
- // For a glue break at position i:
126
- // The line goes from after a's break to just before this glue.
127
- // Content width = sumWidthAt[i] - a.totalWidth
128
- // (sumWidthAt[i] includes items 0..i-1, which covers everything up to but not including the glue)
129
- //
130
- // For a penalty break at position i:
131
- // The line includes everything up to but not including the penalty, plus breakWidth
132
- // Content width = sumWidthAt[i] - a.totalWidth + breakWidth
133
- const contentWidth = sumWidthAt[i] - a.totalWidth + breakWidth;
134
- const available = lineWidth(a.line);
135
- const slack = available - contentWidth;
136
- // Stretch/shrink from the items on this line
137
- const lineStretch = sumStretchAt[i] - a.totalStretch;
138
- const lineShrink = sumShrinkAt[i] - a.totalShrink;
139
- let r;
140
- if (slack >= 0) {
141
- r = lineStretch > 0 ? slack / lineStretch : KP_INFINITY;
142
- }
143
- else {
144
- r = lineShrink > 0 ? slack / lineShrink : -KP_INFINITY;
145
- }
146
- // Deactivate nodes that are too far behind (line too wide, can't shrink enough)
147
- if (r < -1) {
148
- toDeactivate.add(ai);
149
- continue;
150
- }
151
- // Feasibility: don't allow extreme stretching
152
- // We allow r up to a generous threshold; the demerits will naturally penalize loose lines
153
- if (r > KP_INFINITY) {
154
- // Infeasible — too few items to fill the line
155
- continue;
156
- }
157
- const badness = computeBadness(r);
158
- const pen = item.type === 'penalty' ? item.penalty : 0;
159
- // Runt: when this break closes the paragraph with a too-short final
160
- // line, inject `runtPenalty` as equivalent badness — so it enters the
161
- // squared demerit formula alongside `badness`, on the same scale as
162
- // hyphen penalties. Applied linearly (old behavior) it was dwarfed by
163
- // badness² (up to 10001² ≈ 1e8) from any stretched alternative.
164
- const isRunt = runtPenalty > 0 &&
165
- i === terminalBreakPosition &&
166
- contentWidth > 0 &&
167
- contentWidth < runtMinWidth;
168
- const effectiveBadness = isRunt ? badness + runtPenalty : badness;
169
- let d;
170
- if (pen >= 0) {
171
- d = (1 + effectiveBadness + pen) * (1 + effectiveBadness + pen);
172
- }
173
- else if (pen > -KP_INFINITY) {
174
- d = (1 + effectiveBadness) * (1 + effectiveBadness) - pen * pen;
175
- }
176
- else {
177
- d = (1 + effectiveBadness) * (1 + effectiveBadness);
178
- }
179
- // Consecutive hyphen demerit
180
- const prevIsFlagged = a.position >= 0 &&
181
- items[a.position].type === 'penalty' &&
182
- items[a.position].flagged;
183
- if (isFlagged && prevIsFlagged) {
184
- d += consecutiveHyphenDemerit;
185
- }
186
- // Fitness class demerit
187
- const fc = classifyFitness(r);
188
- if (Math.abs(fc - a.fitnessClass) > 1) {
189
- d += fitnessClassDemerit;
190
- }
191
- const totalDemerits = a.totalDemerits + d;
192
- // The total width/stretch/shrink after this break:
193
- // After a glue break: the glue is consumed. Next line starts at item i+1.
194
- // totalWidth = sumWidthAt[i+1] (includes the glue's width — but we DON'T want it on the next line)
195
- // Actually, for the "totalWidth" tracked in active nodes, it represents the cumulative
196
- // width at the START of the next line. After a glue break at i, the next line starts at i+1.
197
- // So totalWidth should be sumWidthAt[i+1], which includes the glue.
198
- // When computing content width for the next line: sumWidthAt[j] - totalWidth
199
- // This correctly excludes the consumed glue.
200
- //
201
- // After a penalty break: the penalty is not consumed as content on the next line.
202
- // totalWidth = sumWidthAt[i+1] (which for penalties is same as sumWidthAt[i] since penalties
203
- // don't add to sumWidth). Wait — let me re-check: penalties don't contribute to sumWidth,
204
- // so sumWidthAt[i+1] == sumWidthAt[i] for a penalty at position i.
205
- // But the next line starts at i+1. So totalWidth = sumWidthAt[i+1].
206
- const newTotalWidth = sumWidthAt[i + 1];
207
- const newTotalStretch = sumStretchAt[i + 1];
208
- const newTotalShrink = sumShrinkAt[i + 1];
209
- newNodes.push({
210
- position: i,
211
- line: a.line + 1,
212
- fitnessClass: fc,
213
- totalWidth: newTotalWidth,
214
- totalStretch: newTotalStretch,
215
- totalShrink: newTotalShrink,
216
- totalDemerits: totalDemerits,
217
- previous: a,
218
- });
219
- }
220
- // Remove deactivated nodes
221
- if (toDeactivate.size > 0) {
222
- activeNodes = activeNodes.filter((_, idx) => !toDeactivate.has(idx));
223
- }
224
- // Deduplicate new nodes by (line, fitnessClass): keep best demerits.
225
- // Different breakpoint positions must NOT be merged — only nodes at the
226
- // same position (which all newNodes share) can be compared.
227
- const deduped = [];
228
- for (const node of newNodes) {
229
- const key = node.line * 4 + node.fitnessClass;
230
- let found = false;
231
- for (let di = 0; di < deduped.length; di++) {
232
- const existing = deduped[di];
233
- if (existing.line * 4 + existing.fitnessClass === key) {
234
- if (node.totalDemerits < existing.totalDemerits) {
235
- deduped[di] = node;
236
- }
237
- found = true;
238
- break;
239
- }
240
- }
241
- if (!found) {
242
- deduped.push(node);
243
- }
244
- }
245
- // Append all deduplicated nodes to the active set
246
- for (const node of deduped) {
247
- activeNodes.push(node);
248
- }
249
- // Forced breaks: when penalty = -INFINITY, remove all active nodes that
250
- // are NOT at this position. The break is mandatory — no line can skip it.
251
- if (item.type === 'penalty' && item.penalty <= -KP_INFINITY) {
252
- activeNodes = activeNodes.filter((a) => a.position === i);
253
- }
254
- // Emergency fallback: if active set is empty, force a break here
255
- if (activeNodes.length === 0) {
256
- activeNodes.push({
257
- position: i,
258
- line: 1, // We lost track — start fresh
259
- fitnessClass: 1,
260
- totalWidth: sumWidthAt[i + 1],
261
- totalStretch: sumStretchAt[i + 1],
262
- totalShrink: sumShrinkAt[i + 1],
263
- totalDemerits: 0,
264
- previous: null,
265
- });
266
- }
267
- }
268
- // Find best final node: only consider nodes at the last breakpoint position
269
- // (the forced penalty at the end of the paragraph).
270
- const lastBreakPosition = items.length - 1;
271
- let best = null;
272
- for (const a of activeNodes) {
273
- if (a.position !== lastBreakPosition)
274
- continue;
275
- if (best === null || a.totalDemerits < best.totalDemerits) {
276
- best = a;
277
- }
278
- }
279
- // Fallback: if no node reached the final position, take any node with the
280
- // highest position (nearest to the end).
281
- if (!best) {
282
- for (const a of activeNodes) {
283
- if (a.position < 0)
284
- continue;
285
- if (best === null || a.position > best.position ||
286
- (a.position === best.position && a.totalDemerits < best.totalDemerits)) {
287
- best = a;
288
- }
289
- }
290
- }
291
- if (!best)
292
- return [];
293
- // Traceback
294
- const breaks = [];
295
- let node = best;
296
- while (node !== null && node.position >= 0) {
297
- breaks.push(node.position);
298
- node = node.previous;
299
- }
300
- breaks.reverse();
301
- return breaks;
302
- }
303
- // ---------------------------------------------------------------------------
304
- // Adapter: pretext segments → KPItems
305
- // ---------------------------------------------------------------------------
306
- const SOFT_HYPHEN = '\u00AD';
307
- export function pretextSegmentsToItems(prepared, normalSpaceWidth, maxStretchRatio, minShrinkRatio) {
308
- const items = [];
309
- const segments = prepared.segments;
310
- const widths = prepared.widths;
311
- const kinds = prepared.kinds;
312
- const discretionaryHyphenWidth = prepared.discretionaryHyphenWidth;
313
- const stretchPerSpace = normalSpaceWidth * (maxStretchRatio - 1);
314
- const shrinkPerSpace = normalSpaceWidth * (1 - minShrinkRatio);
315
- for (let i = 0; i < segments.length; i++) {
316
- const kind = kinds[i];
317
- const w = widths[i];
318
- switch (kind) {
319
- case 'text':
320
- items.push({ type: 'box', width: w, sourceIndex: i });
321
- break;
322
- case 'space':
323
- case 'glue':
324
- items.push({
325
- type: 'glue',
326
- width: w,
327
- stretch: stretchPerSpace,
328
- shrink: shrinkPerSpace,
329
- sourceIndex: i,
330
- });
331
- break;
332
- case 'soft-hyphen':
333
- items.push({
334
- type: 'penalty',
335
- width: discretionaryHyphenWidth,
336
- penalty: HYPHEN_PENALTY,
337
- flagged: true,
338
- sourceIndex: i,
339
- });
340
- break;
341
- case 'hard-break':
342
- items.push({
343
- type: 'penalty',
344
- width: 0,
345
- penalty: -KP_INFINITY,
346
- flagged: false,
347
- sourceIndex: i,
348
- });
349
- break;
350
- case 'zero-width-break':
351
- items.push({
352
- type: 'penalty',
353
- width: 0,
354
- penalty: 0,
355
- flagged: false,
356
- sourceIndex: i,
357
- });
358
- break;
359
- case 'preserved-space':
360
- case 'tab':
361
- items.push({ type: 'box', width: w, sourceIndex: i });
362
- break;
363
- default:
364
- items.push({ type: 'box', width: w, sourceIndex: i });
365
- break;
366
- }
367
- }
368
- // Final-line glue (infinite stretch) + forced break
369
- items.push({
370
- type: 'glue',
371
- width: 0,
372
- stretch: MAX_STRETCH,
373
- shrink: 0,
374
- sourceIndex: -1,
375
- });
376
- items.push({
377
- type: 'penalty',
378
- width: 0,
379
- penalty: -KP_INFINITY,
380
- flagged: false,
381
- sourceIndex: -1,
382
- });
383
- return items;
384
- }
385
- export function richTokensToItems(tokens, normalSpaceWidth, maxStretchRatio, minShrinkRatio) {
386
- const items = [];
387
- const stretchPerSpace = normalSpaceWidth * (maxStretchRatio - 1);
388
- const shrinkPerSpace = normalSpaceWidth * (1 - minShrinkRatio);
389
- for (let t = 0; t < tokens.length; t++) {
390
- const token = tokens[t];
391
- const meta = {
392
- bold: token.bold,
393
- italic: token.italic,
394
- originalTokenIndex: t,
395
- };
396
- if (token.kind === 'space') {
397
- items.push({
398
- type: 'glue',
399
- width: token.width,
400
- stretch: stretchPerSpace,
401
- shrink: shrinkPerSpace,
402
- sourceIndex: t,
403
- meta: { ...meta },
404
- });
405
- continue;
406
- }
407
- // Text token — may have soft-hyphen break points
408
- if (token.breakPoints && token.breakPoints.length > 0) {
409
- const hyphenW = token.hyphenWidth ?? 0;
410
- let prevCharIndex = 0;
411
- let prevWidth = 0;
412
- for (let bp = 0; bp < token.breakPoints.length; bp++) {
413
- const breakPoint = token.breakPoints[bp];
414
- const fragmentWidth = breakPoint.widthBefore - prevWidth;
415
- // Box for the fragment before this break point
416
- items.push({
417
- type: 'box',
418
- width: fragmentWidth,
419
- sourceIndex: t,
420
- meta: { ...meta, subStart: prevCharIndex, subEnd: breakPoint.charIndex },
421
- });
422
- // Penalty at the soft hyphen
423
- items.push({
424
- type: 'penalty',
425
- width: hyphenW,
426
- penalty: HYPHEN_PENALTY,
427
- flagged: true,
428
- sourceIndex: t,
429
- meta: { ...meta },
430
- });
431
- prevCharIndex = breakPoint.charIndex;
432
- prevWidth = breakPoint.widthBefore;
433
- }
434
- // Final fragment after last break point
435
- items.push({
436
- type: 'box',
437
- width: token.width - prevWidth,
438
- sourceIndex: t,
439
- meta: { ...meta, subStart: prevCharIndex, subEnd: token.text.length },
440
- });
441
- }
442
- else {
443
- // Simple text token without break points
444
- items.push({
445
- type: 'box',
446
- width: token.width,
447
- sourceIndex: t,
448
- meta: { ...meta, subStart: 0, subEnd: token.text.length },
449
- });
450
- }
451
- }
452
- // Final-line glue + forced break
453
- items.push({
454
- type: 'glue',
455
- width: 0,
456
- stretch: MAX_STRETCH,
457
- shrink: 0,
458
- sourceIndex: -1,
459
- });
460
- items.push({
461
- type: 'penalty',
462
- width: 0,
463
- penalty: -KP_INFINITY,
464
- flagged: false,
465
- sourceIndex: -1,
466
- });
467
- return items;
468
- }
469
- // ---------------------------------------------------------------------------
470
- // Reconstruction: breakpoints → VDTLine[] (pretext path)
471
- // ---------------------------------------------------------------------------
472
- function cleanSoftHyphens(text) {
473
- return text.replace(/\u00AD/g, '');
474
- }
475
- export function reconstructPretextLines(items, breaks, prepared, lineHeightPx, lineWidthFn, lineIndentFn, normalSpaceWidth, textAlign) {
476
- const segments = prepared.segments;
477
- const widths = prepared.widths;
478
- const discretionaryHyphenWidth = prepared.discretionaryHyphenWidth;
479
- const lines = [];
480
- let lineStart = 0; // item index where current line content starts
481
- for (let li = 0; li < breaks.length; li++) {
482
- const breakAt = breaks[li];
483
- const isLastLine = li === breaks.length - 1;
484
- const lineIndent = lineIndentFn(li);
485
- const lineMaxWidth = lineWidthFn(li);
486
- // Determine if this break is at a penalty (hyphenation)
487
- const breakItem = items[breakAt];
488
- const hyphenated = breakItem.type === 'penalty' && breakItem.flagged;
489
- // Collect segments for this line: items from lineStart to breakAt
490
- // For glue breaks: line content is items lineStart..breakAt-1 (exclude the breaking glue)
491
- // For penalty breaks: line content is items lineStart..breakAt-1 (exclude the penalty itself)
492
- const contentEnd = breakItem.type === 'glue' ? breakAt : breakAt;
493
- const lineSegments = [];
494
- const textParts = [];
495
- for (let j = lineStart; j < contentEnd; j++) {
496
- const it = items[j];
497
- if (it.sourceIndex < 0)
498
- continue; // skip synthetic items
499
- if (it.type === 'box') {
500
- const seg = segments[it.sourceIndex];
501
- const w = widths[it.sourceIndex];
502
- if (seg === SOFT_HYPHEN)
503
- continue;
504
- const cleanText = cleanSoftHyphens(seg);
505
- lineSegments.push({ kind: 'text', text: cleanText, width: w });
506
- textParts.push(cleanText);
507
- }
508
- else if (it.type === 'glue') {
509
- const seg = segments[it.sourceIndex];
510
- const w = widths[it.sourceIndex];
511
- lineSegments.push({ kind: 'space', text: seg, width: w });
512
- textParts.push(seg);
513
- }
514
- // Penalties within the line are skipped (they're just potential break points)
515
- }
516
- // Trim trailing spaces
517
- while (lineSegments.length > 0 && lineSegments[lineSegments.length - 1].kind === 'space') {
518
- lineSegments.pop();
519
- textParts.pop();
520
- }
521
- // If hyphenated, append '-' to the last text segment
522
- if (hyphenated && lineSegments.length > 0) {
523
- const lastIdx = lineSegments.length - 1;
524
- const last = lineSegments[lastIdx];
525
- if (last.kind === 'text') {
526
- lineSegments[lastIdx] = {
527
- kind: 'text',
528
- text: last.text + '-',
529
- width: last.width + discretionaryHyphenWidth,
530
- };
531
- textParts[textParts.length - 1] = last.text + '-';
532
- }
533
- }
534
- const lineText = textParts.join('');
535
- const contentWidth = lineSegments.reduce((s, seg) => s + seg.width, 0);
536
- // Compute justifiedSpaceRatio
537
- let justifiedSpaceRatio;
538
- if (textAlign === 'justify' && !isLastLine && normalSpaceWidth > 0) {
539
- let wordWidth = 0;
540
- let spaceCount = 0;
541
- for (const seg of lineSegments) {
542
- if (seg.kind === 'space')
543
- spaceCount++;
544
- else
545
- wordWidth += seg.width;
546
- }
547
- if (spaceCount > 0) {
548
- const justifiedSpaceWidth = (lineMaxWidth - wordWidth) / spaceCount;
549
- justifiedSpaceRatio = justifiedSpaceWidth / normalSpaceWidth;
550
- }
551
- }
552
- lines.push({
553
- text: lineText,
554
- bbox: createBoundingBox(lineIndent, li * lineHeightPx, contentWidth, lineHeightPx),
555
- baseline: li * lineHeightPx + lineHeightPx * 0.8,
556
- hyphenated,
557
- segments: lineSegments,
558
- isLastLine,
559
- ...(justifiedSpaceRatio !== undefined ? { justifiedSpaceRatio } : {}),
560
- });
561
- // Next line starts after the break
562
- lineStart = breakAt + 1;
563
- }
564
- return lines;
565
- }
566
- // ---------------------------------------------------------------------------
567
- // Reconstruction: breakpoints → VDTLine[] (rich text path)
568
- // ---------------------------------------------------------------------------
569
- export function reconstructRichLines(items, breaks, tokens, lineHeightPx, lineWidthFn, lineIndentFn, normalSpaceWidth, textAlign) {
570
- const lines = [];
571
- let lineStart = 0;
572
- for (let li = 0; li < breaks.length; li++) {
573
- const breakAt = breaks[li];
574
- const isLastLine = li === breaks.length - 1;
575
- const lineIndent = lineIndentFn(li);
576
- const lineMaxWidth = lineWidthFn(li);
577
- const breakItem = items[breakAt];
578
- const hyphenated = breakItem.type === 'penalty' && breakItem.flagged;
579
- const lineSegments = [];
580
- const textParts = [];
581
- for (let j = lineStart; j < breakAt; j++) {
582
- const it = items[j];
583
- if (it.sourceIndex < 0)
584
- continue;
585
- const meta = it.meta;
586
- if (it.type === 'box' && meta) {
587
- const token = tokens[meta.originalTokenIndex];
588
- const subText = meta.subStart !== undefined && meta.subEnd !== undefined
589
- ? token.text.slice(meta.subStart, meta.subEnd)
590
- : token.text;
591
- const cleanText = cleanSoftHyphens(subText);
592
- lineSegments.push({
593
- kind: token.mathRender ? 'math' : 'text',
594
- text: cleanText,
595
- width: it.width,
596
- bold: meta.bold || undefined,
597
- italic: meta.italic || undefined,
598
- ...(token.mathRender ? { mathRender: token.mathRender } : {}),
599
- });
600
- textParts.push(cleanText);
601
- }
602
- else if (it.type === 'glue' && meta) {
603
- const token = tokens[meta.originalTokenIndex];
604
- lineSegments.push({
605
- kind: 'space',
606
- text: token.text,
607
- width: it.width,
608
- });
609
- textParts.push(token.text);
610
- }
611
- }
612
- // Trim trailing spaces
613
- while (lineSegments.length > 0 && lineSegments[lineSegments.length - 1].kind === 'space') {
614
- lineSegments.pop();
615
- textParts.pop();
616
- }
617
- // If hyphenated, append '-' to the last text segment
618
- if (hyphenated && lineSegments.length > 0) {
619
- const breakMeta = breakItem.meta;
620
- const hyphenW = breakMeta
621
- ? (tokens[breakMeta.originalTokenIndex]?.hyphenWidth ?? 0)
622
- : 0;
623
- const lastIdx = lineSegments.length - 1;
624
- const last = lineSegments[lastIdx];
625
- if (last.kind === 'text') {
626
- lineSegments[lastIdx] = {
627
- ...last,
628
- text: last.text + '-',
629
- width: last.width + hyphenW,
630
- };
631
- textParts[textParts.length - 1] = last.text + '-';
632
- }
633
- }
634
- const lineText = textParts.join('');
635
- const contentWidth = lineSegments.reduce((s, seg) => s + seg.width, 0);
636
- // Compute justifiedSpaceRatio
637
- let justifiedSpaceRatio;
638
- if (textAlign === 'justify' && !isLastLine && normalSpaceWidth > 0) {
639
- let wordWidth = 0;
640
- let spaceCount = 0;
641
- for (const seg of lineSegments) {
642
- if (seg.kind === 'space')
643
- spaceCount++;
644
- else
645
- wordWidth += seg.width;
646
- }
647
- if (spaceCount > 0) {
648
- const justifiedSpaceWidth = (lineMaxWidth - wordWidth) / spaceCount;
649
- justifiedSpaceRatio = justifiedSpaceWidth / normalSpaceWidth;
650
- }
651
- }
652
- lines.push({
653
- text: lineText,
654
- bbox: createBoundingBox(lineIndent, li * lineHeightPx, contentWidth, lineHeightPx),
655
- baseline: li * lineHeightPx + lineHeightPx * 0.8,
656
- hyphenated,
657
- segments: lineSegments,
658
- isLastLine,
659
- ...(justifiedSpaceRatio !== undefined ? { justifiedSpaceRatio } : {}),
660
- });
661
- lineStart = breakAt + 1;
662
- }
663
- return lines;
664
- }
665
- //# sourceMappingURL=knuthPlass.js.map