205 lines
7.8 KiB
JavaScript
205 lines
7.8 KiB
JavaScript
|
|
// 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 };
|
||
|
|
}
|