@stll/folio-core 0.27.1 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docx/server/extractDocxText.js +108 -33
- package/dist/docx/server/materializeYjsDocx.d.ts +31 -0
- package/dist/docx/server/materializeYjsDocx.js +61 -0
- package/dist/docx/styleParser.js +15 -14
- package/dist/layout-bridge/convert/toFlowBlocks.js +74 -23
- package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
- package/dist/layout-engine/footnoteColumnReflow.js +74 -0
- package/dist/layout-engine/index.d.ts +3 -5
- package/dist/layout-engine/index.js +36 -15
- package/dist/layout-engine/measure/cache.d.ts +1 -1
- package/dist/layout-engine/measure/cache.js +11 -2
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
- package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
- package/dist/layout-engine/paginator.d.ts +6 -0
- package/dist/layout-engine/paginator.js +30 -11
- package/dist/layout-engine/types.d.ts +11 -6
- package/dist/prosemirror/attrs/index.js +3 -2
- package/dist/prosemirror/commands/formatPainter.js +45 -1
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +59 -44
- package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/toProseDoc.js +156 -33
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
- package/dist/prosemirror/schema/marks.d.ts +3 -1
- package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
- package/dist/prosemirror/styles/styleResolver.js +22 -29
- package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
- package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -1
- package/dist/utils/textFormattingMerge.d.ts +9 -1
- package/dist/utils/textFormattingMerge.js +32 -1
- package/package.json +2 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { escapeTableCell } from "../../markdown/escape.js";
|
|
2
2
|
import { RELATIONSHIP_TYPES, parseRelationships } from "../relsParser.js";
|
|
3
3
|
import { findAllDeep, findDeep, getAttribute, getAttributeByNamespaceUri, getLocalName, getNamespaceUri, getTextContent, parseXml } from "../xmlParser.js";
|
|
4
|
-
import { loadDocxArchive } from "./boundedArchive.js";
|
|
4
|
+
import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
|
|
5
5
|
//#region src/docx/server/extractDocxText.ts
|
|
6
6
|
const DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
|
|
7
|
+
const ARCHIVE_LIMIT_REASON = "total-too-large";
|
|
7
8
|
const WORDPROCESSINGML_NAMESPACES = /* @__PURE__ */ new Set(["http://schemas.openxmlformats.org/wordprocessingml/2006/main", "http://purl.oclc.org/ooxml/wordprocessingml/main"]);
|
|
8
9
|
const childElements = (element) => element.elements?.filter((child) => child.type === "element") ?? [];
|
|
9
10
|
const wordElementName = (element) => WORDPROCESSINGML_NAMESPACES.has(getNamespaceUri(element) ?? "") ? getLocalName(element.name) : null;
|
|
@@ -105,6 +106,23 @@ const MAX_TABLE_COLUMNS = 256;
|
|
|
105
106
|
const MAX_NESTED_TABLE_DEPTH = 8;
|
|
106
107
|
/** Rows collected from one `w:tbl`. The column cap alone leaves row count unbounded. */
|
|
107
108
|
const MAX_TABLE_ROWS = 8192;
|
|
109
|
+
/** Source and rendered characters retained across every table in one extraction. */
|
|
110
|
+
const MAX_TABLE_CHARACTERS = 8 * 1024 * 1024;
|
|
111
|
+
const createTableExtractionBudget = () => ({
|
|
112
|
+
remainingSourceCharacters: MAX_TABLE_CHARACTERS,
|
|
113
|
+
remainingRenderedCharacters: MAX_TABLE_CHARACTERS
|
|
114
|
+
});
|
|
115
|
+
const chargeTableCharacters = ({ budget, characters, kind }) => {
|
|
116
|
+
if (characters > (kind === "source" ? budget.remainingSourceCharacters : budget.remainingRenderedCharacters)) throw new DocxArchiveError({
|
|
117
|
+
message: `Extracted DOCX table ${kind} text exceeded the ${MAX_TABLE_CHARACTERS}-character limit`,
|
|
118
|
+
reason: ARCHIVE_LIMIT_REASON
|
|
119
|
+
});
|
|
120
|
+
if (kind === "source") {
|
|
121
|
+
budget.remainingSourceCharacters -= characters;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
budget.remainingRenderedCharacters -= characters;
|
|
125
|
+
};
|
|
108
126
|
/**
|
|
109
127
|
* Characters one extraction emits, shared by the body and every header/footer
|
|
110
128
|
* part. Element count is bounded at unzip, but a bounded element count still
|
|
@@ -112,19 +130,16 @@ const MAX_TABLE_ROWS = 8192;
|
|
|
112
130
|
* GFM scaffolding are counted, so the emitted side carries its own ceiling.
|
|
113
131
|
*/
|
|
114
132
|
const MAX_EXTRACTED_CHARS = 8e6;
|
|
115
|
-
|
|
116
|
-
* Collect a table's `w:tr`, or a row's `w:tc`, seeing through the wrappers Word
|
|
117
|
-
* puts around them (`w:sdt` / `w:sdtContent` content controls, `w:customXml`).
|
|
118
|
-
* The walk stops at `w:tbl` and `w:p` so a nested table's rows and cells never
|
|
119
|
-
* leak into the grid of the table that contains them.
|
|
120
|
-
*/
|
|
121
|
-
const collectTableParts = (parent, localName, limit) => {
|
|
133
|
+
const collectTableParts = ({ parent, localName, limit }) => {
|
|
122
134
|
const parts = [];
|
|
123
135
|
const walk = (node) => {
|
|
124
136
|
for (const child of childElements(node)) {
|
|
125
|
-
if (parts.length >= limit) return;
|
|
126
137
|
const childName = wordElementName(child);
|
|
127
138
|
if (childName === localName) {
|
|
139
|
+
if (parts.length >= limit) throw new DocxArchiveError({
|
|
140
|
+
message: `Extracted DOCX table exceeded the ${limit}-${localName} limit`,
|
|
141
|
+
reason: ARCHIVE_LIMIT_REASON
|
|
142
|
+
});
|
|
128
143
|
parts.push(child);
|
|
129
144
|
continue;
|
|
130
145
|
}
|
|
@@ -152,7 +167,15 @@ const readCellSourceParagraphs = (cell, depth) => {
|
|
|
152
167
|
}
|
|
153
168
|
if (childName === "tbl") {
|
|
154
169
|
if (depth >= MAX_NESTED_TABLE_DEPTH) continue;
|
|
155
|
-
for (const row of collectTableParts(
|
|
170
|
+
for (const row of collectTableParts({
|
|
171
|
+
parent: child,
|
|
172
|
+
localName: "tr",
|
|
173
|
+
limit: MAX_TABLE_ROWS
|
|
174
|
+
})) for (const nestedCell of collectTableParts({
|
|
175
|
+
parent: row,
|
|
176
|
+
localName: "tc",
|
|
177
|
+
limit: MAX_TABLE_COLUMNS
|
|
178
|
+
})) for (const paragraph of readCellSourceParagraphs(nestedCell, depth + 1)) paragraphs.push(paragraph);
|
|
156
179
|
continue;
|
|
157
180
|
}
|
|
158
181
|
walk(child);
|
|
@@ -161,18 +184,26 @@ const readCellSourceParagraphs = (cell, depth) => {
|
|
|
161
184
|
walk(cell);
|
|
162
185
|
return paragraphs;
|
|
163
186
|
};
|
|
164
|
-
const readCellRenderedLines = (cell, depth) => {
|
|
187
|
+
const readCellRenderedLines = (cell, { depth, budget }) => {
|
|
165
188
|
const lines = [];
|
|
166
189
|
const walk = (node) => {
|
|
167
190
|
for (const child of childElements(node)) {
|
|
168
191
|
const childName = wordElementName(child);
|
|
169
192
|
if (childName === "p") {
|
|
170
193
|
const text = collectText(child);
|
|
194
|
+
chargeTableCharacters({
|
|
195
|
+
budget,
|
|
196
|
+
characters: text.length,
|
|
197
|
+
kind: "source"
|
|
198
|
+
});
|
|
171
199
|
if (text.length > 0) lines.push(text);
|
|
172
200
|
continue;
|
|
173
201
|
}
|
|
174
202
|
if (childName === "tbl") {
|
|
175
|
-
if (depth < MAX_NESTED_TABLE_DEPTH) for (const line of flattenNestedTable(child,
|
|
203
|
+
if (depth < MAX_NESTED_TABLE_DEPTH) for (const line of flattenNestedTable(child, {
|
|
204
|
+
depth: depth + 1,
|
|
205
|
+
budget
|
|
206
|
+
})) lines.push(line);
|
|
176
207
|
continue;
|
|
177
208
|
}
|
|
178
209
|
walk(child);
|
|
@@ -181,10 +212,21 @@ const readCellRenderedLines = (cell, depth) => {
|
|
|
181
212
|
walk(cell);
|
|
182
213
|
return lines;
|
|
183
214
|
};
|
|
184
|
-
const flattenNestedTable = (table, depth) => {
|
|
215
|
+
const flattenNestedTable = (table, { depth, budget }) => {
|
|
185
216
|
const lines = [];
|
|
186
|
-
for (const row of collectTableParts(
|
|
187
|
-
|
|
217
|
+
for (const row of collectTableParts({
|
|
218
|
+
parent: table,
|
|
219
|
+
localName: "tr",
|
|
220
|
+
limit: MAX_TABLE_ROWS
|
|
221
|
+
})) {
|
|
222
|
+
const cells = collectTableParts({
|
|
223
|
+
parent: row,
|
|
224
|
+
localName: "tc",
|
|
225
|
+
limit: MAX_TABLE_COLUMNS
|
|
226
|
+
}).map((cell) => readCellRenderedLines(cell, {
|
|
227
|
+
depth,
|
|
228
|
+
budget
|
|
229
|
+
}).join("\n"));
|
|
188
230
|
if (cells.some((text) => text.length > 0)) lines.push(cells.join(NESTED_TABLE_CELL_SEPARATOR));
|
|
189
231
|
}
|
|
190
232
|
return lines;
|
|
@@ -194,7 +236,7 @@ const emptyTableCell = () => ({
|
|
|
194
236
|
paragraphs: [],
|
|
195
237
|
gridSpan: 1
|
|
196
238
|
});
|
|
197
|
-
const readTableCell = (cell, depth) => {
|
|
239
|
+
const readTableCell = (cell, { depth, budget }) => {
|
|
198
240
|
const properties = findWordChild(cell, "tcPr");
|
|
199
241
|
const gridSpanValue = getWordAttribute(findWordChild(properties, "gridSpan"), "val");
|
|
200
242
|
const parsedGridSpan = gridSpanValue === null ? 1 : Number.parseInt(gridSpanValue, 10);
|
|
@@ -205,10 +247,12 @@ const readTableCell = (cell, depth) => {
|
|
|
205
247
|
paragraphs: [],
|
|
206
248
|
gridSpan
|
|
207
249
|
};
|
|
208
|
-
const paragraphs = readCellSourceParagraphs(cell, depth);
|
|
209
250
|
return {
|
|
210
|
-
text: readCellRenderedLines(cell,
|
|
211
|
-
|
|
251
|
+
text: readCellRenderedLines(cell, {
|
|
252
|
+
depth,
|
|
253
|
+
budget
|
|
254
|
+
}).join("\n"),
|
|
255
|
+
paragraphs: readCellSourceParagraphs(cell, depth),
|
|
212
256
|
gridSpan
|
|
213
257
|
};
|
|
214
258
|
};
|
|
@@ -235,18 +279,29 @@ const readRowGridOffset = (row, localName) => {
|
|
|
235
279
|
const parsed = value === null ? 0 : Number.parseInt(value, 10);
|
|
236
280
|
return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, MAX_TABLE_COLUMNS) : 0;
|
|
237
281
|
};
|
|
238
|
-
const readTableGrid = (table) => {
|
|
282
|
+
const readTableGrid = (table, budget) => {
|
|
239
283
|
const rows = [];
|
|
240
284
|
let columnCount = 0;
|
|
241
285
|
let firstRowIsHeader = false;
|
|
242
|
-
for (const [rowIndex, row] of collectTableParts(
|
|
286
|
+
for (const [rowIndex, row] of collectTableParts({
|
|
287
|
+
parent: table,
|
|
288
|
+
localName: "tr",
|
|
289
|
+
limit: MAX_TABLE_ROWS
|
|
290
|
+
}).entries()) {
|
|
243
291
|
if (rowIndex === 0) firstRowIsHeader = declaresHeaderRow(row);
|
|
244
292
|
const columns = [];
|
|
245
293
|
const gridBefore = readRowGridOffset(row, "gridBefore");
|
|
246
294
|
for (let index = 0; index < gridBefore; index += 1) columns.push(emptyTableCell());
|
|
247
|
-
for (const cell of collectTableParts(
|
|
295
|
+
for (const cell of collectTableParts({
|
|
296
|
+
parent: row,
|
|
297
|
+
localName: "tc",
|
|
298
|
+
limit: MAX_TABLE_COLUMNS
|
|
299
|
+
})) {
|
|
248
300
|
if (columns.length >= MAX_TABLE_COLUMNS) break;
|
|
249
|
-
const extractedCell = readTableCell(cell,
|
|
301
|
+
const extractedCell = readTableCell(cell, {
|
|
302
|
+
depth: 0,
|
|
303
|
+
budget
|
|
304
|
+
});
|
|
250
305
|
columns.push(extractedCell);
|
|
251
306
|
const padding = Math.min(extractedCell.gridSpan - 1, MAX_TABLE_COLUMNS - columns.length);
|
|
252
307
|
for (let index = 0; index < padding; index++) columns.push(emptyTableCell());
|
|
@@ -269,12 +324,17 @@ const toRowLine = (columns, columnCount) => {
|
|
|
269
324
|
return `| ${cells.join(" | ")} |`;
|
|
270
325
|
};
|
|
271
326
|
/** Render a `w:tbl` as GFM rows. A table with no cell at all renders nothing. */
|
|
272
|
-
const renderTableRows = (table, tableIndex) => {
|
|
273
|
-
const { rows, columnCount, firstRowIsHeader } = readTableGrid(table);
|
|
327
|
+
const renderTableRows = ({ table, tableIndex, budget }) => {
|
|
328
|
+
const { rows, columnCount, firstRowIsHeader } = readTableGrid(table, budget);
|
|
274
329
|
const [firstRow, ...remainingRows] = rows;
|
|
275
330
|
if (columnCount === 0 || firstRow === void 0) return [];
|
|
276
331
|
const rendered = [];
|
|
277
332
|
const pushScaffolding = (text, kind) => {
|
|
333
|
+
chargeTableCharacters({
|
|
334
|
+
budget,
|
|
335
|
+
characters: text.length,
|
|
336
|
+
kind: "rendered"
|
|
337
|
+
});
|
|
278
338
|
rendered.push({
|
|
279
339
|
text,
|
|
280
340
|
position: {
|
|
@@ -284,8 +344,14 @@ const renderTableRows = (table, tableIndex) => {
|
|
|
284
344
|
});
|
|
285
345
|
};
|
|
286
346
|
const pushCells = (cells) => {
|
|
347
|
+
const text = toRowLine(cells, columnCount);
|
|
348
|
+
chargeTableCharacters({
|
|
349
|
+
budget,
|
|
350
|
+
characters: text.length,
|
|
351
|
+
kind: "rendered"
|
|
352
|
+
});
|
|
287
353
|
rendered.push({
|
|
288
|
-
text
|
|
354
|
+
text,
|
|
289
355
|
position: {
|
|
290
356
|
table: tableIndex,
|
|
291
357
|
kind: "cells",
|
|
@@ -304,7 +370,7 @@ const renderTableRows = (table, tableIndex) => {
|
|
|
304
370
|
return rendered;
|
|
305
371
|
};
|
|
306
372
|
const createCharBudget = () => ({ remaining: MAX_EXTRACTED_CHARS });
|
|
307
|
-
const extractContainer = ({ container, source, startIndex, startTableIndex, budget }) => {
|
|
373
|
+
const extractContainer = ({ container, source, startIndex, startTableIndex, budget, tableBudget }) => {
|
|
308
374
|
const paragraphs = [];
|
|
309
375
|
let charCount = 0;
|
|
310
376
|
let tableCount = 0;
|
|
@@ -341,7 +407,11 @@ const extractContainer = ({ container, source, startIndex, startTableIndex, budg
|
|
|
341
407
|
if (budget.remaining <= 0) return;
|
|
342
408
|
const childName = wordElementName(child);
|
|
343
409
|
if (childName === "tbl") {
|
|
344
|
-
for (const row of renderTableRows(
|
|
410
|
+
for (const row of renderTableRows({
|
|
411
|
+
table: child,
|
|
412
|
+
tableIndex: startTableIndex + tableCount,
|
|
413
|
+
budget: tableBudget
|
|
414
|
+
})) pushTableRow(row);
|
|
345
415
|
tableCount += 1;
|
|
346
416
|
continue;
|
|
347
417
|
}
|
|
@@ -356,7 +426,7 @@ const extractContainer = ({ container, source, startIndex, startTableIndex, budg
|
|
|
356
426
|
tableCount
|
|
357
427
|
};
|
|
358
428
|
};
|
|
359
|
-
const extractParts = async ({ archive, source, rootName, startIndex, startTableIndex, paths, budget }) => {
|
|
429
|
+
const extractParts = async ({ archive, source, rootName, startIndex, startTableIndex, paths, budget, tableBudget }) => {
|
|
360
430
|
const paragraphs = [];
|
|
361
431
|
let charCount = 0;
|
|
362
432
|
let tableCount = 0;
|
|
@@ -371,7 +441,8 @@ const extractParts = async ({ archive, source, rootName, startIndex, startTableI
|
|
|
371
441
|
source,
|
|
372
442
|
startIndex: nextIndex,
|
|
373
443
|
startTableIndex: startTableIndex + tableCount,
|
|
374
|
-
budget
|
|
444
|
+
budget,
|
|
445
|
+
tableBudget
|
|
375
446
|
});
|
|
376
447
|
for (const paragraph of result.paragraphs) paragraphs.push(paragraph);
|
|
377
448
|
charCount += result.charCount;
|
|
@@ -443,6 +514,7 @@ const extractDocxText = async (bytes) => {
|
|
|
443
514
|
if (!body) return createEmptyResult();
|
|
444
515
|
const referencedParts = await resolveReferencedHeaderFooterParts(archive, root);
|
|
445
516
|
const budget = createCharBudget();
|
|
517
|
+
const tableBudget = createTableExtractionBudget();
|
|
446
518
|
const headers = await extractParts({
|
|
447
519
|
archive,
|
|
448
520
|
source: "header",
|
|
@@ -450,14 +522,16 @@ const extractDocxText = async (bytes) => {
|
|
|
450
522
|
startIndex: 0,
|
|
451
523
|
startTableIndex: 0,
|
|
452
524
|
paths: referencedParts.headers,
|
|
453
|
-
budget
|
|
525
|
+
budget,
|
|
526
|
+
tableBudget
|
|
454
527
|
});
|
|
455
528
|
const bodyResult = extractContainer({
|
|
456
529
|
container: body,
|
|
457
530
|
source: "body",
|
|
458
531
|
startIndex: headers.paragraphs.length,
|
|
459
532
|
startTableIndex: headers.tableCount,
|
|
460
|
-
budget
|
|
533
|
+
budget,
|
|
534
|
+
tableBudget
|
|
461
535
|
});
|
|
462
536
|
const footers = await extractParts({
|
|
463
537
|
archive,
|
|
@@ -466,7 +540,8 @@ const extractDocxText = async (bytes) => {
|
|
|
466
540
|
startIndex: headers.paragraphs.length + bodyResult.paragraphs.length,
|
|
467
541
|
startTableIndex: headers.tableCount + bodyResult.tableCount,
|
|
468
542
|
paths: referencedParts.footers,
|
|
469
|
-
budget
|
|
543
|
+
budget,
|
|
544
|
+
tableBudget
|
|
470
545
|
});
|
|
471
546
|
return {
|
|
472
547
|
paragraphs: [
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/docx/server/materializeYjsDocx.d.ts
|
|
2
|
+
/** Yjs fragment that stores Folio's canonical ProseMirror document. */
|
|
3
|
+
declare const FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME = "prosemirror";
|
|
4
|
+
/** Maximum accepted size of a complete Yjs collaboration state update. */
|
|
5
|
+
declare const FOLIO_YJS_UPDATE_MAX_BYTES: number;
|
|
6
|
+
/** Stable failure codes returned by server-side Yjs-to-DOCX materialization. */
|
|
7
|
+
declare const FOLIO_YJS_DOCX_MATERIALIZATION_ERROR_CODES: readonly ["empty_update", "invalid_update", "missing_document", "update_too_large"];
|
|
8
|
+
/** Failure code for a rejected Yjs-to-DOCX materialization request. */
|
|
9
|
+
type FolioYjsDocxMaterializationErrorCode = (typeof FOLIO_YJS_DOCX_MATERIALIZATION_ERROR_CODES)[number];
|
|
10
|
+
declare const FolioYjsDocxMaterializationError_base: import("better-result").TaggedErrorClass<"FolioYjsDocxMaterializationError">;
|
|
11
|
+
/** Typed failure raised when a collaboration snapshot cannot be materialized. */
|
|
12
|
+
declare class FolioYjsDocxMaterializationError extends FolioYjsDocxMaterializationError_base<{
|
|
13
|
+
code: FolioYjsDocxMaterializationErrorCode;
|
|
14
|
+
message: string;
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
}> {}
|
|
17
|
+
/** Inputs for materializing a complete Yjs state update into a DOCX package. */
|
|
18
|
+
type MaterializeYjsDocxOptions = {
|
|
19
|
+
/** Original DOCX whose package parts and non-body stories must be preserved. */
|
|
20
|
+
sourceDocx: ArrayBuffer | Uint8Array;
|
|
21
|
+
/** Complete Yjs state update containing Folio's ProseMirror fragment. */
|
|
22
|
+
yjsUpdate: Uint8Array;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Materialize a complete Folio Yjs state update into a DOCX while preserving
|
|
26
|
+
* package parts from the source document. This is the server-side equivalent
|
|
27
|
+
* of the browser editor's full save path for the main document story.
|
|
28
|
+
*/
|
|
29
|
+
declare const materializeYjsDocx: ({ sourceDocx, yjsUpdate }: MaterializeYjsDocxOptions) => Promise<ArrayBuffer>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { FOLIO_YJS_DOCX_MATERIALIZATION_ERROR_CODES, FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME, FOLIO_YJS_UPDATE_MAX_BYTES, FolioYjsDocxMaterializationError, FolioYjsDocxMaterializationErrorCode, MaterializeYjsDocxOptions, materializeYjsDocx };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { fromProseDoc } from "../../prosemirror/conversion/fromProseDoc.js";
|
|
2
|
+
import { schema } from "../../prosemirror/schema/index.js";
|
|
3
|
+
import { parseDocx } from "../parser.js";
|
|
4
|
+
import { repackDocx } from "../rezip.js";
|
|
5
|
+
import { Result, TaggedError } from "better-result";
|
|
6
|
+
import { initProseMirrorDoc } from "y-prosemirror";
|
|
7
|
+
import * as Y from "yjs";
|
|
8
|
+
//#region src/docx/server/materializeYjsDocx.ts
|
|
9
|
+
/** Yjs fragment that stores Folio's canonical ProseMirror document. */
|
|
10
|
+
const FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME = "prosemirror";
|
|
11
|
+
/** Maximum accepted size of a complete Yjs collaboration state update. */
|
|
12
|
+
const FOLIO_YJS_UPDATE_MAX_BYTES = 10 * 1024 * 1024;
|
|
13
|
+
/** Stable failure codes returned by server-side Yjs-to-DOCX materialization. */
|
|
14
|
+
const FOLIO_YJS_DOCX_MATERIALIZATION_ERROR_CODES = [
|
|
15
|
+
"empty_update",
|
|
16
|
+
"invalid_update",
|
|
17
|
+
"missing_document",
|
|
18
|
+
"update_too_large"
|
|
19
|
+
];
|
|
20
|
+
/** Typed failure raised when a collaboration snapshot cannot be materialized. */
|
|
21
|
+
var FolioYjsDocxMaterializationError = class extends TaggedError("FolioYjsDocxMaterializationError") {};
|
|
22
|
+
const readProseMirrorDocument = (yjsUpdate) => {
|
|
23
|
+
if (yjsUpdate.byteLength === 0) throw new FolioYjsDocxMaterializationError({
|
|
24
|
+
code: "empty_update",
|
|
25
|
+
message: "Cannot materialize DOCX from an empty Yjs update."
|
|
26
|
+
});
|
|
27
|
+
if (yjsUpdate.byteLength > 10485760) throw new FolioYjsDocxMaterializationError({
|
|
28
|
+
code: "update_too_large",
|
|
29
|
+
message: "Yjs update exceeds the DOCX materialization limit."
|
|
30
|
+
});
|
|
31
|
+
const ydoc = new Y.Doc();
|
|
32
|
+
const parsed = Result.try({
|
|
33
|
+
try: () => {
|
|
34
|
+
Y.applyUpdate(ydoc, yjsUpdate);
|
|
35
|
+
const fragment = ydoc.getXmlFragment(FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME);
|
|
36
|
+
if (fragment.length === 0) throw new FolioYjsDocxMaterializationError({
|
|
37
|
+
code: "missing_document",
|
|
38
|
+
message: "Yjs update does not contain a Folio document."
|
|
39
|
+
});
|
|
40
|
+
return initProseMirrorDoc(fragment, schema).doc;
|
|
41
|
+
},
|
|
42
|
+
catch: (cause) => cause instanceof FolioYjsDocxMaterializationError ? cause : new FolioYjsDocxMaterializationError({
|
|
43
|
+
code: "invalid_update",
|
|
44
|
+
message: "Yjs update is not a valid Folio collaboration snapshot.",
|
|
45
|
+
cause
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
ydoc.destroy();
|
|
49
|
+
if (parsed.isOk()) return parsed.value;
|
|
50
|
+
throw parsed.error;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Materialize a complete Folio Yjs state update into a DOCX while preserving
|
|
54
|
+
* package parts from the source document. This is the server-side equivalent
|
|
55
|
+
* of the browser editor's full save path for the main document story.
|
|
56
|
+
*/
|
|
57
|
+
const materializeYjsDocx = async ({ sourceDocx, yjsUpdate }) => {
|
|
58
|
+
return await repackDocx(fromProseDoc(readProseMirrorDocument(yjsUpdate), await parseDocx(sourceDocx, { preloadFonts: false })));
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { FOLIO_YJS_DOCX_MATERIALIZATION_ERROR_CODES, FOLIO_YJS_PROSEMIRROR_FRAGMENT_NAME, FOLIO_YJS_UPDATE_MAX_BYTES, FolioYjsDocxMaterializationError, materializeYjsDocx };
|
package/dist/docx/styleParser.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { isValidHexColor } from "../utils/colorResolver.js";
|
|
2
2
|
import { mergeParagraphFormatting } from "../utils/paragraphFormattingMerge.js";
|
|
3
|
-
import {
|
|
3
|
+
import { mergeStyleTextFormatting } from "../utils/textFormattingMerge.js";
|
|
4
4
|
import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
5
5
|
import { resolveThemeFontRef } from "./themeParser.js";
|
|
6
6
|
import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXmlDocument } from "./xmlParser.js";
|
|
7
7
|
//#region src/docx/styleParser.ts
|
|
8
|
+
const findLastRunToggle = (rPr, localName) => findChildren(rPr, "w", localName).at(-1) ?? null;
|
|
8
9
|
/**
|
|
9
10
|
* Parse text formatting properties (w:rPr)
|
|
10
11
|
*/
|
|
11
12
|
function parseRunProperties(rPr, theme) {
|
|
12
13
|
if (!rPr) return;
|
|
13
14
|
const formatting = {};
|
|
14
|
-
const b =
|
|
15
|
+
const b = findLastRunToggle(rPr, "b");
|
|
15
16
|
if (b) formatting.bold = parseBooleanElement(b);
|
|
16
|
-
const bCs =
|
|
17
|
+
const bCs = findLastRunToggle(rPr, "bCs");
|
|
17
18
|
if (bCs) formatting.boldCs = parseBooleanElement(bCs);
|
|
18
|
-
const i =
|
|
19
|
+
const i = findLastRunToggle(rPr, "i");
|
|
19
20
|
if (i) formatting.italic = parseBooleanElement(i);
|
|
20
|
-
const iCs =
|
|
21
|
+
const iCs = findLastRunToggle(rPr, "iCs");
|
|
21
22
|
if (iCs) formatting.italicCs = parseBooleanElement(iCs);
|
|
22
23
|
const u = findChild(rPr, "w", "u");
|
|
23
24
|
if (u) {
|
|
@@ -29,7 +30,7 @@ function parseRunProperties(rPr, theme) {
|
|
|
29
30
|
if (colorVal || themeColor) formatting.underline.color = parseColorValue(colorVal, themeColor, getAttribute(u, "w", "themeTint"), getAttribute(u, "w", "themeShade"));
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
const strike =
|
|
33
|
+
const strike = findLastRunToggle(rPr, "strike");
|
|
33
34
|
if (strike) formatting.strike = parseBooleanElement(strike);
|
|
34
35
|
const dstrike = findChild(rPr, "w", "dstrike");
|
|
35
36
|
if (dstrike) formatting.doubleStrike = parseBooleanElement(dstrike);
|
|
@@ -38,11 +39,11 @@ function parseRunProperties(rPr, theme) {
|
|
|
38
39
|
const val = getAttribute(vertAlign, "w", "val");
|
|
39
40
|
if (val === "superscript" || val === "subscript" || val === "baseline") formatting.vertAlign = val;
|
|
40
41
|
}
|
|
41
|
-
const smallCaps =
|
|
42
|
+
const smallCaps = findLastRunToggle(rPr, "smallCaps");
|
|
42
43
|
if (smallCaps) formatting.smallCaps = parseBooleanElement(smallCaps);
|
|
43
|
-
const caps =
|
|
44
|
+
const caps = findLastRunToggle(rPr, "caps");
|
|
44
45
|
if (caps) formatting.allCaps = parseBooleanElement(caps);
|
|
45
|
-
const vanish =
|
|
46
|
+
const vanish = findLastRunToggle(rPr, "vanish");
|
|
46
47
|
if (vanish) formatting.hidden = parseBooleanElement(vanish);
|
|
47
48
|
const color = findChild(rPr, "w", "color");
|
|
48
49
|
if (color) formatting.color = parseColorValue(getAttribute(color, "w", "val"), getAttribute(color, "w", "themeColor"), getAttribute(color, "w", "themeTint"), getAttribute(color, "w", "themeShade"));
|
|
@@ -154,13 +155,13 @@ function parseRunProperties(rPr, theme) {
|
|
|
154
155
|
const val = narrowEnum(getAttribute(em, "w", "val"), EmphasisMarkSchema);
|
|
155
156
|
if (val) formatting.emphasisMark = val;
|
|
156
157
|
}
|
|
157
|
-
const emboss =
|
|
158
|
+
const emboss = findLastRunToggle(rPr, "emboss");
|
|
158
159
|
if (emboss) formatting.emboss = parseBooleanElement(emboss);
|
|
159
|
-
const imprint =
|
|
160
|
+
const imprint = findLastRunToggle(rPr, "imprint");
|
|
160
161
|
if (imprint) formatting.imprint = parseBooleanElement(imprint);
|
|
161
|
-
const outline =
|
|
162
|
+
const outline = findLastRunToggle(rPr, "outline");
|
|
162
163
|
if (outline) formatting.outline = parseBooleanElement(outline);
|
|
163
|
-
const shadow =
|
|
164
|
+
const shadow = findLastRunToggle(rPr, "shadow");
|
|
164
165
|
if (shadow) formatting.shadow = parseBooleanElement(shadow);
|
|
165
166
|
const rtl = findChild(rPr, "w", "rtl");
|
|
166
167
|
if (rtl) formatting.rtl = parseBooleanElement(rtl);
|
|
@@ -807,7 +808,7 @@ function resolveStyleInheritance(style, styleMap, visited = /* @__PURE__ */ new
|
|
|
807
808
|
const resolved = { ...style };
|
|
808
809
|
const mergedPPr = mergeParagraphFormatting(resolvedParent.pPr, style.pPr);
|
|
809
810
|
if (mergedPPr) resolved.pPr = mergedPPr;
|
|
810
|
-
const mergedRPr =
|
|
811
|
+
const mergedRPr = mergeStyleTextFormatting(resolvedParent.rPr, style.rPr);
|
|
811
812
|
if (mergedRPr) resolved.rPr = mergedRPr;
|
|
812
813
|
if (style.type === "table") {
|
|
813
814
|
if (resolvedParent.tblPr || style.tblPr) resolved.tblPr = {
|
|
@@ -6,10 +6,11 @@ import { setParagraphFrame } from "../../layout-engine/paragraphFrame.js";
|
|
|
6
6
|
import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
|
|
7
7
|
import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
|
|
8
8
|
import { getPageNumbering } from "../../paged-layout/sectionGeometry.js";
|
|
9
|
-
import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectSymbolAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
|
|
9
|
+
import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectSymbolAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
|
|
10
10
|
import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
|
|
11
11
|
import { runShadingAttrsToShading } from "../../prosemirror/conversion/runShadingMark.js";
|
|
12
12
|
import { directionToBidi } from "../../prosemirror/paragraphDirection.js";
|
|
13
|
+
import { cascadeStyleTextFormatting } from "../../prosemirror/styles/styleToggleCascade.js";
|
|
13
14
|
import { assertValidProseMirrorDocument } from "../../prosemirror/validation.js";
|
|
14
15
|
import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
|
|
15
16
|
import { resolveThemeFont } from "../../utils/fontResolver.js";
|
|
@@ -348,17 +349,49 @@ function mergeRunFormatting(paraDefaults, formatting) {
|
|
|
348
349
|
if (merged.letterSpacing === 0) delete merged.letterSpacing;
|
|
349
350
|
return merged;
|
|
350
351
|
}
|
|
352
|
+
/** Restore character-style toggle values that plain visual marks cannot represent. */
|
|
353
|
+
function applyCharacterStyleToggleFormatting({ formatting, marks, paraDefaults }) {
|
|
354
|
+
const characterStyleMark = marks.find((mark) => mark.type.name === "characterStyle");
|
|
355
|
+
if (!characterStyleMark) return;
|
|
356
|
+
const styleRPr = expectCharacterStyleMarkAttrs(characterStyleMark)._styleRPr;
|
|
357
|
+
if (!styleRPr) return;
|
|
358
|
+
const effectiveStyleFormatting = cascadeStyleTextFormatting([{
|
|
359
|
+
formatting: {
|
|
360
|
+
bold: paraDefaults.bold ?? false,
|
|
361
|
+
boldCs: paraDefaults.complexScriptBold ?? false,
|
|
362
|
+
italic: paraDefaults.italic ?? false,
|
|
363
|
+
italicCs: paraDefaults.complexScriptItalic ?? false
|
|
364
|
+
},
|
|
365
|
+
type: "direct"
|
|
366
|
+
}, {
|
|
367
|
+
formatting: styleRPr,
|
|
368
|
+
type: "style"
|
|
369
|
+
}]).formatting;
|
|
370
|
+
if (styleRPr.bold !== void 0 && formatting.bold === void 0) formatting.bold = effectiveStyleFormatting?.bold ?? false;
|
|
371
|
+
if (styleRPr.boldCs !== void 0 && formatting.complexScriptBold === void 0) formatting.complexScriptBold = effectiveStyleFormatting?.boldCs ?? false;
|
|
372
|
+
if (styleRPr.italic !== void 0 && formatting.italic === void 0) formatting.italic = effectiveStyleFormatting?.italic ?? false;
|
|
373
|
+
if (styleRPr.italicCs !== void 0 && formatting.complexScriptItalic === void 0) formatting.complexScriptItalic = effectiveStyleFormatting?.italicCs ?? false;
|
|
374
|
+
if (styleRPr.allCaps === true && formatting.allCaps === void 0) formatting.allCaps = false;
|
|
375
|
+
if (styleRPr.emboss === true && formatting.emboss === void 0) formatting.emboss = false;
|
|
376
|
+
if (styleRPr.imprint === true && formatting.imprint === void 0) formatting.imprint = false;
|
|
377
|
+
if (styleRPr.outline === true && formatting.textOutline === void 0) formatting.textOutline = false;
|
|
378
|
+
if (styleRPr.shadow === true && formatting.textShadow === void 0) formatting.textShadow = false;
|
|
379
|
+
if (styleRPr.smallCaps === true && formatting.smallCaps === void 0) formatting.smallCaps = false;
|
|
380
|
+
if (styleRPr.strike === true && formatting.strike === void 0) formatting.strike = false;
|
|
381
|
+
if (styleRPr.hidden === true && formatting.hidden === void 0) formatting.hidden = false;
|
|
382
|
+
}
|
|
351
383
|
function applyRunFormattingOverrides(formatting, attrs) {
|
|
352
|
-
if (attrs.bold
|
|
353
|
-
if (attrs.italic
|
|
384
|
+
if (attrs.bold !== void 0) formatting.bold = attrs.bold;
|
|
385
|
+
if (attrs.italic !== void 0) formatting.italic = attrs.italic;
|
|
354
386
|
if (attrs.underline === "none") formatting.underline = false;
|
|
355
|
-
if (attrs.strike
|
|
356
|
-
if (attrs.allCaps
|
|
357
|
-
if (attrs.smallCaps
|
|
358
|
-
if (attrs.
|
|
359
|
-
if (attrs.
|
|
360
|
-
if (attrs.
|
|
361
|
-
if (attrs.
|
|
387
|
+
if (attrs.strike !== void 0) formatting.strike = attrs.strike;
|
|
388
|
+
if (attrs.allCaps !== void 0) formatting.allCaps = attrs.allCaps;
|
|
389
|
+
if (attrs.smallCaps !== void 0) formatting.smallCaps = attrs.smallCaps;
|
|
390
|
+
if (attrs.hidden !== void 0) formatting.hidden = attrs.hidden;
|
|
391
|
+
if (attrs.emboss !== void 0) formatting.emboss = attrs.emboss;
|
|
392
|
+
if (attrs.imprint !== void 0) formatting.imprint = attrs.imprint;
|
|
393
|
+
if (attrs.shadow !== void 0) formatting.textShadow = attrs.shadow;
|
|
394
|
+
if (attrs.outline !== void 0) formatting.textOutline = attrs.outline;
|
|
362
395
|
if (attrs.rtl === false) formatting.rtl = false;
|
|
363
396
|
if (attrs.boldCs !== void 0) formatting.complexScriptBold = attrs.boldCs;
|
|
364
397
|
if (attrs.italicCs !== void 0) formatting.complexScriptItalic = attrs.italicCs;
|
|
@@ -497,6 +530,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
497
530
|
if (child.type.name !== "sdt") leadingRenderedPageBreakPending = false;
|
|
498
531
|
if (child.isText && child.text) {
|
|
499
532
|
const formatting = extractRunFormatting(child.marks, theme);
|
|
533
|
+
applyCharacterStyleToggleFormatting({
|
|
534
|
+
formatting,
|
|
535
|
+
marks: child.marks,
|
|
536
|
+
paraDefaults
|
|
537
|
+
});
|
|
500
538
|
if (inTocParagraph) stripTocHyperlinkStyle(formatting);
|
|
501
539
|
const run = {
|
|
502
540
|
kind: "text",
|
|
@@ -513,6 +551,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
513
551
|
const text = decodeOoxmlSymbolCharacter(attrs.char);
|
|
514
552
|
if (text === null) return;
|
|
515
553
|
const formatting = extractRunFormatting(child.marks, theme);
|
|
554
|
+
applyCharacterStyleToggleFormatting({
|
|
555
|
+
formatting,
|
|
556
|
+
marks: child.marks,
|
|
557
|
+
paraDefaults
|
|
558
|
+
});
|
|
516
559
|
if (inTocParagraph) stripTocHyperlinkStyle(formatting);
|
|
517
560
|
runs.push({
|
|
518
561
|
kind: "text",
|
|
@@ -534,6 +577,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
534
577
|
}
|
|
535
578
|
if (child.type.name === "tab") {
|
|
536
579
|
const formatting = extractRunFormatting(child.marks, theme);
|
|
580
|
+
applyCharacterStyleToggleFormatting({
|
|
581
|
+
formatting,
|
|
582
|
+
marks: child.marks,
|
|
583
|
+
paraDefaults
|
|
584
|
+
});
|
|
537
585
|
const run = {
|
|
538
586
|
kind: "tab",
|
|
539
587
|
...mergeRunFormatting(paraDefaults, formatting),
|
|
@@ -561,6 +609,11 @@ function paragraphToRuns(node, startPos, _options) {
|
|
|
561
609
|
else if (ft === "DATE") mappedType = "DATE";
|
|
562
610
|
else if (ft === "TIME") mappedType = "TIME";
|
|
563
611
|
const extractedFieldFormatting = extractRunFormatting(child.marks, theme);
|
|
612
|
+
applyCharacterStyleToggleFormatting({
|
|
613
|
+
formatting: extractedFieldFormatting,
|
|
614
|
+
marks: child.marks,
|
|
615
|
+
paraDefaults
|
|
616
|
+
});
|
|
564
617
|
if (inTocParagraph) stripTocHyperlinkStyle(extractedFieldFormatting);
|
|
565
618
|
const fieldFormatting = markDefaultBlackTextColorSource(extractedFieldFormatting, paraDefaults);
|
|
566
619
|
const run = {
|
|
@@ -902,27 +955,25 @@ function convertParagraph(node, startPos, options) {
|
|
|
902
955
|
return block;
|
|
903
956
|
}
|
|
904
957
|
/**
|
|
905
|
-
* Word keeps
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
958
|
+
* Word keeps a final empty body paragraph after a table as an editable anchor,
|
|
959
|
+
* but that final anchor does not create a page of its own. Earlier authored
|
|
960
|
+
* empty paragraphs retain their height and may carry the document onto a blank
|
|
961
|
+
* page. Preserve every block and PM range while collapsing only the final one.
|
|
909
962
|
*/
|
|
910
963
|
function isPaintlessTerminalParagraph(block) {
|
|
911
964
|
if (block?.kind !== "paragraph" || block.runs.length !== 0) return false;
|
|
912
965
|
const attrs = block.attrs;
|
|
913
966
|
return !(attrs?.listMarker !== void 0 && !attrs.listMarkerHidden || attrs?.borders?.top || attrs?.borders?.bottom || attrs?.borders?.left || attrs?.borders?.right || attrs?.borders?.between || attrs?.borders?.bar || attrs?.shading || attrs?.spacingExplicit?.before || attrs?.spacingExplicit?.after || attrs?.pageBreakBefore || attrs?.renderedPageBreakBefore);
|
|
914
967
|
}
|
|
915
|
-
function
|
|
968
|
+
function suppressFinalEmptyParagraphAfterTable(blocks) {
|
|
916
969
|
let suffixStart = blocks.length;
|
|
917
970
|
while (suffixStart > 0 && isPaintlessTerminalParagraph(blocks[suffixStart - 1])) suffixStart -= 1;
|
|
918
971
|
if (suffixStart === blocks.length || suffixStart === 0 || blocks[suffixStart - 1]?.kind !== "table") return;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
};
|
|
925
|
-
}
|
|
972
|
+
const finalBlock = blocks.at(-1);
|
|
973
|
+
if (isPaintlessTerminalParagraph(finalBlock)) finalBlock.attrs = {
|
|
974
|
+
...finalBlock.attrs,
|
|
975
|
+
suppressEmptyParagraphHeight: true
|
|
976
|
+
};
|
|
926
977
|
}
|
|
927
978
|
function suppressFinalParagraphInRepeatedEmptySuffix(blocks) {
|
|
928
979
|
let suffixStart = blocks.length;
|
|
@@ -1477,7 +1528,7 @@ function toFlowBlocks(doc, options = {}) {
|
|
|
1477
1528
|
visit(node, offset + nodeOffset);
|
|
1478
1529
|
});
|
|
1479
1530
|
reserveLeadingEmptyOutlineHeight(blocks);
|
|
1480
|
-
|
|
1531
|
+
suppressFinalEmptyParagraphAfterTable(blocks);
|
|
1481
1532
|
suppressFinalParagraphInRepeatedEmptySuffix(blocks);
|
|
1482
1533
|
return groupParagraphFrames(applySectionDocumentGrid(mergeRunInParagraphs(blocks), opts.finalSectionDocumentGridLinePitchTwips), nextBlockId);
|
|
1483
1534
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Layout } from "./types.js";
|
|
2
|
+
//#region src/layout-engine/footnoteColumnReflow.d.ts
|
|
3
|
+
type ReflowFootnoteColumnsOptions = {
|
|
4
|
+
initialLayout: Layout;
|
|
5
|
+
initialReserveFloors?: ReadonlyMap<number, number>;
|
|
6
|
+
runLayout: (reserveFloors: Map<number, number>) => Layout;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Re-run a multi-column layout until its body flow clears the shared footnote band.
|
|
10
|
+
* Reserve floors follow the current page-to-footnote assignment. Repeated floor states
|
|
11
|
+
* and persistent overlap use a stable fallback layout instead of throwing.
|
|
12
|
+
*/
|
|
13
|
+
declare function reflowFootnoteColumns({ initialLayout, initialReserveFloors, runLayout }: ReflowFootnoteColumnsOptions): Layout;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { reflowFootnoteColumns };
|