@stll/folio-core 0.27.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/table-cell-mutations.js +38 -3
- 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/layout-painter/renderParagraph.js +36 -9
- 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
|
@@ -14,6 +14,7 @@ const mergeTableRectangle = ({ tr, tablePosition, table, rectangle }) => {
|
|
|
14
14
|
const tableStart = tablePosition + 1;
|
|
15
15
|
const seen = /* @__PURE__ */ new Set();
|
|
16
16
|
const cells = [];
|
|
17
|
+
const topRowCells = [];
|
|
17
18
|
let appendedContent = Fragment.empty;
|
|
18
19
|
for (let row = rectangle.top; row < rectangle.bottom; row++) for (let column = rectangle.left; column < rectangle.right; column++) {
|
|
19
20
|
const cellPosition = map.map[row * map.width + column];
|
|
@@ -25,6 +26,7 @@ const mergeTableRectangle = ({ tr, tablePosition, table, rectangle }) => {
|
|
|
25
26
|
position: cellPosition,
|
|
26
27
|
cell
|
|
27
28
|
});
|
|
29
|
+
if (row === rectangle.top) topRowCells.push(cell);
|
|
28
30
|
if (cells.length > 1 && !isEmptyTableCell(cell)) appendedContent = appendedContent.append(cell.content);
|
|
29
31
|
}
|
|
30
32
|
const merged = cells.at(0);
|
|
@@ -34,8 +36,8 @@ const mergeTableRectangle = ({ tr, tablePosition, table, rectangle }) => {
|
|
|
34
36
|
const colwidth = merged.cell.attrs["colwidth"];
|
|
35
37
|
if (typeof colspan !== "number" || !Number.isInteger(colspan) || colspan < 1 || typeof rowspan !== "number" || !Number.isInteger(rowspan) || rowspan < 1 || colwidth !== null && colwidth !== void 0 && (!Array.isArray(colwidth) || colwidth.length !== colspan || !colwidth.every((width) => typeof width === "number"))) return null;
|
|
36
38
|
const mergedColspan = rectangle.right - rectangle.left;
|
|
37
|
-
const nextColwidth =
|
|
38
|
-
|
|
39
|
+
const nextColwidth = mergeTopRowColwidths(topRowCells, mergedColspan);
|
|
40
|
+
const preferredWidth = mergeTopRowPreferredWidths(topRowCells);
|
|
39
41
|
const mapFrom = tr.mapping.maps.length;
|
|
40
42
|
for (const { position: cellPosition, cell } of cells.slice(1)) {
|
|
41
43
|
const position = tr.mapping.slice(mapFrom).map(tableStart + cellPosition);
|
|
@@ -46,7 +48,9 @@ const mergeTableRectangle = ({ tr, tablePosition, table, rectangle }) => {
|
|
|
46
48
|
...merged.cell.attrs,
|
|
47
49
|
colspan: mergedColspan,
|
|
48
50
|
rowspan: rectangle.bottom - rectangle.top,
|
|
49
|
-
colwidth: nextColwidth
|
|
51
|
+
colwidth: nextColwidth,
|
|
52
|
+
width: preferredWidth.type === "value" ? preferredWidth.width : null,
|
|
53
|
+
widthType: preferredWidth.type === "value" ? preferredWidth.widthType : null
|
|
50
54
|
});
|
|
51
55
|
if (appendedContent.size > 0) {
|
|
52
56
|
const contentEnd = absoluteMergedPosition + 1 + merged.cell.content.size;
|
|
@@ -55,6 +59,37 @@ const mergeTableRectangle = ({ tr, tablePosition, table, rectangle }) => {
|
|
|
55
59
|
}
|
|
56
60
|
return tr;
|
|
57
61
|
};
|
|
62
|
+
const mergeTopRowColwidths = (cells, mergedColspan) => {
|
|
63
|
+
const widths = [];
|
|
64
|
+
for (const cell of cells) {
|
|
65
|
+
const colspan = cell.attrs["colspan"];
|
|
66
|
+
const colwidth = cell.attrs["colwidth"];
|
|
67
|
+
if (typeof colspan !== "number" || !Number.isInteger(colspan) || colspan < 1 || !Array.isArray(colwidth) || colwidth.length !== colspan || !colwidth.every((width) => typeof width === "number" && width > 0)) return null;
|
|
68
|
+
widths.push(...colwidth);
|
|
69
|
+
}
|
|
70
|
+
return widths.length === mergedColspan ? widths : null;
|
|
71
|
+
};
|
|
72
|
+
/** A horizontal merge owns the whole top-row span. Sum compatible explicit
|
|
73
|
+
* preferred widths; retaining only the first cell's width makes a 50%+50%
|
|
74
|
+
* bilingual row serialize as a 50%-wide spanning cell. Mixed/implicit units
|
|
75
|
+
* cannot be composed safely, so clear the preference and let the table grid
|
|
76
|
+
* define the merged width. */
|
|
77
|
+
const mergeTopRowPreferredWidths = (cells) => {
|
|
78
|
+
let width = 0;
|
|
79
|
+
let widthType;
|
|
80
|
+
for (const cell of cells) {
|
|
81
|
+
const candidateWidth = cell.attrs["width"];
|
|
82
|
+
const candidateType = cell.attrs["widthType"];
|
|
83
|
+
if (typeof candidateWidth !== "number" || !Number.isFinite(candidateWidth) || candidateType !== "dxa" && candidateType !== "pct" || widthType !== void 0 && candidateType !== widthType) return { type: "absent" };
|
|
84
|
+
width += candidateWidth;
|
|
85
|
+
widthType = candidateType;
|
|
86
|
+
}
|
|
87
|
+
return widthType === void 0 ? { type: "absent" } : {
|
|
88
|
+
type: "value",
|
|
89
|
+
width,
|
|
90
|
+
widthType
|
|
91
|
+
};
|
|
92
|
+
};
|
|
58
93
|
const mergeTrackedVerticalTableCells = ({ tr, tablePosition, table, rectangle, revisionId, author, date }) => {
|
|
59
94
|
const map = TableMap.get(table);
|
|
60
95
|
if (rectangle.right - rectangle.left !== 1 || rectangle.bottom - rectangle.top < 2 || rectangle.left < 0 || rectangle.top < 0 || rectangle.right > map.width || rectangle.bottom > map.height || tableRectangleCutsMergedCell(map, rectangle)) return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidHexColor } from "../utils/colorResolver.js";
|
|
1
2
|
import { isValidHexId } from "../utils/hexId.js";
|
|
2
3
|
import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
|
|
3
4
|
import { parseFieldType } from "./fieldParser.js";
|
|
@@ -48,9 +49,9 @@ function parseShadingProperties(shd) {
|
|
|
48
49
|
if (!shd) return;
|
|
49
50
|
const props = {};
|
|
50
51
|
const color = getAttribute(shd, "w", "color");
|
|
51
|
-
if (color && color !== "auto") props.color = { rgb: color };
|
|
52
|
+
if (color && color !== "auto" && isValidHexColor(color)) props.color = { rgb: color };
|
|
52
53
|
const fill = getAttribute(shd, "w", "fill");
|
|
53
|
-
if (fill && fill !== "auto") props.fill = { rgb: fill };
|
|
54
|
+
if (fill && fill !== "auto" && isValidHexColor(fill)) props.fill = { rgb: fill };
|
|
54
55
|
const validatedThemeFill = narrowEnum(getAttribute(shd, "w", "themeFill"), ThemeColorSlotSchema);
|
|
55
56
|
if (validatedThemeFill) {
|
|
56
57
|
props.fill = props.fill || {};
|
package/dist/docx/parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toArrayBuffer } from "../utils/docxInput.js";
|
|
2
2
|
import { loadFontsWithMapping } from "../utils/fontLoader.js";
|
|
3
|
-
import { convertTiffToPngDataUrl, isTiffMimeType } from "../utils/tiffConverter.js";
|
|
3
|
+
import { MAX_PACKAGE_TIFF_PIXELS, convertTiffToPngDataUrl, isTiffMimeType } from "../utils/tiffConverter.js";
|
|
4
4
|
import { parseComments } from "./commentParser.js";
|
|
5
5
|
import { normalizeCommentReferences } from "./commentReferenceNormalization.js";
|
|
6
6
|
import { detectDocxConformanceClass } from "./conformance.js";
|
|
@@ -237,14 +237,37 @@ function copyBytesToArrayBuffer(bytes) {
|
|
|
237
237
|
new Uint8Array(buffer).set(bytes);
|
|
238
238
|
return buffer;
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Media paths reachable from the package relationship graph: the document's own
|
|
242
|
+
* relationships plus every `.rels` part the unzip kept. Entries outside it are
|
|
243
|
+
* still stored so a round-trip keeps their bytes, but nothing decodes them.
|
|
244
|
+
*/
|
|
245
|
+
function collectReferencedMediaPaths(raw, rels) {
|
|
246
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
247
|
+
const addTargets = (map, relsPath) => {
|
|
248
|
+
for (const relationship of map.values()) {
|
|
249
|
+
if (!relationship.target || relationship.targetMode === "External") continue;
|
|
250
|
+
const partPath = resolveRelativePath(relsPath, relationship.target);
|
|
251
|
+
referenced.add(partPath.toLowerCase());
|
|
252
|
+
referenced.add(partPath.replace(/^word\//u, "").toLowerCase());
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
addTargets(rels, DOCUMENT_RELATIONSHIPS_PATH);
|
|
256
|
+
for (const [path, xml] of raw.allXml.entries()) if (path.toLowerCase().endsWith(".rels")) addTargets(parseRelationships(xml), path);
|
|
257
|
+
return referenced;
|
|
258
|
+
}
|
|
259
|
+
async function buildMediaMap(raw, rels) {
|
|
241
260
|
const media = /* @__PURE__ */ new Map();
|
|
261
|
+
const referenced = collectReferencedMediaPaths(raw, rels);
|
|
262
|
+
let remainingTiffPixels = MAX_PACKAGE_TIFF_PIXELS;
|
|
242
263
|
for (const [path, data] of raw.media.entries()) {
|
|
243
264
|
const filename = path.split("/").pop() || path;
|
|
244
265
|
const mimeType = getMediaMimeType(path);
|
|
245
|
-
|
|
246
|
-
|
|
266
|
+
const isReferenced = referenced.has(path.toLowerCase());
|
|
267
|
+
if (isReferenced && isTiffMimeType(mimeType) && remainingTiffPixels > 0) {
|
|
268
|
+
const converted = await convertTiffToPngDataUrl(data, remainingTiffPixels);
|
|
247
269
|
if (converted) {
|
|
270
|
+
remainingTiffPixels -= converted.pixels;
|
|
248
271
|
const mediaFile = {
|
|
249
272
|
path,
|
|
250
273
|
filename: filename.replace(/\.tiff?$/iu, ".png"),
|
|
@@ -258,7 +281,7 @@ async function buildMediaMap(raw, _rels) {
|
|
|
258
281
|
continue;
|
|
259
282
|
}
|
|
260
283
|
}
|
|
261
|
-
const raster = isMetafileMimeType(mimeType) ? extractMetafileRaster(data) : null;
|
|
284
|
+
const raster = isReferenced && isMetafileMimeType(mimeType) ? extractMetafileRaster(data) : null;
|
|
262
285
|
if (raster) {
|
|
263
286
|
const mediaFile = {
|
|
264
287
|
path,
|
package/dist/docx/rezip.d.ts
CHANGED
|
@@ -54,6 +54,26 @@ declare function addCommentsExtendedOverride(contentTypesXml: string): string;
|
|
|
54
54
|
declare function removeCommentsExtendedOverride(contentTypesXml: string): string;
|
|
55
55
|
declare function addCommentsExtendedRelationship(relsXml: string): string;
|
|
56
56
|
declare function removeCommentsExtendedRelationship(relsXml: string): string;
|
|
57
|
+
/** Result of filtering the settings part; `undefined` means "leave as it is". */
|
|
58
|
+
type SettingsWithoutAttachedTemplate = {
|
|
59
|
+
settingsXml: string | undefined;
|
|
60
|
+
relsXml: string | undefined;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Drop `w:attachedTemplate` from a `word/settings.xml` payload together with the
|
|
64
|
+
* relationships it resolves through.
|
|
65
|
+
*
|
|
66
|
+
* The elements are located in the parsed tree by namespace URI plus local name,
|
|
67
|
+
* so both the Transitional and the Strict WordprocessingML namespace are
|
|
68
|
+
* covered and a same-named element from a foreign namespace is left alone. Only
|
|
69
|
+
* the relationship ids those elements reference are removed from the `.rels`
|
|
70
|
+
* part: the settings part may also carry mail-merge and transform
|
|
71
|
+
* relationships, and their `r:id` values must keep resolving.
|
|
72
|
+
*
|
|
73
|
+
* The removal itself is a byte splice, so everything else in both parts
|
|
74
|
+
* round-trips exactly as authored.
|
|
75
|
+
*/
|
|
76
|
+
declare function withoutAttachedTemplate(settingsXml: string, relsXml: string | undefined): SettingsWithoutAttachedTemplate;
|
|
57
77
|
/**
|
|
58
78
|
* Update only document.xml in a DOCX buffer (minimal changes)
|
|
59
79
|
*
|
|
@@ -173,4 +193,4 @@ declare function createEmptyDocx(): Promise<ArrayBuffer>;
|
|
|
173
193
|
*/
|
|
174
194
|
declare function createDocx(doc: document_d_exports.Document): Promise<ArrayBuffer>;
|
|
175
195
|
//#endregion
|
|
176
|
-
export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocxPackageFidelityError, RepackOptions, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx };
|
|
196
|
+
export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocxPackageFidelityError, RepackOptions, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx, withoutAttachedTemplate };
|
package/dist/docx/rezip.js
CHANGED
|
@@ -18,7 +18,7 @@ import { serializeStyle, serializeStylesXml } from "./serializer/stylesSerialize
|
|
|
18
18
|
import { serializeThemeXml } from "./serializer/themeSerializer.js";
|
|
19
19
|
import { escapeXml } from "./serializer/xmlUtils.js";
|
|
20
20
|
import { isPreservableDocxEntry } from "./unzip.js";
|
|
21
|
-
import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml } from "./xmlParser.js";
|
|
21
|
+
import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getAttribute, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml, parseXmlDocument } from "./xmlParser.js";
|
|
22
22
|
import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
|
|
23
23
|
import { panic } from "better-result";
|
|
24
24
|
import { validateDocxPackage } from "@stll/docx-core";
|
|
@@ -459,6 +459,7 @@ const finishRepack = async ({ document, originalZip, outputZip, originalDocument
|
|
|
459
459
|
await serializeNumberingIntoZip(document, originalZip, outputZip, compressionLevel);
|
|
460
460
|
await serializeAddedStylesIntoZip(document, originalZip, outputZip, compressionLevel);
|
|
461
461
|
await serializeCommentsToZip(document, outputZip, compressionLevel);
|
|
462
|
+
await dropAttachedTemplateReference(outputZip, compressionLevel);
|
|
462
463
|
if (updateModifiedDate && originalCorePropertiesXml) {
|
|
463
464
|
const updatedCoreProperties = updateCoreProperties(originalCorePropertiesXml, {
|
|
464
465
|
updateModifiedDate,
|
|
@@ -547,6 +548,7 @@ async function repackDocxFromRaw(doc, rawContent, options = {}) {
|
|
|
547
548
|
await serializeNumberingIntoZip(exportDocument, rawContent.originalZip, newZip, compressionLevel);
|
|
548
549
|
await serializeAddedStylesIntoZip(exportDocument, rawContent.originalZip, newZip, compressionLevel);
|
|
549
550
|
await serializeCommentsToZip(exportDocument, newZip, compressionLevel);
|
|
551
|
+
await dropAttachedTemplateReference(newZip, compressionLevel);
|
|
550
552
|
if (updateModifiedDate && rawContent.corePropsXml) {
|
|
551
553
|
const updatedCoreProps = updateCoreProperties(rawContent.corePropsXml, {
|
|
552
554
|
updateModifiedDate,
|
|
@@ -582,6 +584,82 @@ function addCommentsExtendedRelationship(relsXml) {
|
|
|
582
584
|
function removeCommentsExtendedRelationship(relsXml) {
|
|
583
585
|
return relsXml.replace(/<Relationship\b[^>]*commentsExtended\.xml[^>]*\/>/giu, "");
|
|
584
586
|
}
|
|
587
|
+
const SETTINGS_PART = "word/settings.xml";
|
|
588
|
+
const SETTINGS_RELS_PART = "word/_rels/settings.xml.rels";
|
|
589
|
+
const ATTACHED_TEMPLATE_LOCAL_NAME = "attachedTemplate";
|
|
590
|
+
const ATTACHED_TEMPLATE_ELEMENT = /<(?<prefix>[\w.-]+:)?attachedTemplate\b[^>]*?(?:\/>|>\s*<\/(?:[\w.-]+:)?attachedTemplate>)/giu;
|
|
591
|
+
const RELATIONSHIP_ELEMENT = /<Relationship\b[^>]*?(?:\/>|>\s*<\/Relationship>)/giu;
|
|
592
|
+
const RELATIONSHIP_ID_ATTRIBUTE = /\bId\s*=\s*(?<quote>["'])(?<value>[^"']*)\k<quote>/u;
|
|
593
|
+
/**
|
|
594
|
+
* Drop `w:attachedTemplate` from a `word/settings.xml` payload together with the
|
|
595
|
+
* relationships it resolves through.
|
|
596
|
+
*
|
|
597
|
+
* The elements are located in the parsed tree by namespace URI plus local name,
|
|
598
|
+
* so both the Transitional and the Strict WordprocessingML namespace are
|
|
599
|
+
* covered and a same-named element from a foreign namespace is left alone. Only
|
|
600
|
+
* the relationship ids those elements reference are removed from the `.rels`
|
|
601
|
+
* part: the settings part may also carry mail-merge and transform
|
|
602
|
+
* relationships, and their `r:id` values must keep resolving.
|
|
603
|
+
*
|
|
604
|
+
* The removal itself is a byte splice, so everything else in both parts
|
|
605
|
+
* round-trips exactly as authored.
|
|
606
|
+
*/
|
|
607
|
+
function withoutAttachedTemplate(settingsXml, relsXml) {
|
|
608
|
+
const root = parseXmlDocument(settingsXml);
|
|
609
|
+
if (!root) return {
|
|
610
|
+
settingsXml: void 0,
|
|
611
|
+
relsXml: void 0
|
|
612
|
+
};
|
|
613
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
614
|
+
const referencedRIds = /* @__PURE__ */ new Set();
|
|
615
|
+
for (const element of getChildElements(root)) {
|
|
616
|
+
if (getLocalName(element.name) !== ATTACHED_TEMPLATE_LOCAL_NAME || !WORDPROCESSINGML_NAMESPACE_URIS.has(getNamespaceUri(element) ?? "")) continue;
|
|
617
|
+
const name = element.name ?? "";
|
|
618
|
+
const separatorIndex = name.indexOf(":");
|
|
619
|
+
prefixes.add(separatorIndex === -1 ? "" : name.slice(0, separatorIndex));
|
|
620
|
+
const rId = getAttribute(element, "r", "id");
|
|
621
|
+
if (rId) referencedRIds.add(rId);
|
|
622
|
+
}
|
|
623
|
+
if (prefixes.size === 0) return {
|
|
624
|
+
settingsXml: void 0,
|
|
625
|
+
relsXml: void 0
|
|
626
|
+
};
|
|
627
|
+
const filteredSettings = settingsXml.replace(ATTACHED_TEMPLATE_ELEMENT, (match, prefix) => prefixes.has(prefix?.slice(0, -1) ?? "") ? "" : match);
|
|
628
|
+
return {
|
|
629
|
+
settingsXml: filteredSettings === settingsXml ? void 0 : filteredSettings,
|
|
630
|
+
relsXml: relsXml === void 0 ? void 0 : withoutRelationships(relsXml, referencedRIds)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
/** Drop the named relationships from a `.rels` payload; `undefined` when unchanged. */
|
|
634
|
+
function withoutRelationships(relsXml, ids) {
|
|
635
|
+
if (ids.size === 0) return;
|
|
636
|
+
const filtered = relsXml.replace(RELATIONSHIP_ELEMENT, (relationship) => {
|
|
637
|
+
const id = RELATIONSHIP_ID_ATTRIBUTE.exec(relationship)?.groups?.["value"];
|
|
638
|
+
return id !== void 0 && ids.has(id) ? "" : relationship;
|
|
639
|
+
});
|
|
640
|
+
return filtered === relsXml ? void 0 : filtered;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* The settings part and its relationships are otherwise copied from the source
|
|
644
|
+
* package byte for byte. `w:attachedTemplate` resolves through a relationship
|
|
645
|
+
* whose target sits outside the package (`TargetMode="External"`); the document
|
|
646
|
+
* model has no field for it, so a preserved copy would carry a reference folio
|
|
647
|
+
* can neither read nor rewrite. Both sides are filtered on save.
|
|
648
|
+
*/
|
|
649
|
+
async function dropAttachedTemplateReference(zip, compressionLevel) {
|
|
650
|
+
const settingsFile = zip.file(SETTINGS_PART);
|
|
651
|
+
if (!settingsFile) return;
|
|
652
|
+
const relsFile = zip.file(SETTINGS_RELS_PART);
|
|
653
|
+
const filtered = withoutAttachedTemplate(await settingsFile.async("text"), await relsFile?.async("text"));
|
|
654
|
+
if (filtered.settingsXml !== void 0) zip.file(SETTINGS_PART, filtered.settingsXml, {
|
|
655
|
+
compression: "DEFLATE",
|
|
656
|
+
compressionOptions: { level: compressionLevel }
|
|
657
|
+
});
|
|
658
|
+
if (filtered.relsXml !== void 0) zip.file(SETTINGS_RELS_PART, filtered.relsXml, {
|
|
659
|
+
compression: "DEFLATE",
|
|
660
|
+
compressionOptions: { level: compressionLevel }
|
|
661
|
+
});
|
|
662
|
+
}
|
|
585
663
|
/**
|
|
586
664
|
* Ensure [Content_Types].xml contains an Override for word/comments.xml.
|
|
587
665
|
* If the document already had comments, this is a no-op.
|
|
@@ -1413,4 +1491,4 @@ const assertStyleNumberingReferences = (doc) => {
|
|
|
1413
1491
|
for (const numbering of doc.package.numbering?.nums ?? []) if (!availableAbstract.has(numbering.abstractNumId)) panic(`Numbering definition ${numbering.numId} references missing abstract numbering`);
|
|
1414
1492
|
};
|
|
1415
1493
|
//#endregion
|
|
1416
|
-
export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocxPackageFidelityError, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx };
|
|
1494
|
+
export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocxPackageFidelityError, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx, withoutAttachedTemplate };
|
package/dist/docx/runParser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidHexColor } from "../utils/colorResolver.js";
|
|
1
2
|
import { parseGroupDrawing } from "./groupDrawingParser.js";
|
|
2
3
|
import { parseImage } from "./imageParser.js";
|
|
3
4
|
import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, PositionalTabAlignmentSchema, PositionalTabLeaderSchema, PositionalTabRelativeToSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
@@ -37,9 +38,9 @@ function parseShadingProperties(shd) {
|
|
|
37
38
|
if (!shd) return;
|
|
38
39
|
const props = {};
|
|
39
40
|
const color = getAttribute(shd, "w", "color");
|
|
40
|
-
if (color && color !== "auto") props.color = { rgb: color };
|
|
41
|
+
if (color && color !== "auto" && isValidHexColor(color)) props.color = { rgb: color };
|
|
41
42
|
const fill = getAttribute(shd, "w", "fill");
|
|
42
|
-
if (fill && fill !== "auto") props.fill = { rgb: fill };
|
|
43
|
+
if (fill && fill !== "auto" && isValidHexColor(fill)) props.fill = { rgb: fill };
|
|
43
44
|
const validatedThemeFill = narrowEnum(getAttribute(shd, "w", "themeFill"), ThemeColorSlotSchema);
|
|
44
45
|
if (validatedThemeFill) {
|
|
45
46
|
if (!props.fill) props.fill = {};
|
|
@@ -252,7 +253,7 @@ function parseRunProperties(rPr, theme, _styles) {
|
|
|
252
253
|
if (resolved) fontFamily.ascii = resolved;
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
|
-
const hAnsiTheme = getAttribute(rFonts, "w", "hAnsiTheme");
|
|
256
|
+
const hAnsiTheme = narrowEnum(getAttribute(rFonts, "w", "hAnsiTheme"), FontThemeSchema);
|
|
256
257
|
if (hAnsiTheme) {
|
|
257
258
|
fontFamily.hAnsiTheme = hAnsiTheme;
|
|
258
259
|
if (theme && !fontFamily.hAnsi) {
|
|
@@ -260,7 +261,7 @@ function parseRunProperties(rPr, theme, _styles) {
|
|
|
260
261
|
if (resolved) fontFamily.hAnsi = resolved;
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
|
-
const eastAsiaTheme = getAttribute(rFonts, "w", "eastAsiaTheme");
|
|
264
|
+
const eastAsiaTheme = narrowEnum(getAttribute(rFonts, "w", "eastAsiaTheme"), FontThemeSchema);
|
|
264
265
|
if (eastAsiaTheme) {
|
|
265
266
|
fontFamily.eastAsiaTheme = eastAsiaTheme;
|
|
266
267
|
if (theme && !fontFamily.eastAsia) {
|
|
@@ -268,7 +269,7 @@ function parseRunProperties(rPr, theme, _styles) {
|
|
|
268
269
|
if (resolved) fontFamily.eastAsia = resolved;
|
|
269
270
|
}
|
|
270
271
|
}
|
|
271
|
-
const csTheme = getAttribute(rFonts, "w", "cstheme");
|
|
272
|
+
const csTheme = narrowEnum(getAttribute(rFonts, "w", "cstheme"), FontThemeSchema);
|
|
272
273
|
if (csTheme) {
|
|
273
274
|
fontFamily.csTheme = csTheme;
|
|
274
275
|
if (theme && !fontFamily.cs) {
|
|
@@ -14,14 +14,6 @@ type SelectiveSaveOptions = {
|
|
|
14
14
|
*/
|
|
15
15
|
maxBytes?: number;
|
|
16
16
|
};
|
|
17
|
-
/**
|
|
18
|
-
* Attempt a selective save — patch only changed paragraphs in document.xml.
|
|
19
|
-
* Also updates comments, headers/footers, and core properties so that
|
|
20
|
-
* all document parts stay in sync even when only paragraphs are patched.
|
|
21
|
-
*
|
|
22
|
-
* Returns the saved ArrayBuffer, or null if selective save is not possible
|
|
23
|
-
* (caller should fall back to full repack).
|
|
24
|
-
*/
|
|
25
17
|
declare function attemptSelectiveSave(doc: document_d_exports.Document, originalBuffer: ArrayBuffer, options: SelectiveSaveOptions): Promise<ArrayBuffer | null>;
|
|
26
18
|
//#endregion
|
|
27
19
|
export { SelectiveSaveOptions, attemptSelectiveSave };
|
|
@@ -5,7 +5,7 @@ import { validateFolioDocumentModel } from "./modelValidation.js";
|
|
|
5
5
|
import { isNewDataUrlDrawing } from "./newImage.js";
|
|
6
6
|
import { parseNumbering } from "./numberingParser.js";
|
|
7
7
|
import { RELATIONSHIP_TYPES } from "./relsParser.js";
|
|
8
|
-
import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, updateCoreProperties } from "./rezip.js";
|
|
8
|
+
import { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_PART_LOWER, addCommentsExtendedOverride, addCommentsExtendedRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, updateCoreProperties, withoutAttachedTemplate } from "./rezip.js";
|
|
9
9
|
import "./selectiveSaveFlags.js";
|
|
10
10
|
import { buildPatchedDocumentXml, buildPatchedNoteXml, buildPatchedNumberingXml, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
|
|
11
11
|
import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExtended } from "./serializer/commentSerializer.js";
|
|
@@ -174,6 +174,20 @@ async function patchNumberingPart(zip, doc, updates) {
|
|
|
174
174
|
* Returns the saved ArrayBuffer, or null if selective save is not possible
|
|
175
175
|
* (caller should fall back to full repack).
|
|
176
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* Queue the filtered `word/settings.xml` and `word/_rels/settings.xml.rels`
|
|
179
|
+
* when the source package carries an attached-template reference. Shares
|
|
180
|
+
* `withoutAttachedTemplate` with the full-repack path so both saves emit the
|
|
181
|
+
* same package for the same source.
|
|
182
|
+
*/
|
|
183
|
+
const queueSettingsUpdates = async (zip, updates) => {
|
|
184
|
+
const settingsFile = zip.file("word/settings.xml");
|
|
185
|
+
if (!settingsFile) return;
|
|
186
|
+
const relsFile = zip.file("word/_rels/settings.xml.rels");
|
|
187
|
+
const filtered = withoutAttachedTemplate(await settingsFile.async("text"), await relsFile?.async("text"));
|
|
188
|
+
if (filtered.settingsXml !== void 0) updates.set("word/settings.xml", filtered.settingsXml);
|
|
189
|
+
if (filtered.relsXml !== void 0) updates.set("word/_rels/settings.xml.rels", filtered.relsXml);
|
|
190
|
+
};
|
|
177
191
|
async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
178
192
|
const { changedParaIds, structuralChange, hasUntrackedChanges } = options;
|
|
179
193
|
const maxBytes = options.maxBytes ?? 104857600;
|
|
@@ -243,6 +257,7 @@ async function attemptSelectiveSave(doc, originalBuffer, options) {
|
|
|
243
257
|
if (!await patchCommentsExtended(zip, comments, updates)) return null;
|
|
244
258
|
await patchNumberingPart(zip, doc, updates);
|
|
245
259
|
for (const [path, xml] of headerFooterUpdates) updates.set(path, xml);
|
|
260
|
+
await queueSettingsUpdates(zip, updates);
|
|
246
261
|
const corePropsFile = zip.file("docProps/core.xml");
|
|
247
262
|
if (corePropsFile) {
|
|
248
263
|
const corePropsXml = await corePropsFile.async("text");
|
|
@@ -85,10 +85,10 @@ function serializeTextFormatting(formatting) {
|
|
|
85
85
|
if (formatting.fontFamily.eastAsia) fontAttrs.push(`w:eastAsia="${escapeXml(formatting.fontFamily.eastAsia)}"`);
|
|
86
86
|
if (formatting.fontFamily.cs) fontAttrs.push(`w:cs="${escapeXml(formatting.fontFamily.cs)}"`);
|
|
87
87
|
if (formatting.fontFamily.hint) fontAttrs.push(`w:hint="${escapeXml(formatting.fontFamily.hint)}"`);
|
|
88
|
-
if (formatting.fontFamily.asciiTheme) fontAttrs.push(`w:asciiTheme="${formatting.fontFamily.asciiTheme}"`);
|
|
89
|
-
if (formatting.fontFamily.hAnsiTheme) fontAttrs.push(`w:hAnsiTheme="${formatting.fontFamily.hAnsiTheme}"`);
|
|
90
|
-
if (formatting.fontFamily.eastAsiaTheme) fontAttrs.push(`w:eastAsiaTheme="${formatting.fontFamily.eastAsiaTheme}"`);
|
|
91
|
-
if (formatting.fontFamily.csTheme) fontAttrs.push(`w:cstheme="${formatting.fontFamily.csTheme}"`);
|
|
88
|
+
if (formatting.fontFamily.asciiTheme) fontAttrs.push(`w:asciiTheme="${escapeXml(formatting.fontFamily.asciiTheme)}"`);
|
|
89
|
+
if (formatting.fontFamily.hAnsiTheme) fontAttrs.push(`w:hAnsiTheme="${escapeXml(formatting.fontFamily.hAnsiTheme)}"`);
|
|
90
|
+
if (formatting.fontFamily.eastAsiaTheme) fontAttrs.push(`w:eastAsiaTheme="${escapeXml(formatting.fontFamily.eastAsiaTheme)}"`);
|
|
91
|
+
if (formatting.fontFamily.csTheme) fontAttrs.push(`w:cstheme="${escapeXml(formatting.fontFamily.csTheme)}"`);
|
|
92
92
|
if (fontAttrs.length > 0) parts.push(`<w:rFonts ${fontAttrs.join(" ")}/>`);
|
|
93
93
|
}
|
|
94
94
|
if (formatting.language) {
|
|
@@ -83,7 +83,7 @@ function serializePageNumbering(props) {
|
|
|
83
83
|
if (pageNumbering.format) attrs.push(`w:fmt="${pageNumbering.format}"`);
|
|
84
84
|
if (pageNumbering.start !== void 0) attrs.push(`w:start="${intAttr(pageNumbering.start)}"`);
|
|
85
85
|
if (pageNumbering.chapterStyle !== void 0) attrs.push(`w:chapStyle="${intAttr(pageNumbering.chapterStyle)}"`);
|
|
86
|
-
if (pageNumbering.chapterSeparator) attrs.push(`w:chapSep="${pageNumbering.chapterSeparator}"`);
|
|
86
|
+
if (pageNumbering.chapterSeparator) attrs.push(`w:chapSep="${escapeXml(pageNumbering.chapterSeparator)}"`);
|
|
87
87
|
return attrs.length > 0 ? `<w:pgNumType ${attrs.join(" ")}/>` : "";
|
|
88
88
|
}
|
|
89
89
|
function serializePageBorders(props) {
|
|
@@ -113,10 +113,10 @@ function serializeBackground(props) {
|
|
|
113
113
|
const attrs = [];
|
|
114
114
|
const { background } = props;
|
|
115
115
|
if (background.color?.auto) attrs.push("w:color=\"auto\"");
|
|
116
|
-
else if (background.color?.rgb) attrs.push(`w:color="${background.color.rgb}"`);
|
|
117
|
-
if (background.themeColor ?? background.color?.themeColor) attrs.push(`w:themeColor="${background.themeColor ?? background.color?.themeColor}"`);
|
|
118
|
-
if (background.themeTint ?? background.color?.themeTint) attrs.push(`w:themeTint="${background.themeTint ?? background.color?.themeTint}"`);
|
|
119
|
-
if (background.themeShade ?? background.color?.themeShade) attrs.push(`w:themeShade="${background.themeShade ?? background.color?.themeShade}"`);
|
|
116
|
+
else if (background.color?.rgb) attrs.push(`w:color="${escapeXml(background.color.rgb)}"`);
|
|
117
|
+
if (background.themeColor ?? background.color?.themeColor) attrs.push(`w:themeColor="${escapeXml(background.themeColor ?? background.color?.themeColor ?? "")}"`);
|
|
118
|
+
if (background.themeTint ?? background.color?.themeTint) attrs.push(`w:themeTint="${escapeXml(background.themeTint ?? background.color?.themeTint ?? "")}"`);
|
|
119
|
+
if (background.themeShade ?? background.color?.themeShade) attrs.push(`w:themeShade="${escapeXml(background.themeShade ?? background.color?.themeShade ?? "")}"`);
|
|
120
120
|
return attrs.length > 0 ? `<w:background ${attrs.join(" ")}/>` : "";
|
|
121
121
|
}
|
|
122
122
|
function serializeDocGrid(props) {
|
|
@@ -10,7 +10,10 @@ import { TaggedError } from "better-result";
|
|
|
10
10
|
* cloned per language, so both columns count independently (1. / 1. instead of
|
|
11
11
|
* 1. / 2.) and stay live in Word. Right-column paragraphs receive fresh
|
|
12
12
|
* `paraId`s so callers can address each row later (for example to replace the
|
|
13
|
-
* placeholder copy with a translation by block id).
|
|
13
|
+
* placeholder copy with a translation by block id). Horizontal paragraph
|
|
14
|
+
* geometry is projected into the half-width cells: full-page indents and tab
|
|
15
|
+
* stops otherwise place signature fields outside their column and let prose
|
|
16
|
+
* overlap the translation.
|
|
14
17
|
*
|
|
15
18
|
* Section breaks cannot live inside a table cell, so the body is split at
|
|
16
19
|
* paragraphs carrying `sectionProperties`: each section becomes its own table
|
|
@@ -28,6 +31,8 @@ const HALF_WIDTH_PCT = 2500;
|
|
|
28
31
|
const A4_TEXT_WIDTH_TWIPS = 9072;
|
|
29
32
|
const ROW_ID_NAMESPACE = "folio-bilingual";
|
|
30
33
|
const BILINGUAL_TABLE_STYLE_ID = "FolioBilingualTranslation";
|
|
34
|
+
const MIN_COLUMN_TEXT_WIDTH_TWIPS = 720;
|
|
35
|
+
const MIN_TAB_TRAILING_WIDTH_TWIPS = 360;
|
|
31
36
|
const GRID_BORDER = {
|
|
32
37
|
style: "single",
|
|
33
38
|
size: 4,
|
|
@@ -111,7 +116,7 @@ function createBilingualDocument(source, options) {
|
|
|
111
116
|
rowId: ref.targetParaId,
|
|
112
117
|
...ref
|
|
113
118
|
});
|
|
114
|
-
sectionRows.push(buildRow(block, copy));
|
|
119
|
+
sectionRows.push(buildRow(block, copy, styleById, textWidth));
|
|
115
120
|
continue;
|
|
116
121
|
}
|
|
117
122
|
const paragraphs = collectTableParagraphs(block).filter((paragraph) => paragraph.paraId !== void 0 && options.editableParagraphIds.has(paragraph.paraId)).map((paragraph) => ({
|
|
@@ -362,11 +367,72 @@ const collectTableParagraphs = (table) => {
|
|
|
362
367
|
else out.push(...collectTableParagraphs(item));
|
|
363
368
|
return out;
|
|
364
369
|
};
|
|
365
|
-
const buildRow = (left, right) =>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
+
const buildRow = (left, right, styleById, textWidth) => {
|
|
371
|
+
const columnWidth = Math.floor(textWidth / 2);
|
|
372
|
+
const geometry = resolveHorizontalParagraphGeometry(left, styleById);
|
|
373
|
+
return {
|
|
374
|
+
type: "tableRow",
|
|
375
|
+
formatting: { cantSplit: true },
|
|
376
|
+
cells: [buildCell(projectParagraphIntoColumn(left, geometry, textWidth, columnWidth)), buildCell(projectParagraphIntoColumn(right, geometry, textWidth, columnWidth))]
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
const HORIZONTAL_PARAGRAPH_KEYS = [
|
|
380
|
+
"indentLeft",
|
|
381
|
+
"indentRight",
|
|
382
|
+
"indentFirstLine",
|
|
383
|
+
"hangingIndent",
|
|
384
|
+
"tabs"
|
|
385
|
+
];
|
|
386
|
+
/** Resolve only the paragraph properties whose coordinates change when a
|
|
387
|
+
* full-width paragraph is placed in a half-width cell. Direct pPr wins over
|
|
388
|
+
* the basedOn style chain, matching Word's paragraph-style cascade. */
|
|
389
|
+
const resolveHorizontalParagraphGeometry = (paragraph, styleById) => {
|
|
390
|
+
const chain = [];
|
|
391
|
+
const seen = /* @__PURE__ */ new Set();
|
|
392
|
+
let style = paragraph.formatting?.styleId ? styleById.get(paragraph.formatting.styleId) : void 0;
|
|
393
|
+
while (style && !seen.has(style.styleId)) {
|
|
394
|
+
seen.add(style.styleId);
|
|
395
|
+
chain.push(style);
|
|
396
|
+
style = style.basedOn ? styleById.get(style.basedOn) : void 0;
|
|
397
|
+
}
|
|
398
|
+
const geometry = {};
|
|
399
|
+
for (const current of chain.toReversed()) assignHorizontalParagraphGeometry(geometry, current.pPr);
|
|
400
|
+
assignHorizontalParagraphGeometry(geometry, paragraph.formatting);
|
|
401
|
+
return geometry;
|
|
402
|
+
};
|
|
403
|
+
const assignHorizontalParagraphGeometry = (target, source) => {
|
|
404
|
+
for (const key of HORIZONTAL_PARAGRAPH_KEYS) {
|
|
405
|
+
const value = source?.[key];
|
|
406
|
+
if (value !== void 0) Object.assign(target, { [key]: value });
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const projectParagraphIntoColumn = (paragraph, geometry, sourceWidth, columnWidth) => {
|
|
410
|
+
const scale = columnWidth / sourceWidth;
|
|
411
|
+
const maxSideIndent = Math.max(0, columnWidth - MIN_COLUMN_TEXT_WIDTH_TWIPS);
|
|
412
|
+
let indentLeft = projectSideIndent(geometry.indentLeft, scale, maxSideIndent);
|
|
413
|
+
let indentRight = projectSideIndent(geometry.indentRight, scale, maxSideIndent);
|
|
414
|
+
if (indentLeft + indentRight - maxSideIndent > 0) {
|
|
415
|
+
const total = indentLeft + indentRight;
|
|
416
|
+
indentLeft = Math.round(indentLeft / total * maxSideIndent);
|
|
417
|
+
indentRight = maxSideIndent - indentLeft;
|
|
418
|
+
}
|
|
419
|
+
const formatting = {
|
|
420
|
+
...paragraph.formatting,
|
|
421
|
+
...geometry.indentLeft !== void 0 && { indentLeft },
|
|
422
|
+
...geometry.indentRight !== void 0 && { indentRight },
|
|
423
|
+
...geometry.indentFirstLine !== void 0 && { indentFirstLine: Math.round(geometry.indentFirstLine * scale) },
|
|
424
|
+
...geometry.hangingIndent !== void 0 && { hangingIndent: geometry.hangingIndent },
|
|
425
|
+
...geometry.tabs !== void 0 && { tabs: geometry.tabs.map((tab) => ({
|
|
426
|
+
...tab,
|
|
427
|
+
position: Math.min(Math.max(0, Math.round(tab.position * scale)), Math.max(0, columnWidth - MIN_TAB_TRAILING_WIDTH_TWIPS))
|
|
428
|
+
})) }
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
...paragraph,
|
|
432
|
+
formatting
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
const projectSideIndent = (value, scale, maximum) => Math.min(Math.max(0, Math.round((value ?? 0) * scale)), maximum);
|
|
370
436
|
const buildCell = (paragraph) => ({
|
|
371
437
|
type: "tableCell",
|
|
372
438
|
formatting: {
|
|
@@ -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([
|
|
@@ -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
|
|
@@ -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: {
|
|
@@ -8,11 +8,20 @@
|
|
|
8
8
|
* back to the raw TIFF data URL, which won't render in browsers but is
|
|
9
9
|
* fine for headless round-trips.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* Budget shared by every TIFF in one package. The per-image cap bounds a single
|
|
13
|
+
* decode; without a running total a package of many large TIFFs still adds up.
|
|
14
|
+
* Callers pass what is left of the budget and subtract the pixels each
|
|
15
|
+
* conversion reports.
|
|
16
|
+
*/
|
|
17
|
+
declare const MAX_PACKAGE_TIFF_PIXELS = 256000000;
|
|
11
18
|
declare function isTiffMimeType(mimeType: string): boolean;
|
|
12
19
|
type ConvertedTiff = {
|
|
13
20
|
dataUrl: string;
|
|
14
21
|
data: ArrayBuffer;
|
|
22
|
+
/** Pixels decoded, so callers can draw down a package-wide budget. */
|
|
23
|
+
pixels: number;
|
|
15
24
|
};
|
|
16
|
-
declare function convertTiffToPngDataUrl(tiffData: ArrayBuffer): Promise<ConvertedTiff | null>;
|
|
25
|
+
declare function convertTiffToPngDataUrl(tiffData: ArrayBuffer, maxPixels?: number): Promise<ConvertedTiff | null>;
|
|
17
26
|
//#endregion
|
|
18
|
-
export { ConvertedTiff, convertTiffToPngDataUrl, isTiffMimeType };
|
|
27
|
+
export { ConvertedTiff, MAX_PACKAGE_TIFF_PIXELS, convertTiffToPngDataUrl, isTiffMimeType };
|
|
@@ -16,11 +16,18 @@
|
|
|
16
16
|
* RGBA buffer, which would otherwise hang or OOM the tab.
|
|
17
17
|
*/
|
|
18
18
|
const MAX_TIFF_PIXELS = 64e6;
|
|
19
|
+
/**
|
|
20
|
+
* Budget shared by every TIFF in one package. The per-image cap bounds a single
|
|
21
|
+
* decode; without a running total a package of many large TIFFs still adds up.
|
|
22
|
+
* Callers pass what is left of the budget and subtract the pixels each
|
|
23
|
+
* conversion reports.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_PACKAGE_TIFF_PIXELS = 256e6;
|
|
19
26
|
function isTiffMimeType(mimeType) {
|
|
20
27
|
const lower = mimeType.toLowerCase();
|
|
21
28
|
return lower === "image/tiff" || lower === "image/tif";
|
|
22
29
|
}
|
|
23
|
-
async function convertTiffToPngDataUrl(tiffData) {
|
|
30
|
+
async function convertTiffToPngDataUrl(tiffData, maxPixels = MAX_TIFF_PIXELS) {
|
|
24
31
|
if (typeof document === "undefined" || typeof document.createElement !== "function") return null;
|
|
25
32
|
try {
|
|
26
33
|
const UTIF = await import("utif2");
|
|
@@ -29,7 +36,7 @@ async function convertTiffToPngDataUrl(tiffData) {
|
|
|
29
36
|
const declaredWidth = readTiffTagNumber(firstImage["t256"]);
|
|
30
37
|
const declaredHeight = readTiffTagNumber(firstImage["t257"]);
|
|
31
38
|
if (!declaredWidth || !declaredHeight) return null;
|
|
32
|
-
if (declaredWidth * declaredHeight > MAX_TIFF_PIXELS) return null;
|
|
39
|
+
if (declaredWidth * declaredHeight > Math.min(maxPixels, MAX_TIFF_PIXELS)) return null;
|
|
33
40
|
UTIF.decodeImage(tiffData, firstImage);
|
|
34
41
|
const rgba = UTIF.toRGBA8(firstImage);
|
|
35
42
|
if (rgba.length === 0) return null;
|
|
@@ -50,7 +57,8 @@ async function convertTiffToPngDataUrl(tiffData) {
|
|
|
50
57
|
if (!data) return null;
|
|
51
58
|
return {
|
|
52
59
|
dataUrl,
|
|
53
|
-
data
|
|
60
|
+
data,
|
|
61
|
+
pixels: width * height
|
|
54
62
|
};
|
|
55
63
|
} catch {
|
|
56
64
|
return null;
|
|
@@ -80,4 +88,4 @@ function dataUrlToArrayBuffer(dataUrl) {
|
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
//#endregion
|
|
83
|
-
export { convertTiffToPngDataUrl, isTiffMimeType };
|
|
91
|
+
export { MAX_PACKAGE_TIFF_PIXELS, convertTiffToPngDataUrl, isTiffMimeType };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|