@stll/folio-core 0.26.0 → 0.27.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/ai-edits/blockRange.d.ts +17 -2
- package/dist/ai-edits/blockRange.js +1 -1
- package/dist/ai-edits/table-cell-mutations.js +38 -3
- package/dist/compat/eigenpal.d.ts +2 -1
- package/dist/docx/paragraphParser.js +3 -2
- package/dist/docx/parser.js +28 -5
- package/dist/docx/rezip.d.ts +21 -1
- package/dist/docx/rezip.js +80 -2
- package/dist/docx/runParser.js +6 -5
- package/dist/docx/selectiveSave.d.ts +0 -8
- package/dist/docx/selectiveSave.js +16 -1
- package/dist/docx/serializer/runSerializer.js +4 -4
- package/dist/docx/serializer/sectionPropertiesSerializer.js +5 -5
- package/dist/docx/server/createBilingualDocument.js +73 -7
- package/dist/docx/server/extractDocxText.js +31 -12
- package/dist/docx/styleParser.js +6 -5
- package/dist/docx/unzip.js +17 -3
- package/dist/docx/xmlParser.js +34 -8
- package/dist/index.d.ts +2 -1
- package/dist/layout-painter/renderPage.js +6 -1
- package/dist/layout-painter/renderParagraph.js +36 -9
- package/dist/paged-layout/blockGeometry.d.ts +32 -0
- package/dist/paged-layout/blockGeometry.js +115 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +4 -3
- package/dist/utils/headerFooter.js +0 -12
- package/dist/utils/tiffConverter.d.ts +11 -2
- package/dist/utils/tiffConverter.js +12 -4
- package/package.json +1 -1
|
@@ -103,16 +103,26 @@ const NESTED_TABLE_CELL_SEPARATOR = " / ";
|
|
|
103
103
|
const MAX_TABLE_COLUMNS = 256;
|
|
104
104
|
/** Bound the mutual recursion between a cell and the tables nested inside it. */
|
|
105
105
|
const MAX_NESTED_TABLE_DEPTH = 8;
|
|
106
|
+
/** Rows collected from one `w:tbl`. The column cap alone leaves row count unbounded. */
|
|
107
|
+
const MAX_TABLE_ROWS = 8192;
|
|
108
|
+
/**
|
|
109
|
+
* Characters one extraction emits, shared by the body and every header/footer
|
|
110
|
+
* part. Element count is bounded at unzip, but a bounded element count still
|
|
111
|
+
* renders an unbounded number of table rows once `w:gridSpan` padding and the
|
|
112
|
+
* GFM scaffolding are counted, so the emitted side carries its own ceiling.
|
|
113
|
+
*/
|
|
114
|
+
const MAX_EXTRACTED_CHARS = 8e6;
|
|
106
115
|
/**
|
|
107
116
|
* Collect a table's `w:tr`, or a row's `w:tc`, seeing through the wrappers Word
|
|
108
117
|
* puts around them (`w:sdt` / `w:sdtContent` content controls, `w:customXml`).
|
|
109
118
|
* The walk stops at `w:tbl` and `w:p` so a nested table's rows and cells never
|
|
110
119
|
* leak into the grid of the table that contains them.
|
|
111
120
|
*/
|
|
112
|
-
const collectTableParts = (parent, localName) => {
|
|
121
|
+
const collectTableParts = (parent, localName, limit) => {
|
|
113
122
|
const parts = [];
|
|
114
123
|
const walk = (node) => {
|
|
115
124
|
for (const child of childElements(node)) {
|
|
125
|
+
if (parts.length >= limit) return;
|
|
116
126
|
const childName = wordElementName(child);
|
|
117
127
|
if (childName === localName) {
|
|
118
128
|
parts.push(child);
|
|
@@ -142,7 +152,7 @@ const readCellSourceParagraphs = (cell, depth) => {
|
|
|
142
152
|
}
|
|
143
153
|
if (childName === "tbl") {
|
|
144
154
|
if (depth >= MAX_NESTED_TABLE_DEPTH) continue;
|
|
145
|
-
for (const row of collectTableParts(child, "tr")) for (const nestedCell of collectTableParts(row, "tc")) for (const paragraph of readCellSourceParagraphs(nestedCell, depth + 1)) paragraphs.push(paragraph);
|
|
155
|
+
for (const row of collectTableParts(child, "tr", MAX_TABLE_ROWS)) for (const nestedCell of collectTableParts(row, "tc", MAX_TABLE_COLUMNS)) for (const paragraph of readCellSourceParagraphs(nestedCell, depth + 1)) paragraphs.push(paragraph);
|
|
146
156
|
continue;
|
|
147
157
|
}
|
|
148
158
|
walk(child);
|
|
@@ -173,8 +183,8 @@ const readCellRenderedLines = (cell, depth) => {
|
|
|
173
183
|
};
|
|
174
184
|
const flattenNestedTable = (table, depth) => {
|
|
175
185
|
const lines = [];
|
|
176
|
-
for (const row of collectTableParts(table, "tr")) {
|
|
177
|
-
const cells = collectTableParts(row, "tc").map((cell) => readCellRenderedLines(cell, depth).join("\n"));
|
|
186
|
+
for (const row of collectTableParts(table, "tr", MAX_TABLE_ROWS)) {
|
|
187
|
+
const cells = collectTableParts(row, "tc", MAX_TABLE_COLUMNS).map((cell) => readCellRenderedLines(cell, depth).join("\n"));
|
|
178
188
|
if (cells.some((text) => text.length > 0)) lines.push(cells.join(NESTED_TABLE_CELL_SEPARATOR));
|
|
179
189
|
}
|
|
180
190
|
return lines;
|
|
@@ -229,12 +239,12 @@ const readTableGrid = (table) => {
|
|
|
229
239
|
const rows = [];
|
|
230
240
|
let columnCount = 0;
|
|
231
241
|
let firstRowIsHeader = false;
|
|
232
|
-
for (const [rowIndex, row] of collectTableParts(table, "tr").entries()) {
|
|
242
|
+
for (const [rowIndex, row] of collectTableParts(table, "tr", MAX_TABLE_ROWS).entries()) {
|
|
233
243
|
if (rowIndex === 0) firstRowIsHeader = declaresHeaderRow(row);
|
|
234
244
|
const columns = [];
|
|
235
245
|
const gridBefore = readRowGridOffset(row, "gridBefore");
|
|
236
246
|
for (let index = 0; index < gridBefore; index += 1) columns.push(emptyTableCell());
|
|
237
|
-
for (const cell of collectTableParts(row, "tc")) {
|
|
247
|
+
for (const cell of collectTableParts(row, "tc", MAX_TABLE_COLUMNS)) {
|
|
238
248
|
if (columns.length >= MAX_TABLE_COLUMNS) break;
|
|
239
249
|
const extractedCell = readTableCell(cell, 0);
|
|
240
250
|
columns.push(extractedCell);
|
|
@@ -293,7 +303,8 @@ const renderTableRows = (table, tableIndex) => {
|
|
|
293
303
|
for (const row of firstRowIsHeader ? remainingRows : rows) pushCells(row);
|
|
294
304
|
return rendered;
|
|
295
305
|
};
|
|
296
|
-
const
|
|
306
|
+
const createCharBudget = () => ({ remaining: MAX_EXTRACTED_CHARS });
|
|
307
|
+
const extractContainer = ({ container, source, startIndex, startTableIndex, budget }) => {
|
|
297
308
|
const paragraphs = [];
|
|
298
309
|
let charCount = 0;
|
|
299
310
|
let tableCount = 0;
|
|
@@ -307,6 +318,7 @@ const extractContainer = ({ container, source, startIndex, startTableIndex }) =>
|
|
|
307
318
|
};
|
|
308
319
|
paragraphs.push(entry);
|
|
309
320
|
charCount += text.length;
|
|
321
|
+
budget.remaining -= text.length;
|
|
310
322
|
};
|
|
311
323
|
const pushTableRow = ({ text, position }) => {
|
|
312
324
|
paragraphs.push({
|
|
@@ -316,6 +328,7 @@ const extractContainer = ({ container, source, startIndex, startTableIndex }) =>
|
|
|
316
328
|
tableRow: position
|
|
317
329
|
});
|
|
318
330
|
charCount += text.length;
|
|
331
|
+
budget.remaining -= text.length;
|
|
319
332
|
};
|
|
320
333
|
/**
|
|
321
334
|
* Walk block content in document order. Descent mirrors the previous
|
|
@@ -325,6 +338,7 @@ const extractContainer = ({ container, source, startIndex, startTableIndex }) =>
|
|
|
325
338
|
*/
|
|
326
339
|
const walkBlocks = (node) => {
|
|
327
340
|
for (const child of childElements(node)) {
|
|
341
|
+
if (budget.remaining <= 0) return;
|
|
328
342
|
const childName = wordElementName(child);
|
|
329
343
|
if (childName === "tbl") {
|
|
330
344
|
for (const row of renderTableRows(child, startTableIndex + tableCount)) pushTableRow(row);
|
|
@@ -342,7 +356,7 @@ const extractContainer = ({ container, source, startIndex, startTableIndex }) =>
|
|
|
342
356
|
tableCount
|
|
343
357
|
};
|
|
344
358
|
};
|
|
345
|
-
const extractParts = async ({ archive, source, rootName, startIndex, startTableIndex, paths }) => {
|
|
359
|
+
const extractParts = async ({ archive, source, rootName, startIndex, startTableIndex, paths, budget }) => {
|
|
346
360
|
const paragraphs = [];
|
|
347
361
|
let charCount = 0;
|
|
348
362
|
let tableCount = 0;
|
|
@@ -356,7 +370,8 @@ const extractParts = async ({ archive, source, rootName, startIndex, startTableI
|
|
|
356
370
|
container,
|
|
357
371
|
source,
|
|
358
372
|
startIndex: nextIndex,
|
|
359
|
-
startTableIndex: startTableIndex + tableCount
|
|
373
|
+
startTableIndex: startTableIndex + tableCount,
|
|
374
|
+
budget
|
|
360
375
|
});
|
|
361
376
|
for (const paragraph of result.paragraphs) paragraphs.push(paragraph);
|
|
362
377
|
charCount += result.charCount;
|
|
@@ -427,19 +442,22 @@ const extractDocxText = async (bytes) => {
|
|
|
427
442
|
const body = findDeep(root, "w", "body");
|
|
428
443
|
if (!body) return createEmptyResult();
|
|
429
444
|
const referencedParts = await resolveReferencedHeaderFooterParts(archive, root);
|
|
445
|
+
const budget = createCharBudget();
|
|
430
446
|
const headers = await extractParts({
|
|
431
447
|
archive,
|
|
432
448
|
source: "header",
|
|
433
449
|
rootName: "hdr",
|
|
434
450
|
startIndex: 0,
|
|
435
451
|
startTableIndex: 0,
|
|
436
|
-
paths: referencedParts.headers
|
|
452
|
+
paths: referencedParts.headers,
|
|
453
|
+
budget
|
|
437
454
|
});
|
|
438
455
|
const bodyResult = extractContainer({
|
|
439
456
|
container: body,
|
|
440
457
|
source: "body",
|
|
441
458
|
startIndex: headers.paragraphs.length,
|
|
442
|
-
startTableIndex: headers.tableCount
|
|
459
|
+
startTableIndex: headers.tableCount,
|
|
460
|
+
budget
|
|
443
461
|
});
|
|
444
462
|
const footers = await extractParts({
|
|
445
463
|
archive,
|
|
@@ -447,7 +465,8 @@ const extractDocxText = async (bytes) => {
|
|
|
447
465
|
rootName: "ftr",
|
|
448
466
|
startIndex: headers.paragraphs.length + bodyResult.paragraphs.length,
|
|
449
467
|
startTableIndex: headers.tableCount + bodyResult.tableCount,
|
|
450
|
-
paths: referencedParts.footers
|
|
468
|
+
paths: referencedParts.footers,
|
|
469
|
+
budget
|
|
451
470
|
});
|
|
452
471
|
return {
|
|
453
472
|
paragraphs: [
|
package/dist/docx/styleParser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidHexColor } from "../utils/colorResolver.js";
|
|
1
2
|
import { mergeParagraphFormatting } from "../utils/paragraphFormattingMerge.js";
|
|
2
3
|
import { mergeTextFormatting } from "../utils/textFormattingMerge.js";
|
|
3
4
|
import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
@@ -86,7 +87,7 @@ function parseRunProperties(rPr, theme) {
|
|
|
86
87
|
if (resolved) fontFamily.ascii = resolved;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
|
-
const hAnsiTheme = getAttribute(rFonts, "w", "hAnsiTheme");
|
|
90
|
+
const hAnsiTheme = narrowEnum(getAttribute(rFonts, "w", "hAnsiTheme"), FontThemeSchema);
|
|
90
91
|
if (hAnsiTheme) {
|
|
91
92
|
fontFamily.hAnsiTheme = hAnsiTheme;
|
|
92
93
|
if (theme && !fontFamily.hAnsi) {
|
|
@@ -94,7 +95,7 @@ function parseRunProperties(rPr, theme) {
|
|
|
94
95
|
if (resolved) fontFamily.hAnsi = resolved;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
const eastAsiaTheme = getAttribute(rFonts, "w", "eastAsiaTheme");
|
|
98
|
+
const eastAsiaTheme = narrowEnum(getAttribute(rFonts, "w", "eastAsiaTheme"), FontThemeSchema);
|
|
98
99
|
if (eastAsiaTheme) {
|
|
99
100
|
fontFamily.eastAsiaTheme = eastAsiaTheme;
|
|
100
101
|
if (theme && !fontFamily.eastAsia) {
|
|
@@ -102,7 +103,7 @@ function parseRunProperties(rPr, theme) {
|
|
|
102
103
|
if (resolved) fontFamily.eastAsia = resolved;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
|
-
const csTheme = getAttribute(rFonts, "w", "cstheme");
|
|
106
|
+
const csTheme = narrowEnum(getAttribute(rFonts, "w", "cstheme"), FontThemeSchema);
|
|
106
107
|
if (csTheme) {
|
|
107
108
|
fontFamily.csTheme = csTheme;
|
|
108
109
|
if (theme && !fontFamily.cs) {
|
|
@@ -192,9 +193,9 @@ function parseShadingProperties(shd) {
|
|
|
192
193
|
if (!shd) return;
|
|
193
194
|
const props = {};
|
|
194
195
|
const color = getAttribute(shd, "w", "color");
|
|
195
|
-
if (color && color !== "auto") props.color = { rgb: color };
|
|
196
|
+
if (color && color !== "auto" && isValidHexColor(color)) props.color = { rgb: color };
|
|
196
197
|
const fill = getAttribute(shd, "w", "fill");
|
|
197
|
-
if (fill && fill !== "auto") props.fill = { rgb: fill };
|
|
198
|
+
if (fill && fill !== "auto" && isValidHexColor(fill)) props.fill = { rgb: fill };
|
|
198
199
|
const validatedThemeFill = narrowEnum(getAttribute(shd, "w", "themeFill"), ThemeColorSlotSchema);
|
|
199
200
|
if (validatedThemeFill) {
|
|
200
201
|
if (!props.fill) props.fill = {};
|
package/dist/docx/unzip.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./encryption/containerFormat.js";
|
|
2
2
|
import { openDocxBuffer } from "./encryption/openEncryptedDocx.js";
|
|
3
|
-
import { FOLIO_XML_RESOURCE_LIMITS } from "./xmlResourceLimits.js";
|
|
3
|
+
import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits } from "./xmlResourceLimits.js";
|
|
4
4
|
import JSZip from "jszip";
|
|
5
5
|
//#region src/docx/unzip.ts
|
|
6
6
|
/**
|
|
@@ -191,7 +191,7 @@ async function unzipDocx(buffer, options = {}) {
|
|
|
191
191
|
for (const extracted of await Promise.all(extractionTasks.map((extract) => extract()))) {
|
|
192
192
|
if (!extracted) continue;
|
|
193
193
|
if (extracted.type === "xml") {
|
|
194
|
-
assignXmlContent(content, extracted);
|
|
194
|
+
assignXmlContent(content, extracted, limits);
|
|
195
195
|
continue;
|
|
196
196
|
}
|
|
197
197
|
if (extracted.type === "media") {
|
|
@@ -202,7 +202,21 @@ async function unzipDocx(buffer, options = {}) {
|
|
|
202
202
|
}
|
|
203
203
|
return content;
|
|
204
204
|
}
|
|
205
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Parts that every consumer expands into an object tree. Preflighting them once
|
|
207
|
+
* here puts the bound on the unzip, so `parseDocx`, the selective save and the
|
|
208
|
+
* repack path all share it instead of each entry point carrying its own.
|
|
209
|
+
*/
|
|
210
|
+
const PREFLIGHT_XML_PARTS = /* @__PURE__ */ new Set([
|
|
211
|
+
"word/document.xml",
|
|
212
|
+
"word/styles.xml",
|
|
213
|
+
"word/numbering.xml"
|
|
214
|
+
]);
|
|
215
|
+
function assignXmlContent(content, { path, lowerPath, content: xmlContent }, limits) {
|
|
216
|
+
if (PREFLIGHT_XML_PARTS.has(lowerPath)) assertXmlResourceLimits(xmlContent, {
|
|
217
|
+
...FOLIO_XML_RESOURCE_LIMITS,
|
|
218
|
+
maxBytes: limits.maxXmlBytes
|
|
219
|
+
});
|
|
206
220
|
content.allXml.set(path, xmlContent);
|
|
207
221
|
if (lowerPath === "word/document.xml") content.documentXml = xmlContent;
|
|
208
222
|
else if (lowerPath === "word/styles.xml") content.stylesXml = xmlContent;
|
package/dist/docx/xmlParser.js
CHANGED
|
@@ -610,6 +610,24 @@ function findAllDeep(root, namespace, localName) {
|
|
|
610
610
|
*/
|
|
611
611
|
const MAX_XMLNS_DECLARATIONS_PER_ELEMENT = 64;
|
|
612
612
|
/**
|
|
613
|
+
* Sanity cap on one declaration's value. Namespace URIs are short; a longer
|
|
614
|
+
* binding is dropped rather than replayed onto every captured subtree that
|
|
615
|
+
* inherits from the declaring element.
|
|
616
|
+
*/
|
|
617
|
+
const MAX_XMLNS_VALUE_LENGTH = 512;
|
|
618
|
+
/**
|
|
619
|
+
* Sanity cap on an accumulated declaration set. Applied both when collecting one
|
|
620
|
+
* element's declarations and when merging down the ancestor chain, so every set
|
|
621
|
+
* this module produces or returns is bounded and a captured `w:pict` subtree
|
|
622
|
+
* replays at most this much regardless of how the chain was built.
|
|
623
|
+
*/
|
|
624
|
+
const MAX_XMLNS_DECLARATION_CHARS = 8192;
|
|
625
|
+
const xmlnsDeclarationChars = (declarations) => {
|
|
626
|
+
let chars = 0;
|
|
627
|
+
for (const [name, value] of Object.entries(declarations)) chars += name.length + value.length;
|
|
628
|
+
return chars;
|
|
629
|
+
};
|
|
630
|
+
/**
|
|
613
631
|
* Collect every `xmlns` / `xmlns:*` declaration from an element's attributes.
|
|
614
632
|
*
|
|
615
633
|
* The serializer's hard-coded root namespaces only cover canonical prefixes
|
|
@@ -623,13 +641,18 @@ function collectXmlnsDeclarations(element) {
|
|
|
623
641
|
const attrs = element.attributes;
|
|
624
642
|
if (!attrs) return out;
|
|
625
643
|
let declarationCount = 0;
|
|
644
|
+
let declarationChars = 0;
|
|
626
645
|
for (const key in attrs) {
|
|
627
646
|
if (declarationCount >= MAX_XMLNS_DECLARATIONS_PER_ELEMENT) break;
|
|
628
647
|
const value = attrs[key];
|
|
629
|
-
if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
648
|
+
if (key !== "xmlns" && !key.startsWith("xmlns:")) continue;
|
|
649
|
+
if (value === void 0) continue;
|
|
650
|
+
const declaration = String(value);
|
|
651
|
+
if (declaration.length > MAX_XMLNS_VALUE_LENGTH) continue;
|
|
652
|
+
if (declarationChars + key.length + declaration.length > MAX_XMLNS_DECLARATION_CHARS) break;
|
|
653
|
+
out[key] = declaration;
|
|
654
|
+
declarationCount += 1;
|
|
655
|
+
declarationChars += key.length + declaration.length;
|
|
633
656
|
}
|
|
634
657
|
return out;
|
|
635
658
|
}
|
|
@@ -643,10 +666,13 @@ function collectXmlnsDeclarations(element) {
|
|
|
643
666
|
*/
|
|
644
667
|
function mergeXmlnsDeclarations(inherited, element) {
|
|
645
668
|
const own = collectXmlnsDeclarations(element);
|
|
646
|
-
for (const _key in own)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
669
|
+
for (const _key in own) {
|
|
670
|
+
const merged = {
|
|
671
|
+
...inherited,
|
|
672
|
+
...own
|
|
673
|
+
};
|
|
674
|
+
return xmlnsDeclarationChars(merged) > MAX_XMLNS_DECLARATION_CHARS ? inherited : merged;
|
|
675
|
+
}
|
|
650
676
|
return inherited;
|
|
651
677
|
}
|
|
652
678
|
const QNAME_VALUE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { DocumentStyleCatalog, DocumentStyleCatalogEntry, ExtractDocumentStyleSe
|
|
|
17
17
|
import { STELLA_STYLE_SET_NAME, createStellaStyleDocumentPreset, createStellaStyleSet } from "./style-sets/stellaStyle.js";
|
|
18
18
|
import { createDocx } from "./docx/rezip.js";
|
|
19
19
|
import { DocxCompatibility, DocxCompatibilityContext, DocxCompatibilityIssue, DocxCompatibilityLocation, DocxCompatibilityPart, FolioDocxCompatibilityHost, FolioDocxCompatibilityProfile, InspectDocxCompatibilityOptions, inspectDocxCompatibility } from "./docx/compatibility.js";
|
|
20
|
+
import { BlockRect } from "./paged-layout/blockGeometry.js";
|
|
20
21
|
import { setAISuggestionsMeta, setFocusedSuggestionMeta } from "./prosemirror/plugins/aiSuggestionDecorations.js";
|
|
21
22
|
import { scrollFolioPositionIntoView } from "./paged-layout/scrollToPmPosition.js";
|
|
22
23
|
import { getFolioCaretViewportRect, getFolioSelectionViewportRect } from "./paged-layout/selectionViewportRect.js";
|
|
@@ -34,4 +35,4 @@ import { getGoogleFontsEnabled, setEmbeddedFontFamilyMap, setGoogleFontsEnabled
|
|
|
34
35
|
import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
|
|
35
36
|
type Document = document_d_exports.Document;
|
|
36
37
|
type DocxConformanceClass = document_d_exports.DocxConformanceClass;
|
|
37
|
-
export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DOCX_CONFORMANCE_CLASSES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, type DocxCompatibility, type DocxCompatibilityContext, type DocxCompatibilityIssue, type DocxCompatibilityLocation, type DocxCompatibilityPart, type DocxConformanceClass, type EmbeddedFont, type EmbeddedFontParts, type ExtractDocumentStyleSetOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocxCompatibilityHost, type FolioDocxCompatibilityProfile, type ImageMeta, type ImageRef, type InspectDocxCompatibilityOptions, InvalidFolioDocumentOperationBatchError, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, STELLA_STYLE_SET_NAME, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildEmbeddedFontFamilyMap, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, diffWordSegments, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxCompatibility, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, mergeDocumentContent, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scopeEmbeddedFontFamily, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setEmbeddedFontFamilyMap, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
|
|
38
|
+
export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type BlockRect, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DOCX_CONFORMANCE_CLASSES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, type DocxCompatibility, type DocxCompatibilityContext, type DocxCompatibilityIssue, type DocxCompatibilityLocation, type DocxCompatibilityPart, type DocxConformanceClass, type EmbeddedFont, type EmbeddedFontParts, type ExtractDocumentStyleSetOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocxCompatibilityHost, type FolioDocxCompatibilityProfile, type ImageMeta, type ImageRef, type InspectDocxCompatibilityOptions, InvalidFolioDocumentOperationBatchError, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, STELLA_STYLE_SET_NAME, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildEmbeddedFontFamilyMap, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, diffWordSegments, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxCompatibility, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, mergeDocumentContent, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scopeEmbeddedFontFamily, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setEmbeddedFontFamilyMap, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
|
|
@@ -1597,6 +1597,7 @@ function renderPages(pages, container, options = {}) {
|
|
|
1597
1597
|
const viewportHeight = window.innerHeight;
|
|
1598
1598
|
const nearThreshold = viewportHeight * 3;
|
|
1599
1599
|
const nearIndices = /* @__PURE__ */ new Set();
|
|
1600
|
+
let depopulated = false;
|
|
1600
1601
|
for (const [el, data] of liveDataMap) {
|
|
1601
1602
|
if (!data.rendered) continue;
|
|
1602
1603
|
const rect = el.getBoundingClientRect();
|
|
@@ -1609,8 +1610,12 @@ function renderPages(pages, container, options = {}) {
|
|
|
1609
1610
|
keepRendered = true;
|
|
1610
1611
|
break;
|
|
1611
1612
|
}
|
|
1612
|
-
if (!keepRendered && nearIndices.size > 0)
|
|
1613
|
+
if (!keepRendered && nearIndices.size > 0) {
|
|
1614
|
+
depopulatePageShell(el, liveDataMap);
|
|
1615
|
+
depopulated = true;
|
|
1616
|
+
}
|
|
1613
1617
|
}
|
|
1618
|
+
if (depopulated) emitPainterPainted(container);
|
|
1614
1619
|
}, {
|
|
1615
1620
|
root: null,
|
|
1616
1621
|
rootMargin: `${VIRTUALIZATION_ROOT_MARGIN_PX}px 0px ${VIRTUALIZATION_ROOT_MARGIN_PX}px 0px`
|
|
@@ -15,6 +15,7 @@ import { resolvePhysicalParagraphInlineLayout } from "../utils/paragraphInlineLa
|
|
|
15
15
|
import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
|
|
16
16
|
import { applySanitizedImageSrc } from "../utils/sanitizeImageSrc.js";
|
|
17
17
|
import { SCRIPT_CLASS, hasCjk, hasComplexScript, segmentByScript } from "../utils/scriptSegments.js";
|
|
18
|
+
import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
|
|
18
19
|
import { borderStrokeToCss, resolveParagraphBorderHorizontalOutsets } from "./borderStroke.js";
|
|
19
20
|
import { planCursiveJoiners, withCursiveJoiners } from "./cursiveJoiners.js";
|
|
20
21
|
import { getAutomaticTextColorForBackground } from "./documentColors.js";
|
|
@@ -114,6 +115,12 @@ const DEFAULT_BLACK_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["000000", "000"
|
|
|
114
115
|
const SUGGESTION_COLOR_CSS = "var(--suggestion-color, #6d3bd6)";
|
|
115
116
|
const SUGGESTION_TINT_CSS = "var(--suggestion-bg, color-mix(in oklch, #6d3bd6 12%, transparent))";
|
|
116
117
|
const SUGGESTION_TINT_LAYER_CSS = `linear-gradient(${SUGGESTION_TINT_CSS}, ${SUGGESTION_TINT_CSS})`;
|
|
118
|
+
const RUN_BACKGROUND_TEXT_COLOR_VAR = "--doc-run-background-text-color";
|
|
119
|
+
const setRunBackgroundTextColor = (element, color) => {
|
|
120
|
+
element.classList.add("docx-run-background-text");
|
|
121
|
+
element.style.setProperty(RUN_BACKGROUND_TEXT_COLOR_VAR, color);
|
|
122
|
+
};
|
|
123
|
+
const hasRunBackgroundTextSurface = (run) => Boolean(run.highlight ?? run.shading) && !run.isInsertion && !run.isDeletion && !(run.commentIds !== void 0 && run.commentIds.length > 0);
|
|
117
124
|
function normalizeTextColorValue(color) {
|
|
118
125
|
return color.trim().toLowerCase().replace(/^#/u, "");
|
|
119
126
|
}
|
|
@@ -205,6 +212,8 @@ function applyRunStyles(element, run) {
|
|
|
205
212
|
const hasCommentHighlight = run.commentIds !== void 0 && run.commentIds.length > 0;
|
|
206
213
|
const automaticTextColor = hasExplicitTextColor || hasTrackedChangeColor || hasCommentHighlight ? void 0 : getAutomaticTextColorForBackground(runBackground);
|
|
207
214
|
if (automaticTextColor) element.style.color = automaticTextColor;
|
|
215
|
+
const backgroundTextColor = hasExplicitTextColor ? textColor : automaticTextColor;
|
|
216
|
+
if (backgroundTextColor && hasRunBackgroundTextSurface(run)) setRunBackgroundTextColor(element, backgroundTextColor);
|
|
208
217
|
}
|
|
209
218
|
const decorations = [];
|
|
210
219
|
let explicitDecorationStyle = false;
|
|
@@ -315,11 +324,13 @@ function renderTextRun(run, doc, hyperlinkDirection) {
|
|
|
315
324
|
applyRunStyles(span, run);
|
|
316
325
|
applyPmPositions(span, run.pmStart, run.pmEnd);
|
|
317
326
|
const paintedText = toPaintedText(run.text);
|
|
318
|
-
|
|
327
|
+
const isBookmarkTarget = run.hyperlink?.href.startsWith("#") === true;
|
|
328
|
+
const hyperlinkHref = resolveHyperlinkHref(run.hyperlink?.href, isBookmarkTarget);
|
|
329
|
+
if (run.hyperlink && hyperlinkHref !== void 0) {
|
|
319
330
|
const anchor = doc.createElement("a");
|
|
320
|
-
anchor.href =
|
|
331
|
+
anchor.href = hyperlinkHref;
|
|
321
332
|
if (hyperlinkDirection || DISPLAYED_URL_PATTERN.test(paintedText.trim())) anchor.dir = LEFT_TO_RIGHT_DIRECTION;
|
|
322
|
-
if (!
|
|
333
|
+
if (!isBookmarkTarget) {
|
|
323
334
|
anchor.target = "_blank";
|
|
324
335
|
anchor.rel = "noopener noreferrer";
|
|
325
336
|
}
|
|
@@ -332,12 +343,27 @@ function renderTextRun(run, doc, hyperlinkDirection) {
|
|
|
332
343
|
span.style.color = hyperlinkColor;
|
|
333
344
|
anchor.style.setProperty("--doc-run-color", hyperlinkColor);
|
|
334
345
|
span.style.setProperty("--doc-run-color", hyperlinkColor);
|
|
346
|
+
if (hasRunBackgroundTextSurface(run)) {
|
|
347
|
+
setRunBackgroundTextColor(span, hyperlinkColor);
|
|
348
|
+
setRunBackgroundTextColor(anchor, hyperlinkColor);
|
|
349
|
+
}
|
|
335
350
|
}
|
|
336
351
|
span.append(anchor);
|
|
337
352
|
} else span.textContent = paintedText;
|
|
338
353
|
applyWhitespaceUnderline(span, run);
|
|
339
354
|
return span;
|
|
340
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Bookmark targets (`#name`) scroll within the document and stay verbatim;
|
|
358
|
+
* every other target is narrowed to the protocols the painter navigates to,
|
|
359
|
+
* matching the image hyperlink path. `undefined` means the run gets no anchor:
|
|
360
|
+
* an empty `href` resolves to the current document, so a rejected target would
|
|
361
|
+
* otherwise stay navigable.
|
|
362
|
+
*/
|
|
363
|
+
function resolveHyperlinkHref(href, isBookmarkTarget) {
|
|
364
|
+
if (href === void 0) return;
|
|
365
|
+
return isBookmarkTarget ? href : sanitizeExternalUrl(href);
|
|
366
|
+
}
|
|
341
367
|
function isNoteReferenceRun(run) {
|
|
342
368
|
return run.footnoteRefId !== void 0 || run.endnoteRefId !== void 0;
|
|
343
369
|
}
|
|
@@ -345,11 +371,14 @@ function removeUnderlineTextDecoration(element) {
|
|
|
345
371
|
const textDecorationLines = (element.style.textDecorationLine || "").split(/\s+/u).filter((line) => line && line !== "underline");
|
|
346
372
|
element.style.textDecorationLine = textDecorationLines.join(" ");
|
|
347
373
|
}
|
|
374
|
+
function applyContinuousUnderline(element, underline) {
|
|
375
|
+
removeUnderlineTextDecoration(element);
|
|
376
|
+
const color = typeof underline === "object" && underline.color ? underline.color : "currentColor";
|
|
377
|
+
element.style.boxShadow = `inset 0 -1px 0 ${color}`;
|
|
378
|
+
}
|
|
348
379
|
function applyWhitespaceUnderline(element, run) {
|
|
349
380
|
if (!run.underline || run.text.trim().length > 0) return;
|
|
350
|
-
|
|
351
|
-
element.style.borderBottom = "1px solid currentColor";
|
|
352
|
-
if (typeof run.underline === "object" && run.underline.color) element.style.borderBottomColor = run.underline.color;
|
|
381
|
+
applyContinuousUnderline(element, run.underline);
|
|
353
382
|
}
|
|
354
383
|
/**
|
|
355
384
|
* Number of leader characters to fill the tab's inner span. The inner span
|
|
@@ -401,9 +430,7 @@ function canClampTabToRightEdge(alignment, hasPriorRenderedContent, hasPriorTab,
|
|
|
401
430
|
}
|
|
402
431
|
function applyTabUnderline(element, run) {
|
|
403
432
|
if (!run.underline) return;
|
|
404
|
-
|
|
405
|
-
element.style.borderBottom = "1px solid currentColor";
|
|
406
|
-
if (typeof run.underline === "object" && run.underline.color) element.style.borderBottomColor = run.underline.color;
|
|
433
|
+
applyContinuousUnderline(element, run.underline);
|
|
407
434
|
}
|
|
408
435
|
/**
|
|
409
436
|
* Get leader character for tab
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FolioAIEditSnapshot } from "../ai-edits/types.js";
|
|
2
|
+
//#region src/paged-layout/blockGeometry.d.ts
|
|
3
|
+
/** Painted geometry for one AI-snapshot block in scroll-content coordinates. */
|
|
4
|
+
type BlockRect = {
|
|
5
|
+
blockId: string;
|
|
6
|
+
/** One-based painted page number. */
|
|
7
|
+
page: number;
|
|
8
|
+
/** Pixels from the scroll root's content origin. */
|
|
9
|
+
top: number;
|
|
10
|
+
/** Pixels from the scroll root's content origin. */
|
|
11
|
+
left: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
type ReadBlockRectsOptions = {
|
|
16
|
+
blockIds: readonly string[];
|
|
17
|
+
snapshot: FolioAIEditSnapshot;
|
|
18
|
+
pagesContainer: HTMLElement;
|
|
19
|
+
scrollRoot: HTMLElement;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Measure painted snapshot blocks relative to one editor's scroll root.
|
|
23
|
+
*
|
|
24
|
+
* Missing snapshot ids and blocks on unpainted virtual pages are omitted. Each
|
|
25
|
+
* requested block is measured at most once; painted elements are collected in
|
|
26
|
+
* one DOM query per relevant page.
|
|
27
|
+
*/
|
|
28
|
+
declare const readBlockRects: ({ blockIds, snapshot, pagesContainer, scrollRoot }: ReadBlockRectsOptions) => ReadonlyMap<string, BlockRect>;
|
|
29
|
+
/** Subscribe to instance-scoped painted-page changes. */
|
|
30
|
+
declare const onPaintedLayoutChange: (pagesContainer: HTMLElement, listener: () => void) => (() => void);
|
|
31
|
+
//#endregion
|
|
32
|
+
export { BlockRect, ReadBlockRectsOptions, onPaintedLayoutChange, readBlockRects };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { resolveSequentialBlockAnchor } from "../ai-edits/blockRange.js";
|
|
2
|
+
import { PAINTER_PAINTED_EVENT, findPageShellForPmPos } from "../layout-painter/renderPage.js";
|
|
3
|
+
//#region src/paged-layout/blockGeometry.ts
|
|
4
|
+
const PAINTED_BLOCK_SELECTOR = "[data-block-id][data-pm-start]";
|
|
5
|
+
const PAGE_SELECTOR = "[data-page-number]";
|
|
6
|
+
const pageNumberOf = (page) => {
|
|
7
|
+
const raw = page.dataset["pageNumber"];
|
|
8
|
+
const pageNumber = raw === void 0 ? NaN : Number(raw);
|
|
9
|
+
return Number.isInteger(pageNumber) && pageNumber > 0 ? pageNumber : null;
|
|
10
|
+
};
|
|
11
|
+
const requestedBlocks = (blockIds, snapshot) => {
|
|
12
|
+
const uniqueIds = new Set(blockIds);
|
|
13
|
+
const requested = [];
|
|
14
|
+
for (const blockId of uniqueIds) {
|
|
15
|
+
const anchor = Object.hasOwn(snapshot.anchors, blockId) ? snapshot.anchors[blockId] : resolveSequentialBlockAnchor(blockId, snapshot);
|
|
16
|
+
if (anchor) requested.push({
|
|
17
|
+
blockId,
|
|
18
|
+
pmStart: anchor.from
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return requested;
|
|
22
|
+
};
|
|
23
|
+
const paintedBlocksByPmStart = (page) => {
|
|
24
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const element of page.querySelectorAll(PAINTED_BLOCK_SELECTOR)) {
|
|
26
|
+
const pmStart = Number(element.dataset["pmStart"]);
|
|
27
|
+
if (Number.isFinite(pmStart) && !blocks.has(pmStart)) blocks.set(pmStart, element);
|
|
28
|
+
}
|
|
29
|
+
return blocks;
|
|
30
|
+
};
|
|
31
|
+
const groupVirtualizedRequestsByPage = (requests, pagesContainer) => {
|
|
32
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const request of requests) {
|
|
34
|
+
const page = findPageShellForPmPos(pagesContainer, request.pmStart)?.element;
|
|
35
|
+
if (!page) return null;
|
|
36
|
+
const pageRequests = byPage.get(page);
|
|
37
|
+
if (pageRequests) pageRequests.push(request);
|
|
38
|
+
else byPage.set(page, [request]);
|
|
39
|
+
}
|
|
40
|
+
return byPage;
|
|
41
|
+
};
|
|
42
|
+
const findPaintedElements = (requests, pagesContainer) => {
|
|
43
|
+
const found = /* @__PURE__ */ new Map();
|
|
44
|
+
const virtualizedRequests = groupVirtualizedRequestsByPage(requests, pagesContainer);
|
|
45
|
+
if (virtualizedRequests) {
|
|
46
|
+
for (const [page, pageRequests] of virtualizedRequests) {
|
|
47
|
+
const pageNumber = pageNumberOf(page);
|
|
48
|
+
if (pageNumber === null) continue;
|
|
49
|
+
const blocks = paintedBlocksByPmStart(page);
|
|
50
|
+
for (const request of pageRequests) {
|
|
51
|
+
const element = blocks.get(request.pmStart);
|
|
52
|
+
if (element) found.set(request.blockId, {
|
|
53
|
+
element,
|
|
54
|
+
page: pageNumber
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
const requestsByPmStart = new Map(requests.map((request) => [request.pmStart, request]));
|
|
61
|
+
for (const page of pagesContainer.querySelectorAll(PAGE_SELECTOR)) {
|
|
62
|
+
const pageNumber = pageNumberOf(page);
|
|
63
|
+
if (pageNumber === null) continue;
|
|
64
|
+
const blocks = paintedBlocksByPmStart(page);
|
|
65
|
+
for (const [pmStart, request] of requestsByPmStart) {
|
|
66
|
+
const element = blocks.get(pmStart);
|
|
67
|
+
if (element) {
|
|
68
|
+
found.set(request.blockId, {
|
|
69
|
+
element,
|
|
70
|
+
page: pageNumber
|
|
71
|
+
});
|
|
72
|
+
requestsByPmStart.delete(pmStart);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (requestsByPmStart.size === 0) break;
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Measure painted snapshot blocks relative to one editor's scroll root.
|
|
81
|
+
*
|
|
82
|
+
* Missing snapshot ids and blocks on unpainted virtual pages are omitted. Each
|
|
83
|
+
* requested block is measured at most once; painted elements are collected in
|
|
84
|
+
* one DOM query per relevant page.
|
|
85
|
+
*/
|
|
86
|
+
const readBlockRects = ({ blockIds, snapshot, pagesContainer, scrollRoot }) => {
|
|
87
|
+
const requests = requestedBlocks(blockIds, snapshot);
|
|
88
|
+
const paintedElements = findPaintedElements(requests, pagesContainer);
|
|
89
|
+
if (paintedElements.size === 0) return /* @__PURE__ */ new Map();
|
|
90
|
+
const rootRect = scrollRoot.getBoundingClientRect();
|
|
91
|
+
const rects = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const request of requests) {
|
|
93
|
+
const painted = paintedElements.get(request.blockId);
|
|
94
|
+
if (!painted) continue;
|
|
95
|
+
const rect = painted.element.getBoundingClientRect();
|
|
96
|
+
rects.set(request.blockId, {
|
|
97
|
+
blockId: request.blockId,
|
|
98
|
+
page: painted.page,
|
|
99
|
+
top: rect.top - rootRect.top - scrollRoot.clientTop + scrollRoot.scrollTop,
|
|
100
|
+
left: rect.left - rootRect.left - scrollRoot.clientLeft + scrollRoot.scrollLeft,
|
|
101
|
+
width: rect.width,
|
|
102
|
+
height: rect.height
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return rects;
|
|
106
|
+
};
|
|
107
|
+
/** Subscribe to instance-scoped painted-page changes. */
|
|
108
|
+
const onPaintedLayoutChange = (pagesContainer, listener) => {
|
|
109
|
+
pagesContainer.addEventListener(PAINTER_PAINTED_EVENT, listener);
|
|
110
|
+
return () => {
|
|
111
|
+
pagesContainer.removeEventListener(PAINTER_PAINTED_EVENT, listener);
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
export { onPaintedLayoutChange, readBlockRects };
|
|
@@ -1845,10 +1845,11 @@ function tableCellAttrsToFormatting(attrs) {
|
|
|
1845
1845
|
if (attrs.rowspan > 1) result.vMerge = "restart";
|
|
1846
1846
|
else if (result.vMerge === "restart" && !attrs._preserveVMergeRestart) delete result.vMerge;
|
|
1847
1847
|
const cellWidth = attrs.width;
|
|
1848
|
-
if (cellWidth
|
|
1848
|
+
if (typeof cellWidth === "number") result.width = {
|
|
1849
1849
|
value: cellWidth,
|
|
1850
1850
|
type: attrs.widthType ?? "dxa"
|
|
1851
1851
|
};
|
|
1852
|
+
else delete result.width;
|
|
1852
1853
|
if (attrs.verticalAlign !== (orig.verticalAlign ?? void 0)) if (attrs.verticalAlign) result.verticalAlign = attrs.verticalAlign;
|
|
1853
1854
|
else delete result.verticalAlign;
|
|
1854
1855
|
if (backgroundChanged) result.shading = cellShadingFromAttrs(attrs);
|
|
@@ -1859,11 +1860,11 @@ function tableCellAttrsToFormatting(attrs) {
|
|
|
1859
1860
|
return result;
|
|
1860
1861
|
}
|
|
1861
1862
|
const cellWidth = attrs.width;
|
|
1862
|
-
if (!(attrs.colspan > 1 || attrs.rowspan > 1 || cellWidth
|
|
1863
|
+
if (!(attrs.colspan > 1 || attrs.rowspan > 1 || typeof cellWidth === "number" || attrs.verticalAlign || backgroundChanged || attrs.borders || attrs.margins || attrs.textDirection)) return;
|
|
1863
1864
|
const f = {};
|
|
1864
1865
|
if (attrs.colspan > 1) f.gridSpan = attrs.colspan;
|
|
1865
1866
|
if (attrs.rowspan > 1) f.vMerge = "restart";
|
|
1866
|
-
if (cellWidth
|
|
1867
|
+
if (typeof cellWidth === "number") f.width = {
|
|
1867
1868
|
value: cellWidth,
|
|
1868
1869
|
type: attrs.widthType ?? "dxa"
|
|
1869
1870
|
};
|
|
@@ -185,23 +185,11 @@ const createEmptyHeaderFooter = (document, position, isFirstPage) => {
|
|
|
185
185
|
type: hdrFtrType,
|
|
186
186
|
rId
|
|
187
187
|
};
|
|
188
|
-
const usedTargets = /* @__PURE__ */ new Set();
|
|
189
|
-
for (const relationship of pkg.relationships?.values() ?? []) if (relationship.target) usedTargets.add(relationship.target);
|
|
190
|
-
let targetNumber = 1;
|
|
191
|
-
while (usedTargets.has(`${position}${targetNumber}.xml`)) targetNumber++;
|
|
192
|
-
const relationshipType = position === "header" ? "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" : "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
|
|
193
|
-
const relationships = new Map(pkg.relationships);
|
|
194
|
-
relationships.set(rId, {
|
|
195
|
-
id: rId,
|
|
196
|
-
type: relationshipType,
|
|
197
|
-
target: `${position}${targetNumber}.xml`
|
|
198
|
-
});
|
|
199
188
|
return {
|
|
200
189
|
...document,
|
|
201
190
|
package: {
|
|
202
191
|
...pkg,
|
|
203
192
|
[mapKey]: newMap,
|
|
204
|
-
relationships,
|
|
205
193
|
document: {
|
|
206
194
|
...pkg.document,
|
|
207
195
|
finalSectionProperties: {
|