smartrte-core 0.2.1 → 1.0.0-beta.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/CHANGELOG.md +12 -0
- package/README.md +23 -0
- package/dist/command.d.ts +8 -8
- package/dist/editor.d.ts +32 -0
- package/dist/editor.js +121 -0
- package/dist/foundation/annotations/index.d.ts +2 -0
- package/dist/foundation/annotations/index.js +2 -0
- package/dist/foundation/annotations/range.d.ts +54 -0
- package/dist/foundation/annotations/range.js +158 -0
- package/dist/foundation/annotations/types.d.ts +33 -0
- package/dist/foundation/annotations/types.js +1 -0
- package/dist/foundation/atom/commands.d.ts +11 -0
- package/dist/foundation/atom/commands.js +118 -0
- package/dist/foundation/atom/composition.d.ts +8 -0
- package/dist/foundation/atom/composition.js +22 -0
- package/dist/foundation/atom/declarations.d.ts +2 -0
- package/dist/foundation/atom/declarations.js +17 -0
- package/dist/foundation/atom/formats.d.ts +21 -0
- package/dist/foundation/atom/formats.js +77 -0
- package/dist/foundation/atom/index.d.ts +10 -0
- package/dist/foundation/atom/index.js +10 -0
- package/dist/foundation/atom/lifecycle.d.ts +12 -0
- package/dist/foundation/atom/lifecycle.js +41 -0
- package/dist/foundation/atom/persistence.d.ts +3 -0
- package/dist/foundation/atom/persistence.js +20 -0
- package/dist/foundation/atom/plugin.d.ts +20 -0
- package/dist/foundation/atom/plugin.js +55 -0
- package/dist/foundation/atom/schema.d.ts +3 -0
- package/dist/foundation/atom/schema.js +29 -0
- package/dist/foundation/atom/security.d.ts +10 -0
- package/dist/foundation/atom/security.js +20 -0
- package/dist/foundation/atom/selection.d.ts +3 -0
- package/dist/foundation/atom/selection.js +4 -0
- package/dist/foundation/atom/types.d.ts +47 -0
- package/dist/foundation/atom/types.js +1 -0
- package/dist/foundation/block/commands.d.ts +17 -0
- package/dist/foundation/block/commands.js +197 -0
- package/dist/foundation/block/formats.d.ts +33 -0
- package/dist/foundation/block/formats.js +57 -0
- package/dist/foundation/block/index.d.ts +5 -0
- package/dist/foundation/block/index.js +5 -0
- package/dist/foundation/block/input.d.ts +19 -0
- package/dist/foundation/block/input.js +67 -0
- package/dist/foundation/block/plugin.d.ts +26 -0
- package/dist/foundation/block/plugin.js +79 -0
- package/dist/foundation/block/schema.d.ts +49 -0
- package/dist/foundation/block/schema.js +27 -0
- package/dist/foundation/block/types.d.ts +40 -0
- package/dist/foundation/block/types.js +1 -0
- package/dist/foundation/boundaries.d.ts +11 -0
- package/dist/foundation/boundaries.js +117 -0
- package/dist/foundation/clipboard/detection.d.ts +3 -0
- package/dist/foundation/clipboard/detection.js +29 -0
- package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
- package/dist/foundation/clipboard/diagnostics.js +59 -0
- package/dist/foundation/clipboard/index.d.ts +8 -0
- package/dist/foundation/clipboard/index.js +8 -0
- package/dist/foundation/clipboard/insertion.d.ts +22 -0
- package/dist/foundation/clipboard/insertion.js +210 -0
- package/dist/foundation/clipboard/normalizers.d.ts +8 -0
- package/dist/foundation/clipboard/normalizers.js +229 -0
- package/dist/foundation/clipboard/pipeline.d.ts +13 -0
- package/dist/foundation/clipboard/pipeline.js +58 -0
- package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
- package/dist/foundation/clipboard/sanitizer.js +58 -0
- package/dist/foundation/clipboard/serialization.d.ts +5 -0
- package/dist/foundation/clipboard/serialization.js +33 -0
- package/dist/foundation/clipboard/transfer.d.ts +11 -0
- package/dist/foundation/clipboard/transfer.js +108 -0
- package/dist/foundation/clipboard/types.d.ts +65 -0
- package/dist/foundation/clipboard/types.js +1 -0
- package/dist/foundation/collab/index.d.ts +3 -0
- package/dist/foundation/collab/index.js +3 -0
- package/dist/foundation/collab/rebase.d.ts +28 -0
- package/dist/foundation/collab/rebase.js +82 -0
- package/dist/foundation/collab/transform.d.ts +73 -0
- package/dist/foundation/collab/transform.js +251 -0
- package/dist/foundation/collab/types.d.ts +52 -0
- package/dist/foundation/collab/types.js +1 -0
- package/dist/foundation/comments/fromSelection.d.ts +16 -0
- package/dist/foundation/comments/fromSelection.js +24 -0
- package/dist/foundation/comments/index.d.ts +3 -0
- package/dist/foundation/comments/index.js +3 -0
- package/dist/foundation/comments/rebase.d.ts +31 -0
- package/dist/foundation/comments/rebase.js +43 -0
- package/dist/foundation/comments/types.d.ts +37 -0
- package/dist/foundation/comments/types.js +1 -0
- package/dist/foundation/diff/documentDiff.d.ts +12 -0
- package/dist/foundation/diff/documentDiff.js +148 -0
- package/dist/foundation/diff/index.d.ts +3 -0
- package/dist/foundation/diff/index.js +3 -0
- package/dist/foundation/diff/textDiff.d.ts +12 -0
- package/dist/foundation/diff/textDiff.js +114 -0
- package/dist/foundation/diff/types.d.ts +74 -0
- package/dist/foundation/diff/types.js +1 -0
- package/dist/foundation/editor.d.ts +138 -0
- package/dist/foundation/editor.js +398 -0
- package/dist/foundation/formats/codec.d.ts +45 -0
- package/dist/foundation/formats/codec.js +1 -0
- package/dist/foundation/formats/docx/export.d.ts +4 -0
- package/dist/foundation/formats/docx/export.js +285 -0
- package/dist/foundation/formats/docx/import.d.ts +9 -0
- package/dist/foundation/formats/docx/import.js +37 -0
- package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
- package/dist/foundation/formats/docx/nestingGuard.js +66 -0
- package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
- package/dist/foundation/formats/docx/portableAtoms.js +66 -0
- package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
- package/dist/foundation/formats/docx/styledImport.js +205 -0
- package/dist/foundation/formats/featureCodecs.d.ts +37 -0
- package/dist/foundation/formats/featureCodecs.js +69 -0
- package/dist/foundation/formats/fidelity.d.ts +31 -0
- package/dist/foundation/formats/fidelity.js +123 -0
- package/dist/foundation/formats/index.d.ts +7 -0
- package/dist/foundation/formats/index.js +7 -0
- package/dist/foundation/formats/pdf/format.d.ts +29 -0
- package/dist/foundation/formats/pdf/format.js +190 -0
- package/dist/foundation/history.d.ts +20 -0
- package/dist/foundation/history.js +121 -0
- package/dist/foundation/identity.d.ts +8 -0
- package/dist/foundation/identity.js +30 -0
- package/dist/foundation/index.d.ts +30 -0
- package/dist/foundation/index.js +30 -0
- package/dist/foundation/list/commands.d.ts +28 -0
- package/dist/foundation/list/commands.js +422 -0
- package/dist/foundation/list/formats.d.ts +20 -0
- package/dist/foundation/list/formats.js +795 -0
- package/dist/foundation/list/index.d.ts +8 -0
- package/dist/foundation/list/index.js +8 -0
- package/dist/foundation/list/input.d.ts +35 -0
- package/dist/foundation/list/input.js +320 -0
- package/dist/foundation/list/plugin.d.ts +21 -0
- package/dist/foundation/list/plugin.js +71 -0
- package/dist/foundation/list/presets.d.ts +16 -0
- package/dist/foundation/list/presets.js +21 -0
- package/dist/foundation/list/schema.d.ts +35 -0
- package/dist/foundation/list/schema.js +29 -0
- package/dist/foundation/list/shadow.d.ts +54 -0
- package/dist/foundation/list/shadow.js +75 -0
- package/dist/foundation/list/shadowCorpus.d.ts +10 -0
- package/dist/foundation/list/shadowCorpus.js +41 -0
- package/dist/foundation/list/types.d.ts +50 -0
- package/dist/foundation/list/types.js +1 -0
- package/dist/foundation/mapping.d.ts +11 -0
- package/dist/foundation/mapping.js +40 -0
- package/dist/foundation/marks/canonical.d.ts +9 -0
- package/dist/foundation/marks/canonical.js +72 -0
- package/dist/foundation/marks/commands.d.ts +21 -0
- package/dist/foundation/marks/commands.js +136 -0
- package/dist/foundation/marks/dom.d.ts +2 -0
- package/dist/foundation/marks/dom.js +46 -0
- package/dist/foundation/marks/editor.d.ts +8 -0
- package/dist/foundation/marks/editor.js +50 -0
- package/dist/foundation/marks/formats.d.ts +37 -0
- package/dist/foundation/marks/formats.js +49 -0
- package/dist/foundation/marks/hardBreak.d.ts +8 -0
- package/dist/foundation/marks/hardBreak.js +80 -0
- package/dist/foundation/marks/index.d.ts +11 -0
- package/dist/foundation/marks/index.js +11 -0
- package/dist/foundation/marks/normalization.d.ts +2 -0
- package/dist/foundation/marks/normalization.js +49 -0
- package/dist/foundation/marks/plugin.d.ts +14 -0
- package/dist/foundation/marks/plugin.js +63 -0
- package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
- package/dist/foundation/marks/resolveMarkRun.js +47 -0
- package/dist/foundation/marks/schema.d.ts +167 -0
- package/dist/foundation/marks/schema.js +50 -0
- package/dist/foundation/marks/stored.d.ts +2 -0
- package/dist/foundation/marks/stored.js +31 -0
- package/dist/foundation/marks/types.d.ts +40 -0
- package/dist/foundation/marks/types.js +1 -0
- package/dist/foundation/modelDom.d.ts +38 -0
- package/dist/foundation/modelDom.js +327 -0
- package/dist/foundation/normalization.d.ts +22 -0
- package/dist/foundation/normalization.js +132 -0
- package/dist/foundation/operations.d.ts +28 -0
- package/dist/foundation/operations.js +568 -0
- package/dist/foundation/plugin/builtins.d.ts +10 -0
- package/dist/foundation/plugin/builtins.js +27 -0
- package/dist/foundation/plugin/dispatch.d.ts +33 -0
- package/dist/foundation/plugin/dispatch.js +34 -0
- package/dist/foundation/plugin/index.d.ts +4 -0
- package/dist/foundation/plugin/index.js +4 -0
- package/dist/foundation/plugin/registry.d.ts +36 -0
- package/dist/foundation/plugin/registry.js +115 -0
- package/dist/foundation/plugin/types.d.ts +141 -0
- package/dist/foundation/plugin/types.js +13 -0
- package/dist/foundation/positions.d.ts +20 -0
- package/dist/foundation/positions.js +121 -0
- package/dist/foundation/schema.d.ts +57 -0
- package/dist/foundation/schema.js +371 -0
- package/dist/foundation/schemaBuilder.d.ts +41 -0
- package/dist/foundation/schemaBuilder.js +96 -0
- package/dist/foundation/scope/index.d.ts +2 -0
- package/dist/foundation/scope/index.js +2 -0
- package/dist/foundation/scope/resolveScope.d.ts +13 -0
- package/dist/foundation/scope/resolveScope.js +860 -0
- package/dist/foundation/scope/types.d.ts +122 -0
- package/dist/foundation/scope/types.js +1 -0
- package/dist/foundation/security/urlPolicy.d.ts +21 -0
- package/dist/foundation/security/urlPolicy.js +75 -0
- package/dist/foundation/structural/contentTarget.d.ts +21 -0
- package/dist/foundation/structural/contentTarget.js +51 -0
- package/dist/foundation/structural/index.d.ts +2 -0
- package/dist/foundation/structural/index.js +2 -0
- package/dist/foundation/structural/move.d.ts +7 -0
- package/dist/foundation/structural/move.js +35 -0
- package/dist/foundation/suggestions/index.d.ts +3 -0
- package/dist/foundation/suggestions/index.js +3 -0
- package/dist/foundation/suggestions/inline.d.ts +64 -0
- package/dist/foundation/suggestions/inline.js +116 -0
- package/dist/foundation/suggestions/structural.d.ts +40 -0
- package/dist/foundation/suggestions/structural.js +47 -0
- package/dist/foundation/suggestions/types.d.ts +19 -0
- package/dist/foundation/suggestions/types.js +1 -0
- package/dist/foundation/surface/index.d.ts +3 -0
- package/dist/foundation/surface/index.js +3 -0
- package/dist/foundation/surface/input.d.ts +127 -0
- package/dist/foundation/surface/input.js +1761 -0
- package/dist/foundation/surface/renderer.d.ts +84 -0
- package/dist/foundation/surface/renderer.js +974 -0
- package/dist/foundation/surface/types.d.ts +48 -0
- package/dist/foundation/surface/types.js +1 -0
- package/dist/foundation/table/commands.d.ts +31 -0
- package/dist/foundation/table/commands.js +470 -0
- package/dist/foundation/table/formats.d.ts +26 -0
- package/dist/foundation/table/formats.js +40 -0
- package/dist/foundation/table/grid.d.ts +8 -0
- package/dist/foundation/table/grid.js +178 -0
- package/dist/foundation/table/index.d.ts +6 -0
- package/dist/foundation/table/index.js +6 -0
- package/dist/foundation/table/plugin.d.ts +23 -0
- package/dist/foundation/table/plugin.js +119 -0
- package/dist/foundation/table/schema.d.ts +2 -0
- package/dist/foundation/table/schema.js +38 -0
- package/dist/foundation/table/selection.d.ts +7 -0
- package/dist/foundation/table/selection.js +35 -0
- package/dist/foundation/table/types.d.ts +109 -0
- package/dist/foundation/table/types.js +1 -0
- package/dist/foundation/transactions.d.ts +41 -0
- package/dist/foundation/transactions.js +90 -0
- package/dist/foundation/types.d.ts +261 -0
- package/dist/foundation/types.js +1 -0
- package/dist/foundation/versioning/diffVersions.d.ts +5 -0
- package/dist/foundation/versioning/diffVersions.js +3 -0
- package/dist/foundation/versioning/index.d.ts +2 -0
- package/dist/foundation/versioning/index.js +2 -0
- package/dist/foundation/versioning/types.d.ts +17 -0
- package/dist/foundation/versioning/types.js +1 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +165 -0
- package/dist/html/compatibility.js +7 -1
- package/dist/index.d.ts +1 -14
- package/dist/index.js +9 -14
- package/dist/legacy/index.d.ts +45 -0
- package/dist/legacy/index.js +45 -0
- package/dist/legacyCommands/alignment.d.ts +20 -0
- package/dist/{commands → legacyCommands}/alignment.js +17 -0
- package/dist/legacyCommands/blocks.d.ts +14 -0
- package/dist/legacyCommands/blocks.js +105 -0
- package/dist/legacyCommands/checklist.d.ts +11 -0
- package/dist/legacyCommands/checklist.js +45 -0
- package/dist/legacyCommands/code.d.ts +15 -0
- package/dist/{commands → legacyCommands}/code.js +25 -1
- package/dist/legacyCommands/formula.d.ts +7 -0
- package/dist/legacyCommands/formula.js +67 -0
- package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
- package/dist/legacyCommands/inlineAtoms.js +58 -0
- package/dist/legacyCommands/list.d.ts +38 -0
- package/dist/legacyCommands/list.js +477 -0
- package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
- package/dist/{commands → legacyCommands}/marks.js +28 -11
- package/dist/legacyCommands/media.d.ts +31 -0
- package/dist/legacyCommands/media.js +205 -0
- package/dist/legacyCommands/move.d.ts +14 -0
- package/dist/legacyCommands/move.js +91 -0
- package/dist/legacyCommands/table.d.ts +91 -0
- package/dist/legacyCommands/table.js +547 -0
- package/dist/listPresets.d.ts +12 -0
- package/dist/listPresets.js +6 -0
- package/dist/listScope.d.ts +27 -0
- package/dist/listScope.js +104 -0
- package/dist/markdown/compatibility.d.ts +2 -0
- package/dist/markdown/compatibility.js +94 -2
- package/dist/marks.d.ts +5 -5
- package/dist/model.d.ts +65 -9
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.js +31 -0
- package/dist/plugins/alignment.d.ts +3 -0
- package/dist/plugins/alignment.js +6 -0
- package/dist/plugins/basicFormatting.d.ts +9 -0
- package/dist/plugins/basicFormatting.js +24 -0
- package/dist/plugins/blockType.d.ts +3 -0
- package/dist/plugins/blockType.js +6 -0
- package/dist/plugins/blockquote.d.ts +3 -0
- package/dist/plugins/blockquote.js +6 -0
- package/dist/plugins/checklist.d.ts +3 -0
- package/dist/plugins/checklist.js +10 -0
- package/dist/plugins/codeBlock.d.ts +3 -0
- package/dist/plugins/codeBlock.js +6 -0
- package/dist/plugins/formula.d.ts +3 -0
- package/dist/plugins/formula.js +9 -0
- package/dist/plugins/list.d.ts +3 -0
- package/dist/plugins/list.js +10 -0
- package/dist/plugins/media.d.ts +3 -0
- package/dist/plugins/media.js +12 -0
- package/dist/plugins/move.d.ts +3 -0
- package/dist/plugins/move.js +9 -0
- package/dist/plugins/table.d.ts +3 -0
- package/dist/plugins/table.js +22 -0
- package/dist/preset.d.ts +12 -0
- package/dist/preset.js +40 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +333 -0
- package/dist/security/urlPolicy.d.ts +1 -17
- package/dist/security/urlPolicy.js +3 -69
- package/dist/selection.d.ts +1 -1
- package/dist/selectionMapping.d.ts +4 -0
- package/dist/selectionMapping.js +222 -0
- package/dist/table.d.ts +3 -3
- package/dist/transaction.d.ts +35 -12
- package/dist/transaction.js +174 -28
- package/dist/tree.d.ts +8 -2
- package/dist/tree.js +44 -0
- package/package.json +27 -8
- package/dist/commands/alignment.d.ts +0 -18
- package/dist/commands/code.d.ts +0 -13
- package/dist/commands/list.d.ts +0 -20
- package/dist/commands/list.js +0 -52
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import JSZip from "jszip";
|
|
2
|
+
import { isTextNode } from "../../identity.js";
|
|
3
|
+
import { occupancyGridFor } from "../../table/grid.js";
|
|
4
|
+
import { foundationListStyleForPresetDepth, isFoundationSmartListPreset } from "../../list/presets.js";
|
|
5
|
+
import { portableFormulaMarker, portableImageMarker } from "./portableAtoms.js";
|
|
6
|
+
export const DOCX_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
7
|
+
const xmlEscape = (value) => String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8
|
+
const colorValue = (value) => {
|
|
9
|
+
const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
10
|
+
if (hex)
|
|
11
|
+
return hex[1].toUpperCase();
|
|
12
|
+
const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value.trim());
|
|
13
|
+
return rgb
|
|
14
|
+
? rgb.slice(1, 4).map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0")).join("").toUpperCase()
|
|
15
|
+
: "";
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* table_cell's border attrs are free-form CSS border shorthand strings - the
|
|
19
|
+
* editor's own UI only ever writes "{width}px {style} {hex|rgb}" (style one
|
|
20
|
+
* of solid/dashed/dotted; see CanonicalAuthorityEditor.tsx's
|
|
21
|
+
* composeBorderShorthand), so that's the one shape parsed into real OOXML
|
|
22
|
+
* here; anything else (arbitrary pasted CSS) falls through to no override,
|
|
23
|
+
* same as before per-side support existed - not a regression, since nothing
|
|
24
|
+
* but the "none" sentinel was ever handled here originally. w:sz is eighths
|
|
25
|
+
* of a point; this codebase's own px->pt conversion elsewhere
|
|
26
|
+
* (columnWidths/row height * 15 for twips, i.e. 1px = 0.75pt) gives
|
|
27
|
+
* eighths = pt * 8 = px * 6.
|
|
28
|
+
*/
|
|
29
|
+
const DOCX_BORDER_STYLE = { solid: "single", dashed: "dashed", dotted: "dotted", double: "double" };
|
|
30
|
+
const parseCellBorderValue = (value) => {
|
|
31
|
+
if (typeof value !== "string")
|
|
32
|
+
return null;
|
|
33
|
+
const width = /(\d+(?:\.\d+)?)\s*px/.exec(value);
|
|
34
|
+
const color = /#[0-9a-f]{3,6}\b|rgba?\([^)]+\)/i.exec(value);
|
|
35
|
+
if (!width || !color)
|
|
36
|
+
return null;
|
|
37
|
+
const hex = colorValue(color[0]);
|
|
38
|
+
if (!hex)
|
|
39
|
+
return null;
|
|
40
|
+
const styleMatch = /\b(solid|dashed|dotted|double)\b/i.exec(value);
|
|
41
|
+
return { sz: Math.max(2, Math.round(Number(width[1]) * 6)), val: DOCX_BORDER_STYLE[styleMatch?.[1].toLowerCase() ?? "solid"] ?? "single", color: hex };
|
|
42
|
+
};
|
|
43
|
+
/** borderTop/Right/Bottom/Left (the border-options UI's per-side control) win over the uniform `borders` fallback for that one side, same precedence order the live renderer applies them in. */
|
|
44
|
+
const cellSideBorderXml = (name, sideValue, uniformValue) => {
|
|
45
|
+
const parsed = parseCellBorderValue(sideValue) ?? parseCellBorderValue(uniformValue);
|
|
46
|
+
return parsed ? `<w:${name} w:val="${parsed.val}" w:sz="${parsed.sz}" w:color="${parsed.color}"/>` : "";
|
|
47
|
+
};
|
|
48
|
+
const cellBorderXml = (attrs, borderNone) => {
|
|
49
|
+
if (attrs?.borders === "none")
|
|
50
|
+
return borderNone;
|
|
51
|
+
const xml = [
|
|
52
|
+
cellSideBorderXml("top", attrs?.borderTop, attrs?.borders),
|
|
53
|
+
cellSideBorderXml("left", attrs?.borderLeft, attrs?.borders),
|
|
54
|
+
cellSideBorderXml("bottom", attrs?.borderBottom, attrs?.borders),
|
|
55
|
+
cellSideBorderXml("right", attrs?.borderRight, attrs?.borders),
|
|
56
|
+
].join("");
|
|
57
|
+
return xml ? `<w:tcBorders>${xml}</w:tcBorders>` : "";
|
|
58
|
+
};
|
|
59
|
+
const runProperties = (marks = []) => marks.map((mark) => {
|
|
60
|
+
if (mark.type === "bold")
|
|
61
|
+
return "<w:b/>";
|
|
62
|
+
if (mark.type === "italic")
|
|
63
|
+
return "<w:i/>";
|
|
64
|
+
if (mark.type === "underline")
|
|
65
|
+
return '<w:u w:val="single"/>';
|
|
66
|
+
if (mark.type === "strike")
|
|
67
|
+
return "<w:strike/>";
|
|
68
|
+
if (mark.type === "superscript")
|
|
69
|
+
return '<w:vertAlign w:val="superscript"/>';
|
|
70
|
+
if (mark.type === "subscript")
|
|
71
|
+
return '<w:vertAlign w:val="subscript"/>';
|
|
72
|
+
if (mark.type === "textColor") {
|
|
73
|
+
const color = colorValue(String(mark.attrs?.value || ""));
|
|
74
|
+
return color ? `<w:color w:val="${color}"/>` : "";
|
|
75
|
+
}
|
|
76
|
+
if (mark.type === "backgroundColor") {
|
|
77
|
+
const color = colorValue(String(mark.attrs?.value || ""));
|
|
78
|
+
return color ? `<w:shd w:val="clear" w:color="auto" w:fill="${color}"/>` : "";
|
|
79
|
+
}
|
|
80
|
+
if (mark.type === "fontSize")
|
|
81
|
+
return `<w:sz w:val="${Math.max(2, Math.round(Number(mark.attrs?.valuePx || 0) * 1.5))}"/>`;
|
|
82
|
+
return "";
|
|
83
|
+
}).join("");
|
|
84
|
+
const textRun = (text, properties = "") => `<w:r>${properties ? `<w:rPr>${properties}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>`;
|
|
85
|
+
const createSerializationContext = () => ({
|
|
86
|
+
relationships: [], hyperlinkIds: new Map(), media: [], drawingId: 1,
|
|
87
|
+
});
|
|
88
|
+
const hyperlinkRelationshipId = (context, href) => {
|
|
89
|
+
const existing = context.hyperlinkIds.get(href);
|
|
90
|
+
if (existing)
|
|
91
|
+
return existing;
|
|
92
|
+
const id = `rId${context.relationships.length + 2}`;
|
|
93
|
+
context.hyperlinkIds.set(href, id);
|
|
94
|
+
context.relationships.push({ id, target: href, kind: "hyperlink" });
|
|
95
|
+
return id;
|
|
96
|
+
};
|
|
97
|
+
const embeddableImage = (src) => {
|
|
98
|
+
const match = /^data:(image\/(?:png|jpeg|gif));base64,([a-z0-9+/=\s]+)$/i.exec(src);
|
|
99
|
+
if (!match)
|
|
100
|
+
return;
|
|
101
|
+
return { mimeType: match[1].toLowerCase(), base64: match[2].replace(/\s/g, "") };
|
|
102
|
+
};
|
|
103
|
+
const imageRun = (node, context) => {
|
|
104
|
+
const src = String(node.attrs?.src || "");
|
|
105
|
+
const alt = node.attrs?.alt !== undefined ? String(node.attrs.alt) : undefined;
|
|
106
|
+
const title = node.attrs?.title !== undefined ? String(node.attrs.title) : undefined;
|
|
107
|
+
const image = embeddableImage(src);
|
|
108
|
+
if (!image)
|
|
109
|
+
return textRun(portableImageMarker(src, alt, title));
|
|
110
|
+
const extension = image.mimeType === "image/jpeg" ? "jpg" : image.mimeType.split("/")[1];
|
|
111
|
+
const filename = `image${context.media.length + 1}.${extension}`;
|
|
112
|
+
const relationshipId = `rId${context.relationships.length + 2}`;
|
|
113
|
+
const drawingId = context.drawingId++;
|
|
114
|
+
context.media.push({ filename, ...image });
|
|
115
|
+
context.relationships.push({ id: relationshipId, target: `media/${filename}`, kind: "image" });
|
|
116
|
+
const widthEmu = Math.max(1, Math.round(Number(node.attrs?.width || 96) * 9525));
|
|
117
|
+
const heightEmu = Math.max(1, Math.round(Number(node.attrs?.height || 96) * 9525));
|
|
118
|
+
const name = xmlEscape(title || alt || filename);
|
|
119
|
+
const description = xmlEscape(alt || "");
|
|
120
|
+
return `<w:r><w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${widthEmu}" cy="${heightEmu}"/><wp:docPr id="${drawingId}" name="${name}" descr="${description}"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="0" name="${name}" descr="${description}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relationshipId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
|
|
121
|
+
};
|
|
122
|
+
const formulaRun = (node) => {
|
|
123
|
+
const source = xmlEscape(String(node.attrs?.source || ""));
|
|
124
|
+
const displayText = node.attrs?.displayText !== undefined ? String(node.attrs.displayText) : undefined;
|
|
125
|
+
const marker = xmlEscape(portableFormulaMarker(String(node.attrs?.source || ""), displayText));
|
|
126
|
+
return `<m:oMath><m:r><m:t>${source}</m:t></m:r></m:oMath><w:r><w:rPr><w:vanish/></w:rPr><w:t>${marker}</w:t></w:r>`;
|
|
127
|
+
};
|
|
128
|
+
/** Fidelity: media type "file" (a legacy concept) has no canonical atom equivalent and is skipped. */
|
|
129
|
+
const inlineRun = (node, context) => {
|
|
130
|
+
if (isTextNode(node)) {
|
|
131
|
+
const properties = runProperties(node.marks);
|
|
132
|
+
const runs = node.text.split("\n").map((part, index) => `${index ? "<w:r><w:br/></w:r>" : ""}${part ? textRun(part, properties) : ""}`).join("");
|
|
133
|
+
const link = node.marks?.find((mark) => mark.type === "link");
|
|
134
|
+
if (!link || !runs)
|
|
135
|
+
return runs;
|
|
136
|
+
return `<w:hyperlink r:id="${hyperlinkRelationshipId(context, String(link.attrs?.href || ""))}"${link.attrs?.target ? ' w:history="1"' : ""}>${runs}</w:hyperlink>`;
|
|
137
|
+
}
|
|
138
|
+
if (node.type === "hard_break")
|
|
139
|
+
return "<w:r><w:br/></w:r>";
|
|
140
|
+
if (node.type === "formula")
|
|
141
|
+
return formulaRun(node);
|
|
142
|
+
if (node.type === "image")
|
|
143
|
+
return imageRun(node, context);
|
|
144
|
+
return "";
|
|
145
|
+
};
|
|
146
|
+
const alignmentOf = (node) => typeof node.attrs?.align === "string" ? node.attrs.align : undefined;
|
|
147
|
+
const indentLevelOf = (node) => Number(node.attrs?.indentLevel) || 0;
|
|
148
|
+
const paragraphProperties = (block, extra = "") => [
|
|
149
|
+
alignmentOf(block) ? `<w:jc w:val="${xmlEscape(alignmentOf(block))}"/>` : "",
|
|
150
|
+
indentLevelOf(block) ? `<w:ind w:left="${indentLevelOf(block) * 480}"/>` : "",
|
|
151
|
+
extra,
|
|
152
|
+
].join("");
|
|
153
|
+
const paragraphXml = (children, block, context, extraProperties = "") => {
|
|
154
|
+
const properties = paragraphProperties(block, extraProperties);
|
|
155
|
+
const content = children.map((node) => inlineRun(node, context)).join("") || textRun("");
|
|
156
|
+
return `<w:p>${properties ? `<w:pPr>${properties}</w:pPr>` : ""}${content}</w:p>`;
|
|
157
|
+
};
|
|
158
|
+
const listStyleNumId = {
|
|
159
|
+
disc: 1, circle: 2, square: 3, decimal: 4,
|
|
160
|
+
"decimal-leading-zero": 4,
|
|
161
|
+
"lower-alpha": 5, "upper-alpha": 6, "lower-roman": 7, "upper-roman": 8,
|
|
162
|
+
};
|
|
163
|
+
const orderedList = (node) => {
|
|
164
|
+
const marker = String(node.attrs?.style || node.attrs?.preset || "");
|
|
165
|
+
return /^(?:decimal|lower-|upper-|ordered)/.test(marker) || node.attrs?.start !== undefined;
|
|
166
|
+
};
|
|
167
|
+
const effectiveStyle = (node, depth) => {
|
|
168
|
+
if (typeof node.attrs?.style === "string")
|
|
169
|
+
return node.attrs.style;
|
|
170
|
+
if (isFoundationSmartListPreset(node.attrs?.preset))
|
|
171
|
+
return foundationListStyleForPresetDepth(node.attrs.preset, depth);
|
|
172
|
+
return "disc";
|
|
173
|
+
};
|
|
174
|
+
const blockChildren = (node) => (node.children || []);
|
|
175
|
+
const elementChildren = (node) => blockChildren(node).filter((child) => !isTextNode(child));
|
|
176
|
+
const tableXml = (block, context) => {
|
|
177
|
+
const grid = occupancyGridFor(block);
|
|
178
|
+
const widths = Array.from({ length: grid.columns }, (_, index) => Number(block.attrs?.columnWidths?.[index]) || 96);
|
|
179
|
+
const gridXml = `<w:tblGrid>${widths.map((widthPx) => `<w:gridCol w:w="${Math.max(1, Math.round(widthPx * 15))}"/>`).join("")}</w:tblGrid>`;
|
|
180
|
+
const borderNone = '<w:tcBorders><w:top w:val="nil"/><w:left w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/></w:tcBorders>';
|
|
181
|
+
const rows = elementChildren(block).map((row, rowIndex) => {
|
|
182
|
+
const header = Array.from({ length: grid.columns }, (_, column) => grid.at(rowIndex, column)?.node.attrs?.header === true).every(Boolean);
|
|
183
|
+
const rowProperties = [
|
|
184
|
+
header ? "<w:tblHeader/>" : "",
|
|
185
|
+
Number(row.attrs?.height) > 0 ? `<w:trHeight w:val="${Math.round(Number(row.attrs?.height) * 15)}" w:hRule="atLeast"/>` : "",
|
|
186
|
+
].join("");
|
|
187
|
+
let cells = "";
|
|
188
|
+
for (let column = 0; column < grid.columns;) {
|
|
189
|
+
const cell = grid.at(rowIndex, column);
|
|
190
|
+
if (!cell || cell.left !== column) {
|
|
191
|
+
column += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const rowspan = cell.bottom - cell.top;
|
|
195
|
+
const colspan = cell.right - cell.left;
|
|
196
|
+
const continuation = cell.top < rowIndex;
|
|
197
|
+
const shade = colorValue(String(cell.node.attrs?.background || ""));
|
|
198
|
+
const properties = [
|
|
199
|
+
colspan > 1 ? `<w:gridSpan w:val="${colspan}"/>` : "",
|
|
200
|
+
rowspan > 1 ? (continuation ? "<w:vMerge/>" : '<w:vMerge w:val="restart"/>') : "",
|
|
201
|
+
shade ? `<w:shd w:val="clear" w:color="auto" w:fill="${shade}"/>` : "",
|
|
202
|
+
cellBorderXml(cell.node.attrs, borderNone),
|
|
203
|
+
].join("");
|
|
204
|
+
const content = continuation ? "<w:p/>" : elementChildren(cell.node).map((child) => blockXml(child, context)).join("") || "<w:p/>";
|
|
205
|
+
cells += `<w:tc><w:tcPr>${properties}</w:tcPr>${content}</w:tc>`;
|
|
206
|
+
column += colspan;
|
|
207
|
+
}
|
|
208
|
+
return `<w:tr>${rowProperties ? `<w:trPr>${rowProperties}</w:trPr>` : ""}${cells}</w:tr>`;
|
|
209
|
+
}).join("");
|
|
210
|
+
return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideH w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideV w:val="single" w:sz="4" w:color="D1D5DB"/></w:tblBorders></w:tblPr>${gridXml}${rows}</w:tbl>`;
|
|
211
|
+
};
|
|
212
|
+
/** Fidelity: DOCX export is `semantic` for structure; formulas fall back to a portable text marker (`lossy`), matching Phase 7's explicit decision not to build partial OMML. */
|
|
213
|
+
const blockXml = (block, context, listLevel = 0) => {
|
|
214
|
+
if (block.type === "paragraph")
|
|
215
|
+
return paragraphXml(blockChildren(block), block, context);
|
|
216
|
+
if (block.type === "heading")
|
|
217
|
+
return paragraphXml(blockChildren(block), block, context, `<w:pStyle w:val="Heading${Number(block.attrs?.level) || 1}"/>`);
|
|
218
|
+
if (block.type === "code_block") {
|
|
219
|
+
const text = blockChildren(block).map((child) => isTextNode(child) ? child.text : child.type === "hard_break" ? "\n" : "").join("");
|
|
220
|
+
return paragraphXml([{ type: "text", text, marks: [{ type: "code" }] }], block, context);
|
|
221
|
+
}
|
|
222
|
+
if (block.type === "blockquote") {
|
|
223
|
+
return elementChildren(block).map((child) => child.type === "paragraph"
|
|
224
|
+
? paragraphXml(blockChildren(child), child, context, '<w:ind w:left="720"/>')
|
|
225
|
+
: blockXml(child, context, listLevel)).join("");
|
|
226
|
+
}
|
|
227
|
+
if (block.type === "list") {
|
|
228
|
+
const style = effectiveStyle(block, listLevel);
|
|
229
|
+
return elementChildren(block).flatMap((item) => elementChildren(item).map((child) => child.type === "paragraph"
|
|
230
|
+
? paragraphXml(blockChildren(child), child, context, `<w:numPr><w:ilvl w:val="${Math.min(listLevel, 8)}"/><w:numId w:val="${listStyleNumId[style]}"/></w:numPr>`)
|
|
231
|
+
: blockXml(child, context, listLevel + 1))).join("");
|
|
232
|
+
}
|
|
233
|
+
if (block.type === "table")
|
|
234
|
+
return tableXml(block, context);
|
|
235
|
+
if (block.type === "block_image")
|
|
236
|
+
return paragraphXml([{ ...block, type: "image" }], block, context);
|
|
237
|
+
if (block.type === "block_formula")
|
|
238
|
+
return paragraphXml([{ ...block, type: "formula" }], block, context);
|
|
239
|
+
return paragraphXml([{ type: "text", text: String(block.attrs?.title || block.attrs?.src || "") }], block, context);
|
|
240
|
+
};
|
|
241
|
+
const numberingLevelXml = (level, format, text, font) => `<w:lvl w:ilvl="${level}"><w:start w:val="1"/><w:numFmt w:val="${format}"/><w:lvlText w:val="${text}"/><w:lvlJc w:val="left"/><w:pPr><w:tabs><w:tab w:val="num" w:pos="${720 + level * 360}"/></w:tabs><w:ind w:left="${720 + level * 360}" w:hanging="360"/></w:pPr>${font ? `<w:rPr><w:rFonts w:ascii="${font}" w:hAnsi="${font}"/></w:rPr>` : ""}</w:lvl>`;
|
|
242
|
+
const numberingXml = () => {
|
|
243
|
+
const definitions = [
|
|
244
|
+
{ id: 1, format: "bullet", text: "•", font: "Symbol" },
|
|
245
|
+
{ id: 2, format: "bullet", text: "○", font: "Arial" },
|
|
246
|
+
{ id: 3, format: "bullet", text: "▪", font: "Arial" },
|
|
247
|
+
{ id: 4, format: "decimal", text: "%1." },
|
|
248
|
+
{ id: 5, format: "lowerLetter", text: "%1." },
|
|
249
|
+
{ id: 6, format: "upperLetter", text: "%1." },
|
|
250
|
+
{ id: 7, format: "lowerRoman", text: "%1." },
|
|
251
|
+
{ id: 8, format: "upperRoman", text: "%1." },
|
|
252
|
+
];
|
|
253
|
+
const abstracts = definitions.map(({ id, format, text, font }) => `<w:abstractNum w:abstractNumId="${id}"><w:multiLevelType w:val="multilevel"/>${Array.from({ length: 9 }, (_, level) => numberingLevelXml(level, format, text.replace("%1", `%${level + 1}`), font)).join("")}</w:abstractNum>`).join("");
|
|
254
|
+
const instances = definitions.map(({ id }) => `<w:num w:numId="${id}"><w:abstractNumId w:val="${id}"/></w:num>`).join("");
|
|
255
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">${abstracts}${instances}</w:numbering>`;
|
|
256
|
+
};
|
|
257
|
+
const serializeDocxDocument = (document) => {
|
|
258
|
+
const context = createSerializationContext();
|
|
259
|
+
const body = elementChildren(document).map((block) => blockXml(block, context)).join("");
|
|
260
|
+
return {
|
|
261
|
+
documentXml: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><w:body>${body}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/></w:sectPr></w:body></w:document>`,
|
|
262
|
+
relationships: context.relationships,
|
|
263
|
+
media: context.media,
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
export const smartDocumentToDocxXml = (document) => serializeDocxDocument(document).documentXml;
|
|
267
|
+
export const exportDocxDocument = async (document) => {
|
|
268
|
+
const zip = new JSZip();
|
|
269
|
+
const serialized = serializeDocxDocument(document);
|
|
270
|
+
const imageTypes = [...new Set(serialized.media.map(({ filename, mimeType }) => JSON.stringify({
|
|
271
|
+
extension: filename.split(".").pop(), mimeType,
|
|
272
|
+
})))].map((entry) => JSON.parse(entry));
|
|
273
|
+
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${imageTypes.map(({ extension, mimeType }) => `<Default Extension="${extension}" ContentType="${mimeType}"/>`).join("")}<Override PartName="/word/document.xml" ContentType="${DOCX_MEDIA_TYPE}.main+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>`);
|
|
274
|
+
zip.folder("_rels")?.file(".rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`);
|
|
275
|
+
zip.folder("word")?.file("document.xml", serialized.documentXml);
|
|
276
|
+
zip.folder("word")?.file("numbering.xml", numberingXml());
|
|
277
|
+
serialized.media.forEach(({ filename, base64 }) => {
|
|
278
|
+
zip.folder("word")?.folder("media")?.file(filename, base64, { base64: true });
|
|
279
|
+
});
|
|
280
|
+
const relationships = serialized.relationships.map((relationship) => relationship.kind === "hyperlink"
|
|
281
|
+
? `<Relationship Id="${relationship.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${xmlEscape(relationship.target)}" TargetMode="External"/>`
|
|
282
|
+
: `<Relationship Id="${relationship.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="${relationship.target}"/>`).join("");
|
|
283
|
+
zip.folder("word")?.folder("_rels")?.file("document.xml.rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>${relationships}</Relationships>`);
|
|
284
|
+
return zip.generateAsync({ type: "blob", mimeType: DOCX_MEDIA_TYPE });
|
|
285
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SmartDocument } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fidelity: `semantic` for structure. Matches the established pattern for
|
|
4
|
+
* plain HTML import (CanonicalAuthorityEditor's runImport) - mammoth already
|
|
5
|
+
* produces clean semantic HTML, so no compatibility/normalize pass is
|
|
6
|
+
* inserted here; parseCanonicalListHtml's own fallback ("unknown" node with
|
|
7
|
+
* raw HTML) handles anything it doesn't recognize.
|
|
8
|
+
*/
|
|
9
|
+
export declare const importDocxDocumentWithMammoth: (arrayBuffer: ArrayBuffer) => Promise<SmartDocument>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import JSZip from "jszip";
|
|
2
|
+
import mammoth from "mammoth";
|
|
3
|
+
import { parseCanonicalListHtml } from "../../list/formats.js";
|
|
4
|
+
import { restorePortableDocxAtoms } from "./portableAtoms.js";
|
|
5
|
+
import { assertDocxXmlWithinDepthLimit } from "./nestingGuard.js";
|
|
6
|
+
/**
|
|
7
|
+
* Fidelity: `semantic` for structure. Matches the established pattern for
|
|
8
|
+
* plain HTML import (CanonicalAuthorityEditor's runImport) - mammoth already
|
|
9
|
+
* produces clean semantic HTML, so no compatibility/normalize pass is
|
|
10
|
+
* inserted here; parseCanonicalListHtml's own fallback ("unknown" node with
|
|
11
|
+
* raw HTML) handles anything it doesn't recognize.
|
|
12
|
+
*/
|
|
13
|
+
export const importDocxDocumentWithMammoth = async (arrayBuffer) => {
|
|
14
|
+
// Defense-in-depth against pathologically deep XML - see nestingGuard.ts's
|
|
15
|
+
// own doc comment for why this exists alongside (not instead of) the real
|
|
16
|
+
// fixes in mammoth's own patched code. Checked against document.xml
|
|
17
|
+
// specifically: the primary, always-present part every .docx has, and the
|
|
18
|
+
// one the disclosed attack targets. A package that fails to unzip or has
|
|
19
|
+
// no document.xml is left for mammoth's own error handling below, not
|
|
20
|
+
// rejected here - this guard only rejects on a confirmed-deep document.
|
|
21
|
+
try {
|
|
22
|
+
const zip = await JSZip.loadAsync(arrayBuffer);
|
|
23
|
+
const documentXml = await zip.file("word/document.xml")?.async("text");
|
|
24
|
+
if (documentXml)
|
|
25
|
+
assertDocxXmlWithinDepthLimit(documentXml);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && error.name === "DocxNestingTooDeepError")
|
|
29
|
+
throw error;
|
|
30
|
+
// Any other failure here (corrupt zip, etc.) is mammoth's to report.
|
|
31
|
+
}
|
|
32
|
+
// Mammoth's browser entry reads `arrayBuffer`, while its Node entry reads
|
|
33
|
+
// `buffer`. Supplying both keeps the adapter portable across bundlers/tests.
|
|
34
|
+
const result = await mammoth.convertToHtml({ arrayBuffer, buffer: arrayBuffer });
|
|
35
|
+
const html = restorePortableDocxAtoms(result.value);
|
|
36
|
+
return parseCanonicalListHtml(html);
|
|
37
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defense-in-depth against pathologically deep XML inside a `.docx` package.
|
|
3
|
+
*
|
|
4
|
+
* Not the real fix for any specific recursion bug - see
|
|
5
|
+
* docs/bugs/mammoth-own-reader-unguarded-recursion-dos.md and
|
|
6
|
+
* docs/bugs/mammoth-bundles-vulnerable-transitive-xmldom.md for those (an
|
|
7
|
+
* iterative rewrite of mammoth's own XML-to-node conversion, and a pinned/
|
|
8
|
+
* patched @xmldom/xmldom). This exists because verifying those two fixes
|
|
9
|
+
* against the real attack surfaced a *third*, independent unguarded-recursion
|
|
10
|
+
* function elsewhere in mammoth's own code (`office-xml-reader.js`'s
|
|
11
|
+
* `collapseAlternateContent`, out of scope to patch here) - a pattern of
|
|
12
|
+
* multiple such functions, not one isolated bug. A cheap, iterative (never
|
|
13
|
+
* recursive - this guard must not become another instance of the same class
|
|
14
|
+
* of bug) upfront depth check protects against that specific function and
|
|
15
|
+
* any other not-yet-discovered one, without waiting on an exhaustive audit
|
|
16
|
+
* of mammoth's entire codebase.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately coarse, not a real XML parser: a linear scan counting
|
|
19
|
+
* tag-open/tag-close balance. Does not correctly account for `<`/`>`
|
|
20
|
+
* characters inside comments, CDATA sections, or attribute values - for a
|
|
21
|
+
* generously-set threshold, that only risks *under*-counting depth (a
|
|
22
|
+
* false negative, i.e. missing a genuinely deep document), never rejecting
|
|
23
|
+
* a shallow legitimate one, so it stays a safe, one-directional guard.
|
|
24
|
+
*/
|
|
25
|
+
/** Real Word documents essentially never nest structurally beyond a few
|
|
26
|
+
* dozen levels even in pathological cases (tables nested in tables nested
|
|
27
|
+
* in tracked-change wrappers nested in content controls). The disclosed
|
|
28
|
+
* @xmldom/xmldom recursion advisory measured crash thresholds starting
|
|
29
|
+
* around 5,000 levels for the lightest-per-frame operations tested. 1,000
|
|
30
|
+
* sits an order of magnitude above any plausible real document and a
|
|
31
|
+
* comfortable margin below the lowest crash threshold observed for any
|
|
32
|
+
* function measured so far, leaving room for an undiscovered function with
|
|
33
|
+
* heavier per-frame stack usage than anything tested to date. */
|
|
34
|
+
export declare const MAX_DOCX_XML_NESTING_DEPTH = 1000;
|
|
35
|
+
export declare class DocxNestingTooDeepError extends Error {
|
|
36
|
+
constructor(depth: number, max: number);
|
|
37
|
+
}
|
|
38
|
+
/** Returns the deepest tag-nesting level found, without recursing. */
|
|
39
|
+
export declare const maxXmlNestingDepth: (xml: string) => number;
|
|
40
|
+
export declare const assertDocxXmlWithinDepthLimit: (xml: string, max?: number) => void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defense-in-depth against pathologically deep XML inside a `.docx` package.
|
|
3
|
+
*
|
|
4
|
+
* Not the real fix for any specific recursion bug - see
|
|
5
|
+
* docs/bugs/mammoth-own-reader-unguarded-recursion-dos.md and
|
|
6
|
+
* docs/bugs/mammoth-bundles-vulnerable-transitive-xmldom.md for those (an
|
|
7
|
+
* iterative rewrite of mammoth's own XML-to-node conversion, and a pinned/
|
|
8
|
+
* patched @xmldom/xmldom). This exists because verifying those two fixes
|
|
9
|
+
* against the real attack surfaced a *third*, independent unguarded-recursion
|
|
10
|
+
* function elsewhere in mammoth's own code (`office-xml-reader.js`'s
|
|
11
|
+
* `collapseAlternateContent`, out of scope to patch here) - a pattern of
|
|
12
|
+
* multiple such functions, not one isolated bug. A cheap, iterative (never
|
|
13
|
+
* recursive - this guard must not become another instance of the same class
|
|
14
|
+
* of bug) upfront depth check protects against that specific function and
|
|
15
|
+
* any other not-yet-discovered one, without waiting on an exhaustive audit
|
|
16
|
+
* of mammoth's entire codebase.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately coarse, not a real XML parser: a linear scan counting
|
|
19
|
+
* tag-open/tag-close balance. Does not correctly account for `<`/`>`
|
|
20
|
+
* characters inside comments, CDATA sections, or attribute values - for a
|
|
21
|
+
* generously-set threshold, that only risks *under*-counting depth (a
|
|
22
|
+
* false negative, i.e. missing a genuinely deep document), never rejecting
|
|
23
|
+
* a shallow legitimate one, so it stays a safe, one-directional guard.
|
|
24
|
+
*/
|
|
25
|
+
/** Real Word documents essentially never nest structurally beyond a few
|
|
26
|
+
* dozen levels even in pathological cases (tables nested in tables nested
|
|
27
|
+
* in tracked-change wrappers nested in content controls). The disclosed
|
|
28
|
+
* @xmldom/xmldom recursion advisory measured crash thresholds starting
|
|
29
|
+
* around 5,000 levels for the lightest-per-frame operations tested. 1,000
|
|
30
|
+
* sits an order of magnitude above any plausible real document and a
|
|
31
|
+
* comfortable margin below the lowest crash threshold observed for any
|
|
32
|
+
* function measured so far, leaving room for an undiscovered function with
|
|
33
|
+
* heavier per-frame stack usage than anything tested to date. */
|
|
34
|
+
export const MAX_DOCX_XML_NESTING_DEPTH = 1000;
|
|
35
|
+
export class DocxNestingTooDeepError extends Error {
|
|
36
|
+
constructor(depth, max) {
|
|
37
|
+
super(`DOCX document.xml nests ${depth} levels deep, exceeding the ${max}-level safety limit.`);
|
|
38
|
+
this.name = "DocxNestingTooDeepError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const TAG_PATTERN = /<(\/?)([^\s/>!?][^>]*?)(\/?)>/g;
|
|
42
|
+
/** Returns the deepest tag-nesting level found, without recursing. */
|
|
43
|
+
export const maxXmlNestingDepth = (xml) => {
|
|
44
|
+
let depth = 0;
|
|
45
|
+
let peak = 0;
|
|
46
|
+
TAG_PATTERN.lastIndex = 0;
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = TAG_PATTERN.exec(xml))) {
|
|
49
|
+
const isClosing = match[1] === "/";
|
|
50
|
+
const isSelfClosing = match[3] === "/";
|
|
51
|
+
if (isClosing) {
|
|
52
|
+
depth = Math.max(0, depth - 1);
|
|
53
|
+
}
|
|
54
|
+
else if (!isSelfClosing) {
|
|
55
|
+
depth += 1;
|
|
56
|
+
if (depth > peak)
|
|
57
|
+
peak = depth;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return peak;
|
|
61
|
+
};
|
|
62
|
+
export const assertDocxXmlWithinDepthLimit = (xml, max = MAX_DOCX_XML_NESTING_DEPTH) => {
|
|
63
|
+
const depth = maxXmlNestingDepth(xml);
|
|
64
|
+
if (depth > max)
|
|
65
|
+
throw new DocxNestingTooDeepError(depth, max);
|
|
66
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const portableFormulaMarker: (value: string, displayText?: string) => string;
|
|
2
|
+
export declare const portableImageMarker: (src: string, alt?: string, title?: string) => string;
|
|
3
|
+
/** Restores Smart RTE atom fallback markers retained by DOCX text runs. */
|
|
4
|
+
export declare const restorePortableDocxAtoms: (html: string) => string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseFragment, serialize } from "parse5";
|
|
2
|
+
const marker = /⟦SRTE_(FORMULA|IMAGE):([^⟧]+)⟧/g;
|
|
3
|
+
export const portableFormulaMarker = (value, displayText) => `⟦SRTE_FORMULA:${encodeURIComponent(JSON.stringify({ value, displayText }))}⟧`;
|
|
4
|
+
export const portableImageMarker = (src, alt, title) => `⟦SRTE_IMAGE:${encodeURIComponent(JSON.stringify({ src, alt, title }))}⟧`;
|
|
5
|
+
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
6
|
+
const textNode = (value) => ({ nodeName: "#text", value });
|
|
7
|
+
const elementNode = (tagName, attrs, childNodes = []) => ({
|
|
8
|
+
nodeName: tagName, tagName, namespaceURI: XHTML_NS, attrs, childNodes,
|
|
9
|
+
});
|
|
10
|
+
const splitPortableAtoms = (text) => {
|
|
11
|
+
marker.lastIndex = 0;
|
|
12
|
+
if (!marker.test(text))
|
|
13
|
+
return [textNode(text)];
|
|
14
|
+
marker.lastIndex = 0;
|
|
15
|
+
const output = [];
|
|
16
|
+
let cursor = 0;
|
|
17
|
+
let match;
|
|
18
|
+
while ((match = marker.exec(text))) {
|
|
19
|
+
if (match.index > cursor)
|
|
20
|
+
output.push(textNode(text.slice(cursor, match.index)));
|
|
21
|
+
try {
|
|
22
|
+
const payload = JSON.parse(decodeURIComponent(match[2]));
|
|
23
|
+
if (match[1] === "FORMULA" && payload.value) {
|
|
24
|
+
// Fidelity note: canonical's own native HTML round-trip never
|
|
25
|
+
// preserves formula displayText either (atomToHtml doesn't write
|
|
26
|
+
// it, the parser doesn't read it back) - only source/notation
|
|
27
|
+
// survive. Not a DOCX-specific gap.
|
|
28
|
+
output.push(elementNode("span", [
|
|
29
|
+
{ name: "data-smart-type", value: "formula" },
|
|
30
|
+
{ name: "data-smart-formula", value: payload.value },
|
|
31
|
+
], [textNode(payload.displayText || `$${payload.value}$`)]));
|
|
32
|
+
}
|
|
33
|
+
else if (match[1] === "IMAGE" && payload.src) {
|
|
34
|
+
const attrs = [
|
|
35
|
+
{ name: "src", value: payload.src },
|
|
36
|
+
{ name: "alt", value: payload.alt || "" },
|
|
37
|
+
{ name: "data-srte-inline", value: "true" },
|
|
38
|
+
];
|
|
39
|
+
if (payload.title)
|
|
40
|
+
attrs.push({ name: "title", value: payload.title });
|
|
41
|
+
output.push(elementNode("img", attrs));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
output.push(textNode(match[0]));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
output.push(textNode(match[0]));
|
|
49
|
+
}
|
|
50
|
+
cursor = marker.lastIndex;
|
|
51
|
+
}
|
|
52
|
+
if (cursor < text.length)
|
|
53
|
+
output.push(textNode(text.slice(cursor)));
|
|
54
|
+
return output;
|
|
55
|
+
};
|
|
56
|
+
const visit = (node) => {
|
|
57
|
+
if (!node.childNodes)
|
|
58
|
+
return node;
|
|
59
|
+
const childNodes = node.childNodes.flatMap((child) => child.nodeName === "#text" && child.value ? splitPortableAtoms(child.value) : [visit(child)]);
|
|
60
|
+
return { ...node, childNodes };
|
|
61
|
+
};
|
|
62
|
+
/** Restores Smart RTE atom fallback markers retained by DOCX text runs. */
|
|
63
|
+
export const restorePortableDocxAtoms = (html) => {
|
|
64
|
+
const fragment = visit(parseFragment(html));
|
|
65
|
+
return serialize(fragment);
|
|
66
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SmartDocument } from "../../types.js";
|
|
2
|
+
export interface StyledDocxImportResult {
|
|
3
|
+
document: SmartDocument;
|
|
4
|
+
layoutHtml: string;
|
|
5
|
+
source: "wordprocessingml" | "mammoth";
|
|
6
|
+
}
|
|
7
|
+
/** Defensive baseline styling for any table missing explicit inline styles (e.g. the mammoth fallback path, which produces plain semantic HTML with no inline CSS at all). */
|
|
8
|
+
export declare const enhanceDocxTables: (html: string) => string;
|
|
9
|
+
/** Fidelity: `semantic` for structure, richer visual reproduction than plain mammoth import via inline CSS reconstructed from raw wordprocessingml formatting XML, with graceful fallback to mammoth if that XML is missing or malformed. */
|
|
10
|
+
export declare const importStyledDocxDocument: (arrayBuffer: ArrayBuffer) => Promise<StyledDocxImportResult>;
|