// Build-time patches applied to vendored mammoth. // // Why: mammoth's document model deliberately drops direct formatting that // isn't semantic — run colour, paragraph line/space spacing — and flattens // every numbering level to a bare isOrdered boolean. All three are v1 editor // features (text colour, line spacing, numbered-list formats), so without // these a document would visibly lose them on the first round-trip. // // Each patch asserts its anchor: if a mammoth upgrade moves the code, the // build fails loudly instead of silently shipping an editor that eats // formatting. export const patches = [ { file: "lib/docx/body-reader.js", find: ` highlight: readHighlightValue(element.firstOrEmpty("w:highlight").attributes["w:val"])`, replace: ` highlight: readHighlightValue(element.firstOrEmpty("w:highlight").attributes["w:val"]), color: readColorValue(element.firstOrEmpty("w:color").attributes["w:val"])`, }, { file: "lib/docx/body-reader.js", find: ` function readUnderline(element) {`, replace: ` function readColorValue(value) { // w:color w:val is RRGGBB, or "auto" meaning "let the renderer pick". return /^[0-9a-fA-F]{6}$/.test(value || "") ? value.toUpperCase() : null; } function readParagraphSpacing(element) { var attrs = element.attributes; var line = attrs["w:line"]; var num = function(v) { return /^-?[0-9]+$/.test(v || "") ? parseInt(v, 10) : null; }; return { // w:line is 240ths of a line when w:lineRule is auto (the common // case); exact/atLeast are in twips and the reader leaves them be. line: num(line), lineRule: attrs["w:lineRule"] || null, before: num(attrs["w:before"]), after: num(attrs["w:after"]) }; } function readUnderline(element) {`, }, { file: "lib/docx/body-reader.js", find: ` indent: readParagraphIndent(element.firstOrEmpty("w:ind"))`, replace: ` indent: readParagraphIndent(element.firstOrEmpty("w:ind")), spacing: readParagraphSpacing(element.firstOrEmpty("w:spacing")), // Word's horizontal rule is an empty paragraph with a bottom // border; without this the editor can't tell one from a // blank line, and can't write one back either. hasBottomBorder: readHasBottomBorder(element.firstOrEmpty("w:pBdr"))`, }, { file: "lib/docx/body-reader.js", find: ` function readParagraphIndent(element) {`, replace: ` function readHasBottomBorder(element) { var bottom = element.firstOrEmpty("w:bottom").attributes["w:val"]; return !!bottom && bottom !== "none" && bottom !== "nil"; } function readParagraphIndent(element) {`, }, // Images: mammoth hands back the file but not the size Word was drawing it // at (wp:extent, in EMU). Without it a picture the author scaled down to a // thumbnail would come back at full natural size on the next save. { file: "lib/docx/body-reader.js", find: ` return readImage(blipImageFile, altText).map(function(imageElement) {`, replace: ` var extentAttributes = element.firstOrEmpty("wp:extent").attributes; return readImage(blipImageFile, altText, extentAttributes).map(function(imageElement) {`, }, { file: "lib/docx/body-reader.js", find: ` function readImage(imageFile, altText) { var contentType = contentTypes.findContentType(imageFile.path); var image = documents.Image({ readImage: imageFile.read, altText: altText, contentType: contentType });`, replace: ` function readImage(imageFile, altText, extent) { var contentType = contentTypes.findContentType(imageFile.path); // 12700 EMU to the point. var emuToPt = function(v) { return /^[0-9]+$/.test(v || "") ? Math.round(parseInt(v, 10) / 12700 * 100) / 100 : null; }; var image = documents.Image({ readImage: imageFile.read, altText: altText, contentType: contentType, widthPt: emuToPt(extent && extent.cx), heightPt: emuToPt(extent && extent.cy) });`, }, { file: "lib/documents.js", find: ` altText: options.altText, contentType: options.contentType`, replace: ` altText: options.altText, contentType: options.contentType, widthPt: options.widthPt == null ? null : options.widthPt, heightPt: options.heightPt == null ? null : options.heightPt`, }, { file: "lib/documents.js", find: ` highlight: properties.highlight || null }; }`, replace: ` highlight: properties.highlight || null, color: properties.color || null }; }`, }, { file: "lib/documents.js", find: ` indent: { start: indent.start || null, end: indent.end || null, firstLine: indent.firstLine || null, hanging: indent.hanging || null } };`, replace: ` indent: { start: indent.start || null, end: indent.end || null, firstLine: indent.firstLine || null, hanging: indent.hanging || null }, spacing: properties.spacing || null, hasBottomBorder: !!properties.hasBottomBorder };`, }, // Which numbering definition a list item belongs to. Word uses numId to // tell two adjacent lists apart — the point at which the numbering starts // again at 1 — and mammoth resolves it to a level and then forgets it, // which leaves the reader unable to see where one list ends and the next // begins. { file: "lib/docx/body-reader.js", find: `function readNumberingProperties(styleId, element, numbering) { var level = element.firstOrEmpty("w:ilvl").attributes["w:val"]; var numId = element.firstOrEmpty("w:numId").attributes["w:val"]; if (level !== undefined && numId !== undefined) { return numbering.findLevel(numId, level); }`, replace: `function readNumberingProperties(styleId, element, numbering) { var level = element.firstOrEmpty("w:ilvl").attributes["w:val"]; var numId = element.firstOrEmpty("w:numId").attributes["w:val"]; var withNumId = function(found, id) { return found == null ? found : Object.assign({}, found, {numId: id == null ? null : String(id)}); }; if (level !== undefined && numId !== undefined) { return withNumId(numbering.findLevel(numId, level), numId); }`, }, { file: "lib/docx/body-reader.js", find: ` if (numId !== undefined) { return numbering.findLevel(numId, "0"); } return null; }`, replace: ` if (numId !== undefined) { return withNumId(numbering.findLevel(numId, "0"), numId); } return null; }`, }, { file: "lib/docx/numbering-xml.js", find: ` levelWithoutIndex = { isOrdered: isOrdered,`, replace: ` levelWithoutIndex = { numFmt: numFmt || null, isOrdered: isOrdered,`, }, { file: "lib/docx/numbering-xml.js", find: ` levels[levelIndex] = { isOrdered: isOrdered,`, replace: ` levels[levelIndex] = { numFmt: numFmt || null, isOrdered: isOrdered,`, }, ]; export function applyPatches(relPath, source) { let out = source; let hits = 0; for (const p of patches) { if (relPath !== "mammoth/" + p.file) continue; if (!out.includes(p.find)) { throw new Error(`mammoth patch anchor missing in ${p.file}:\n${p.find.slice(0, 90)}…\n` + `A mammoth upgrade probably moved it. Re-check the patch before shipping.`); } out = out.replace(p.find, p.replace); hits++; } return { code: out, hits }; }