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.
490 lines
22 KiB
JavaScript
490 lines
22 KiB
JavaScript
// Package-level work on a .docx: everything that happens at the zip layer,
|
|
// either side of mammoth and the docx builder.
|
|
//
|
|
// Two jobs:
|
|
//
|
|
// scan(zip) — walk the original package and report what's in it that the
|
|
// v1 editor can't render, so the user is told BEFORE they
|
|
// edit rather than after they've lost something.
|
|
//
|
|
// graft(...) — the save-side half of the preservation deal. docx (the npm
|
|
// builder) always emits a brand-new package, so anything that
|
|
// lives outside the document body would vanish on save. We
|
|
// take the parts that survive a body rewrite unharmed —
|
|
// headers, footers, footnotes, endnotes, the style catalogue,
|
|
// the theme, page setup — and carry them across from the
|
|
// original into the freshly built package, re-wiring
|
|
// relationship ids and content types as we go.
|
|
//
|
|
// Body-level things we can't model (textboxes, shapes, equations, content
|
|
// controls, fields) are NOT preserved; scan() names them so the loss is
|
|
// visible. See ROUND-TRIP.md for the full ledger.
|
|
(function (root, factory) {
|
|
const api = factory();
|
|
if (typeof module === "object" && module.exports) module.exports = api;
|
|
root.DocxEditor = Object.assign(root.DocxEditor || {}, { pkg: api });
|
|
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
"use strict";
|
|
|
|
const V = () => globalThis.DOCXV;
|
|
|
|
const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
const R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
const CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
const PR_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
|
|
const CONTENT_TYPES = {
|
|
header: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
|
footer: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
|
footnotes: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
endnotes: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
};
|
|
const REL_TYPES = {
|
|
header: R_NS + "/header",
|
|
footer: R_NS + "/footer",
|
|
footnotes: R_NS + "/footnotes",
|
|
endnotes: R_NS + "/endnotes",
|
|
image: R_NS + "/image",
|
|
};
|
|
|
|
function parseXml(text) {
|
|
const doc = new DOMParser().parseFromString(text, "application/xml");
|
|
const err = doc.getElementsByTagName("parsererror")[0];
|
|
if (err) throw new Error("malformed XML in package: " + err.textContent.slice(0, 200));
|
|
return doc;
|
|
}
|
|
function serializeXml(doc) {
|
|
const body = new XMLSerializer().serializeToString(doc);
|
|
return body.startsWith("<?xml")
|
|
? body
|
|
: '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n' + body;
|
|
}
|
|
// Namespace-agnostic child lookup: packages in the wild are inconsistent
|
|
// about prefixes, and getElementsByTagNameNS is the only reliable route.
|
|
function kids(el, ns, local) {
|
|
return Array.prototype.filter.call(el.childNodes,
|
|
(n) => n.nodeType === 1 && n.namespaceURI === ns && n.localName === local);
|
|
}
|
|
function firstKid(el, ns, local) { return kids(el, ns, local)[0] || null; }
|
|
function descendants(doc, ns, local) {
|
|
return Array.prototype.slice.call(doc.getElementsByTagNameNS(ns, local));
|
|
}
|
|
|
|
async function loadZip(bytes) {
|
|
return V().JSZip.loadAsync(bytes);
|
|
}
|
|
async function textOf(zip, path) {
|
|
const f = zip.file(path);
|
|
return f ? f.async("string") : null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- scan ---
|
|
|
|
// What the scan can say about a feature.
|
|
// preserved — survives a save untouched (carried across by graft()).
|
|
// lossy — the content survives but not exactly as Word wrote it.
|
|
// dropped — gone on save; the user needs to know before editing.
|
|
// blocked — dangerous to flatten silently; editing is gated on consent.
|
|
const FEATURES = [
|
|
{ key: "trackedChanges", level: "blocked", label: "Tracked changes",
|
|
note: "Saving would silently accept every pending revision.",
|
|
test: (d) => descendants(d, W_NS, "ins").length + descendants(d, W_NS, "del").length > 0 },
|
|
{ key: "comments", level: "blocked", label: "Comments",
|
|
note: "Comment anchors and the comment text are not carried across.",
|
|
test: (d, z) => !!z.file("word/comments.xml") &&
|
|
descendants(d, W_NS, "commentRangeStart").length > 0 },
|
|
|
|
{ key: "headers", level: "preserved", label: "Headers",
|
|
test: (d, z) => z.file(/^word\/header\d*\.xml$/).length > 0 },
|
|
{ key: "footers", level: "preserved", label: "Footers",
|
|
test: (d, z) => z.file(/^word\/footer\d*\.xml$/).length > 0 },
|
|
{ key: "footnotes", level: "preserved", label: "Footnotes",
|
|
test: (d, z) => !!z.file("word/footnotes.xml") &&
|
|
descendants(d, W_NS, "footnoteReference").length > 0 },
|
|
{ key: "endnotes", level: "preserved", label: "Endnotes",
|
|
test: (d, z) => !!z.file("word/endnotes.xml") &&
|
|
descendants(d, W_NS, "endnoteReference").length > 0 },
|
|
{ key: "pageSetup", level: "preserved", label: "Page size and margins",
|
|
test: (d) => descendants(d, W_NS, "sectPr").length > 0 },
|
|
|
|
{ key: "equations", level: "dropped", label: "Equations",
|
|
note: "OMML equations are removed from the body.",
|
|
test: (d) => d.getElementsByTagNameNS("http://schemas.openxmlformats.org/officeDocument/2006/math", "oMath").length > 0 },
|
|
{ key: "shapes", level: "dropped", label: "Shapes, text boxes and WordArt",
|
|
test: (d) => descendants(d, W_NS, "pict").length > 0 ||
|
|
d.getElementsByTagNameNS("http://schemas.openxmlformats.org/markup-compatibility/2006", "AlternateContent").length > 0 },
|
|
{ key: "contentControls", level: "dropped", label: "Content controls",
|
|
test: (d) => descendants(d, W_NS, "sdt").length > 0 },
|
|
{ key: "fields", level: "dropped", label: "Fields (page numbers, tables of contents, cross-references)",
|
|
note: "Field codes are dropped; the text Word last calculated is kept.",
|
|
test: (d) => descendants(d, W_NS, "fldSimple").length > 0 ||
|
|
descendants(d, W_NS, "instrText").length > 0 },
|
|
{ key: "bookmarks", level: "dropped", label: "Bookmarks",
|
|
test: (d) => descendants(d, W_NS, "bookmarkStart")
|
|
.some((b) => !(b.getAttributeNS(W_NS, "name") || "").startsWith("_GoBack") ) },
|
|
{ key: "sections", level: "dropped", label: "Multiple sections",
|
|
note: "Only the first section's page setup is kept; section breaks are lost.",
|
|
test: (d) => descendants(d, W_NS, "sectPr").length > 1 },
|
|
{ key: "columns", level: "dropped", label: "Multi-column layout",
|
|
test: (d) => descendants(d, W_NS, "cols").some((c) => {
|
|
const n = c.getAttributeNS(W_NS, "num");
|
|
return n && parseInt(n, 10) > 1;
|
|
}) },
|
|
|
|
{ key: "metafiles", level: "dropped", label: "Metafile pictures (WMF/EMF)",
|
|
note: "Word's vector picture format. Browsers can't display it and it can't be written back, so those pictures are lost on save.",
|
|
test: (d, z) => z.file(/^word\/media\/.*\.(wmf|emf)$/i).length > 0 },
|
|
|
|
{ key: "paragraphBorders", level: "lossy", label: "Paragraph borders and shading",
|
|
note: "A rule under an empty paragraph is kept as a horizontal rule; other borders are dropped.",
|
|
test: (d) => descendants(d, W_NS, "pBdr").length > 0 || descendants(d, W_NS, "shd").length > 0 },
|
|
{ key: "tabStops", level: "lossy", label: "Custom tab stops",
|
|
note: "Tab characters are kept, custom stop positions are not.",
|
|
test: (d) => descendants(d, W_NS, "tabs").length > 0 },
|
|
];
|
|
|
|
async function scan(zip) {
|
|
const xml = await textOf(zip, "word/document.xml");
|
|
if (!xml) throw new Error("not a Word document: word/document.xml is missing");
|
|
const doc = parseXml(xml);
|
|
const found = [];
|
|
for (const f of FEATURES) {
|
|
let hit = false;
|
|
try { hit = !!f.test(doc, zip); } catch { hit = false; }
|
|
if (hit) found.push({ key: f.key, level: f.level, label: f.label, note: f.note || "" });
|
|
}
|
|
return {
|
|
features: found,
|
|
blocked: found.filter((f) => f.level === "blocked"),
|
|
dropped: found.filter((f) => f.level === "dropped"),
|
|
lossy: found.filter((f) => f.level === "lossy"),
|
|
preserved: found.filter((f) => f.level === "preserved"),
|
|
};
|
|
}
|
|
|
|
// ------------------------------------------------------------ page setup ---
|
|
|
|
// The first sectPr, mapped onto what the docx builder wants. Word writes
|
|
// these in twips; the builder takes twips too, so this is mostly a rename.
|
|
async function readSectionSetup(zip) {
|
|
const xml = await textOf(zip, "word/document.xml");
|
|
if (!xml) return null;
|
|
const doc = parseXml(xml);
|
|
const sect = descendants(doc, W_NS, "sectPr")[0];
|
|
if (!sect) return null;
|
|
const num = (el, attr) => {
|
|
if (!el) return null;
|
|
const v = el.getAttributeNS(W_NS, attr);
|
|
return /^-?\d+$/.test(v || "") ? parseInt(v, 10) : null;
|
|
};
|
|
const pgSz = firstKid(sect, W_NS, "pgSz");
|
|
const pgMar = firstKid(sect, W_NS, "pgMar");
|
|
const setup = { page: {} };
|
|
if (pgSz) {
|
|
const w = num(pgSz, "w"), h = num(pgSz, "h");
|
|
const orient = pgSz.getAttributeNS(W_NS, "orient");
|
|
if (w && h) setup.page.size = { width: w, height: h, orientation: orient === "landscape" ? "landscape" : "portrait" };
|
|
}
|
|
if (pgMar) {
|
|
const m = {};
|
|
for (const [k, a] of [["top", "top"], ["right", "right"], ["bottom", "bottom"],
|
|
["left", "left"], ["header", "header"], ["footer", "footer"], ["gutter", "gutter"]]) {
|
|
const v = num(pgMar, a);
|
|
if (v !== null) m[k] = v;
|
|
}
|
|
if (Object.keys(m).length) setup.page.margin = m;
|
|
}
|
|
setup.titlePg = !!firstKid(sect, W_NS, "titlePg");
|
|
// Which header/footer parts this section points at, by type.
|
|
setup.refs = [];
|
|
for (const kind of ["header", "footer"]) {
|
|
for (const ref of kids(sect, W_NS, kind + "Reference")) {
|
|
setup.refs.push({
|
|
kind,
|
|
type: ref.getAttributeNS(W_NS, "type") || "default",
|
|
rId: ref.getAttributeNS(R_NS, "id") || "",
|
|
});
|
|
}
|
|
}
|
|
return setup;
|
|
}
|
|
|
|
// docProps/core.xml — cheap to carry, and losing the author of a document
|
|
// is the kind of small betrayal people notice.
|
|
async function readCoreProps(zip) {
|
|
const xml = await textOf(zip, "docProps/core.xml");
|
|
if (!xml) return {};
|
|
let doc; try { doc = parseXml(xml); } catch { return {}; }
|
|
const pick = (ns, local) => {
|
|
const el = doc.getElementsByTagNameNS(ns, local)[0];
|
|
return el && el.textContent ? el.textContent : undefined;
|
|
};
|
|
const DC = "http://purl.org/dc/elements/1.1/";
|
|
const CP = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
|
|
return {
|
|
title: pick(DC, "title"),
|
|
creator: pick(DC, "creator"),
|
|
description: pick(DC, "description"),
|
|
subject: pick(DC, "subject"),
|
|
keywords: pick(CP, "keywords"),
|
|
lastModifiedBy: pick(CP, "lastModifiedBy"),
|
|
};
|
|
}
|
|
|
|
// --------------------------------------------------------------- graft ---
|
|
|
|
function relsPathFor(partPath) {
|
|
const i = partPath.lastIndexOf("/");
|
|
return partPath.slice(0, i) + "/_rels" + partPath.slice(i) + ".rels";
|
|
}
|
|
|
|
function nextRelId(relsDoc) {
|
|
let max = 0;
|
|
for (const r of descendants(relsDoc, PR_NS, "Relationship")) {
|
|
const m = /^rId(\d+)$/.exec(r.getAttribute("Id") || "");
|
|
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
}
|
|
return (n) => "rId" + (max + n);
|
|
}
|
|
|
|
function addRelationship(relsDoc, id, type, target) {
|
|
const el = relsDoc.createElementNS(PR_NS, "Relationship");
|
|
el.setAttribute("Id", id);
|
|
el.setAttribute("Type", type);
|
|
el.setAttribute("Target", target);
|
|
relsDoc.documentElement.appendChild(el);
|
|
}
|
|
|
|
function addOverride(ctDoc, partName, contentType) {
|
|
const already = descendants(ctDoc, CT_NS, "Override")
|
|
.some((o) => o.getAttribute("PartName") === partName);
|
|
if (already) return;
|
|
const el = ctDoc.createElementNS(CT_NS, "Override");
|
|
el.setAttribute("PartName", partName);
|
|
el.setAttribute("ContentType", contentType);
|
|
ctDoc.documentElement.appendChild(el);
|
|
}
|
|
|
|
function addDefaultExt(ctDoc, ext, contentType) {
|
|
const already = descendants(ctDoc, CT_NS, "Default")
|
|
.some((o) => (o.getAttribute("Extension") || "").toLowerCase() === ext.toLowerCase());
|
|
if (already) return;
|
|
const el = ctDoc.createElementNS(CT_NS, "Default");
|
|
el.setAttribute("Extension", ext);
|
|
el.setAttribute("ContentType", contentType);
|
|
ctDoc.documentElement.insertBefore(el, ctDoc.documentElement.firstChild);
|
|
}
|
|
|
|
// Copy one part plus anything its own .rels file points at. Media gets a
|
|
// fresh name whenever the generated package already has a file there, and
|
|
// the part's rels are rewritten to match — otherwise a header's logo would
|
|
// quietly replace an image from the body.
|
|
async function copyPartWithRels(orig, gen, partPath, usedMedia) {
|
|
const data = await orig.file(partPath).async("uint8array");
|
|
gen.file(partPath, data);
|
|
const rp = relsPathFor(partPath);
|
|
const relsText = await textOf(orig, rp);
|
|
if (!relsText) return;
|
|
const relsDoc = parseXml(relsText);
|
|
for (const rel of descendants(relsDoc, PR_NS, "Relationship")) {
|
|
if (rel.getAttribute("TargetMode") === "External") continue;
|
|
const target = rel.getAttribute("Target") || "";
|
|
// Targets in word/_rels/*.rels are relative to word/.
|
|
const src = ("word/" + target.replace(/^\.\//, "")).replace(/\/+/g, "/");
|
|
const f = orig.file(src);
|
|
if (!f) continue;
|
|
let dest = src;
|
|
if (gen.file(dest) && !usedMedia.has(src)) {
|
|
const dot = src.lastIndexOf(".");
|
|
dest = src.slice(0, dot) + "-carried" + usedMedia.size + src.slice(dot);
|
|
rel.setAttribute("Target", dest.replace(/^word\//, ""));
|
|
}
|
|
usedMedia.set(src, dest);
|
|
gen.file(dest, await f.async("uint8array"));
|
|
}
|
|
gen.file(rp, serializeXml(relsDoc));
|
|
}
|
|
|
|
// Merge the original style catalogue into the generated one. Where both
|
|
// define a style id, the ORIGINAL wins: it is what the document actually
|
|
// looked like, and the builder's defaults are only there to make a blank
|
|
// document presentable. Styles the original doesn't have (the editor's own
|
|
// SourceCode, for instance) are left in place.
|
|
async function mergeStyles(orig, gen) {
|
|
const origText = await textOf(orig, "word/styles.xml");
|
|
const genText = await textOf(gen, "word/styles.xml");
|
|
if (!origText || !genText) return { merged: 0 };
|
|
const origDoc = parseXml(origText);
|
|
const genDoc = parseXml(genText);
|
|
const genRoot = genDoc.documentElement;
|
|
|
|
const byId = new Map();
|
|
for (const s of kids(genRoot, W_NS, "style")) {
|
|
byId.set(s.getAttributeNS(W_NS, "styleId"), s);
|
|
}
|
|
let merged = 0;
|
|
for (const s of descendants(origDoc, W_NS, "style")) {
|
|
const id = s.getAttributeNS(W_NS, "styleId");
|
|
if (!id) continue;
|
|
const imported = genDoc.importNode(s, true);
|
|
const existing = byId.get(id);
|
|
if (existing) genRoot.replaceChild(imported, existing);
|
|
else genRoot.appendChild(imported);
|
|
byId.set(id, imported);
|
|
merged++;
|
|
}
|
|
// docDefaults carries the document's base font and spacing; without it a
|
|
// grafted style catalogue sits on the builder's defaults and every
|
|
// unstyled paragraph shifts.
|
|
const origDefaults = descendants(origDoc, W_NS, "docDefaults")[0];
|
|
if (origDefaults) {
|
|
const genDefaults = kids(genRoot, W_NS, "docDefaults")[0];
|
|
const imported = genDoc.importNode(origDefaults, true);
|
|
if (genDefaults) genRoot.replaceChild(imported, genDefaults);
|
|
else genRoot.insertBefore(imported, genRoot.firstChild);
|
|
}
|
|
gen.file("word/styles.xml", serializeXml(genDoc));
|
|
return { merged };
|
|
}
|
|
|
|
// Header/footer references have to be the FIRST children of sectPr — the
|
|
// schema is order-sensitive and Word refuses a file that gets it wrong.
|
|
function injectSectionRefs(docDoc, refs, titlePg) {
|
|
const sect = descendants(docDoc, W_NS, "sectPr")[0];
|
|
if (!sect) return 0;
|
|
let n = 0;
|
|
const anchor = sect.firstChild;
|
|
for (const ref of refs) {
|
|
const el = docDoc.createElementNS(W_NS, "w:" + ref.kind + "Reference");
|
|
el.setAttributeNS(W_NS, "w:type", ref.type);
|
|
el.setAttributeNS(R_NS, "r:id", ref.newRId);
|
|
sect.insertBefore(el, anchor);
|
|
n++;
|
|
}
|
|
if (titlePg && !firstKid(sect, W_NS, "titlePg")) {
|
|
// titlePg sits after the references but before pgSz; appending is fine
|
|
// because Word tolerates it at the tail of sectPr in practice, and the
|
|
// references above are the order-critical part.
|
|
sect.appendChild(docDoc.createElementNS(W_NS, "w:titlePg"));
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* Carry preserved parts from the original package into the generated one.
|
|
*
|
|
* @param {Uint8Array} originalBytes the .docx the user opened
|
|
* @param {Uint8Array} generatedBytes what the docx builder just produced
|
|
* @param {object} opts { headers, footers, notes, styles, theme }
|
|
* @returns {Promise<{bytes: Uint8Array, carried: string[]}>}
|
|
*/
|
|
async function graft(originalBytes, generatedBytes, opts) {
|
|
const o = Object.assign({ headers: true, footers: true, notes: true, styles: true, theme: true }, opts || {});
|
|
const orig = await loadZip(originalBytes);
|
|
const gen = await loadZip(generatedBytes);
|
|
const carried = [];
|
|
|
|
const ctText = await textOf(gen, "[Content_Types].xml");
|
|
const ctDoc = parseXml(ctText);
|
|
const relsText = await textOf(gen, "word/_rels/document.xml.rels");
|
|
const relsDoc = parseXml(relsText);
|
|
const mkId = nextRelId(relsDoc);
|
|
let idN = 0;
|
|
const usedMedia = new Map();
|
|
|
|
// --- headers and footers -------------------------------------------
|
|
const setup = await readSectionSetup(orig);
|
|
const origRels = await textOf(orig, "word/_rels/document.xml.rels");
|
|
const origRelsDoc = origRels ? parseXml(origRels) : null;
|
|
const targetOf = (rId) => {
|
|
if (!origRelsDoc) return null;
|
|
const hit = descendants(origRelsDoc, PR_NS, "Relationship")
|
|
.find((r) => r.getAttribute("Id") === rId);
|
|
return hit ? "word/" + (hit.getAttribute("Target") || "").replace(/^\.\//, "") : null;
|
|
};
|
|
|
|
const newRefs = [];
|
|
if (setup && setup.refs.length) {
|
|
for (const ref of setup.refs) {
|
|
if (ref.kind === "header" && !o.headers) continue;
|
|
if (ref.kind === "footer" && !o.footers) continue;
|
|
const part = targetOf(ref.rId);
|
|
if (!part || !orig.file(part)) continue;
|
|
await copyPartWithRels(orig, gen, part, usedMedia);
|
|
const newRId = mkId(++idN);
|
|
addRelationship(relsDoc, newRId, REL_TYPES[ref.kind], part.replace(/^word\//, ""));
|
|
addOverride(ctDoc, "/" + part, CONTENT_TYPES[ref.kind]);
|
|
newRefs.push({ kind: ref.kind, type: ref.type, newRId });
|
|
}
|
|
if (newRefs.length) carried.push(`${newRefs.length} header/footer part(s)`);
|
|
}
|
|
|
|
// --- footnotes and endnotes -----------------------------------------
|
|
// The body keeps its footnote references (see read.js), so the note text
|
|
// has to come across with the same ids the references use — which is
|
|
// exactly what copying the original part wholesale gives us. The builder
|
|
// writes its own footnotes.xml only when the document declares notes, so
|
|
// in practice this replaces an absent or separator-only part.
|
|
if (o.notes) {
|
|
for (const kind of ["footnotes", "endnotes"]) {
|
|
const part = `word/${kind}.xml`;
|
|
if (!orig.file(part)) continue;
|
|
await copyPartWithRels(orig, gen, part, usedMedia);
|
|
addOverride(ctDoc, "/" + part, CONTENT_TYPES[kind]);
|
|
const has = descendants(relsDoc, PR_NS, "Relationship")
|
|
.some((r) => r.getAttribute("Type") === REL_TYPES[kind]);
|
|
if (!has) addRelationship(relsDoc, mkId(++idN), REL_TYPES[kind], `${kind}.xml`);
|
|
carried.push(kind);
|
|
}
|
|
}
|
|
|
|
// --- styles and theme ------------------------------------------------
|
|
if (o.styles) {
|
|
const r = await mergeStyles(orig, gen);
|
|
if (r.merged) carried.push(`${r.merged} style definition(s)`);
|
|
}
|
|
if (o.theme) {
|
|
const themeFile = orig.file(/^word\/theme\/theme\d*\.xml$/)[0];
|
|
if (themeFile) {
|
|
const genTheme = gen.file(/^word\/theme\/theme\d*\.xml$/)[0];
|
|
const dest = genTheme ? genTheme.name : "word/theme/theme1.xml";
|
|
gen.file(dest, await themeFile.async("uint8array"));
|
|
if (!genTheme) {
|
|
addOverride(ctDoc, "/" + dest, "application/vnd.openxmlformats-officedocument.theme+xml");
|
|
addRelationship(relsDoc, mkId(++idN), R_NS + "/theme", dest.replace(/^word\//, ""));
|
|
}
|
|
carried.push("theme");
|
|
}
|
|
}
|
|
|
|
// Any media extension the carried parts brought with them needs a Default
|
|
// entry or Word rejects the package.
|
|
for (const dest of usedMedia.values()) {
|
|
const ext = (dest.split(".").pop() || "").toLowerCase();
|
|
const mime = { png: "image/png", jpeg: "image/jpeg", jpg: "image/jpeg", gif: "image/gif",
|
|
bmp: "image/bmp", tiff: "image/tiff", svg: "image/svg+xml",
|
|
emf: "image/x-emf", wmf: "image/x-wmf" }[ext];
|
|
if (mime) addDefaultExt(ctDoc, ext, mime);
|
|
}
|
|
|
|
// --- rewrite the parts we changed ------------------------------------
|
|
if (newRefs.length || (setup && setup.titlePg)) {
|
|
const docText = await textOf(gen, "word/document.xml");
|
|
const docDoc = parseXml(docText);
|
|
injectSectionRefs(docDoc, newRefs, setup && setup.titlePg);
|
|
gen.file("word/document.xml", serializeXml(docDoc));
|
|
}
|
|
gen.file("[Content_Types].xml", serializeXml(ctDoc));
|
|
gen.file("word/_rels/document.xml.rels", serializeXml(relsDoc));
|
|
|
|
const bytes = await gen.generateAsync({
|
|
type: "uint8array",
|
|
compression: "DEFLATE",
|
|
compressionOptions: { level: 6 },
|
|
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
});
|
|
return { bytes, carried };
|
|
}
|
|
|
|
return { loadZip, scan, graft, readSectionSetup, readCoreProps, parseXml, serializeXml, FEATURES };
|
|
});
|