@shadow-garden/bapbong-layout-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/index.cjs +1476 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1447 -0
- package/dist/lib/layout-engine.d.ts +73 -0
- package/dist/lib/layout-engine.d.ts.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1447 @@
|
|
|
1
|
+
// packages/layout-engine/src/lib/layout-engine.ts
|
|
2
|
+
import {
|
|
3
|
+
createNumberingCounter
|
|
4
|
+
} from "@shadow-garden/bapbong-model";
|
|
5
|
+
var DEFAULT_FONT = { family: "Arial", sizePt: 11, bold: false, italic: false };
|
|
6
|
+
var PT_TO_PX = 96 / 72;
|
|
7
|
+
var HEADING_PT = { 1: 24, 2: 18, 3: 14, 4: 12, 5: 11, 6: 11 };
|
|
8
|
+
var LINE_HEIGHT_FACTOR = 1.2;
|
|
9
|
+
var BASELINE_FACTOR = 0.8;
|
|
10
|
+
var DEFAULT_TAB_WIDTH = 48;
|
|
11
|
+
var SUPERSUB_SCALE = 0.66;
|
|
12
|
+
var CELL_PAD_X = 7.2;
|
|
13
|
+
var CELL_PAD_Y = 0;
|
|
14
|
+
var FOOTNOTE_FONT_SCALE = 0.85;
|
|
15
|
+
var FOOTNOTE_AREA_GAP = 12;
|
|
16
|
+
var sizePx = (font) => font.sizePt * PT_TO_PX;
|
|
17
|
+
function findMark(marks, name) {
|
|
18
|
+
return marks.find((m) => m.type.name === name);
|
|
19
|
+
}
|
|
20
|
+
function resolveRun(node, base, pos) {
|
|
21
|
+
const marks = node.marks;
|
|
22
|
+
const font = { ...base };
|
|
23
|
+
if (findMark(marks, "strong")) font.bold = true;
|
|
24
|
+
if (findMark(marks, "em")) font.italic = true;
|
|
25
|
+
const size = findMark(marks, "fontSize");
|
|
26
|
+
if (size) font.sizePt = Number(size.attrs["size"]) || base.sizePt;
|
|
27
|
+
const family = findMark(marks, "fontFamily");
|
|
28
|
+
if (family) font.family = String(family.attrs["family"] ?? base.family);
|
|
29
|
+
const color = findMark(marks, "textColor");
|
|
30
|
+
const link = findMark(marks, "link");
|
|
31
|
+
const run = {
|
|
32
|
+
text: node.text ?? "",
|
|
33
|
+
font,
|
|
34
|
+
color: color ? String(color.attrs["color"]) : void 0,
|
|
35
|
+
link: link ? String(link.attrs["href"]) : void 0,
|
|
36
|
+
pos
|
|
37
|
+
};
|
|
38
|
+
if (findMark(marks, "underline")) run.underline = true;
|
|
39
|
+
if (findMark(marks, "strike")) run.strike = true;
|
|
40
|
+
const highlight = findMark(marks, "highlight");
|
|
41
|
+
if (highlight) run.background = String(highlight.attrs["color"]);
|
|
42
|
+
const va = findMark(marks, "vertAlign");
|
|
43
|
+
if (va) run.vertAlign = va.attrs["value"] === "sub" ? "sub" : "super";
|
|
44
|
+
const fn = findMark(marks, "footnote");
|
|
45
|
+
if (fn) run.footnoteRef = Number(fn.attrs["num"]) || void 0;
|
|
46
|
+
const cm = findMark(marks, "comment");
|
|
47
|
+
if (cm) run.commentIds = cm.attrs["ids"];
|
|
48
|
+
return run;
|
|
49
|
+
}
|
|
50
|
+
function resolveField(node, base, pos) {
|
|
51
|
+
const marks = node.marks;
|
|
52
|
+
const font = { ...base };
|
|
53
|
+
if (findMark(marks, "strong")) font.bold = true;
|
|
54
|
+
if (findMark(marks, "em")) font.italic = true;
|
|
55
|
+
const size = findMark(marks, "fontSize");
|
|
56
|
+
if (size) font.sizePt = Number(size.attrs["size"]) || base.sizePt;
|
|
57
|
+
const family = findMark(marks, "fontFamily");
|
|
58
|
+
if (family) font.family = String(family.attrs["family"] ?? base.family);
|
|
59
|
+
const color = findMark(marks, "textColor");
|
|
60
|
+
return {
|
|
61
|
+
field: node.attrs["kind"] === "pages" ? "pageCount" : "pageNumber",
|
|
62
|
+
font,
|
|
63
|
+
color: color ? String(color.attrs["color"]) : void 0,
|
|
64
|
+
pos
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveImage(node, pos) {
|
|
68
|
+
const a = node.attrs;
|
|
69
|
+
const link = findMark(node.marks, "link");
|
|
70
|
+
return {
|
|
71
|
+
src: String(a["src"] ?? ""),
|
|
72
|
+
width: Number(a["width"]) || 0,
|
|
73
|
+
height: Number(a["height"]) || 0,
|
|
74
|
+
link: link ? String(link.attrs["href"]) : void 0,
|
|
75
|
+
...a["shape"] ? { shape: a["shape"] } : {},
|
|
76
|
+
...Number(a["rotation"]) ? { rotation: Number(a["rotation"]) } : {},
|
|
77
|
+
pos
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function paragraphToFlow(node, base, nodePos, allowFloats = false, marker) {
|
|
81
|
+
const contentStart = nodePos + 1;
|
|
82
|
+
const headingLevel = node.attrs["heading"];
|
|
83
|
+
const runBase = headingLevel ? { ...base, sizePt: HEADING_PT[headingLevel] ?? base.sizePt, bold: true } : base;
|
|
84
|
+
const runs = [];
|
|
85
|
+
const floats = [];
|
|
86
|
+
node.forEach((child, offset) => {
|
|
87
|
+
if (child.isText) runs.push(resolveRun(child, runBase, contentStart + offset));
|
|
88
|
+
else if (child.type.name === "image") {
|
|
89
|
+
const float = child.attrs["float"];
|
|
90
|
+
if (float && allowFloats) {
|
|
91
|
+
const f = {
|
|
92
|
+
...float,
|
|
93
|
+
src: String(child.attrs["src"] ?? ""),
|
|
94
|
+
width: Number(child.attrs["width"]) || 0,
|
|
95
|
+
height: Number(child.attrs["height"]) || 0,
|
|
96
|
+
pos: contentStart + offset,
|
|
97
|
+
...child.attrs["shape"] ? { shape: child.attrs["shape"] } : {},
|
|
98
|
+
...Number(child.attrs["rotation"]) ? { rotation: Number(child.attrs["rotation"]) } : {}
|
|
99
|
+
};
|
|
100
|
+
const tb = child.attrs["textbox"];
|
|
101
|
+
if (tb && tb.paragraphs.length > 0) {
|
|
102
|
+
const schema = child.type.schema;
|
|
103
|
+
f.content = tb.paragraphs.map(
|
|
104
|
+
(json, i) => paragraphToFlow(schema.nodeFromJSON(json), base, i)
|
|
105
|
+
);
|
|
106
|
+
if (tb.inset) f.inset = tb.inset;
|
|
107
|
+
}
|
|
108
|
+
floats.push(f);
|
|
109
|
+
} else {
|
|
110
|
+
runs.push(resolveImage(child, contentStart + offset));
|
|
111
|
+
}
|
|
112
|
+
} else if (child.type.name === "page_field")
|
|
113
|
+
runs.push(resolveField(child, runBase, contentStart + offset));
|
|
114
|
+
else if (child.type.name === "hard_break") runs.push({ break: true, pos: contentStart + offset });
|
|
115
|
+
});
|
|
116
|
+
const list = node.attrs["list"];
|
|
117
|
+
const align = node.attrs["align"];
|
|
118
|
+
const indent = node.attrs["indent"];
|
|
119
|
+
const flow = {
|
|
120
|
+
type: "paragraph",
|
|
121
|
+
runs,
|
|
122
|
+
marker: marker ?? (list?.marker || void 0),
|
|
123
|
+
align: align ?? void 0,
|
|
124
|
+
indent: indent ?? void 0,
|
|
125
|
+
pos: contentStart,
|
|
126
|
+
end: contentStart + node.content.size
|
|
127
|
+
};
|
|
128
|
+
if (floats.length > 0) flow.floats = floats;
|
|
129
|
+
const tabs = node.attrs["tabs"];
|
|
130
|
+
if (tabs) flow.tabs = tabs;
|
|
131
|
+
const spacing = node.attrs["spacing"];
|
|
132
|
+
if (spacing) flow.spacing = spacing;
|
|
133
|
+
if (node.attrs["pageBreakBefore"] === true) flow.pageBreakBefore = true;
|
|
134
|
+
return flow;
|
|
135
|
+
}
|
|
136
|
+
function nodeHasFloats(node) {
|
|
137
|
+
let found = false;
|
|
138
|
+
node.forEach((child) => {
|
|
139
|
+
if (child.type.name === "image" && child.attrs["float"]) found = true;
|
|
140
|
+
});
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
function markerFor(node, counter) {
|
|
144
|
+
const list = node.attrs["list"];
|
|
145
|
+
if (!list) return void 0;
|
|
146
|
+
return (counter?.next(list.numId, list.level) || list.marker) ?? void 0;
|
|
147
|
+
}
|
|
148
|
+
function nodeToBlock(node, base, nodePos, allowFloats = false, counter) {
|
|
149
|
+
if (node.type.name === "paragraph")
|
|
150
|
+
return paragraphToFlow(node, base, nodePos, allowFloats, markerFor(node, counter));
|
|
151
|
+
if (node.type.name === "table") return tableToFlow(node, base, nodePos, counter);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function tableToFlow(node, base, nodePos, counter) {
|
|
155
|
+
const rows = [];
|
|
156
|
+
node.forEach((rowNode, rowOffset) => {
|
|
157
|
+
const rowPos = nodePos + 1 + rowOffset;
|
|
158
|
+
const cells = [];
|
|
159
|
+
rowNode.forEach((cellNode, cellOffset) => {
|
|
160
|
+
const cellPos = rowPos + 1 + cellOffset;
|
|
161
|
+
const content = [];
|
|
162
|
+
cellNode.forEach((child, childOffset) => {
|
|
163
|
+
const block = nodeToBlock(child, base, cellPos + 1 + childOffset, true, counter);
|
|
164
|
+
if (block) content.push(block);
|
|
165
|
+
});
|
|
166
|
+
const a = cellNode.attrs;
|
|
167
|
+
cells.push({
|
|
168
|
+
colspan: Number(a["colspan"]) || 1,
|
|
169
|
+
rowspan: Number(a["rowspan"]) || 1,
|
|
170
|
+
colwidth: a["colwidth"] ?? null,
|
|
171
|
+
background: a["background"] ?? void 0,
|
|
172
|
+
vAlign: a["vAlign"] ?? void 0,
|
|
173
|
+
borders: a["borders"] ?? void 0,
|
|
174
|
+
content
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
const row = { cells };
|
|
178
|
+
if (rowNode.attrs["header"] === true) row.header = true;
|
|
179
|
+
if (rowNode.attrs["cantSplit"] === true) row.cantSplit = true;
|
|
180
|
+
const height = rowNode.attrs["height"];
|
|
181
|
+
if (height) row.height = height;
|
|
182
|
+
rows.push(row);
|
|
183
|
+
});
|
|
184
|
+
const flow = { type: "table", rows };
|
|
185
|
+
const cellPadding = node.attrs["cellPadding"];
|
|
186
|
+
if (cellPadding) flow.cellPadding = cellPadding;
|
|
187
|
+
const align = node.attrs["align"];
|
|
188
|
+
if (align) flow.align = align;
|
|
189
|
+
const borders = node.attrs["borders"];
|
|
190
|
+
if (borders) flow.borders = borders;
|
|
191
|
+
return flow;
|
|
192
|
+
}
|
|
193
|
+
function toFlowBlocks(doc, defaultFont = {}, allowFloats = true) {
|
|
194
|
+
const base = { ...DEFAULT_FONT, ...defaultFont };
|
|
195
|
+
const counter = createNumberingCounter(doc.attrs["numbering"]);
|
|
196
|
+
const blocks = [];
|
|
197
|
+
doc.forEach((node, offset) => {
|
|
198
|
+
const block = nodeToBlock(node, base, offset, allowFloats, counter);
|
|
199
|
+
if (block) blocks.push(block);
|
|
200
|
+
});
|
|
201
|
+
return blocks;
|
|
202
|
+
}
|
|
203
|
+
function tokenizeInline(inline, ctx) {
|
|
204
|
+
if ("break" in inline) {
|
|
205
|
+
return [{ isBreak: true, font: ctx.base, width: 0, isSpace: false, pos: inline.pos, size: 1 }];
|
|
206
|
+
}
|
|
207
|
+
if ("src" in inline) {
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
image: inline,
|
|
211
|
+
font: ctx.base,
|
|
212
|
+
link: inline.link,
|
|
213
|
+
width: inline.width,
|
|
214
|
+
isSpace: false,
|
|
215
|
+
pos: inline.pos,
|
|
216
|
+
size: 1
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
if ("field" in inline) {
|
|
221
|
+
return [
|
|
222
|
+
{
|
|
223
|
+
field: inline.field,
|
|
224
|
+
text: ctx.fieldPlaceholder,
|
|
225
|
+
font: inline.font,
|
|
226
|
+
color: inline.color,
|
|
227
|
+
width: ctx.measure(ctx.fieldPlaceholder, inline.font),
|
|
228
|
+
isSpace: false,
|
|
229
|
+
pos: inline.pos,
|
|
230
|
+
size: 1
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
const font = inline.vertAlign ? { ...inline.font, sizePt: inline.font.sizePt * SUPERSUB_SCALE } : inline.font;
|
|
235
|
+
let offset = 0;
|
|
236
|
+
return inline.text.split(/(\t| +)/).filter((part) => part.length > 0).map((part) => {
|
|
237
|
+
const isTab = part === " ";
|
|
238
|
+
const isSpace = isTab || /^ +$/.test(part);
|
|
239
|
+
const pos = inline.pos != null ? inline.pos + offset : void 0;
|
|
240
|
+
offset += part.length;
|
|
241
|
+
return {
|
|
242
|
+
text: part,
|
|
243
|
+
font,
|
|
244
|
+
color: inline.color,
|
|
245
|
+
link: inline.link,
|
|
246
|
+
underline: inline.underline,
|
|
247
|
+
strike: inline.strike,
|
|
248
|
+
background: inline.background,
|
|
249
|
+
vertAlign: inline.vertAlign,
|
|
250
|
+
footnoteRef: inline.footnoteRef,
|
|
251
|
+
commentIds: inline.commentIds,
|
|
252
|
+
width: isTab ? 0 : ctx.measure(part, font),
|
|
253
|
+
isSpace,
|
|
254
|
+
isTab,
|
|
255
|
+
pos,
|
|
256
|
+
size: part.length
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function draftToLine(d, y, dx = 0) {
|
|
261
|
+
const line = {
|
|
262
|
+
x: d.x + dx,
|
|
263
|
+
y,
|
|
264
|
+
width: d.width,
|
|
265
|
+
height: d.height,
|
|
266
|
+
baseline: d.baseline,
|
|
267
|
+
segments: dx === 0 ? d.segments : d.segments.map((s) => ({ ...s, x: s.x + dx }))
|
|
268
|
+
};
|
|
269
|
+
if (d.images.length > 0) {
|
|
270
|
+
line.images = dx === 0 ? d.images : d.images.map((im) => ({ ...im, x: im.x + dx }));
|
|
271
|
+
}
|
|
272
|
+
if (d.from != null) line.from = d.from;
|
|
273
|
+
if (d.to != null) line.to = d.to;
|
|
274
|
+
return line;
|
|
275
|
+
}
|
|
276
|
+
function wrapParagraph(block, ctx, bandFn, emit) {
|
|
277
|
+
const { base, measure, metrics, tabWidth } = ctx;
|
|
278
|
+
const indent = block.indent;
|
|
279
|
+
const indentLeft = indent?.left ?? 0;
|
|
280
|
+
const indentRight = indent?.right ?? 0;
|
|
281
|
+
const firstLineDelta = indent?.hanging != null ? -indent.hanging : indent?.firstLine ?? 0;
|
|
282
|
+
const align = block.align ?? "left";
|
|
283
|
+
const tokens = block.runs.flatMap((inline) => tokenizeInline(inline, ctx));
|
|
284
|
+
const baseMetrics = metrics ? metrics(base) : null;
|
|
285
|
+
const nominalH = baseMetrics ? baseMetrics.ascent + baseMetrics.descent : sizePx(base) * LINE_HEIGHT_FACTOR;
|
|
286
|
+
let band = bandFn(nominalH);
|
|
287
|
+
let lineLeft = band.left + indentLeft;
|
|
288
|
+
let lineRight = band.right - indentRight;
|
|
289
|
+
let marker = null;
|
|
290
|
+
let markerTextX = 0;
|
|
291
|
+
if (block.marker) {
|
|
292
|
+
marker = {
|
|
293
|
+
x: lineLeft + firstLineDelta,
|
|
294
|
+
text: block.marker,
|
|
295
|
+
font: base,
|
|
296
|
+
width: measure(block.marker, base)
|
|
297
|
+
};
|
|
298
|
+
markerTextX = marker.x + measure(`${block.marker} `, base);
|
|
299
|
+
}
|
|
300
|
+
const firstLineStart = marker ? markerTextX : lineLeft + firstLineDelta;
|
|
301
|
+
let contStart = marker ? Math.max(markerTextX, lineLeft) : lineLeft;
|
|
302
|
+
let lineTokens = [];
|
|
303
|
+
let lineWidth = 0;
|
|
304
|
+
let firstLine = true;
|
|
305
|
+
let prevTo;
|
|
306
|
+
let maxFontPx = sizePx(base);
|
|
307
|
+
let maxImagePx = 0;
|
|
308
|
+
let maxAscent = baseMetrics?.ascent ?? 0;
|
|
309
|
+
let maxDescent = baseMetrics?.descent ?? 0;
|
|
310
|
+
const lineStart = () => firstLine ? firstLineStart : contStart;
|
|
311
|
+
const tabAdvance = (x) => {
|
|
312
|
+
const k = Math.floor((x - lineLeft) / tabWidth) + 1;
|
|
313
|
+
return lineLeft + k * tabWidth - x;
|
|
314
|
+
};
|
|
315
|
+
const tabStops = block.tabs ? [...block.tabs].sort((a, b) => a.pos - b.pos) : [];
|
|
316
|
+
const LEADER_CHARS = { dot: ".", hyphen: "-", underscore: "_", middleDot: "\xB7" };
|
|
317
|
+
const MIN_TAB = 2;
|
|
318
|
+
const tabGroup = (ti) => {
|
|
319
|
+
let width = 0;
|
|
320
|
+
let decimalPrefix = null;
|
|
321
|
+
for (let j = ti + 1; j < tokens.length; j++) {
|
|
322
|
+
const t = tokens[j];
|
|
323
|
+
if (t.isTab) break;
|
|
324
|
+
if (decimalPrefix === null && t.text != null && !t.field) {
|
|
325
|
+
const m = /[.,]/.exec(t.text);
|
|
326
|
+
if (m) decimalPrefix = width + measure(t.text.slice(0, m.index), t.font);
|
|
327
|
+
}
|
|
328
|
+
width += t.width;
|
|
329
|
+
}
|
|
330
|
+
return { width, decimalPrefix };
|
|
331
|
+
};
|
|
332
|
+
const resolveTab = (token, x, ti) => {
|
|
333
|
+
token.origPos ??= token.pos;
|
|
334
|
+
token.pos = token.origPos;
|
|
335
|
+
token.text = " ";
|
|
336
|
+
const stop = tabStops.find((s) => lineLeft + s.pos > x + 0.5);
|
|
337
|
+
if (!stop) {
|
|
338
|
+
token.width = tabAdvance(x);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const stopX = Math.min(lineLeft + stop.pos, lineRight);
|
|
342
|
+
if (stopX <= x + 0.5) {
|
|
343
|
+
token.width = tabAdvance(x);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
let w = stopX - x;
|
|
347
|
+
if (stop.val !== "left") {
|
|
348
|
+
const g = tabGroup(ti);
|
|
349
|
+
const hang = stop.val === "right" ? g.width : stop.val === "center" ? g.width / 2 : g.decimalPrefix ?? g.width;
|
|
350
|
+
w = stopX - x - hang;
|
|
351
|
+
if (w < MIN_TAB) {
|
|
352
|
+
token.width = tabAdvance(x);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
token.width = w;
|
|
357
|
+
if (stop.leader) {
|
|
358
|
+
const ch = LEADER_CHARS[stop.leader] ?? ".";
|
|
359
|
+
const chW = measure(ch, token.font);
|
|
360
|
+
const n = chW > 0 ? Math.floor(w / chW) : 0;
|
|
361
|
+
if (n > 1) {
|
|
362
|
+
token.text = ch.repeat(n - 1);
|
|
363
|
+
token.pos = void 0;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const flushLine = (isLast) => {
|
|
368
|
+
const startX = lineStart();
|
|
369
|
+
const avail = lineRight - startX;
|
|
370
|
+
let end = lineTokens.length;
|
|
371
|
+
let contentWidth = lineWidth;
|
|
372
|
+
while (end > 0 && lineTokens[end - 1].isSpace) {
|
|
373
|
+
contentWidth -= lineTokens[end - 1].width;
|
|
374
|
+
end--;
|
|
375
|
+
}
|
|
376
|
+
let x = startX;
|
|
377
|
+
let extraPerGap = 0;
|
|
378
|
+
if (align === "justify" && !isLast) {
|
|
379
|
+
const gaps = lineTokens.slice(0, end).filter((t) => t.isSpace && !t.isTab).length;
|
|
380
|
+
if (gaps > 0) extraPerGap = (avail - contentWidth) / gaps;
|
|
381
|
+
} else if (align === "center") {
|
|
382
|
+
x += Math.max(0, (avail - contentWidth) / 2);
|
|
383
|
+
} else if (align === "right") {
|
|
384
|
+
x += Math.max(0, avail - contentWidth);
|
|
385
|
+
}
|
|
386
|
+
const segments = [];
|
|
387
|
+
const images = [];
|
|
388
|
+
for (let i = 0; i < end; i++) {
|
|
389
|
+
const t = lineTokens[i];
|
|
390
|
+
if (t.image) {
|
|
391
|
+
images.push({
|
|
392
|
+
x,
|
|
393
|
+
src: t.image.src,
|
|
394
|
+
width: t.image.width,
|
|
395
|
+
height: t.image.height,
|
|
396
|
+
link: t.link,
|
|
397
|
+
...t.image.shape ? { shape: t.image.shape } : {},
|
|
398
|
+
...t.image.rotation ? { rotation: t.image.rotation } : {},
|
|
399
|
+
pos: t.pos
|
|
400
|
+
});
|
|
401
|
+
} else {
|
|
402
|
+
const seg = {
|
|
403
|
+
x,
|
|
404
|
+
text: t.text ?? "",
|
|
405
|
+
font: t.font,
|
|
406
|
+
color: t.color,
|
|
407
|
+
link: t.link,
|
|
408
|
+
underline: t.underline,
|
|
409
|
+
strike: t.strike,
|
|
410
|
+
background: t.background,
|
|
411
|
+
vertAlign: t.vertAlign,
|
|
412
|
+
width: t.width,
|
|
413
|
+
pos: t.pos
|
|
414
|
+
};
|
|
415
|
+
if (t.field) seg.field = t.field;
|
|
416
|
+
if (t.footnoteRef != null) seg.footnoteRef = t.footnoteRef;
|
|
417
|
+
if (t.commentIds) seg.commentIds = t.commentIds;
|
|
418
|
+
segments.push(seg);
|
|
419
|
+
}
|
|
420
|
+
x += t.width + (t.isSpace && !t.isTab ? extraPerGap : 0);
|
|
421
|
+
}
|
|
422
|
+
const firstPos = lineTokens.find((t, i) => i < end && t.pos != null)?.pos;
|
|
423
|
+
let lastEnd;
|
|
424
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
425
|
+
const t = lineTokens[i];
|
|
426
|
+
if (t.pos != null) {
|
|
427
|
+
lastEnd = t.pos + t.size;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const from = firstPos ?? prevTo ?? block.pos;
|
|
432
|
+
const to = lastEnd ?? from;
|
|
433
|
+
let height;
|
|
434
|
+
let baseline;
|
|
435
|
+
if (metrics) {
|
|
436
|
+
height = maxAscent + maxDescent;
|
|
437
|
+
baseline = maxAscent;
|
|
438
|
+
} else {
|
|
439
|
+
height = Math.max(maxFontPx * LINE_HEIGHT_FACTOR, maxImagePx);
|
|
440
|
+
baseline = height * BASELINE_FACTOR;
|
|
441
|
+
}
|
|
442
|
+
const sp = block.spacing;
|
|
443
|
+
if (sp?.line) {
|
|
444
|
+
const target = sp.lineRule === "exact" ? sp.line : sp.lineRule === "atLeast" ? Math.max(height, sp.line) : height * sp.line;
|
|
445
|
+
baseline += Math.max(0, target - height) * (sp.lineRule === "exact" ? baseline / height : 1);
|
|
446
|
+
height = target;
|
|
447
|
+
}
|
|
448
|
+
const painted = firstLine && marker ? [marker, ...segments] : segments;
|
|
449
|
+
emit({ x: startX, width: lineRight - startX, height, baseline, segments: painted, images, from, to });
|
|
450
|
+
prevTo = to;
|
|
451
|
+
lineTokens = [];
|
|
452
|
+
lineWidth = 0;
|
|
453
|
+
maxFontPx = sizePx(base);
|
|
454
|
+
maxImagePx = 0;
|
|
455
|
+
maxAscent = baseMetrics?.ascent ?? 0;
|
|
456
|
+
maxDescent = baseMetrics?.descent ?? 0;
|
|
457
|
+
firstLine = false;
|
|
458
|
+
band = bandFn(nominalH);
|
|
459
|
+
lineLeft = band.left + indentLeft;
|
|
460
|
+
lineRight = band.right - indentRight;
|
|
461
|
+
contStart = marker ? Math.max(markerTextX, lineLeft) : lineLeft;
|
|
462
|
+
};
|
|
463
|
+
let clusterText = "";
|
|
464
|
+
let clusterWidth = 0;
|
|
465
|
+
let clusterFont = null;
|
|
466
|
+
const sameFont = (a, b) => a.family === b.family && a.sizePt === b.sizePt && a.bold === b.bold && a.italic === b.italic;
|
|
467
|
+
const resetCluster = () => {
|
|
468
|
+
clusterText = "";
|
|
469
|
+
clusterWidth = 0;
|
|
470
|
+
clusterFont = null;
|
|
471
|
+
};
|
|
472
|
+
const clusterable = (t) => t.text != null && !t.isSpace && !t.isTab && !t.field && !t.image;
|
|
473
|
+
let softWrapped = false;
|
|
474
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
475
|
+
const token = tokens[ti];
|
|
476
|
+
if (token.isBreak) {
|
|
477
|
+
if (token.pos != null) prevTo = token.pos + 1;
|
|
478
|
+
flushLine(false);
|
|
479
|
+
resetCluster();
|
|
480
|
+
softWrapped = false;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (token.isSpace && !token.isTab && lineTokens.length === 0 && softWrapped) continue;
|
|
484
|
+
if (token.isTab) resolveTab(token, lineStart() + lineWidth, ti);
|
|
485
|
+
if (clusterable(token) && clusterFont && sameFont(clusterFont, token.font)) {
|
|
486
|
+
token.width = Math.max(0, measure(clusterText + token.text, token.font) - clusterWidth);
|
|
487
|
+
}
|
|
488
|
+
const cursor = lineStart() + lineWidth;
|
|
489
|
+
if (!token.isSpace && lineTokens.length > 0 && cursor + token.width > lineRight) {
|
|
490
|
+
flushLine(false);
|
|
491
|
+
resetCluster();
|
|
492
|
+
softWrapped = true;
|
|
493
|
+
if (token.isTab) resolveTab(token, lineStart() + lineWidth, ti);
|
|
494
|
+
else if (clusterable(token)) token.width = measure(token.text, token.font);
|
|
495
|
+
}
|
|
496
|
+
if (clusterable(token) && lineTokens.length === 0 && token.text.length > 1 && lineStart() + token.width > lineRight) {
|
|
497
|
+
const avail = lineRight - lineStart();
|
|
498
|
+
let n = 1;
|
|
499
|
+
while (n < token.text.length && measure(token.text.slice(0, n + 1), token.font) <= avail) n++;
|
|
500
|
+
const restText = token.text.slice(n);
|
|
501
|
+
const rest = { ...token, text: restText, width: measure(restText, token.font), size: restText.length };
|
|
502
|
+
if (token.pos != null) rest.pos = token.pos + n;
|
|
503
|
+
token.text = token.text.slice(0, n);
|
|
504
|
+
token.size = n;
|
|
505
|
+
token.width = measure(token.text, token.font);
|
|
506
|
+
tokens.splice(ti + 1, 0, rest);
|
|
507
|
+
}
|
|
508
|
+
if (clusterable(token)) {
|
|
509
|
+
if (clusterFont && !sameFont(clusterFont, token.font)) resetCluster();
|
|
510
|
+
clusterText += token.text;
|
|
511
|
+
clusterWidth += token.width;
|
|
512
|
+
clusterFont = token.font;
|
|
513
|
+
} else {
|
|
514
|
+
resetCluster();
|
|
515
|
+
}
|
|
516
|
+
lineTokens.push(token);
|
|
517
|
+
lineWidth += token.width;
|
|
518
|
+
if (token.image) {
|
|
519
|
+
const rotH = rotatedBoxHeight(token.image);
|
|
520
|
+
maxImagePx = Math.max(maxImagePx, rotH);
|
|
521
|
+
maxAscent = Math.max(maxAscent, token.image.height / 2 + rotH / 2);
|
|
522
|
+
maxDescent = Math.max(maxDescent, Math.max(0, (rotH - token.image.height) / 2));
|
|
523
|
+
} else {
|
|
524
|
+
maxFontPx = Math.max(maxFontPx, sizePx(token.font));
|
|
525
|
+
if (metrics) {
|
|
526
|
+
const m = metrics(token.font);
|
|
527
|
+
maxAscent = Math.max(maxAscent, m.ascent);
|
|
528
|
+
maxDescent = Math.max(maxDescent, m.descent);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
flushLine(true);
|
|
533
|
+
}
|
|
534
|
+
function rotatedBoxHeight(img) {
|
|
535
|
+
if (!img.rotation) return img.height;
|
|
536
|
+
const rad = img.rotation * Math.PI / 180;
|
|
537
|
+
return Math.abs(img.width * Math.sin(rad)) + Math.abs(img.height * Math.cos(rad));
|
|
538
|
+
}
|
|
539
|
+
function layoutParagraph(block, contentLeft, contentRight, ctx) {
|
|
540
|
+
const drafts = [];
|
|
541
|
+
wrapParagraph(
|
|
542
|
+
block,
|
|
543
|
+
ctx,
|
|
544
|
+
() => ({ left: contentLeft, right: contentRight }),
|
|
545
|
+
(d) => drafts.push(d)
|
|
546
|
+
);
|
|
547
|
+
return drafts;
|
|
548
|
+
}
|
|
549
|
+
function offsetTable(table, dy) {
|
|
550
|
+
table.y += dy;
|
|
551
|
+
for (const cell of table.cells) {
|
|
552
|
+
cell.y += dy;
|
|
553
|
+
for (const line of cell.lines) line.y += dy;
|
|
554
|
+
if (cell.floats) for (const f of cell.floats) f.y += dy;
|
|
555
|
+
if (cell.tables) for (const t of cell.tables) offsetTable(t, dy);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function shiftLineX(line, dx) {
|
|
559
|
+
line.x += dx;
|
|
560
|
+
for (const seg of line.segments) seg.x += dx;
|
|
561
|
+
if (line.images) for (const im of line.images) im.x += dx;
|
|
562
|
+
}
|
|
563
|
+
function shiftTableX(table, dx) {
|
|
564
|
+
if (dx === 0) return;
|
|
565
|
+
table.x += dx;
|
|
566
|
+
for (const cell of table.cells) {
|
|
567
|
+
cell.x += dx;
|
|
568
|
+
for (const line of cell.lines) shiftLineX(line, dx);
|
|
569
|
+
if (cell.floats) for (const f of cell.floats) f.x += dx;
|
|
570
|
+
if (cell.tables) for (const t of cell.tables) shiftTableX(t, dx);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
var TEXTBOX_INSET = { l: 10, t: 5, r: 10, b: 5 };
|
|
574
|
+
function resolveFloat(f, x, y, ctx) {
|
|
575
|
+
const rf = { x, y, width: f.width, height: f.height, src: f.src };
|
|
576
|
+
if (f.pos != null) rf.pos = f.pos;
|
|
577
|
+
if (f.rotation) rf.rotation = f.rotation;
|
|
578
|
+
if (f.shape) rf.shape = f.shape;
|
|
579
|
+
if (f.content && f.content.length > 0) {
|
|
580
|
+
const inset = f.inset ?? TEXTBOX_INSET;
|
|
581
|
+
const right = Math.max(inset.l + MIN_BAND, f.width - inset.r);
|
|
582
|
+
const inner = layoutFlow(f.content, inset.l, right, ctx);
|
|
583
|
+
const lines = inner.lines.map((l) => ({ ...l, y: l.y + inset.t }));
|
|
584
|
+
stripPositions(lines, inner.tables);
|
|
585
|
+
if (lines.length > 0) rf.lines = lines;
|
|
586
|
+
}
|
|
587
|
+
return rf;
|
|
588
|
+
}
|
|
589
|
+
function layoutFlow(blocks, contentLeft, contentRight, ctx) {
|
|
590
|
+
const lines = [];
|
|
591
|
+
const tables = [];
|
|
592
|
+
const floats = [];
|
|
593
|
+
let y = 0;
|
|
594
|
+
for (const block of blocks) {
|
|
595
|
+
if (block.type === "paragraph") {
|
|
596
|
+
for (const f of block.floats ?? []) {
|
|
597
|
+
const fx = f.hAlign === "right" ? contentRight - f.width : f.hAlign === "center" ? (contentLeft + contentRight - f.width) / 2 : contentLeft + (f.hOffset ?? 0);
|
|
598
|
+
const fy = y + (f.vOffset ?? 0);
|
|
599
|
+
floats.push(resolveFloat(f, fx, fy, ctx));
|
|
600
|
+
}
|
|
601
|
+
for (const d of layoutParagraph(block, contentLeft, contentRight, ctx)) {
|
|
602
|
+
lines.push(draftToLine(d, y));
|
|
603
|
+
y += d.height;
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
const table = layoutTable(block, contentLeft, contentRight, ctx);
|
|
607
|
+
offsetTable(table, y);
|
|
608
|
+
tables.push(table);
|
|
609
|
+
y += table.height;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { lines, tables, floats, height: y };
|
|
613
|
+
}
|
|
614
|
+
function eachCell(rows, ncols, visit) {
|
|
615
|
+
const spanned = new Array(ncols).fill(0);
|
|
616
|
+
for (let r = 0; r < rows.length; r++) {
|
|
617
|
+
let col = 0;
|
|
618
|
+
for (const cell of rows[r].cells) {
|
|
619
|
+
while (col < ncols && spanned[col] > 0) col++;
|
|
620
|
+
visit(r, cell, col);
|
|
621
|
+
for (let k = 0; k < cell.colspan && col + k < ncols; k++) spanned[col + k] = cell.rowspan;
|
|
622
|
+
col += cell.colspan;
|
|
623
|
+
}
|
|
624
|
+
for (let i = 0; i < ncols; i++) if (spanned[i] > 0) spanned[i]--;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function layoutTable(table, contentLeft, contentRight, ctx) {
|
|
628
|
+
const avail = contentRight - contentLeft;
|
|
629
|
+
const nrows = table.rows.length;
|
|
630
|
+
const ncols = table.rows.reduce(
|
|
631
|
+
(m, r) => Math.max(m, r.cells.reduce((s, c) => s + c.colspan, 0)),
|
|
632
|
+
0
|
|
633
|
+
);
|
|
634
|
+
const colWidths = new Array(ncols).fill(0);
|
|
635
|
+
eachCell(table.rows, ncols, (_r, cell, startCol) => {
|
|
636
|
+
if (cell.colwidth && cell.colwidth.length === cell.colspan) {
|
|
637
|
+
for (let k = 0; k < cell.colspan && startCol + k < ncols; k++) {
|
|
638
|
+
if (colWidths[startCol + k] === 0) colWidths[startCol + k] = cell.colwidth[k];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
const known = colWidths.reduce((s, w) => s + w, 0);
|
|
643
|
+
const unknown = colWidths.filter((w) => w === 0).length;
|
|
644
|
+
if (unknown > 0) {
|
|
645
|
+
const share = Math.max(0, (avail - known) / unknown);
|
|
646
|
+
for (let i = 0; i < ncols; i++) if (colWidths[i] === 0) colWidths[i] = share;
|
|
647
|
+
}
|
|
648
|
+
const natural = colWidths.reduce((s, w) => s + w, 0);
|
|
649
|
+
if (natural > avail && natural > 0) {
|
|
650
|
+
const scale = avail / natural;
|
|
651
|
+
for (let i = 0; i < ncols; i++) colWidths[i] *= scale;
|
|
652
|
+
}
|
|
653
|
+
const tableWidth = colWidths.reduce((s, w) => s + w, 0);
|
|
654
|
+
const xShift = table.align === "center" ? Math.max(0, (avail - tableWidth) / 2) : table.align === "right" ? Math.max(0, avail - tableWidth) : 0;
|
|
655
|
+
const colX = new Array(ncols + 1).fill(contentLeft + xShift);
|
|
656
|
+
for (let i = 0; i < ncols; i++) colX[i + 1] = colX[i] + colWidths[i];
|
|
657
|
+
const pad = {
|
|
658
|
+
left: table.cellPadding?.left ?? CELL_PAD_X,
|
|
659
|
+
right: table.cellPadding?.right ?? CELL_PAD_X,
|
|
660
|
+
top: table.cellPadding?.top ?? CELL_PAD_Y,
|
|
661
|
+
bottom: table.cellPadding?.bottom ?? CELL_PAD_Y
|
|
662
|
+
};
|
|
663
|
+
const cellDrafts = [];
|
|
664
|
+
eachCell(table.rows, ncols, (r, cell, col) => {
|
|
665
|
+
let cellWidth = 0;
|
|
666
|
+
for (let k = 0; k < cell.colspan && col + k < ncols; k++) cellWidth += colWidths[col + k];
|
|
667
|
+
const cellLeft = colX[col];
|
|
668
|
+
const flow = layoutFlow(cell.content, cellLeft + pad.left, cellLeft + cellWidth - pad.right, ctx);
|
|
669
|
+
cellDrafts.push({
|
|
670
|
+
startRow: r,
|
|
671
|
+
startCol: col,
|
|
672
|
+
colspan: cell.colspan,
|
|
673
|
+
rowspan: cell.rowspan,
|
|
674
|
+
cellLeft,
|
|
675
|
+
cellWidth,
|
|
676
|
+
lines: flow.lines,
|
|
677
|
+
tables: flow.tables,
|
|
678
|
+
floats: flow.floats,
|
|
679
|
+
contentHeight: flow.height,
|
|
680
|
+
background: cell.background,
|
|
681
|
+
vAlign: cell.vAlign,
|
|
682
|
+
borders: cell.borders
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
const rowHeight = new Array(nrows).fill(0);
|
|
686
|
+
for (const c of cellDrafts) {
|
|
687
|
+
if (c.rowspan === 1) {
|
|
688
|
+
rowHeight[c.startRow] = Math.max(rowHeight[c.startRow], c.contentHeight + pad.top + pad.bottom);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const c of cellDrafts) {
|
|
692
|
+
if (c.rowspan > 1) {
|
|
693
|
+
const need = c.contentHeight + pad.top + pad.bottom;
|
|
694
|
+
let span = 0;
|
|
695
|
+
for (let r = c.startRow; r < c.startRow + c.rowspan && r < nrows; r++) span += rowHeight[r];
|
|
696
|
+
if (need > span) {
|
|
697
|
+
const last = Math.min(c.startRow + c.rowspan - 1, nrows - 1);
|
|
698
|
+
rowHeight[last] += need - span;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
for (let r = 0; r < nrows; r++) {
|
|
703
|
+
const h = table.rows[r]?.height;
|
|
704
|
+
if (h) rowHeight[r] = h.exact ? h.value : Math.max(rowHeight[r], h.value);
|
|
705
|
+
}
|
|
706
|
+
const rowY = new Array(nrows + 1).fill(0);
|
|
707
|
+
for (let r = 0; r < nrows; r++) rowY[r + 1] = rowY[r] + rowHeight[r];
|
|
708
|
+
const cells = cellDrafts.map((c) => {
|
|
709
|
+
let height = 0;
|
|
710
|
+
for (let r = c.startRow; r < c.startRow + c.rowspan && r < nrows; r++) height += rowHeight[r];
|
|
711
|
+
const slack = Math.max(0, height - pad.top - pad.bottom - c.contentHeight);
|
|
712
|
+
const vOffset = c.vAlign === "bottom" ? slack : c.vAlign === "center" ? slack / 2 : 0;
|
|
713
|
+
const dy = rowY[c.startRow] + pad.top + vOffset;
|
|
714
|
+
const lines = c.lines.map((ln) => ({ ...ln, y: ln.y + dy }));
|
|
715
|
+
c.tables.forEach((t) => offsetTable(t, dy));
|
|
716
|
+
const cell = {
|
|
717
|
+
x: c.cellLeft,
|
|
718
|
+
y: rowY[c.startRow],
|
|
719
|
+
width: c.cellWidth,
|
|
720
|
+
height,
|
|
721
|
+
colspan: c.colspan,
|
|
722
|
+
rowspan: c.rowspan,
|
|
723
|
+
lines
|
|
724
|
+
};
|
|
725
|
+
if (c.background) cell.background = c.background;
|
|
726
|
+
if (c.borders) cell.borders = c.borders;
|
|
727
|
+
if (c.tables.length > 0) cell.tables = c.tables;
|
|
728
|
+
if (c.floats.length > 0) cell.floats = c.floats.map((f) => ({ ...f, y: f.y + dy }));
|
|
729
|
+
return cell;
|
|
730
|
+
});
|
|
731
|
+
const resolved = { x: contentLeft + xShift, y: 0, width: tableWidth, height: rowY[nrows], cells };
|
|
732
|
+
if (table.borders) resolved.borders = table.borders;
|
|
733
|
+
let headerRows = 0;
|
|
734
|
+
while (headerRows < nrows && table.rows[headerRows].header) headerRows++;
|
|
735
|
+
if (headerRows > 0 && headerRows < nrows) {
|
|
736
|
+
const headerBottom = rowY[headerRows];
|
|
737
|
+
const spansOut = cells.some((c) => c.y < headerBottom && c.y + c.height > headerBottom);
|
|
738
|
+
if (!spansOut) resolved.headerBottom = headerBottom;
|
|
739
|
+
}
|
|
740
|
+
const cantSplitBands = table.rows.map((row, r) => row.cantSplit ? { top: rowY[r], bottom: rowY[r + 1] } : null).filter((b) => b !== null);
|
|
741
|
+
if (cantSplitBands.length > 0) resolved.cantSplitBands = cantSplitBands;
|
|
742
|
+
return resolved;
|
|
743
|
+
}
|
|
744
|
+
function blockItemHeight(item) {
|
|
745
|
+
if ("para" in item) {
|
|
746
|
+
const lines = item.para.drafts?.reduce((s, d) => s + d.height, 0) ?? 0;
|
|
747
|
+
return lines + (item.para.before ?? 0) + (item.para.after ?? 0);
|
|
748
|
+
}
|
|
749
|
+
return item.table.height;
|
|
750
|
+
}
|
|
751
|
+
function assignSectionHeights(items) {
|
|
752
|
+
let current = null;
|
|
753
|
+
for (const item of items) {
|
|
754
|
+
if (item.section) {
|
|
755
|
+
current = item.section;
|
|
756
|
+
current.height = 0;
|
|
757
|
+
}
|
|
758
|
+
if (current) current.height = (current.height ?? 0) + blockItemHeight(item);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
var MIN_BAND = 24;
|
|
762
|
+
function shiftCell(cell, dy) {
|
|
763
|
+
cell.y += dy;
|
|
764
|
+
for (const line of cell.lines) line.y += dy;
|
|
765
|
+
cell.floats?.forEach((f) => f.y += dy);
|
|
766
|
+
cell.tables?.forEach((t) => offsetTable(t, dy));
|
|
767
|
+
return cell;
|
|
768
|
+
}
|
|
769
|
+
function cloneTable(t) {
|
|
770
|
+
return { ...t, cells: t.cells.map(cloneCell) };
|
|
771
|
+
}
|
|
772
|
+
function cloneCell(cell) {
|
|
773
|
+
const copy = { ...cell, lines: cell.lines.map((l) => ({ ...l })) };
|
|
774
|
+
if (cell.tables) copy.tables = cell.tables.map(cloneTable);
|
|
775
|
+
if (cell.floats) copy.floats = cell.floats.map((f) => ({ ...f }));
|
|
776
|
+
return copy;
|
|
777
|
+
}
|
|
778
|
+
function splitTableAt(table, cut) {
|
|
779
|
+
const straddlers = /* @__PURE__ */ new Map();
|
|
780
|
+
let splitBottom = cut;
|
|
781
|
+
let contHeight = 0;
|
|
782
|
+
for (const cell of table.cells) {
|
|
783
|
+
if (cell.y >= cut || cell.y + cell.height <= cut) continue;
|
|
784
|
+
splitBottom = Math.max(splitBottom, cell.y + cell.height);
|
|
785
|
+
const remLines = cell.lines.filter((l) => l.y + l.height > cut);
|
|
786
|
+
const remTables = (cell.tables ?? []).filter((t) => t.y + t.height > cut);
|
|
787
|
+
const firstY = Math.min(
|
|
788
|
+
remLines.length ? Math.min(...remLines.map((l) => l.y)) : Infinity,
|
|
789
|
+
remTables.length ? Math.min(...remTables.map((t) => t.y)) : Infinity
|
|
790
|
+
);
|
|
791
|
+
const first = firstY === Infinity ? cut : firstY;
|
|
792
|
+
const extent = Math.max(
|
|
793
|
+
remLines.length ? Math.max(...remLines.map((l) => l.y + l.height)) : first,
|
|
794
|
+
remTables.length ? Math.max(...remTables.map((t) => t.y + t.height)) : first
|
|
795
|
+
) - first;
|
|
796
|
+
contHeight = Math.max(contHeight, extent);
|
|
797
|
+
straddlers.set(cell, { cell, remLines, remTables, firstY: first });
|
|
798
|
+
}
|
|
799
|
+
const topCells = [];
|
|
800
|
+
const restCells = [];
|
|
801
|
+
for (const cell of table.cells) {
|
|
802
|
+
if (cell.y + cell.height <= cut) {
|
|
803
|
+
topCells.push(cell);
|
|
804
|
+
} else if (cell.y >= cut) {
|
|
805
|
+
restCells.push(shiftCell(cloneCell(cell), contHeight - splitBottom));
|
|
806
|
+
} else {
|
|
807
|
+
const c = straddlers.get(cell);
|
|
808
|
+
const topLines = cell.lines.filter((l) => l.y + l.height <= cut);
|
|
809
|
+
const topTables = (cell.tables ?? []).filter((t) => t.y + t.height <= cut);
|
|
810
|
+
const topCell = { ...cell, height: cut - cell.y, lines: topLines };
|
|
811
|
+
if (topTables.length > 0) topCell.tables = topTables;
|
|
812
|
+
else delete topCell.tables;
|
|
813
|
+
const topFloats = (cell.floats ?? []).filter((f) => f.y + f.height <= cut);
|
|
814
|
+
if (topFloats.length > 0) topCell.floats = topFloats;
|
|
815
|
+
else delete topCell.floats;
|
|
816
|
+
topCells.push(topCell);
|
|
817
|
+
const delta = -c.firstY;
|
|
818
|
+
const lines = c.remLines.map((l) => ({ ...l, y: l.y + delta }));
|
|
819
|
+
const remTables = c.remTables.map(cloneTable);
|
|
820
|
+
remTables.forEach((t) => offsetTable(t, delta));
|
|
821
|
+
const remFloats = (cell.floats ?? []).filter((f) => f.y + f.height > cut).map((f) => ({ ...f, y: f.y + delta }));
|
|
822
|
+
const contCell = {
|
|
823
|
+
x: cell.x,
|
|
824
|
+
y: 0,
|
|
825
|
+
width: cell.width,
|
|
826
|
+
height: contHeight,
|
|
827
|
+
colspan: cell.colspan,
|
|
828
|
+
rowspan: cell.rowspan,
|
|
829
|
+
lines
|
|
830
|
+
};
|
|
831
|
+
if (remTables.length > 0) contCell.tables = remTables;
|
|
832
|
+
if (remFloats.length > 0) contCell.floats = remFloats;
|
|
833
|
+
restCells.push(contCell);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const top = { x: table.x, y: 0, width: table.width, height: cut, cells: topCells };
|
|
837
|
+
const rest = {
|
|
838
|
+
x: table.x,
|
|
839
|
+
y: 0,
|
|
840
|
+
width: table.width,
|
|
841
|
+
height: contHeight + (table.height - splitBottom),
|
|
842
|
+
cells: restCells
|
|
843
|
+
};
|
|
844
|
+
if (table.borders) {
|
|
845
|
+
top.borders = table.borders;
|
|
846
|
+
rest.borders = table.borders;
|
|
847
|
+
}
|
|
848
|
+
return { top, rest };
|
|
849
|
+
}
|
|
850
|
+
function cloneHeaderCells(table, headerBottom) {
|
|
851
|
+
const band = table.cells.filter((c) => c.y + c.height <= headerBottom);
|
|
852
|
+
if (band.some((c) => c.tables && c.tables.length > 0)) return null;
|
|
853
|
+
return band.map((cell) => ({
|
|
854
|
+
...cell,
|
|
855
|
+
tables: void 0,
|
|
856
|
+
floats: cell.floats?.map((f) => ({ ...f })),
|
|
857
|
+
lines: cell.lines.map((l) => {
|
|
858
|
+
const line = { ...l, segments: l.segments.map((s) => ({ ...s, pos: void 0 })) };
|
|
859
|
+
delete line.from;
|
|
860
|
+
delete line.to;
|
|
861
|
+
if (line.images) line.images = line.images.map((im) => ({ ...im, pos: void 0 }));
|
|
862
|
+
return line;
|
|
863
|
+
})
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
function buildCtx(config) {
|
|
867
|
+
return {
|
|
868
|
+
base: { ...DEFAULT_FONT, ...config.defaultFont },
|
|
869
|
+
measure: config.measureText,
|
|
870
|
+
metrics: config.measureMetrics,
|
|
871
|
+
tabWidth: config.tabWidth ?? DEFAULT_TAB_WIDTH,
|
|
872
|
+
fieldPlaceholder: "1"
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function placeBlocks(items, config, ctx, band, footnotes) {
|
|
876
|
+
const { page } = config;
|
|
877
|
+
const top = band?.top ?? page.margin.top;
|
|
878
|
+
const bottom = band?.bottom ?? page.height - page.margin.bottom;
|
|
879
|
+
const contentLeft = page.margin.left;
|
|
880
|
+
const contentRight = page.width - page.margin.right;
|
|
881
|
+
const pages = [];
|
|
882
|
+
let lines = [];
|
|
883
|
+
let tables = [];
|
|
884
|
+
let pageFloats = [];
|
|
885
|
+
let exclusions = [];
|
|
886
|
+
let y = top;
|
|
887
|
+
const contentWidth = contentRight - contentLeft;
|
|
888
|
+
let colCount = 1;
|
|
889
|
+
let colGap = 0;
|
|
890
|
+
let colWidth = contentWidth;
|
|
891
|
+
let colIndex = 0;
|
|
892
|
+
let bandTop = top;
|
|
893
|
+
let sectionMaxY = top;
|
|
894
|
+
let colDirty = false;
|
|
895
|
+
const xShift = () => colIndex * (colWidth + colGap);
|
|
896
|
+
const colX0 = () => contentLeft + xShift();
|
|
897
|
+
const colX1 = () => colX0() + colWidth;
|
|
898
|
+
const bump = () => {
|
|
899
|
+
if (y > sectionMaxY) sectionMaxY = y;
|
|
900
|
+
};
|
|
901
|
+
const applyColumns = (cols) => {
|
|
902
|
+
colCount = Math.max(1, cols.count);
|
|
903
|
+
colGap = colCount > 1 ? cols.gap : 0;
|
|
904
|
+
colWidth = (contentWidth - colGap * (colCount - 1)) / colCount;
|
|
905
|
+
};
|
|
906
|
+
let pageFnNums = [];
|
|
907
|
+
const pageFnSet = /* @__PURE__ */ new Set();
|
|
908
|
+
const noteHeight = (n) => footnotes?.get(n)?.height ?? 0;
|
|
909
|
+
const reservedFor = (nums) => nums.length === 0 ? 0 : FOOTNOTE_AREA_GAP + nums.reduce((s, n) => s + noteHeight(n), 0);
|
|
910
|
+
const limit = () => bottom - reservedFor(pageFnNums);
|
|
911
|
+
const lineFnNums = (segs) => {
|
|
912
|
+
const out = [];
|
|
913
|
+
for (const s of segs) {
|
|
914
|
+
if (s.footnoteRef != null && footnotes?.has(s.footnoteRef) && !out.includes(s.footnoteRef)) {
|
|
915
|
+
out.push(s.footnoteRef);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return out;
|
|
919
|
+
};
|
|
920
|
+
const commitFns = (nums) => {
|
|
921
|
+
for (const n of nums) {
|
|
922
|
+
if (!pageFnSet.has(n)) {
|
|
923
|
+
pageFnNums.push(n);
|
|
924
|
+
pageFnSet.add(n);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const addedReserve = (nums) => {
|
|
929
|
+
const fresh = nums.filter((n) => !pageFnSet.has(n));
|
|
930
|
+
return fresh.length === 0 ? 0 : reservedFor([...pageFnNums, ...fresh]) - reservedFor(pageFnNums);
|
|
931
|
+
};
|
|
932
|
+
const tableFootnoteNums = (t, maxY = Infinity) => {
|
|
933
|
+
const out = [];
|
|
934
|
+
const scan = (ls) => {
|
|
935
|
+
for (const l of ls)
|
|
936
|
+
for (const s of l.segments) {
|
|
937
|
+
const n = s.footnoteRef;
|
|
938
|
+
if (n != null && footnotes?.has(n) && !pageFnSet.has(n) && !out.includes(n)) out.push(n);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
for (const c of t.cells) {
|
|
942
|
+
if (c.y >= maxY) continue;
|
|
943
|
+
scan(c.lines);
|
|
944
|
+
if (c.tables) for (const nt of c.tables) for (const nc of nt.cells) scan(nc.lines);
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
};
|
|
948
|
+
let balanceTarget = null;
|
|
949
|
+
let sectionRemaining = 0;
|
|
950
|
+
let balancing = false;
|
|
951
|
+
const colBottom = () => balanceTarget != null && colIndex < colCount - 1 ? Math.min(limit(), bandTop + balanceTarget) : limit();
|
|
952
|
+
const rebalance = () => {
|
|
953
|
+
if (!balancing) {
|
|
954
|
+
balanceTarget = null;
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const pageColH = limit() - bandTop;
|
|
958
|
+
balanceTarget = sectionRemaining <= colCount * pageColH ? Math.max(1, sectionRemaining / colCount) : null;
|
|
959
|
+
};
|
|
960
|
+
const buildFootnoteArea = (nums) => {
|
|
961
|
+
const areaTop = bottom - reservedFor(nums);
|
|
962
|
+
let fy = areaTop + FOOTNOTE_AREA_GAP;
|
|
963
|
+
const out = [];
|
|
964
|
+
for (const n of nums) {
|
|
965
|
+
const body = footnotes?.get(n);
|
|
966
|
+
if (!body) continue;
|
|
967
|
+
for (const l of body.lines) out.push({ ...l, y: l.y + fy });
|
|
968
|
+
fy += body.height;
|
|
969
|
+
}
|
|
970
|
+
return { separatorY: areaTop + FOOTNOTE_AREA_GAP / 2, lines: out };
|
|
971
|
+
};
|
|
972
|
+
const finalizePage = () => {
|
|
973
|
+
const resolved = { index: pages.length, width: page.width, height: page.height, lines };
|
|
974
|
+
if (tables.length > 0) resolved.tables = tables;
|
|
975
|
+
if (pageFloats.length > 0) resolved.floats = pageFloats;
|
|
976
|
+
if (pageFnNums.length > 0) resolved.footnotes = buildFootnoteArea(pageFnNums);
|
|
977
|
+
pages.push(resolved);
|
|
978
|
+
lines = [];
|
|
979
|
+
tables = [];
|
|
980
|
+
pageFloats = [];
|
|
981
|
+
exclusions = [];
|
|
982
|
+
pageFnNums = [];
|
|
983
|
+
pageFnSet.clear();
|
|
984
|
+
colIndex = 0;
|
|
985
|
+
bandTop = top;
|
|
986
|
+
sectionMaxY = top;
|
|
987
|
+
colDirty = false;
|
|
988
|
+
y = top;
|
|
989
|
+
};
|
|
990
|
+
const breakBand = () => {
|
|
991
|
+
bump();
|
|
992
|
+
if (colIndex < colCount - 1) {
|
|
993
|
+
colIndex++;
|
|
994
|
+
colDirty = false;
|
|
995
|
+
y = bandTop;
|
|
996
|
+
} else {
|
|
997
|
+
finalizePage();
|
|
998
|
+
rebalance();
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
const emitLine = (draft) => {
|
|
1002
|
+
let add = lineFnNums(draft.segments).filter((n) => !pageFnSet.has(n));
|
|
1003
|
+
const floor = () => Math.min(colBottom(), bottom - reservedFor([...pageFnNums, ...add]));
|
|
1004
|
+
if (y + draft.height > floor() && colDirty) {
|
|
1005
|
+
breakBand();
|
|
1006
|
+
add = lineFnNums(draft.segments).filter((n) => !pageFnSet.has(n));
|
|
1007
|
+
}
|
|
1008
|
+
lines.push(draftToLine(draft, y, xShift()));
|
|
1009
|
+
colDirty = true;
|
|
1010
|
+
y += draft.height;
|
|
1011
|
+
sectionRemaining -= draft.height;
|
|
1012
|
+
bump();
|
|
1013
|
+
commitFns(add);
|
|
1014
|
+
};
|
|
1015
|
+
const pageHasContent = () => lines.length > 0 || tables.length > 0 || pageFloats.length > 0;
|
|
1016
|
+
const registerFloats = (flow, yPara) => {
|
|
1017
|
+
for (const f of flow.floats ?? []) {
|
|
1018
|
+
const baseL = f.hRel === "page" ? 0 : contentLeft;
|
|
1019
|
+
const baseR = f.hRel === "page" ? page.width : contentRight;
|
|
1020
|
+
const fx = f.hAlign === "right" ? baseR - f.width : f.hAlign === "center" ? (baseL + baseR - f.width) / 2 : f.hAlign === "left" ? baseL : baseL + (f.hOffset ?? 0);
|
|
1021
|
+
const fy = f.vRel === "page" ? f.vOffset ?? 0 : f.vRel === "margin" ? top + (f.vOffset ?? 0) : yPara + (f.vOffset ?? 0);
|
|
1022
|
+
pageFloats.push(resolveFloat(f, fx, fy, ctx));
|
|
1023
|
+
colDirty = true;
|
|
1024
|
+
if (f.wrap === "square") {
|
|
1025
|
+
exclusions.push({
|
|
1026
|
+
left: fx - (f.distL ?? 0),
|
|
1027
|
+
right: fx + f.width + (f.distR ?? 0),
|
|
1028
|
+
top: fy - (f.distT ?? 0),
|
|
1029
|
+
bottom: fy + f.height + (f.distB ?? 0)
|
|
1030
|
+
});
|
|
1031
|
+
} else if (f.wrap === "topAndBottom") {
|
|
1032
|
+
exclusions.push({
|
|
1033
|
+
left: -Infinity,
|
|
1034
|
+
right: Infinity,
|
|
1035
|
+
top: fy - (f.distT ?? 0),
|
|
1036
|
+
bottom: fy + f.height + (f.distB ?? 0)
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
const bandAt = (yy, h) => {
|
|
1042
|
+
let L = colX0();
|
|
1043
|
+
let R = colX1();
|
|
1044
|
+
for (const ex of exclusions) {
|
|
1045
|
+
if (ex.bottom <= yy || ex.top >= yy + h) continue;
|
|
1046
|
+
const leftGap = Math.min(R, ex.left) - L;
|
|
1047
|
+
const rightGap = R - Math.max(L, ex.right);
|
|
1048
|
+
if (rightGap >= leftGap) L = Math.max(L, ex.right);
|
|
1049
|
+
else R = Math.min(R, ex.left);
|
|
1050
|
+
}
|
|
1051
|
+
return R - L >= MIN_BAND ? { left: L, right: R } : null;
|
|
1052
|
+
};
|
|
1053
|
+
const placeParaBanded = (flow) => {
|
|
1054
|
+
registerFloats(flow, y);
|
|
1055
|
+
wrapParagraph(
|
|
1056
|
+
flow,
|
|
1057
|
+
ctx,
|
|
1058
|
+
(estH) => {
|
|
1059
|
+
for (; ; ) {
|
|
1060
|
+
if (y + estH > colBottom() && colDirty) {
|
|
1061
|
+
breakBand();
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
const b = bandAt(y, estH);
|
|
1065
|
+
if (b) return b;
|
|
1066
|
+
const blockers = exclusions.filter((ex) => ex.top < y + estH && ex.bottom > y);
|
|
1067
|
+
if (blockers.length === 0) return { left: colX0(), right: colX1() };
|
|
1068
|
+
y = Math.min(...blockers.map((ex) => ex.bottom));
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
(draft) => emitLine(draft)
|
|
1072
|
+
);
|
|
1073
|
+
};
|
|
1074
|
+
let firstItem = true;
|
|
1075
|
+
for (const item of items) {
|
|
1076
|
+
if (item.section) {
|
|
1077
|
+
if (firstItem) {
|
|
1078
|
+
applyColumns(item.section);
|
|
1079
|
+
} else if (item.section.newPage) {
|
|
1080
|
+
if (pageHasContent()) finalizePage();
|
|
1081
|
+
applyColumns(item.section);
|
|
1082
|
+
} else {
|
|
1083
|
+
bump();
|
|
1084
|
+
if (sectionMaxY >= limit()) {
|
|
1085
|
+
finalizePage();
|
|
1086
|
+
} else {
|
|
1087
|
+
bandTop = sectionMaxY;
|
|
1088
|
+
y = bandTop;
|
|
1089
|
+
colIndex = 0;
|
|
1090
|
+
colDirty = false;
|
|
1091
|
+
}
|
|
1092
|
+
applyColumns(item.section);
|
|
1093
|
+
}
|
|
1094
|
+
balancing = colCount > 1;
|
|
1095
|
+
sectionRemaining = item.section.height ?? 0;
|
|
1096
|
+
rebalance();
|
|
1097
|
+
}
|
|
1098
|
+
firstItem = false;
|
|
1099
|
+
if ("para" in item) {
|
|
1100
|
+
if (item.para.pageBreakBefore && pageHasContent()) {
|
|
1101
|
+
finalizePage();
|
|
1102
|
+
rebalance();
|
|
1103
|
+
}
|
|
1104
|
+
if (item.para.before && colDirty) {
|
|
1105
|
+
y += item.para.before;
|
|
1106
|
+
sectionRemaining -= item.para.before;
|
|
1107
|
+
}
|
|
1108
|
+
const drafts = item.para.drafts;
|
|
1109
|
+
const draftsHeight = drafts?.reduce((s, d) => s + d.height, 0) ?? 0;
|
|
1110
|
+
const floatsAhead = exclusions.some((ex) => ex.bottom > y && ex.top < y + draftsHeight);
|
|
1111
|
+
if (drafts && !floatsAhead) {
|
|
1112
|
+
for (const d of drafts) emitLine(d);
|
|
1113
|
+
} else {
|
|
1114
|
+
placeParaBanded(item.para.getFlow());
|
|
1115
|
+
}
|
|
1116
|
+
if (item.para.after) {
|
|
1117
|
+
y += item.para.after;
|
|
1118
|
+
sectionRemaining -= item.para.after;
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
let table = item.table;
|
|
1122
|
+
const placeTable = (t) => {
|
|
1123
|
+
offsetTable(t, y);
|
|
1124
|
+
shiftTableX(t, xShift());
|
|
1125
|
+
tables.push(t);
|
|
1126
|
+
colDirty = true;
|
|
1127
|
+
commitFns(tableFootnoteNums(t));
|
|
1128
|
+
y += t.height;
|
|
1129
|
+
sectionRemaining -= t.height;
|
|
1130
|
+
bump();
|
|
1131
|
+
};
|
|
1132
|
+
for (; ; ) {
|
|
1133
|
+
const avail = colBottom() - y;
|
|
1134
|
+
if (table.height + addedReserve(tableFootnoteNums(table)) <= avail) {
|
|
1135
|
+
placeTable(table);
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
const hb = table.headerBottom != null && table.headerBottom < (limit() - bandTop) / 2 ? table.headerBottom : 0;
|
|
1139
|
+
let cut = 0;
|
|
1140
|
+
for (const cell of table.cells) {
|
|
1141
|
+
if (cell.y > hb && cell.y <= avail) cut = Math.max(cut, cell.y);
|
|
1142
|
+
}
|
|
1143
|
+
if (cut === 0) {
|
|
1144
|
+
let firstRowBottom = table.height;
|
|
1145
|
+
for (const cell of table.cells) {
|
|
1146
|
+
if (cell.y > hb && cell.y < firstRowBottom) firstRowBottom = cell.y;
|
|
1147
|
+
}
|
|
1148
|
+
const fitsFullBand = firstRowBottom - hb <= limit() - bandTop;
|
|
1149
|
+
const rowCantSplit = (table.cantSplitBands ?? []).some(
|
|
1150
|
+
(b) => b.top <= hb + 0.5 && b.bottom >= firstRowBottom - 0.5
|
|
1151
|
+
);
|
|
1152
|
+
if (rowCantSplit && fitsFullBand && colDirty) {
|
|
1153
|
+
breakBand();
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
cut = avail;
|
|
1157
|
+
}
|
|
1158
|
+
if (cut <= hb) {
|
|
1159
|
+
if (colDirty) {
|
|
1160
|
+
breakBand();
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
placeTable(table);
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
const { top: topFrag, rest } = splitTableAt(table, cut);
|
|
1167
|
+
if (rest.height >= table.height) {
|
|
1168
|
+
placeTable(table);
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
const topHasContent = topFrag.cells.some(
|
|
1172
|
+
(c) => c.lines.length > 0 || (c.tables?.length ?? 0) > 0
|
|
1173
|
+
);
|
|
1174
|
+
if (!topHasContent && colDirty) {
|
|
1175
|
+
breakBand();
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (hb > 0) {
|
|
1179
|
+
const ghosts = cloneHeaderCells(table, hb);
|
|
1180
|
+
if (ghosts) {
|
|
1181
|
+
for (const cell of rest.cells) shiftCell(cell, hb);
|
|
1182
|
+
rest.cells.unshift(...ghosts);
|
|
1183
|
+
rest.height += hb;
|
|
1184
|
+
rest.headerBottom = hb;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
placeTable(topFrag);
|
|
1188
|
+
breakBand();
|
|
1189
|
+
table = rest;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (pageHasContent() || pages.length === 0) finalizePage();
|
|
1194
|
+
return { pages };
|
|
1195
|
+
}
|
|
1196
|
+
function layoutBlocks(blocks, config) {
|
|
1197
|
+
const ctx = buildCtx(config);
|
|
1198
|
+
const left = config.page.margin.left;
|
|
1199
|
+
const right = config.page.width - config.page.margin.right;
|
|
1200
|
+
const cols = config.columns ?? { count: 1, gap: 0 };
|
|
1201
|
+
const colWidth = columnWidth(right - left, cols);
|
|
1202
|
+
const colRight = left + colWidth;
|
|
1203
|
+
const items = blocks.map((block, i) => {
|
|
1204
|
+
const item = block.type === "paragraph" ? {
|
|
1205
|
+
para: {
|
|
1206
|
+
getFlow: () => block,
|
|
1207
|
+
drafts: block.floats?.length ? null : layoutParagraph(block, left, colRight, ctx),
|
|
1208
|
+
before: block.spacing?.before,
|
|
1209
|
+
after: block.spacing?.after,
|
|
1210
|
+
pageBreakBefore: block.pageBreakBefore
|
|
1211
|
+
}
|
|
1212
|
+
} : { table: layoutTable(block, left, colRight, ctx) };
|
|
1213
|
+
if (i === 0) item.section = { ...cols, newPage: true };
|
|
1214
|
+
return item;
|
|
1215
|
+
});
|
|
1216
|
+
assignSectionHeights(items);
|
|
1217
|
+
return placeBlocks(items, config, ctx);
|
|
1218
|
+
}
|
|
1219
|
+
function columnWidth(totalWidth, cols) {
|
|
1220
|
+
const count = Math.max(1, cols.count);
|
|
1221
|
+
const gap = count > 1 ? cols.gap : 0;
|
|
1222
|
+
return (totalWidth - gap * (count - 1)) / count;
|
|
1223
|
+
}
|
|
1224
|
+
function shiftDrafts(drafts, delta) {
|
|
1225
|
+
return drafts.map((d) => ({
|
|
1226
|
+
...d,
|
|
1227
|
+
from: d.from != null ? d.from + delta : d.from,
|
|
1228
|
+
to: d.to != null ? d.to + delta : d.to,
|
|
1229
|
+
segments: d.segments.map((s) => s.pos != null ? { ...s, pos: s.pos + delta } : s),
|
|
1230
|
+
images: d.images.map((im) => im.pos != null ? { ...im, pos: im.pos + delta } : im)
|
|
1231
|
+
}));
|
|
1232
|
+
}
|
|
1233
|
+
function cloneLineShifted(l, delta) {
|
|
1234
|
+
const out = {
|
|
1235
|
+
...l,
|
|
1236
|
+
segments: l.segments.map((s) => s.pos != null ? { ...s, pos: s.pos + delta } : { ...s })
|
|
1237
|
+
};
|
|
1238
|
+
if (l.images) out.images = l.images.map((im) => im.pos != null ? { ...im, pos: im.pos + delta } : { ...im });
|
|
1239
|
+
if (l.from != null) out.from = l.from + delta;
|
|
1240
|
+
if (l.to != null) out.to = l.to + delta;
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
function cloneTableShifted(t, delta) {
|
|
1244
|
+
return {
|
|
1245
|
+
...t,
|
|
1246
|
+
borders: t.borders ? { ...t.borders } : t.borders,
|
|
1247
|
+
cells: t.cells.map((c) => ({
|
|
1248
|
+
...c,
|
|
1249
|
+
borders: c.borders ? { ...c.borders } : c.borders,
|
|
1250
|
+
lines: c.lines.map((l) => cloneLineShifted(l, delta)),
|
|
1251
|
+
tables: c.tables?.map((nt) => cloneTableShifted(nt, delta)),
|
|
1252
|
+
floats: c.floats?.map((f) => ({ ...f }))
|
|
1253
|
+
}))
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function tableHasList(node) {
|
|
1257
|
+
let found = false;
|
|
1258
|
+
node.descendants((n) => {
|
|
1259
|
+
if (found) return false;
|
|
1260
|
+
if (n.type.name === "paragraph" && n.attrs["list"]) found = true;
|
|
1261
|
+
return !found;
|
|
1262
|
+
});
|
|
1263
|
+
return found;
|
|
1264
|
+
}
|
|
1265
|
+
var LayoutCache = class {
|
|
1266
|
+
paragraphs = /* @__PURE__ */ new WeakMap();
|
|
1267
|
+
tables = /* @__PURE__ */ new WeakMap();
|
|
1268
|
+
};
|
|
1269
|
+
function createLayoutCache() {
|
|
1270
|
+
return new LayoutCache();
|
|
1271
|
+
}
|
|
1272
|
+
var CHROME_DISTANCE = 48;
|
|
1273
|
+
function layVariants(docs, fn) {
|
|
1274
|
+
const out = {};
|
|
1275
|
+
if (docs.default) out.default = fn(docs.default);
|
|
1276
|
+
if (docs.first) out.first = fn(docs.first);
|
|
1277
|
+
if (docs.even) out.even = fn(docs.even);
|
|
1278
|
+
return out;
|
|
1279
|
+
}
|
|
1280
|
+
function maxBandHeight(bands) {
|
|
1281
|
+
return Math.max(0, ...[bands.default, bands.first, bands.even].filter(Boolean).map((b) => b.height));
|
|
1282
|
+
}
|
|
1283
|
+
function anyBandHasFields(bands) {
|
|
1284
|
+
return [bands.default, bands.first, bands.even].some(
|
|
1285
|
+
(b) => b?.lines.some((l) => l.segments.some((s) => s.field)) ?? false
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
function stripPositions(lines, tables) {
|
|
1289
|
+
for (const line of lines) {
|
|
1290
|
+
delete line.from;
|
|
1291
|
+
delete line.to;
|
|
1292
|
+
for (const seg of line.segments) delete seg.pos;
|
|
1293
|
+
if (line.images) for (const im of line.images) delete im.pos;
|
|
1294
|
+
}
|
|
1295
|
+
for (const t of tables) {
|
|
1296
|
+
for (const c of t.cells) {
|
|
1297
|
+
stripPositions(c.lines, c.tables ?? []);
|
|
1298
|
+
if (c.floats) for (const f of c.floats) delete f.pos;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function layoutChrome(doc, topY, left, right, ctx) {
|
|
1303
|
+
const flow = layoutFlow(toFlowBlocks(doc, ctx.base, true), left, right, ctx);
|
|
1304
|
+
const lines = flow.lines.map((l) => ({ ...l, y: l.y + topY }));
|
|
1305
|
+
flow.tables.forEach((t) => offsetTable(t, topY));
|
|
1306
|
+
stripPositions(lines, flow.tables);
|
|
1307
|
+
const band = { lines, tables: flow.tables, height: flow.height };
|
|
1308
|
+
if (flow.floats.length > 0)
|
|
1309
|
+
band.floats = flow.floats.map((f) => ({ ...f, y: f.y + topY, pos: void 0 }));
|
|
1310
|
+
return band;
|
|
1311
|
+
}
|
|
1312
|
+
function layoutFooterChrome(doc, pageHeight, left, right, ctx) {
|
|
1313
|
+
const flow = layoutChrome(doc, 0, left, right, ctx);
|
|
1314
|
+
const topY = pageHeight - CHROME_DISTANCE - flow.height;
|
|
1315
|
+
const band = {
|
|
1316
|
+
lines: flow.lines.map((l) => ({ ...l, y: l.y + topY })),
|
|
1317
|
+
tables: flow.tables,
|
|
1318
|
+
height: flow.height
|
|
1319
|
+
};
|
|
1320
|
+
band.tables.forEach((t) => offsetTable(t, topY));
|
|
1321
|
+
if (flow.floats) band.floats = flow.floats.map((f) => ({ ...f, y: f.y + topY }));
|
|
1322
|
+
return band;
|
|
1323
|
+
}
|
|
1324
|
+
function layoutFootnoteBody(doc, left, right, ctx) {
|
|
1325
|
+
const fnCtx = { ...ctx, base: { ...ctx.base, sizePt: ctx.base.sizePt * FOOTNOTE_FONT_SCALE } };
|
|
1326
|
+
const flow = layoutFlow(toFlowBlocks(doc, fnCtx.base, false), left, right, fnCtx);
|
|
1327
|
+
stripPositions(flow.lines, flow.tables);
|
|
1328
|
+
return { lines: flow.lines, height: flow.height };
|
|
1329
|
+
}
|
|
1330
|
+
function layout(doc, config, cache, chrome, footnotes) {
|
|
1331
|
+
const ctx = buildCtx(config);
|
|
1332
|
+
const { page } = config;
|
|
1333
|
+
const left = page.margin.left;
|
|
1334
|
+
const right = page.width - page.margin.right;
|
|
1335
|
+
const headerDocs = { default: chrome?.header, first: chrome?.headerFirst, even: chrome?.headerEven };
|
|
1336
|
+
const footerDocs = { default: chrome?.footer, first: chrome?.footerFirst, even: chrome?.footerEven };
|
|
1337
|
+
const layHeaders = (c) => layVariants(headerDocs, (d) => layoutChrome(d, CHROME_DISTANCE, left, right, c));
|
|
1338
|
+
const layFooters = (c) => layVariants(footerDocs, (d) => layoutFooterChrome(d, page.height, left, right, c));
|
|
1339
|
+
let headers = layHeaders(ctx);
|
|
1340
|
+
let footers = layFooters(ctx);
|
|
1341
|
+
let top = page.margin.top;
|
|
1342
|
+
let bottom = page.height - page.margin.bottom;
|
|
1343
|
+
if (headers.default || headers.first || headers.even) {
|
|
1344
|
+
top = Math.max(top, CHROME_DISTANCE + maxBandHeight(headers));
|
|
1345
|
+
}
|
|
1346
|
+
if (footers.default || footers.first || footers.even) {
|
|
1347
|
+
bottom = Math.min(bottom, page.height - CHROME_DISTANCE - maxBandHeight(footers));
|
|
1348
|
+
}
|
|
1349
|
+
const counter = createNumberingCounter(doc.attrs["numbering"]);
|
|
1350
|
+
const sections = doc.attrs["sections"] ?? [
|
|
1351
|
+
{ blockCount: doc.childCount, columns: { count: 1, gap: 0 }, newPage: true }
|
|
1352
|
+
];
|
|
1353
|
+
const blockSection = [];
|
|
1354
|
+
for (const sec of sections) {
|
|
1355
|
+
for (let k = 0; k < sec.blockCount; k++) {
|
|
1356
|
+
blockSection.push({ columns: sec.columns, start: k === 0, newPage: sec.newPage });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const lastSec = sections[sections.length - 1];
|
|
1360
|
+
while (blockSection.length < doc.childCount) {
|
|
1361
|
+
blockSection.push({ columns: lastSec.columns, start: false, newPage: lastSec.newPage });
|
|
1362
|
+
}
|
|
1363
|
+
const items = [];
|
|
1364
|
+
doc.forEach((node, offset, index) => {
|
|
1365
|
+
const bs = blockSection[index] ?? { columns: { count: 1, gap: 0 }, start: false, newPage: true };
|
|
1366
|
+
const colRight = left + columnWidth(right - left, bs.columns);
|
|
1367
|
+
const tag = (item) => {
|
|
1368
|
+
if (bs.start) item.section = { ...bs.columns, newPage: bs.newPage };
|
|
1369
|
+
return item;
|
|
1370
|
+
};
|
|
1371
|
+
if (node.type.name === "paragraph") {
|
|
1372
|
+
const marker = markerFor(node, counter);
|
|
1373
|
+
const getFlow = () => paragraphToFlow(node, ctx.base, offset, true, marker);
|
|
1374
|
+
const sp = node.attrs["spacing"];
|
|
1375
|
+
const para = (drafts2) => ({
|
|
1376
|
+
getFlow,
|
|
1377
|
+
drafts: drafts2,
|
|
1378
|
+
before: sp?.before,
|
|
1379
|
+
after: sp?.after,
|
|
1380
|
+
pageBreakBefore: node.attrs["pageBreakBefore"] === true
|
|
1381
|
+
});
|
|
1382
|
+
if (nodeHasFloats(node)) {
|
|
1383
|
+
items.push(tag({ para: para(null) }));
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const contentStart = offset + 1;
|
|
1387
|
+
const hit = cache?.paragraphs.get(node);
|
|
1388
|
+
if (hit && hit.left === left && hit.right === colRight && hit.marker === marker) {
|
|
1389
|
+
if (hit.basePos !== contentStart) {
|
|
1390
|
+
hit.drafts = shiftDrafts(hit.drafts, contentStart - hit.basePos);
|
|
1391
|
+
hit.basePos = contentStart;
|
|
1392
|
+
}
|
|
1393
|
+
items.push(tag({ para: para(hit.drafts) }));
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const flow = paragraphToFlow(node, ctx.base, offset, true, marker);
|
|
1397
|
+
const drafts = layoutParagraph(flow, left, colRight, ctx);
|
|
1398
|
+
cache?.paragraphs.set(node, { left, right: colRight, basePos: contentStart, marker, drafts });
|
|
1399
|
+
items.push(tag({ para: { ...para(drafts), getFlow: () => flow } }));
|
|
1400
|
+
} else if (node.type.name === "table") {
|
|
1401
|
+
const hit = cache?.tables.get(node);
|
|
1402
|
+
if (hit && hit.left === left && hit.right === colRight) {
|
|
1403
|
+
items.push(tag({ table: cloneTableShifted(hit.table, offset - hit.basePos) }));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const table = layoutTable(tableToFlow(node, ctx.base, offset, counter), left, colRight, ctx);
|
|
1407
|
+
if (cache && !tableHasList(node)) {
|
|
1408
|
+
cache.tables.set(node, { left, right: colRight, basePos: offset, table });
|
|
1409
|
+
items.push(tag({ table: cloneTableShifted(table, 0) }));
|
|
1410
|
+
} else {
|
|
1411
|
+
items.push(tag({ table }));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
let fnMap;
|
|
1416
|
+
if (footnotes) {
|
|
1417
|
+
fnMap = /* @__PURE__ */ new Map();
|
|
1418
|
+
for (const key of Object.keys(footnotes)) {
|
|
1419
|
+
const num = Number(key);
|
|
1420
|
+
fnMap.set(num, layoutFootnoteBody(footnotes[num], left, right, ctx));
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
assignSectionHeights(items);
|
|
1424
|
+
const resolved = placeBlocks(items, config, ctx, { top, bottom }, fnMap);
|
|
1425
|
+
if (anyBandHasFields(headers) || anyBandHasFields(footers)) {
|
|
1426
|
+
const fieldCtx = { ...ctx, fieldPlaceholder: String(resolved.pages.length) };
|
|
1427
|
+
headers = layHeaders(fieldCtx);
|
|
1428
|
+
footers = layFooters(fieldCtx);
|
|
1429
|
+
}
|
|
1430
|
+
if (headers.default) resolved.pageHeader = headers.default;
|
|
1431
|
+
if (headers.first) resolved.pageHeaderFirst = headers.first;
|
|
1432
|
+
if (headers.even) resolved.pageHeaderEven = headers.even;
|
|
1433
|
+
if (footers.default) resolved.pageFooter = footers.default;
|
|
1434
|
+
if (footers.first) resolved.pageFooterFirst = footers.first;
|
|
1435
|
+
if (footers.even) resolved.pageFooterEven = footers.even;
|
|
1436
|
+
if (chrome?.titlePg || chrome?.evenAndOdd) {
|
|
1437
|
+
resolved.chromeSelect = { titlePg: !!chrome.titlePg, evenAndOdd: !!chrome.evenAndOdd };
|
|
1438
|
+
}
|
|
1439
|
+
return resolved;
|
|
1440
|
+
}
|
|
1441
|
+
export {
|
|
1442
|
+
LayoutCache,
|
|
1443
|
+
createLayoutCache,
|
|
1444
|
+
layout,
|
|
1445
|
+
layoutBlocks,
|
|
1446
|
+
toFlowBlocks
|
|
1447
|
+
};
|