Moving it out of the build left it with no way in. Settings can only install from the community catalogue, so a first-party extension that isn't bundled has a working update channel and no first copy for anyone to update — the mechanism was all there and the front door was missing. So it goes back beside screenshot, aegis and pdf-editor: seeded into every profile by the build, listed under "Built into Theseus", and kept current between releases by the operator-signed channel at theseus.x/extensions/docx-editor/. That is the arrangement docs/ADDON-UPDATES.md describes, and the one the signing script was written for. About 400 KB compressed in the installer, most of it the vendored editor libraries — next to the ~4 MB of pdf.js that pdf-editor already ships, the weight argument for keeping it out didn't survive contact with the numbers. The end-to-end driver goes back to checking that a fresh profile seeds it, which is the property that actually matters now.
576 lines
21 KiB
JavaScript
576 lines
21 KiB
JavaScript
// Editor model -> .docx.
|
|
//
|
|
// The docx builder gives us paragraphs, runs, tables, numbering and images;
|
|
// what it can't do is merge into an existing package, so this module builds a
|
|
// complete new document and then hands it to pkg.graft(), which carries the
|
|
// original's headers, footers, notes, styles and theme across.
|
|
//
|
|
// Mapping notes, because the two models disagree in places:
|
|
//
|
|
// * Marks are per-text-node in ProseMirror and per-run in Word, which is a
|
|
// clean fit: one text node with its mark set becomes one TextRun.
|
|
// * A ProseMirror list is a tree; Word's is a flat run of paragraphs each
|
|
// tagged with a numbering id and a level. flattenList() does that, and
|
|
// allocates one numbering instance per top-level list so that a second
|
|
// list on the page starts again at 1 instead of continuing.
|
|
// * Highlight is Word's 15-value enum, not a colour, so it passes straight
|
|
// through (see schema.js).
|
|
// * Tables get single-line borders. Word writes no borders unless a table
|
|
// style says otherwise and mammoth doesn't report the ones it read, so
|
|
// this is a deliberate default rather than a round-trip: see ROUND-TRIP.md.
|
|
(function (root, factory) {
|
|
const api = factory();
|
|
if (typeof module === "object" && module.exports) module.exports = api;
|
|
root.DocxEditor = Object.assign(root.DocxEditor || {}, { write: api });
|
|
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
"use strict";
|
|
|
|
const V = () => globalThis.DOCXV;
|
|
|
|
const TWIPS_PER_INDENT = 720;
|
|
const PT_TO_TWIP = 20;
|
|
const DEFAULT_FONT_PT = 11;
|
|
|
|
function alignmentOf(align) {
|
|
const { AlignmentType } = V().docx;
|
|
switch (align) {
|
|
case "center": return AlignmentType.CENTER;
|
|
case "right": return AlignmentType.RIGHT;
|
|
case "justify": return AlignmentType.JUSTIFIED;
|
|
case "left": return AlignmentType.LEFT;
|
|
default: return undefined;
|
|
}
|
|
}
|
|
|
|
function spacingOf(attrs) {
|
|
const spacing = {};
|
|
if (attrs.lineHeight) {
|
|
spacing.line = Math.round(attrs.lineHeight * 240);
|
|
spacing.lineRule = "auto";
|
|
}
|
|
if (attrs.spaceBefore != null) spacing.before = Math.round(attrs.spaceBefore * PT_TO_TWIP);
|
|
if (attrs.spaceAfter != null) spacing.after = Math.round(attrs.spaceAfter * PT_TO_TWIP);
|
|
return Object.keys(spacing).length ? spacing : undefined;
|
|
}
|
|
|
|
function indentOf(attrs, extraTwips) {
|
|
const left = (attrs.indent || 0) * TWIPS_PER_INDENT + (extraTwips || 0);
|
|
return left ? { left } : undefined;
|
|
}
|
|
|
|
function base64ToBytes(b64) {
|
|
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64"));
|
|
const bin = atob(b64);
|
|
const out = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
return out;
|
|
}
|
|
|
|
const IMAGE_TYPES = {
|
|
"image/png": "png", "image/jpeg": "jpg", "image/jpg": "jpg",
|
|
"image/gif": "gif", "image/bmp": "bmp", "image/svg+xml": "svg",
|
|
};
|
|
|
|
// ---------------------------------------------------------------- runs ---
|
|
|
|
function runsFromInline(node, ctx) {
|
|
const { TextRun, ExternalHyperlink, InternalHyperlink, Tab,
|
|
FootnoteReferenceRun } = V().docx;
|
|
const out = [];
|
|
|
|
node.forEach((child) => {
|
|
if (child.isText) {
|
|
const props = { text: child.text };
|
|
let link = null;
|
|
for (const mark of child.marks) {
|
|
switch (mark.type.name) {
|
|
case "strong": props.bold = true; break;
|
|
case "em": props.italics = true; break;
|
|
case "underline": props.underline = {}; break;
|
|
case "strike": props.strike = true; break;
|
|
case "sup": props.superScript = true; break;
|
|
case "sub": props.subScript = true; break;
|
|
case "caps": props.allCaps = true; break;
|
|
case "smallcaps": props.smallCaps = true; break;
|
|
case "font": props.font = mark.attrs.family; break;
|
|
case "fsize": props.size = Math.round(mark.attrs.pt * 2); break; // half-points
|
|
case "color": props.color = mark.attrs.hex; break;
|
|
case "highlight": props.highlight = mark.attrs.name; break;
|
|
case "link": link = mark.attrs; break;
|
|
default: break;
|
|
}
|
|
}
|
|
// Tabs are their own element in Word, so a run's text has to be cut
|
|
// around them — left inline they'd be written as literal tab
|
|
// characters, which Word renders as nothing at all.
|
|
const pieces = String(props.text).split("\t");
|
|
const finalRuns = [];
|
|
pieces.forEach((piece, i) => {
|
|
if (i > 0) {
|
|
const tabProps = Object.assign({}, props);
|
|
delete tabProps.text;
|
|
finalRuns.push(new TextRun(Object.assign(tabProps, { children: [new Tab()] })));
|
|
}
|
|
if (piece) finalRuns.push(new TextRun(Object.assign({}, props, { text: piece })));
|
|
});
|
|
if (!finalRuns.length) finalRuns.push(new TextRun(Object.assign({}, props, { text: "" })));
|
|
|
|
if (link) {
|
|
if (link.anchor) out.push(new InternalHyperlink({ anchor: link.anchor, children: finalRuns }));
|
|
else if (link.href) out.push(new ExternalHyperlink({ link: link.href, children: finalRuns }));
|
|
else out.push(...finalRuns);
|
|
} else {
|
|
out.push(...finalRuns);
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (child.type.name) {
|
|
case "hard_break":
|
|
out.push(new TextRun({ break: 1 }));
|
|
break;
|
|
case "image": {
|
|
const run = imageRun(child, ctx);
|
|
if (run) out.push(run);
|
|
break;
|
|
}
|
|
case "note_ref": {
|
|
const id = parseInt(child.attrs.noteId, 10);
|
|
if (Number.isFinite(id)) {
|
|
try { out.push(new FootnoteReferenceRun(id)); }
|
|
catch { ctx.warn(`footnote reference ${id} could not be written`); }
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function imageRun(node, ctx) {
|
|
const { ImageRun } = V().docx;
|
|
const m = /^data:([^;,]+);base64,(.*)$/.exec(node.attrs.src || "");
|
|
if (!m) { ctx.warn("an image could not be saved (unsupported source)"); return null; }
|
|
const type = IMAGE_TYPES[m[1].toLowerCase()];
|
|
if (!type) { ctx.warn(`an image of type ${m[1]} could not be saved`); return null; }
|
|
const data = base64ToBytes(m[2]);
|
|
const width = node.attrs.width || 300;
|
|
const height = node.attrs.height || Math.round(width * 0.75);
|
|
const opts = {
|
|
data,
|
|
// The builder wants pixels at 96dpi and rounds to whole EMU itself
|
|
// (1px = 9525 EMU), so these stay fractional — rounding to whole
|
|
// pixels here would quantise every picture to 0.75pt and shift it a
|
|
// little further on every save.
|
|
transformation: { width: width / 0.75, height: height / 0.75 },
|
|
type,
|
|
};
|
|
if (node.attrs.alt) opts.altText = { name: node.attrs.alt, description: node.attrs.alt, title: node.attrs.alt };
|
|
if (type === "svg") {
|
|
// The builder requires a raster fallback for SVG; without one it
|
|
// throws, and a thrown save is worse than a missing picture.
|
|
ctx.warn("an SVG image was skipped (Word needs a raster fallback)");
|
|
return null;
|
|
}
|
|
return new ImageRun(opts);
|
|
}
|
|
|
|
// ---------------------------------------------------------- paragraphs ---
|
|
|
|
function paragraphFrom(node, ctx, extra) {
|
|
const { Paragraph, HeadingLevel, BorderStyle } = V().docx;
|
|
const attrs = node.attrs || {};
|
|
const opts = Object.assign({
|
|
children: runsFromInline(node, ctx),
|
|
alignment: alignmentOf(attrs.align),
|
|
spacing: spacingOf(attrs),
|
|
indent: indentOf(attrs, (extra && extra.extraIndent) || 0),
|
|
}, extra && extra.paragraph);
|
|
|
|
if (node.type.name === "heading") {
|
|
opts.heading = [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3,
|
|
HeadingLevel.HEADING_4, HeadingLevel.HEADING_5, HeadingLevel.HEADING_6][
|
|
Math.max(1, Math.min(6, attrs.level || 1)) - 1];
|
|
}
|
|
return new Paragraph(opts);
|
|
}
|
|
|
|
// A ProseMirror list tree flattened into Word's numbered paragraphs.
|
|
function flattenList(listNode, ctx, out, level, reference) {
|
|
listNode.forEach((item) => {
|
|
let first = true;
|
|
item.forEach((child) => {
|
|
const name = child.type.name;
|
|
if (name === "bullet_list" || name === "ordered_list") {
|
|
flattenList(child, ctx, out, level + 1, reference);
|
|
return;
|
|
}
|
|
if (name === "paragraph" || name === "heading") {
|
|
// Only the item's first paragraph carries the bullet; the rest are
|
|
// continuation paragraphs indented to match, which is what Word
|
|
// does for a multi-paragraph list item.
|
|
if (first) {
|
|
out.push(paragraphFrom(child, ctx, {
|
|
paragraph: { numbering: { reference, level } },
|
|
}));
|
|
first = false;
|
|
} else {
|
|
out.push(paragraphFrom(child, ctx, {
|
|
extraIndent: (level + 1) * TWIPS_PER_INDENT,
|
|
}));
|
|
}
|
|
return;
|
|
}
|
|
// Tables and anything else inside a list item: emit it after, since
|
|
// Word can't nest a table under a bullet in our model.
|
|
blockFrom(child, ctx, out, {});
|
|
});
|
|
if (first) {
|
|
// An empty list item still needs a bullet.
|
|
const { Paragraph } = V().docx;
|
|
out.push(new Paragraph({ numbering: { reference, level } }));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Tables are a grid, not a list of rows: a cell that spans rows downwards
|
|
// is written once with vMerge="restart", and every row it reaches into
|
|
// needs its own vMerge="continue" placeholder in that column. ProseMirror
|
|
// stores the covered cells as absent (the same convention HTML uses), so
|
|
// the writer has to put them back — without them Word reads a 12-row merge
|
|
// as a 2-row one.
|
|
function tableFrom(node, ctx) {
|
|
const { Table, TableRow, TableCell, WidthType, BorderStyle, Paragraph,
|
|
VerticalMergeType } = V().docx;
|
|
const border = { style: BorderStyle.SINGLE, size: 4, color: "999999" };
|
|
|
|
const pmRows = [];
|
|
node.forEach((r) => pmRows.push(r));
|
|
|
|
const continuation = (colspan) => new TableCell(Object.assign(
|
|
{ children: [new Paragraph({})], verticalMerge: VerticalMergeType.CONTINUE },
|
|
colspan > 1 ? { columnSpan: colspan } : {}));
|
|
|
|
const realCell = (cell) => {
|
|
const colspan = cell.attrs.colspan || 1;
|
|
const rowspan = cell.attrs.rowspan || 1;
|
|
const children = [];
|
|
blocksOf(cell, ctx, children, true);
|
|
if (!children.length) children.push(new Paragraph({}));
|
|
const opts = { children };
|
|
if (colspan > 1) opts.columnSpan = colspan;
|
|
if (rowspan > 1) opts.verticalMerge = VerticalMergeType.RESTART;
|
|
if (cell.attrs.background) opts.shading = { fill: String(cell.attrs.background).replace("#", "") };
|
|
return new TableCell(opts);
|
|
};
|
|
|
|
let active = []; // [{col, colspan, rowsLeft}] still merging down
|
|
const rows = [];
|
|
for (const pmRow of pmRows) {
|
|
const pmCells = [];
|
|
pmRow.forEach((c) => pmCells.push(c));
|
|
const outCells = [];
|
|
const nextActive = [];
|
|
let col = 0;
|
|
let i = 0;
|
|
const coveringAt = (c) => active.find((a) => a.col === c);
|
|
|
|
while (i < pmCells.length || coveringAt(col)) {
|
|
const cover = coveringAt(col);
|
|
if (cover) {
|
|
outCells.push(continuation(cover.colspan));
|
|
if (cover.rowsLeft > 1) {
|
|
nextActive.push({ col: cover.col, colspan: cover.colspan, rowsLeft: cover.rowsLeft - 1 });
|
|
}
|
|
col += cover.colspan;
|
|
continue;
|
|
}
|
|
const cell = pmCells[i++];
|
|
const colspan = cell.attrs.colspan || 1;
|
|
const rowspan = cell.attrs.rowspan || 1;
|
|
outCells.push(realCell(cell));
|
|
if (rowspan > 1) nextActive.push({ col, colspan, rowsLeft: rowspan - 1 });
|
|
col += colspan;
|
|
}
|
|
active = nextActive;
|
|
rows.push(new TableRow({ children: outCells }));
|
|
}
|
|
if (!rows.length) return null;
|
|
return new Table({
|
|
rows,
|
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
borders: { top: border, bottom: border, left: border, right: border,
|
|
insideHorizontal: border, insideVertical: border },
|
|
});
|
|
}
|
|
|
|
function blocksOf(parent, ctx, out, inCell) {
|
|
const kids = [];
|
|
parent.forEach((child) => kids.push(child));
|
|
kids.forEach((child, i) => {
|
|
blockFrom(child, ctx, out, {
|
|
next: kids[i + 1] || null,
|
|
isLast: i === kids.length - 1,
|
|
inCell: !!inCell,
|
|
});
|
|
});
|
|
}
|
|
|
|
function blockFrom(node, ctx, out, pos) {
|
|
const { Paragraph, PageBreak, TextRun, BorderStyle } = V().docx;
|
|
const at = pos || {};
|
|
switch (node.type.name) {
|
|
case "paragraph":
|
|
case "heading":
|
|
out.push(paragraphFrom(node, ctx));
|
|
break;
|
|
|
|
case "blockquote": {
|
|
node.forEach((child) => {
|
|
if (child.type.name === "paragraph" || child.type.name === "heading") {
|
|
out.push(paragraphFrom(child, ctx, {
|
|
paragraph: { style: "Quote" },
|
|
extraIndent: TWIPS_PER_INDENT,
|
|
}));
|
|
} else {
|
|
blockFrom(child, ctx, out, {});
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "code_block": {
|
|
// One Word paragraph per line, all in the editor's SourceCode style,
|
|
// so the block reads back as a block rather than as prose.
|
|
const lines = (node.textContent || "").split("\n");
|
|
for (const line of lines) {
|
|
out.push(new Paragraph({
|
|
style: "SourceCode",
|
|
children: [new TextRun({ text: line })],
|
|
}));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "bullet_list":
|
|
case "ordered_list": {
|
|
const reference = ctx.newNumbering(node, collectLevelFormats(node, 0, []));
|
|
flattenList(node, ctx, out, 0, reference);
|
|
break;
|
|
}
|
|
|
|
case "table": {
|
|
const t = tableFrom(node, ctx);
|
|
if (t) out.push(t);
|
|
// OOXML requires a table cell to end with a paragraph, so a table in
|
|
// that position gets one. Between two tables at body level it is
|
|
// only a rendering nicety, and adding one there would come back as a
|
|
// stray empty paragraph on the next read — documents that legitimately
|
|
// hold two adjacent tables would gain a blank line on every save.
|
|
if (at.inCell && at.isLast) out.push(new Paragraph({ spacing: { after: 0 } }));
|
|
break;
|
|
}
|
|
|
|
case "horizontal_rule":
|
|
out.push(new Paragraph({
|
|
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "808080", space: 1 } },
|
|
}));
|
|
break;
|
|
|
|
case "page_break":
|
|
out.push(new Paragraph({ children: [new PageBreak()] }));
|
|
break;
|
|
|
|
default:
|
|
ctx.warn(`"${node.type.name}" is not written to Word`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------- numbering ---
|
|
|
|
const BULLETS = ["●", "○", "▪", "●", "○", "▪", "●", "○", "▪"];
|
|
|
|
function levelFormatFor(format) {
|
|
const { LevelFormat } = V().docx;
|
|
switch (format) {
|
|
case "lowerLetter": return LevelFormat.LOWER_LETTER;
|
|
case "upperLetter": return LevelFormat.UPPER_LETTER;
|
|
case "lowerRoman": return LevelFormat.LOWER_ROMAN;
|
|
case "upperRoman": return LevelFormat.UPPER_ROMAN;
|
|
case "bullet": return LevelFormat.BULLET;
|
|
default: return LevelFormat.DECIMAL;
|
|
}
|
|
}
|
|
|
|
// Word keeps numbering formats per level on one numbering definition, so a
|
|
// list that is bulleted at the top and lettered underneath needs both
|
|
// facts before the definition can be written. Walk the tree first and
|
|
// record what each depth actually uses; where two sibling sub-lists
|
|
// disagree at the same depth, the first one wins (Word has nowhere to put
|
|
// the second answer).
|
|
function collectLevelFormats(node, level, into) {
|
|
if (level > 8) return into;
|
|
const ordered = node.type.name === "ordered_list";
|
|
if (!into[level]) {
|
|
into[level] = {
|
|
ordered,
|
|
format: ordered ? (node.attrs.format || "decimal") : "bullet",
|
|
start: ordered && node.attrs.order > 1 ? node.attrs.order : null,
|
|
};
|
|
}
|
|
node.forEach((item) => {
|
|
item.forEach((child) => {
|
|
const n = child.type.name;
|
|
if (n === "bullet_list" || n === "ordered_list") collectLevelFormats(child, level + 1, into);
|
|
});
|
|
});
|
|
return into;
|
|
}
|
|
|
|
// One numbering instance per top-level list. Nine levels each, because a
|
|
// list can be nested deeper than the levels we actually saw.
|
|
function numberingConfigFor(reference, levelFormats) {
|
|
const { LevelFormat, AlignmentType } = V().docx;
|
|
const levels = [];
|
|
for (let i = 0; i < 9; i++) {
|
|
const indent = { left: (i + 1) * TWIPS_PER_INDENT, hanging: 360 };
|
|
// Depths the document didn't reach still need a definition; fall back
|
|
// to Word's own default rotation.
|
|
const spec = levelFormats[i] ||
|
|
{ ordered: false, format: "bullet", start: null };
|
|
if (spec.ordered) {
|
|
levels.push({
|
|
level: i,
|
|
format: levelFormatFor(spec.format),
|
|
text: `%${i + 1}.`,
|
|
alignment: AlignmentType.START,
|
|
style: { paragraph: { indent } },
|
|
});
|
|
if (spec.start) levels[i].start = spec.start;
|
|
} else {
|
|
levels.push({
|
|
level: i,
|
|
format: LevelFormat.BULLET,
|
|
text: BULLETS[i],
|
|
alignment: AlignmentType.LEFT,
|
|
style: { paragraph: { indent } },
|
|
});
|
|
}
|
|
}
|
|
return { reference, levels };
|
|
}
|
|
|
|
// --------------------------------------------------------------- styles ---
|
|
|
|
// Only the styles the writer itself references. Anything the original
|
|
// document defined is grafted back over the top of these by pkg.graft().
|
|
function paragraphStyles() {
|
|
return [
|
|
{
|
|
id: "SourceCode",
|
|
name: "Source Code",
|
|
basedOn: "Normal",
|
|
quickFormat: true,
|
|
run: { font: "Consolas", size: 20 },
|
|
paragraph: { spacing: { before: 0, after: 0, line: 240, lineRule: "auto" } },
|
|
},
|
|
{
|
|
id: "Quote",
|
|
name: "Quote",
|
|
basedOn: "Normal",
|
|
quickFormat: true,
|
|
run: { italics: true, color: "404040" },
|
|
paragraph: { spacing: { before: 120, after: 120 } },
|
|
},
|
|
];
|
|
}
|
|
|
|
// ------------------------------------------------------------------ api ---
|
|
|
|
/**
|
|
* Serialise an editor document to .docx bytes.
|
|
*
|
|
* @param {Node} doc ProseMirror document
|
|
* @param {object} opts
|
|
* originalBytes the file this document was read from, if any — its
|
|
* headers, footers, notes, styles and theme are grafted
|
|
* onto the result
|
|
* setup page setup read from the original (pkg.readSectionSetup)
|
|
* meta document properties (pkg.readCoreProps)
|
|
* @returns {Promise<{bytes: Uint8Array, warnings: string[], carried: string[]}>}
|
|
*/
|
|
async function docToDocx(doc, opts) {
|
|
const o = opts || {};
|
|
const { docx } = V();
|
|
const { Document, Packer } = docx;
|
|
|
|
const warnings = [];
|
|
const numbering = [];
|
|
let numberingSeq = 0;
|
|
const ctx = {
|
|
warn(msg) { if (!warnings.includes(msg)) warnings.push(msg); },
|
|
newNumbering(node, levelFormats) {
|
|
const reference = `list-${++numberingSeq}`;
|
|
numbering.push(numberingConfigFor(reference, levelFormats));
|
|
return reference;
|
|
},
|
|
};
|
|
|
|
const children = [];
|
|
blocksOf(doc, ctx, children, false);
|
|
if (!children.length) {
|
|
const { Paragraph } = docx;
|
|
children.push(new Paragraph({}));
|
|
}
|
|
|
|
const section = { children, properties: {} };
|
|
if (o.setup && o.setup.page && Object.keys(o.setup.page).length) {
|
|
section.properties.page = o.setup.page;
|
|
}
|
|
if (o.setup && o.setup.titlePg) section.properties.titlePage = true;
|
|
|
|
const meta = o.meta || {};
|
|
const document = new Document({
|
|
title: meta.title || undefined,
|
|
creator: meta.creator || undefined,
|
|
description: meta.description || undefined,
|
|
subject: meta.subject || undefined,
|
|
keywords: meta.keywords || undefined,
|
|
numbering: numbering.length ? { config: numbering } : undefined,
|
|
styles: { paragraphStyles: paragraphStyles() },
|
|
sections: [section],
|
|
});
|
|
|
|
// Packer.toBuffer asks JSZip for a "nodebuffer", which browsers don't
|
|
// support — in the tab it throws before a single byte is written. Blob
|
|
// is the browser's route; node keeps toBuffer so the round-trip tests
|
|
// exercise the same code without a Blob shim.
|
|
let bytes;
|
|
if (typeof Buffer !== "undefined") {
|
|
const buf = await Packer.toBuffer(document);
|
|
bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
} else {
|
|
const blob = await Packer.toBlob(document);
|
|
bytes = new Uint8Array(await blob.arrayBuffer());
|
|
}
|
|
|
|
let carried = [];
|
|
if (o.originalBytes) {
|
|
try {
|
|
const grafted = await globalThis.DocxEditor.pkg.graft(o.originalBytes, bytes, o.graft);
|
|
bytes = grafted.bytes;
|
|
carried = grafted.carried;
|
|
} catch (e) {
|
|
// A failed graft must not cost the user their edits: the rebuilt
|
|
// document is still a valid .docx, just a plainer one.
|
|
ctx.warn(`could not carry over the original's headers/styles (${e && e.message || e})`);
|
|
}
|
|
}
|
|
return { bytes, warnings, carried };
|
|
}
|
|
|
|
return { docToDocx, numberingConfigFor, collectLevelFormats, paragraphStyles };
|
|
});
|