All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
+817
@@ -0,0 +1,817 @@
|
||||
exports.createBodyReader = createBodyReader;
|
||||
exports._readNumberingProperties = readNumberingProperties;
|
||||
|
||||
var dingbatToUnicode = require("dingbat-to-unicode");
|
||||
var _ = require("underscore");
|
||||
|
||||
var documents = require("../documents");
|
||||
var Result = require("../results").Result;
|
||||
var warning = require("../results").warning;
|
||||
var xml = require("../xml");
|
||||
var transforms = require("../transforms");
|
||||
var uris = require("./uris");
|
||||
|
||||
function createBodyReader(options) {
|
||||
return {
|
||||
readXmlElement: function(element) {
|
||||
return new BodyReader(options).readXmlElement(element);
|
||||
},
|
||||
readXmlElements: function(elements) {
|
||||
return new BodyReader(options).readXmlElements(elements);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function BodyReader(options) {
|
||||
var complexFieldStack = [];
|
||||
var currentInstrText = [];
|
||||
|
||||
// When a paragraph is marked as deleted, its contents should be combined
|
||||
// with the following paragraph. See 17.13.5.15 del (Deleted Paragraph) of
|
||||
// ECMA-376 4th edition Part 1.
|
||||
var deletedParagraphContents = [];
|
||||
|
||||
var relationships = options.relationships;
|
||||
var contentTypes = options.contentTypes;
|
||||
var docxFile = options.docxFile;
|
||||
var files = options.files;
|
||||
var numbering = options.numbering;
|
||||
var styles = options.styles;
|
||||
|
||||
function readXmlElements(elements) {
|
||||
var results = elements.map(readXmlElement);
|
||||
return combineResults(results);
|
||||
}
|
||||
|
||||
function readXmlElement(element) {
|
||||
if (element.type === "element") {
|
||||
var handler = xmlElementReaders[element.name];
|
||||
if (handler) {
|
||||
return handler(element);
|
||||
} else if (!Object.prototype.hasOwnProperty.call(ignoreElements, element.name)) {
|
||||
var message = warning("An unrecognised element was ignored: " + element.name);
|
||||
return emptyResultWithMessages([message]);
|
||||
}
|
||||
}
|
||||
return emptyResult();
|
||||
}
|
||||
|
||||
function readParagraphProperties(element) {
|
||||
return readParagraphStyle(element).map(function(style) {
|
||||
return {
|
||||
type: "paragraphProperties",
|
||||
styleId: style.styleId,
|
||||
styleName: style.name,
|
||||
alignment: element.firstOrEmpty("w:jc").attributes["w:val"],
|
||||
numbering: readNumberingProperties(style.styleId, element.firstOrEmpty("w:numPr"), numbering),
|
||||
indent: readParagraphIndent(element.firstOrEmpty("w:ind"))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function readParagraphIndent(element) {
|
||||
return {
|
||||
start: element.attributes["w:start"] || element.attributes["w:left"],
|
||||
end: element.attributes["w:end"] || element.attributes["w:right"],
|
||||
firstLine: element.attributes["w:firstLine"],
|
||||
hanging: element.attributes["w:hanging"]
|
||||
};
|
||||
}
|
||||
|
||||
function readRunProperties(element) {
|
||||
return readRunStyle(element).map(function(style) {
|
||||
var fontSizeString = element.firstOrEmpty("w:sz").attributes["w:val"];
|
||||
// w:sz gives the font size in half points, so halve the value to get the size in points
|
||||
var fontSize = /^[0-9]+$/.test(fontSizeString) ? parseInt(fontSizeString, 10) / 2 : null;
|
||||
|
||||
return {
|
||||
type: "runProperties",
|
||||
styleId: style.styleId,
|
||||
styleName: style.name,
|
||||
verticalAlignment: element.firstOrEmpty("w:vertAlign").attributes["w:val"],
|
||||
font: element.firstOrEmpty("w:rFonts").attributes["w:ascii"],
|
||||
fontSize: fontSize,
|
||||
isBold: readBooleanElement(element.first("w:b")),
|
||||
isUnderline: readUnderline(element.first("w:u")),
|
||||
isItalic: readBooleanElement(element.first("w:i")),
|
||||
isStrikethrough: readBooleanElement(element.first("w:strike")),
|
||||
isAllCaps: readBooleanElement(element.first("w:caps")),
|
||||
isSmallCaps: readBooleanElement(element.first("w:smallCaps")),
|
||||
highlight: readHighlightValue(element.firstOrEmpty("w:highlight").attributes["w:val"])
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function readUnderline(element) {
|
||||
if (element) {
|
||||
var value = element.attributes["w:val"];
|
||||
return value !== undefined && value !== "false" && value !== "0" && value !== "none";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readBooleanElement(element) {
|
||||
if (element) {
|
||||
var value = element.attributes["w:val"];
|
||||
return value !== "false" && value !== "0";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readBooleanAttributeValue(value) {
|
||||
return value !== "false" && value !== "0";
|
||||
}
|
||||
|
||||
function readHighlightValue(value) {
|
||||
if (!value || value === "none") {
|
||||
return null;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function readParagraphStyle(element) {
|
||||
return readStyle(element, "w:pStyle", "Paragraph", styles.findParagraphStyleById);
|
||||
}
|
||||
|
||||
function readRunStyle(element) {
|
||||
return readStyle(element, "w:rStyle", "Run", styles.findCharacterStyleById);
|
||||
}
|
||||
|
||||
function readTableStyle(element) {
|
||||
return readStyle(element, "w:tblStyle", "Table", styles.findTableStyleById);
|
||||
}
|
||||
|
||||
function readStyle(element, styleTagName, styleType, findStyleById) {
|
||||
var messages = [];
|
||||
var styleElement = element.first(styleTagName);
|
||||
var styleId = null;
|
||||
var name = null;
|
||||
if (styleElement) {
|
||||
styleId = styleElement.attributes["w:val"];
|
||||
if (styleId) {
|
||||
var style = findStyleById(styleId);
|
||||
if (style) {
|
||||
name = style.name;
|
||||
} else {
|
||||
messages.push(undefinedStyleWarning(styleType, styleId));
|
||||
}
|
||||
}
|
||||
}
|
||||
return elementResultWithMessages({styleId: styleId, name: name}, messages);
|
||||
}
|
||||
|
||||
function readFldChar(element) {
|
||||
var type = element.attributes["w:fldCharType"];
|
||||
if (type === "begin") {
|
||||
complexFieldStack.push({type: "begin", fldChar: element});
|
||||
currentInstrText = [];
|
||||
} else if (type === "end") {
|
||||
if (complexFieldStack.length === 0) {
|
||||
return emptyResultWithMessages([warning(
|
||||
"Ignoring complex field end character without corresponding start character"
|
||||
)]);
|
||||
}
|
||||
|
||||
var complexFieldEnd = complexFieldStack.pop();
|
||||
if (complexFieldEnd.type === "begin") {
|
||||
complexFieldEnd = parseCurrentInstrText(complexFieldEnd);
|
||||
}
|
||||
if (complexFieldEnd.type === "checkbox") {
|
||||
return elementResult(documents.checkbox({
|
||||
checked: complexFieldEnd.checked
|
||||
}));
|
||||
}
|
||||
} else if (type === "separate") {
|
||||
if (complexFieldStack.length === 0) {
|
||||
return emptyResultWithMessages([warning(
|
||||
"Ignoring complex field separator character without corresponding start character"
|
||||
)]);
|
||||
}
|
||||
|
||||
var complexFieldSeparate = complexFieldStack.pop();
|
||||
var complexField = parseCurrentInstrText(complexFieldSeparate);
|
||||
complexFieldStack.push(complexField);
|
||||
}
|
||||
return emptyResult();
|
||||
}
|
||||
|
||||
function currentHyperlinkOptions() {
|
||||
var topHyperlink = _.last(complexFieldStack.filter(function(complexField) {
|
||||
return complexField.type === "hyperlink";
|
||||
}));
|
||||
return topHyperlink ? topHyperlink.options : null;
|
||||
}
|
||||
|
||||
function parseCurrentInstrText(complexField) {
|
||||
return parseInstrText(
|
||||
currentInstrText.join(''),
|
||||
complexField.type === "begin"
|
||||
? complexField.fldChar
|
||||
: xml.emptyElement
|
||||
);
|
||||
}
|
||||
|
||||
function parseInstrText(instrText, fldChar) {
|
||||
var linkResult = /^\s*HYPERLINK\s+(\\l\s+)?(?:"(.*)"|([^\\]\S*))/.exec(instrText);
|
||||
if (linkResult) {
|
||||
var location = linkResult[2] === undefined ? linkResult[3] : linkResult[2];
|
||||
var options = linkResult[1] === undefined ? {href: location} : {anchor: location};
|
||||
return {type: "hyperlink", options: options};
|
||||
}
|
||||
|
||||
var checkboxResult = /\s*FORMCHECKBOX\s*/.exec(instrText);
|
||||
if (checkboxResult) {
|
||||
var checkboxElement = fldChar
|
||||
.firstOrEmpty("w:ffData")
|
||||
.firstOrEmpty("w:checkBox");
|
||||
var checkedElement = checkboxElement.first("w:checked");
|
||||
var checked = checkedElement == null
|
||||
? readBooleanElement(checkboxElement.first("w:default"))
|
||||
: readBooleanElement(checkedElement);
|
||||
return {type: "checkbox", checked: checked};
|
||||
}
|
||||
|
||||
return {type: "unknown"};
|
||||
}
|
||||
|
||||
function readInstrText(element) {
|
||||
currentInstrText.push(element.text());
|
||||
return emptyResult();
|
||||
}
|
||||
|
||||
function readSymbol(element) {
|
||||
// See 17.3.3.30 sym (Symbol Character) of ECMA-376 4th edition Part 1
|
||||
var font = element.attributes["w:font"];
|
||||
var char = element.attributes["w:char"];
|
||||
var unicodeCharacter = dingbatToUnicode.hex(font, char);
|
||||
if (unicodeCharacter == null && /^F0..$/.test(char)) {
|
||||
unicodeCharacter = dingbatToUnicode.hex(font, char.substring(2));
|
||||
}
|
||||
|
||||
if (unicodeCharacter == null) {
|
||||
return emptyResultWithMessages([warning(
|
||||
"A w:sym element with an unsupported character was ignored: char " + char + " in font " + font
|
||||
)]);
|
||||
} else {
|
||||
return elementResult(new documents.Text(unicodeCharacter.string));
|
||||
}
|
||||
}
|
||||
|
||||
function noteReferenceReader(noteType) {
|
||||
return function(element) {
|
||||
var noteId = element.attributes["w:id"];
|
||||
return elementResult(new documents.NoteReference({
|
||||
noteType: noteType,
|
||||
noteId: noteId
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
function readCommentReference(element) {
|
||||
return elementResult(documents.commentReference({
|
||||
commentId: element.attributes["w:id"]
|
||||
}));
|
||||
}
|
||||
|
||||
function readChildElements(element) {
|
||||
return readXmlElements(element.children);
|
||||
}
|
||||
|
||||
var xmlElementReaders = {
|
||||
"w:p": function(element) {
|
||||
var paragraphPropertiesElement = element.firstOrEmpty("w:pPr");
|
||||
|
||||
var isDeleted = !!paragraphPropertiesElement
|
||||
.firstOrEmpty("w:rPr")
|
||||
.first("w:del");
|
||||
|
||||
if (isDeleted) {
|
||||
element.children.forEach(function(child) {
|
||||
deletedParagraphContents.push(child);
|
||||
});
|
||||
return emptyResult();
|
||||
} else {
|
||||
var childrenXml = element.children;
|
||||
if (deletedParagraphContents.length > 0) {
|
||||
childrenXml = deletedParagraphContents.concat(childrenXml);
|
||||
deletedParagraphContents = [];
|
||||
}
|
||||
return ReadResult.map(
|
||||
readParagraphProperties(paragraphPropertiesElement),
|
||||
readXmlElements(childrenXml),
|
||||
function(properties, children) {
|
||||
return new documents.Paragraph(children, properties);
|
||||
}
|
||||
).insertExtra();
|
||||
}
|
||||
},
|
||||
"w:r": function(element) {
|
||||
return ReadResult.map(
|
||||
readRunProperties(element.firstOrEmpty("w:rPr")),
|
||||
readXmlElements(element.children),
|
||||
function(properties, children) {
|
||||
var hyperlinkOptions = currentHyperlinkOptions();
|
||||
if (hyperlinkOptions !== null) {
|
||||
children = [new documents.Hyperlink(children, hyperlinkOptions)];
|
||||
}
|
||||
|
||||
return new documents.Run(children, properties);
|
||||
}
|
||||
);
|
||||
},
|
||||
"w:fldChar": readFldChar,
|
||||
"w:instrText": readInstrText,
|
||||
"w:t": function(element) {
|
||||
return elementResult(new documents.Text(element.text()));
|
||||
},
|
||||
"w:tab": function(element) {
|
||||
return elementResult(new documents.Tab());
|
||||
},
|
||||
"w:noBreakHyphen": function() {
|
||||
return elementResult(new documents.Text("\u2011"));
|
||||
},
|
||||
"w:softHyphen": function(element) {
|
||||
return elementResult(new documents.Text("\u00AD"));
|
||||
},
|
||||
"w:sym": readSymbol,
|
||||
"w:hyperlink": function(element) {
|
||||
var relationshipId = element.attributes["r:id"];
|
||||
var anchor = element.attributes["w:anchor"];
|
||||
return readXmlElements(element.children).map(function(children) {
|
||||
function create(options) {
|
||||
var targetFrame = element.attributes["w:tgtFrame"] || null;
|
||||
|
||||
return new documents.Hyperlink(
|
||||
children,
|
||||
_.extend({targetFrame: targetFrame}, options)
|
||||
);
|
||||
}
|
||||
|
||||
if (relationshipId) {
|
||||
var href = relationships.findTargetByRelationshipId(relationshipId);
|
||||
if (anchor) {
|
||||
href = uris.replaceFragment(href, anchor);
|
||||
}
|
||||
return create({href: href});
|
||||
} else if (anchor) {
|
||||
return create({anchor: anchor});
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
});
|
||||
},
|
||||
"w:tbl": readTable,
|
||||
"w:tr": readTableRow,
|
||||
"w:tc": readTableCell,
|
||||
"w:footnoteReference": noteReferenceReader("footnote"),
|
||||
"w:endnoteReference": noteReferenceReader("endnote"),
|
||||
"w:commentReference": readCommentReference,
|
||||
"w:br": function(element) {
|
||||
var breakType = element.attributes["w:type"];
|
||||
if (breakType == null || breakType === "textWrapping") {
|
||||
return elementResult(documents.lineBreak);
|
||||
} else if (breakType === "page") {
|
||||
return elementResult(documents.pageBreak);
|
||||
} else if (breakType === "column") {
|
||||
return elementResult(documents.columnBreak);
|
||||
} else {
|
||||
return emptyResultWithMessages([warning("Unsupported break type: " + breakType)]);
|
||||
}
|
||||
},
|
||||
"w:bookmarkStart": function(element){
|
||||
var name = element.attributes["w:name"];
|
||||
if (name === "_GoBack") {
|
||||
return emptyResult();
|
||||
} else {
|
||||
return elementResult(new documents.BookmarkStart({name: name}));
|
||||
}
|
||||
},
|
||||
|
||||
"mc:AlternateContent": function(element) {
|
||||
return readChildElements(element.firstOrEmpty("mc:Fallback"));
|
||||
},
|
||||
|
||||
"w:sdt": function(element) {
|
||||
var contentResult = readXmlElements(element.firstOrEmpty("w:sdtContent").children);
|
||||
return contentResult.map(function(content) {
|
||||
// From the WordML standard: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-docx/3350cb64-931f-41f7-8824-f18b2568ce66
|
||||
//
|
||||
// > A CT_SdtCheckbox element that specifies that the parent
|
||||
// > structured document tag is a checkbox when displayed in the
|
||||
// > document. The parent structured document tag contents MUST
|
||||
// > contain a single character and optionally an additional
|
||||
// > character in a deleted run.
|
||||
|
||||
var checkbox = element
|
||||
.firstOrEmpty("w:sdtPr")
|
||||
.first("wordml:checkbox");
|
||||
|
||||
if (checkbox) {
|
||||
var checkedElement = checkbox.first("wordml:checked");
|
||||
var isChecked = !!checkedElement && readBooleanAttributeValue(
|
||||
checkedElement.attributes["wordml:val"]
|
||||
);
|
||||
var documentCheckbox = documents.checkbox({
|
||||
checked: isChecked
|
||||
});
|
||||
|
||||
var hasCheckbox = false;
|
||||
var replacedContent = content.map(transforms._elementsOfType(
|
||||
documents.types.text,
|
||||
function(text) {
|
||||
if (text.value.length > 0 && !hasCheckbox) {
|
||||
hasCheckbox = true;
|
||||
return documentCheckbox;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
if (hasCheckbox) {
|
||||
return replacedContent;
|
||||
} else {
|
||||
return documentCheckbox;
|
||||
}
|
||||
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
"w:customXml": readChildElements,
|
||||
"w:ins": readChildElements,
|
||||
"w:moveFromRangeEnd": readChildElements,
|
||||
"w:moveFromRangeStart": readChildElements,
|
||||
"w:moveTo": readChildElements,
|
||||
"w:moveToRangeEnd": readChildElements,
|
||||
"w:moveToRangeStart": readChildElements,
|
||||
"w:object": readChildElements,
|
||||
"w:smartTag": readChildElements,
|
||||
"w:drawing": readChildElements,
|
||||
"w:pict": function(element) {
|
||||
return readChildElements(element).toExtra();
|
||||
},
|
||||
"v:roundrect": readChildElements,
|
||||
"v:shape": readChildElements,
|
||||
"v:textbox": readChildElements,
|
||||
"w:txbxContent": readChildElements,
|
||||
"wp:inline": readDrawingElement,
|
||||
"wp:anchor": readDrawingElement,
|
||||
"v:imagedata": readImageData,
|
||||
"v:group": readChildElements,
|
||||
"v:rect": readChildElements
|
||||
};
|
||||
|
||||
return {
|
||||
readXmlElement: readXmlElement,
|
||||
readXmlElements: readXmlElements
|
||||
};
|
||||
|
||||
|
||||
function readTable(element) {
|
||||
var propertiesResult = readTableProperties(element.firstOrEmpty("w:tblPr"));
|
||||
return readXmlElements(element.children)
|
||||
.flatMap(calculateRowSpans)
|
||||
.flatMap(function(children) {
|
||||
return propertiesResult.map(function(properties) {
|
||||
return documents.Table(children, properties);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readTableProperties(element) {
|
||||
return readTableStyle(element).map(function(style) {
|
||||
return {
|
||||
styleId: style.styleId,
|
||||
styleName: style.name
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function readTableRow(element) {
|
||||
var properties = element.firstOrEmpty("w:trPr");
|
||||
|
||||
// See 17.13.5.12 del (Deleted Table Row) of ECMA-376 4th edition Part 1
|
||||
var isDeleted = !!properties.first("w:del");
|
||||
if (isDeleted) {
|
||||
return emptyResult();
|
||||
}
|
||||
|
||||
var isHeader = !!properties.first("w:tblHeader");
|
||||
return readXmlElements(element.children).map(function(children) {
|
||||
return documents.TableRow(children, {isHeader: isHeader});
|
||||
});
|
||||
}
|
||||
|
||||
function readTableCell(element) {
|
||||
return readXmlElements(element.children).map(function(children) {
|
||||
var properties = element.firstOrEmpty("w:tcPr");
|
||||
|
||||
var gridSpan = properties.firstOrEmpty("w:gridSpan").attributes["w:val"];
|
||||
var colSpan = gridSpan ? parseInt(gridSpan, 10) : 1;
|
||||
|
||||
var cell = documents.TableCell(children, {colSpan: colSpan});
|
||||
cell._vMerge = readVMerge(properties);
|
||||
return cell;
|
||||
});
|
||||
}
|
||||
|
||||
function readVMerge(properties) {
|
||||
var element = properties.first("w:vMerge");
|
||||
if (element) {
|
||||
var val = element.attributes["w:val"];
|
||||
return val === "continue" || !val;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateRowSpans(rows) {
|
||||
var unexpectedNonRows = _.any(rows, function(row) {
|
||||
return row.type !== documents.types.tableRow;
|
||||
});
|
||||
if (unexpectedNonRows) {
|
||||
removeVMergeProperties(rows);
|
||||
return elementResultWithMessages(rows, [warning(
|
||||
"unexpected non-row element in table, cell merging may be incorrect"
|
||||
)]);
|
||||
}
|
||||
var unexpectedNonCells = _.any(rows, function(row) {
|
||||
return _.any(row.children, function(cell) {
|
||||
return cell.type !== documents.types.tableCell;
|
||||
});
|
||||
});
|
||||
if (unexpectedNonCells) {
|
||||
removeVMergeProperties(rows);
|
||||
return elementResultWithMessages(rows, [warning(
|
||||
"unexpected non-cell element in table row, cell merging may be incorrect"
|
||||
)]);
|
||||
}
|
||||
|
||||
var columns = {};
|
||||
|
||||
rows.forEach(function(row) {
|
||||
var cellIndex = 0;
|
||||
row.children.forEach(function(cell) {
|
||||
if (cell._vMerge && columns[cellIndex]) {
|
||||
columns[cellIndex].rowSpan++;
|
||||
} else {
|
||||
columns[cellIndex] = cell;
|
||||
cell._vMerge = false;
|
||||
}
|
||||
cellIndex += cell.colSpan;
|
||||
});
|
||||
});
|
||||
|
||||
rows.forEach(function(row) {
|
||||
row.children = row.children.filter(function(cell) {
|
||||
return !cell._vMerge;
|
||||
});
|
||||
row.children.forEach(function(cell) {
|
||||
delete cell._vMerge;
|
||||
});
|
||||
});
|
||||
|
||||
return elementResult(rows);
|
||||
}
|
||||
|
||||
function removeVMergeProperties(rows) {
|
||||
rows.forEach(function(row) {
|
||||
var cells = transforms.getDescendantsOfType(row, documents.types.tableCell);
|
||||
cells.forEach(function(cell) {
|
||||
delete cell._vMerge;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readDrawingElement(element) {
|
||||
var blips = element
|
||||
.getElementsByTagName("a:graphic")
|
||||
.getElementsByTagName("a:graphicData")
|
||||
.getElementsByTagName("pic:pic")
|
||||
.getElementsByTagName("pic:blipFill")
|
||||
.getElementsByTagName("a:blip");
|
||||
|
||||
return combineResults(blips.map(readBlip.bind(null, element)));
|
||||
}
|
||||
|
||||
function readBlip(element, blip) {
|
||||
var propertiesElement = element.firstOrEmpty("wp:docPr");
|
||||
var properties = propertiesElement.attributes;
|
||||
|
||||
var altText = isBlank(properties.descr) ? properties.title : properties.descr;
|
||||
|
||||
var blipImageFile = findBlipImageFile(blip);
|
||||
if (blipImageFile === null) {
|
||||
return emptyResultWithMessages([warning("Could not find image file for a:blip element")]);
|
||||
}
|
||||
|
||||
return readImage(blipImageFile, altText).map(function(imageElement) {
|
||||
var hlinkClickElement = propertiesElement.firstOrEmpty("a:hlinkClick");
|
||||
var relationshipId = hlinkClickElement.attributes["r:id"];
|
||||
if (relationshipId) {
|
||||
var href = relationships.findTargetByRelationshipId(relationshipId);
|
||||
return new documents.Hyperlink([imageElement], {href: href});
|
||||
} else {
|
||||
return imageElement;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isBlank(value) {
|
||||
return value == null || /^\s*$/.test(value);
|
||||
}
|
||||
|
||||
function findBlipImageFile(blip) {
|
||||
var embedRelationshipId = blip.attributes["r:embed"];
|
||||
var linkRelationshipId = blip.attributes["r:link"];
|
||||
if (embedRelationshipId) {
|
||||
return findEmbeddedImageFile(embedRelationshipId);
|
||||
} else if (linkRelationshipId) {
|
||||
var imagePath = relationships.findTargetByRelationshipId(linkRelationshipId);
|
||||
return {
|
||||
path: imagePath,
|
||||
read: files.read.bind(files, imagePath)
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readImageData(element) {
|
||||
var relationshipId = element.attributes['r:id'];
|
||||
|
||||
if (relationshipId) {
|
||||
return readImage(
|
||||
findEmbeddedImageFile(relationshipId),
|
||||
element.attributes["o:title"]);
|
||||
} else {
|
||||
return emptyResultWithMessages([warning("A v:imagedata element without a relationship ID was ignored")]);
|
||||
}
|
||||
}
|
||||
|
||||
function findEmbeddedImageFile(relationshipId) {
|
||||
var path = uris.uriToZipEntryName("word", relationships.findTargetByRelationshipId(relationshipId));
|
||||
return {
|
||||
path: path,
|
||||
read: docxFile.read.bind(docxFile, path)
|
||||
};
|
||||
}
|
||||
|
||||
function readImage(imageFile, altText) {
|
||||
var contentType = contentTypes.findContentType(imageFile.path);
|
||||
|
||||
var image = documents.Image({
|
||||
readImage: imageFile.read,
|
||||
altText: altText,
|
||||
contentType: contentType
|
||||
});
|
||||
var warnings = supportedImageTypes[contentType] ?
|
||||
[] : warning("Image of type " + contentType + " is unlikely to display in web browsers");
|
||||
return elementResultWithMessages(image, warnings);
|
||||
}
|
||||
|
||||
function undefinedStyleWarning(type, styleId) {
|
||||
return warning(
|
||||
type + " style with ID " + styleId + " was referenced but not defined in the document");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (styleId != null) {
|
||||
var levelByStyleId = numbering.findLevelByParagraphStyleId(styleId);
|
||||
if (levelByStyleId != null) {
|
||||
return levelByStyleId;
|
||||
}
|
||||
}
|
||||
|
||||
// Some malformed documents define numbering levels without an index, and
|
||||
// reference the numbering using a w:numPr element without a w:ilvl child.
|
||||
// To handle such cases, we assume a level of 0 as a fallback.
|
||||
if (numId !== undefined) {
|
||||
return numbering.findLevel(numId, "0");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var supportedImageTypes = {
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/jpeg": true,
|
||||
"image/svg+xml": true,
|
||||
"image/tiff": true
|
||||
};
|
||||
|
||||
var ignoreElements = {
|
||||
"office-word:wrap": true,
|
||||
"v:shadow": true,
|
||||
"v:shapetype": true,
|
||||
"w:annotationRef": true,
|
||||
"w:bookmarkEnd": true,
|
||||
"w:sectPr": true,
|
||||
"w:proofErr": true,
|
||||
"w:lastRenderedPageBreak": true,
|
||||
"w:commentRangeStart": true,
|
||||
"w:commentRangeEnd": true,
|
||||
"w:del": true,
|
||||
"w:footnoteRef": true,
|
||||
"w:endnoteRef": true,
|
||||
"w:moveFrom": true,
|
||||
"w:pPr": true,
|
||||
"w:rPr": true,
|
||||
"w:tblPr": true,
|
||||
"w:tblGrid": true,
|
||||
"w:trPr": true,
|
||||
"w:tcPr": true
|
||||
};
|
||||
|
||||
function emptyResultWithMessages(messages) {
|
||||
return new ReadResult(null, null, messages);
|
||||
}
|
||||
|
||||
function emptyResult() {
|
||||
return new ReadResult(null);
|
||||
}
|
||||
|
||||
function elementResult(element) {
|
||||
return new ReadResult(element);
|
||||
}
|
||||
|
||||
function elementResultWithMessages(element, messages) {
|
||||
return new ReadResult(element, null, messages);
|
||||
}
|
||||
|
||||
function ReadResult(element, extra, messages) {
|
||||
this.value = element || [];
|
||||
this.extra = extra || [];
|
||||
this._result = new Result({
|
||||
element: this.value,
|
||||
extra: extra
|
||||
}, messages);
|
||||
this.messages = this._result.messages;
|
||||
}
|
||||
|
||||
ReadResult.prototype.toExtra = function() {
|
||||
return new ReadResult(null, joinElements(this.extra, this.value), this.messages);
|
||||
};
|
||||
|
||||
ReadResult.prototype.insertExtra = function() {
|
||||
var extra = this.extra;
|
||||
if (extra && extra.length) {
|
||||
return new ReadResult(joinElements(this.value, extra), null, this.messages);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
ReadResult.prototype.map = function(func) {
|
||||
var result = this._result.map(function(value) {
|
||||
return func(value.element);
|
||||
});
|
||||
return new ReadResult(result.value, this.extra, result.messages);
|
||||
};
|
||||
|
||||
ReadResult.prototype.flatMap = function(func) {
|
||||
var result = this._result.flatMap(function(value) {
|
||||
return func(value.element)._result;
|
||||
});
|
||||
return new ReadResult(result.value.element, joinElements(this.extra, result.value.extra), result.messages);
|
||||
};
|
||||
|
||||
ReadResult.map = function(first, second, func) {
|
||||
return new ReadResult(
|
||||
func(first.value, second.value),
|
||||
joinElements(first.extra, second.extra),
|
||||
first.messages.concat(second.messages)
|
||||
);
|
||||
};
|
||||
|
||||
function combineResults(results) {
|
||||
var result = Result.combine(_.pluck(results, "_result"));
|
||||
return new ReadResult(
|
||||
_.flatten(_.pluck(result.value, "element")),
|
||||
_.filter(_.flatten(_.pluck(result.value, "extra")), identity),
|
||||
result.messages
|
||||
);
|
||||
}
|
||||
|
||||
function joinElements(first, second) {
|
||||
return _.flatten([first, second]);
|
||||
}
|
||||
|
||||
function identity(value) {
|
||||
return value;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
var documents = require("../documents");
|
||||
var Result = require("../results").Result;
|
||||
|
||||
function createCommentsReader(bodyReader) {
|
||||
function readCommentsXml(element) {
|
||||
return Result.combine(element.getElementsByTagName("w:comment")
|
||||
.map(readCommentElement));
|
||||
}
|
||||
|
||||
function readCommentElement(element) {
|
||||
var id = element.attributes["w:id"];
|
||||
|
||||
function readOptionalAttribute(name) {
|
||||
return (element.attributes[name] || "").trim() || null;
|
||||
}
|
||||
|
||||
return bodyReader.readXmlElements(element.children)
|
||||
.map(function(body) {
|
||||
return documents.comment({
|
||||
commentId: id,
|
||||
body: body,
|
||||
authorName: readOptionalAttribute("w:author"),
|
||||
authorInitials: readOptionalAttribute("w:initials")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return readCommentsXml;
|
||||
}
|
||||
|
||||
exports.createCommentsReader = createCommentsReader;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
exports.readContentTypesFromXml = readContentTypesFromXml;
|
||||
|
||||
var fallbackContentTypes = {
|
||||
"png": "png",
|
||||
"gif": "gif",
|
||||
"jpeg": "jpeg",
|
||||
"jpg": "jpeg",
|
||||
"tif": "tiff",
|
||||
"tiff": "tiff",
|
||||
"bmp": "bmp"
|
||||
};
|
||||
|
||||
exports.defaultContentTypes = contentTypes({}, {});
|
||||
|
||||
|
||||
function readContentTypesFromXml(element) {
|
||||
var extensionDefaults = Object.create(null);
|
||||
var overrides = Object.create(null);
|
||||
|
||||
element.children.forEach(function(child) {
|
||||
if (child.name === "content-types:Default") {
|
||||
extensionDefaults[child.attributes.Extension] = child.attributes.ContentType;
|
||||
}
|
||||
if (child.name === "content-types:Override") {
|
||||
var name = child.attributes.PartName;
|
||||
if (name.charAt(0) === "/") {
|
||||
name = name.substring(1);
|
||||
}
|
||||
overrides[name] = child.attributes.ContentType;
|
||||
}
|
||||
});
|
||||
return contentTypes(overrides, extensionDefaults);
|
||||
}
|
||||
|
||||
function contentTypes(overrides, extensionDefaults) {
|
||||
return {
|
||||
findContentType: function(path) {
|
||||
var overrideContentType = overrides[path];
|
||||
if (overrideContentType) {
|
||||
return overrideContentType;
|
||||
} else {
|
||||
var pathParts = path.split(".");
|
||||
var extension = pathParts[pathParts.length - 1];
|
||||
if (Object.prototype.hasOwnProperty.call(extensionDefaults, extension)) {
|
||||
return extensionDefaults[extension];
|
||||
} else {
|
||||
var fallback = fallbackContentTypes[extension.toLowerCase()];
|
||||
if (fallback) {
|
||||
return "image/" + fallback;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
exports.DocumentXmlReader = DocumentXmlReader;
|
||||
|
||||
var documents = require("../documents");
|
||||
var Result = require("../results").Result;
|
||||
|
||||
|
||||
function DocumentXmlReader(options) {
|
||||
var bodyReader = options.bodyReader;
|
||||
|
||||
function convertXmlToDocument(element) {
|
||||
var body = element.first("w:body");
|
||||
|
||||
if (body == null) {
|
||||
throw new Error("Could not find the body element: are you sure this is a docx file?");
|
||||
}
|
||||
|
||||
var result = bodyReader.readXmlElements(body.children)
|
||||
.map(function(children) {
|
||||
return new documents.Document(children, {
|
||||
notes: options.notes,
|
||||
comments: options.comments
|
||||
});
|
||||
});
|
||||
return new Result(result.value, result.messages);
|
||||
}
|
||||
|
||||
return {
|
||||
convertXmlToDocument: convertXmlToDocument
|
||||
};
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
exports.read = read;
|
||||
exports._findPartPaths = findPartPaths;
|
||||
|
||||
var promises = require("../promises");
|
||||
var documents = require("../documents");
|
||||
var Result = require("../results").Result;
|
||||
var zipfile = require("../zipfile");
|
||||
|
||||
var readXmlFromZipFile = require("./office-xml-reader").readXmlFromZipFile;
|
||||
var createBodyReader = require("./body-reader").createBodyReader;
|
||||
var DocumentXmlReader = require("./document-xml-reader").DocumentXmlReader;
|
||||
var relationshipsReader = require("./relationships-reader");
|
||||
var contentTypesReader = require("./content-types-reader");
|
||||
var numberingXml = require("./numbering-xml");
|
||||
var stylesReader = require("./styles-reader");
|
||||
var notesReader = require("./notes-reader");
|
||||
var commentsReader = require("./comments-reader");
|
||||
var Files = require("./files").Files;
|
||||
|
||||
|
||||
function read(docxFile, input, options) {
|
||||
input = input || {};
|
||||
options = options || {};
|
||||
|
||||
var files = new Files({
|
||||
externalFileAccess: options.externalFileAccess,
|
||||
relativeToFile: input.path
|
||||
});
|
||||
|
||||
return promises.props({
|
||||
contentTypes: readContentTypesFromZipFile(docxFile),
|
||||
partPaths: findPartPaths(docxFile),
|
||||
docxFile: docxFile,
|
||||
files: files
|
||||
}).also(function(result) {
|
||||
return {
|
||||
styles: readStylesFromZipFile(docxFile, result.partPaths.styles)
|
||||
};
|
||||
}).also(function(result) {
|
||||
return {
|
||||
numbering: readNumberingFromZipFile(docxFile, result.partPaths.numbering, result.styles)
|
||||
};
|
||||
}).also(function(result) {
|
||||
return {
|
||||
footnotes: readXmlFileWithBody(result.partPaths.footnotes, result, function(bodyReader, xml) {
|
||||
if (xml) {
|
||||
return notesReader.createFootnotesReader(bodyReader)(xml);
|
||||
} else {
|
||||
return new Result([]);
|
||||
}
|
||||
}),
|
||||
endnotes: readXmlFileWithBody(result.partPaths.endnotes, result, function(bodyReader, xml) {
|
||||
if (xml) {
|
||||
return notesReader.createEndnotesReader(bodyReader)(xml);
|
||||
} else {
|
||||
return new Result([]);
|
||||
}
|
||||
}),
|
||||
comments: readXmlFileWithBody(result.partPaths.comments, result, function(bodyReader, xml) {
|
||||
if (xml) {
|
||||
return commentsReader.createCommentsReader(bodyReader)(xml);
|
||||
} else {
|
||||
return new Result([]);
|
||||
}
|
||||
})
|
||||
};
|
||||
}).also(function(result) {
|
||||
return {
|
||||
notes: result.footnotes.flatMap(function(footnotes) {
|
||||
return result.endnotes.map(function(endnotes) {
|
||||
return new documents.Notes(footnotes.concat(endnotes));
|
||||
});
|
||||
})
|
||||
};
|
||||
}).then(function(result) {
|
||||
return readXmlFileWithBody(result.partPaths.mainDocument, result, function(bodyReader, xml) {
|
||||
return result.notes.flatMap(function(notes) {
|
||||
return result.comments.flatMap(function(comments) {
|
||||
var reader = new DocumentXmlReader({
|
||||
bodyReader: bodyReader,
|
||||
notes: notes,
|
||||
comments: comments
|
||||
});
|
||||
return reader.convertXmlToDocument(xml);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function findPartPaths(docxFile) {
|
||||
return readPackageRelationships(docxFile).then(function(packageRelationships) {
|
||||
var mainDocumentPath = findPartPath({
|
||||
docxFile: docxFile,
|
||||
relationships: packageRelationships,
|
||||
relationshipType: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
||||
basePath: "",
|
||||
fallbackPath: "word/document.xml"
|
||||
});
|
||||
|
||||
if (!docxFile.exists(mainDocumentPath)) {
|
||||
throw new Error("Could not find main document part. Are you sure this is a valid .docx file?");
|
||||
}
|
||||
|
||||
return xmlFileReader({
|
||||
filename: relationshipsFilename(mainDocumentPath),
|
||||
readElement: relationshipsReader.readRelationships,
|
||||
defaultValue: relationshipsReader.defaultValue
|
||||
})(docxFile).then(function(documentRelationships) {
|
||||
function findPartRelatedToMainDocument(name) {
|
||||
return findPartPath({
|
||||
docxFile: docxFile,
|
||||
relationships: documentRelationships,
|
||||
relationshipType: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/" + name,
|
||||
basePath: zipfile.splitPath(mainDocumentPath).dirname,
|
||||
fallbackPath: "word/" + name + ".xml"
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
mainDocument: mainDocumentPath,
|
||||
comments: findPartRelatedToMainDocument("comments"),
|
||||
endnotes: findPartRelatedToMainDocument("endnotes"),
|
||||
footnotes: findPartRelatedToMainDocument("footnotes"),
|
||||
numbering: findPartRelatedToMainDocument("numbering"),
|
||||
styles: findPartRelatedToMainDocument("styles")
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function findPartPath(options) {
|
||||
var docxFile = options.docxFile;
|
||||
var relationships = options.relationships;
|
||||
var relationshipType = options.relationshipType;
|
||||
var basePath = options.basePath;
|
||||
var fallbackPath = options.fallbackPath;
|
||||
|
||||
var targets = relationships.findTargetsByType(relationshipType);
|
||||
var normalisedTargets = targets.map(function(target) {
|
||||
return stripPrefix(zipfile.joinPath(basePath, target), "/");
|
||||
});
|
||||
var validTargets = normalisedTargets.filter(function(target) {
|
||||
return docxFile.exists(target);
|
||||
});
|
||||
if (validTargets.length === 0) {
|
||||
return fallbackPath;
|
||||
} else {
|
||||
return validTargets[0];
|
||||
}
|
||||
}
|
||||
|
||||
function stripPrefix(value, prefix) {
|
||||
if (value.substring(0, prefix.length) === prefix) {
|
||||
return value.substring(prefix.length);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function xmlFileReader(options) {
|
||||
return function(zipFile) {
|
||||
return readXmlFromZipFile(zipFile, options.filename)
|
||||
.then(function(element) {
|
||||
return element ? options.readElement(element) : options.defaultValue;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function readXmlFileWithBody(filename, options, func) {
|
||||
var readRelationshipsFromZipFile = xmlFileReader({
|
||||
filename: relationshipsFilename(filename),
|
||||
readElement: relationshipsReader.readRelationships,
|
||||
defaultValue: relationshipsReader.defaultValue
|
||||
});
|
||||
|
||||
return readRelationshipsFromZipFile(options.docxFile).then(function(relationships) {
|
||||
var bodyReader = new createBodyReader({
|
||||
relationships: relationships,
|
||||
contentTypes: options.contentTypes,
|
||||
docxFile: options.docxFile,
|
||||
numbering: options.numbering,
|
||||
styles: options.styles,
|
||||
files: options.files
|
||||
});
|
||||
return readXmlFromZipFile(options.docxFile, filename)
|
||||
.then(function(xml) {
|
||||
return func(bodyReader, xml);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function relationshipsFilename(filename) {
|
||||
var split = zipfile.splitPath(filename);
|
||||
return zipfile.joinPath(split.dirname, "_rels", split.basename + ".rels");
|
||||
}
|
||||
|
||||
var readContentTypesFromZipFile = xmlFileReader({
|
||||
filename: "[Content_Types].xml",
|
||||
readElement: contentTypesReader.readContentTypesFromXml,
|
||||
defaultValue: contentTypesReader.defaultContentTypes
|
||||
});
|
||||
|
||||
function readNumberingFromZipFile(zipFile, path, styles) {
|
||||
return xmlFileReader({
|
||||
filename: path,
|
||||
readElement: function(element) {
|
||||
return numberingXml.readNumberingXml(element, {styles: styles});
|
||||
},
|
||||
defaultValue: numberingXml.defaultNumbering
|
||||
})(zipFile);
|
||||
}
|
||||
|
||||
function readStylesFromZipFile(zipFile, path) {
|
||||
return xmlFileReader({
|
||||
filename: path,
|
||||
readElement: stylesReader.readStylesXml,
|
||||
defaultValue: stylesReader.defaultStyles
|
||||
})(zipFile);
|
||||
}
|
||||
|
||||
var readPackageRelationships = xmlFileReader({
|
||||
filename: "_rels/.rels",
|
||||
readElement: relationshipsReader.readRelationships,
|
||||
defaultValue: relationshipsReader.defaultValue
|
||||
});
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
var url = require("url");
|
||||
var os = require("os");
|
||||
var dirname = require("path").dirname;
|
||||
var isAbsolutePath = require("path").isAbsolute;
|
||||
var resolvePath = require("path").resolve;
|
||||
|
||||
var fs = require("../fs");
|
||||
var promises = require("../promises");
|
||||
|
||||
|
||||
exports.Files = Files;
|
||||
exports.uriToPath = uriToPath;
|
||||
|
||||
|
||||
function Files(options) {
|
||||
options = options || {};
|
||||
if (!options.externalFileAccess) {
|
||||
return {
|
||||
read: function(uri) {
|
||||
return promises.reject(new Error("could not read external image '" + uri + "', external file access is disabled"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var base = options.relativeToFile ? dirname(options.relativeToFile) : null;
|
||||
|
||||
function read(uri, encoding) {
|
||||
return resolveUri(uri).then(function(path) {
|
||||
return fs.readFile(path, encoding).catch(function(error) {
|
||||
var message = "could not open external image: '" + uri + "' (document directory: '" + base + "')\n" + error.message;
|
||||
return promises.reject(new Error(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resolveUri(uri) {
|
||||
var path = uriToPath(uri);
|
||||
if (isAbsolutePath(path)) {
|
||||
return promises.resolve(path);
|
||||
} else if (base) {
|
||||
return promises.resolve(resolvePath(base, path));
|
||||
} else {
|
||||
return promises.reject(new Error("could not find external image '" + uri + "', path of input document is unknown"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
read: read
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function uriToPath(uriString, platform) {
|
||||
if (!platform) {
|
||||
platform = os.platform();
|
||||
}
|
||||
|
||||
var uri = url.parse(uriString);
|
||||
if (isLocalFileUri(uri) || isRelativeUri(uri)) {
|
||||
var path = decodeURIComponent(uri.path);
|
||||
if (platform === "win32" && /^\/[a-z]:/i.test(path)) {
|
||||
return path.slice(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Could not convert URI to path: " + uriString);
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalFileUri(uri) {
|
||||
return uri.protocol === "file:" && (!uri.host || uri.host === "localhost");
|
||||
}
|
||||
|
||||
function isRelativeUri(uri) {
|
||||
return !uri.protocol && !uri.host;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
var documents = require("../documents");
|
||||
var Result = require("../results").Result;
|
||||
|
||||
exports.createFootnotesReader = createReader.bind(this, "footnote");
|
||||
exports.createEndnotesReader = createReader.bind(this, "endnote");
|
||||
|
||||
function createReader(noteType, bodyReader) {
|
||||
function readNotesXml(element) {
|
||||
return Result.combine(element.getElementsByTagName("w:" + noteType)
|
||||
.filter(isFootnoteElement)
|
||||
.map(readFootnoteElement));
|
||||
}
|
||||
|
||||
function isFootnoteElement(element) {
|
||||
var type = element.attributes["w:type"];
|
||||
return type !== "continuationSeparator" && type !== "separator";
|
||||
}
|
||||
|
||||
function readFootnoteElement(footnoteElement) {
|
||||
var id = footnoteElement.attributes["w:id"];
|
||||
return bodyReader.readXmlElements(footnoteElement.children)
|
||||
.map(function(body) {
|
||||
return documents.Note({noteType: noteType, noteId: id, body: body});
|
||||
});
|
||||
}
|
||||
|
||||
return readNotesXml;
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
var _ = require("underscore");
|
||||
|
||||
exports.readNumberingXml = readNumberingXml;
|
||||
exports.Numbering = Numbering;
|
||||
exports.defaultNumbering = new Numbering({}, {});
|
||||
|
||||
function Numbering(nums, abstractNums, styles) {
|
||||
var allLevels = _.flatten(_.values(abstractNums).map(function(abstractNum) {
|
||||
return _.values(abstractNum.levels);
|
||||
}));
|
||||
|
||||
var levelsByParagraphStyleId = _.indexBy(
|
||||
allLevels.filter(function(level) {
|
||||
return level.paragraphStyleId != null;
|
||||
}),
|
||||
"paragraphStyleId"
|
||||
);
|
||||
|
||||
function findLevel(numId, level) {
|
||||
return findLevelWithSeenNumIds(numId, level, Object.create(null));
|
||||
}
|
||||
|
||||
function findLevelWithSeenNumIds(numId, level, seenNumIds) {
|
||||
if (seenNumIds[numId]) {
|
||||
return null;
|
||||
}
|
||||
seenNumIds[numId] = true;
|
||||
|
||||
var num = nums[numId];
|
||||
if (!num) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var abstractNum = abstractNums[num.abstractNumId];
|
||||
if (!abstractNum) {
|
||||
return null;
|
||||
} else if (abstractNum.numStyleLink == null) {
|
||||
return abstractNums[num.abstractNumId].levels[level];
|
||||
} else {
|
||||
var style = styles.findNumberingStyleById(abstractNum.numStyleLink);
|
||||
return findLevelWithSeenNumIds(style.numId, level, seenNumIds);
|
||||
}
|
||||
}
|
||||
|
||||
function findLevelByParagraphStyleId(styleId) {
|
||||
return levelsByParagraphStyleId[styleId] || null;
|
||||
}
|
||||
|
||||
return {
|
||||
findLevel: findLevel,
|
||||
findLevelByParagraphStyleId: findLevelByParagraphStyleId
|
||||
};
|
||||
}
|
||||
|
||||
function readNumberingXml(root, options) {
|
||||
if (!options || !options.styles) {
|
||||
throw new Error("styles is missing");
|
||||
}
|
||||
|
||||
var abstractNums = readAbstractNums(root);
|
||||
var nums = readNums(root, abstractNums);
|
||||
return new Numbering(nums, abstractNums, options.styles);
|
||||
}
|
||||
|
||||
function readAbstractNums(root) {
|
||||
var abstractNums = Object.create(null);
|
||||
root.getElementsByTagName("w:abstractNum").forEach(function(element) {
|
||||
var id = element.attributes["w:abstractNumId"];
|
||||
abstractNums[id] = readAbstractNum(element);
|
||||
});
|
||||
return abstractNums;
|
||||
}
|
||||
|
||||
function readAbstractNum(element) {
|
||||
var levels = Object.create(null);
|
||||
|
||||
// Some malformed documents define numbering levels without an index, and
|
||||
// reference the numbering using a w:numPr element without a w:ilvl child.
|
||||
// To handle such cases, we assume a level of 0 as a fallback.
|
||||
var levelWithoutIndex = null;
|
||||
|
||||
element.getElementsByTagName("w:lvl").forEach(function(levelElement) {
|
||||
var levelIndex = levelElement.attributes["w:ilvl"];
|
||||
var numFmt = levelElement.firstOrEmpty("w:numFmt").attributes["w:val"];
|
||||
var isOrdered = numFmt !== "bullet";
|
||||
var paragraphStyleId = levelElement.firstOrEmpty("w:pStyle").attributes["w:val"];
|
||||
|
||||
if (levelIndex === undefined) {
|
||||
levelWithoutIndex = {
|
||||
isOrdered: isOrdered,
|
||||
level: "0",
|
||||
paragraphStyleId: paragraphStyleId
|
||||
};
|
||||
} else {
|
||||
levels[levelIndex] = {
|
||||
isOrdered: isOrdered,
|
||||
level: levelIndex,
|
||||
paragraphStyleId: paragraphStyleId
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (levelWithoutIndex !== null && levels[levelWithoutIndex.level] === undefined) {
|
||||
levels[levelWithoutIndex.level] = levelWithoutIndex;
|
||||
}
|
||||
|
||||
var numStyleLink = element.firstOrEmpty("w:numStyleLink").attributes["w:val"];
|
||||
|
||||
return {levels: levels, numStyleLink: numStyleLink};
|
||||
}
|
||||
|
||||
function readNums(root) {
|
||||
var nums = Object.create(null);
|
||||
root.getElementsByTagName("w:num").forEach(function(element) {
|
||||
var numId = element.attributes["w:numId"];
|
||||
var abstractNumId = element.first("w:abstractNumId").attributes["w:val"];
|
||||
nums[numId] = {abstractNumId: abstractNumId};
|
||||
});
|
||||
return nums;
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
var _ = require("underscore");
|
||||
|
||||
var promises = require("../promises");
|
||||
var xml = require("../xml");
|
||||
|
||||
|
||||
exports.read = read;
|
||||
exports.readXmlFromZipFile = readXmlFromZipFile;
|
||||
|
||||
var xmlNamespaceMap = {
|
||||
// Transitional format
|
||||
"http://schemas.openxmlformats.org/wordprocessingml/2006/main": "w",
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships": "r",
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing": "wp",
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/main": "a",
|
||||
"http://schemas.openxmlformats.org/drawingml/2006/picture": "pic",
|
||||
|
||||
// Strict format
|
||||
"http://purl.oclc.org/ooxml/wordprocessingml/main": "w",
|
||||
"http://purl.oclc.org/ooxml/officeDocument/relationships": "r",
|
||||
"http://purl.oclc.org/ooxml/drawingml/wordprocessingDrawing": "wp",
|
||||
"http://purl.oclc.org/ooxml/drawingml/main": "a",
|
||||
"http://purl.oclc.org/ooxml/drawingml/picture": "pic",
|
||||
|
||||
// Common
|
||||
"http://schemas.openxmlformats.org/package/2006/content-types": "content-types",
|
||||
"http://schemas.openxmlformats.org/package/2006/relationships": "relationships",
|
||||
"http://schemas.openxmlformats.org/markup-compatibility/2006": "mc",
|
||||
"urn:schemas-microsoft-com:vml": "v",
|
||||
"urn:schemas-microsoft-com:office:word": "office-word",
|
||||
|
||||
// [MS-DOCX]: Word Extensions to the Office Open XML (.docx) File Format
|
||||
// https://learn.microsoft.com/en-us/openspecs/office_standards/ms-docx/b839fe1f-e1ca-4fa6-8c26-5954d0abbccd
|
||||
"http://schemas.microsoft.com/office/word/2010/wordml": "wordml"
|
||||
};
|
||||
|
||||
|
||||
function read(xmlString) {
|
||||
return xml.readString(xmlString, xmlNamespaceMap)
|
||||
.then(function(document) {
|
||||
return collapseAlternateContent(document)[0];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function readXmlFromZipFile(docxFile, path) {
|
||||
if (docxFile.exists(path)) {
|
||||
return docxFile.read(path, "utf-8")
|
||||
.then(stripUtf8Bom)
|
||||
.then(read);
|
||||
} else {
|
||||
return promises.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stripUtf8Bom(xmlString) {
|
||||
return xmlString.replace(/^\uFEFF/g, '');
|
||||
}
|
||||
|
||||
|
||||
function collapseAlternateContent(node) {
|
||||
if (node.type === "element") {
|
||||
if (node.name === "mc:AlternateContent") {
|
||||
return node.firstOrEmpty("mc:Fallback").children;
|
||||
} else {
|
||||
node.children = _.flatten(node.children.map(collapseAlternateContent, true));
|
||||
return [node];
|
||||
}
|
||||
} else {
|
||||
return [node];
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
exports.readRelationships = readRelationships;
|
||||
exports.defaultValue = new Relationships([]);
|
||||
exports.Relationships = Relationships;
|
||||
|
||||
|
||||
function readRelationships(element) {
|
||||
var relationships = [];
|
||||
element.children.forEach(function(child) {
|
||||
if (child.name === "relationships:Relationship") {
|
||||
var relationship = {
|
||||
relationshipId: child.attributes.Id,
|
||||
target: child.attributes.Target,
|
||||
type: child.attributes.Type
|
||||
};
|
||||
relationships.push(relationship);
|
||||
}
|
||||
});
|
||||
return new Relationships(relationships);
|
||||
}
|
||||
|
||||
function Relationships(relationships) {
|
||||
var targetsByRelationshipId = Object.create(null);
|
||||
relationships.forEach(function(relationship) {
|
||||
targetsByRelationshipId[relationship.relationshipId] = relationship.target;
|
||||
});
|
||||
|
||||
var targetsByType = Object.create(null);
|
||||
relationships.forEach(function(relationship) {
|
||||
if (!targetsByType[relationship.type]) {
|
||||
targetsByType[relationship.type] = [];
|
||||
}
|
||||
targetsByType[relationship.type].push(relationship.target);
|
||||
});
|
||||
|
||||
return {
|
||||
findTargetByRelationshipId: function(relationshipId) {
|
||||
return targetsByRelationshipId[relationshipId];
|
||||
},
|
||||
findTargetsByType: function(type) {
|
||||
return targetsByType[type] || [];
|
||||
}
|
||||
};
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
var _ = require("underscore");
|
||||
|
||||
var promises = require("../promises");
|
||||
var xml = require("../xml");
|
||||
|
||||
exports.writeStyleMap = writeStyleMap;
|
||||
exports.readStyleMap = readStyleMap;
|
||||
|
||||
|
||||
var schema = "http://schemas.zwobble.org/mammoth/style-map";
|
||||
var styleMapPath = "mammoth/style-map";
|
||||
var styleMapAbsolutePath = "/" + styleMapPath;
|
||||
|
||||
function writeStyleMap(docxFile, styleMap) {
|
||||
docxFile.write(styleMapPath, styleMap);
|
||||
return updateRelationships(docxFile).then(function() {
|
||||
return updateContentTypes(docxFile);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRelationships(docxFile) {
|
||||
var path = "word/_rels/document.xml.rels";
|
||||
var relationshipsUri = "http://schemas.openxmlformats.org/package/2006/relationships";
|
||||
var relationshipElementName = "{" + relationshipsUri + "}Relationship";
|
||||
return docxFile.read(path, "utf8")
|
||||
.then(xml.readString)
|
||||
.then(function(relationshipsContainer) {
|
||||
var relationships = relationshipsContainer.children;
|
||||
addOrUpdateElement(relationships, relationshipElementName, "Id", {
|
||||
"Id": "rMammothStyleMap",
|
||||
"Type": schema,
|
||||
"Target": styleMapAbsolutePath
|
||||
});
|
||||
|
||||
var namespaces = {"": relationshipsUri};
|
||||
return docxFile.write(path, xml.writeString(relationshipsContainer, namespaces));
|
||||
});
|
||||
}
|
||||
|
||||
function updateContentTypes(docxFile) {
|
||||
var path = "[Content_Types].xml";
|
||||
var contentTypesUri = "http://schemas.openxmlformats.org/package/2006/content-types";
|
||||
var overrideName = "{" + contentTypesUri + "}Override";
|
||||
return docxFile.read(path, "utf8")
|
||||
.then(xml.readString)
|
||||
.then(function(typesElement) {
|
||||
var children = typesElement.children;
|
||||
addOrUpdateElement(children, overrideName, "PartName", {
|
||||
"PartName": styleMapAbsolutePath,
|
||||
"ContentType": "text/prs.mammoth.style-map"
|
||||
});
|
||||
var namespaces = {"": contentTypesUri};
|
||||
return docxFile.write(path, xml.writeString(typesElement, namespaces));
|
||||
});
|
||||
}
|
||||
|
||||
function addOrUpdateElement(elements, name, identifyingAttribute, attributes) {
|
||||
var existingElement = _.find(elements, function(element) {
|
||||
return element.name === name &&
|
||||
element.attributes[identifyingAttribute] === attributes[identifyingAttribute];
|
||||
});
|
||||
if (existingElement) {
|
||||
existingElement.attributes = attributes;
|
||||
} else {
|
||||
elements.push(xml.element(name, attributes));
|
||||
}
|
||||
}
|
||||
|
||||
function readStyleMap(docxFile) {
|
||||
if (docxFile.exists(styleMapPath)) {
|
||||
return docxFile.read(styleMapPath, "utf8");
|
||||
} else {
|
||||
return promises.resolve(null);
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
exports.readStylesXml = readStylesXml;
|
||||
exports.Styles = Styles;
|
||||
exports.defaultStyles = new Styles({}, {});
|
||||
|
||||
function Styles(paragraphStyles, characterStyles, tableStyles, numberingStyles) {
|
||||
return {
|
||||
findParagraphStyleById: function(styleId) {
|
||||
return paragraphStyles[styleId];
|
||||
},
|
||||
findCharacterStyleById: function(styleId) {
|
||||
return characterStyles[styleId];
|
||||
},
|
||||
findTableStyleById: function(styleId) {
|
||||
return tableStyles[styleId];
|
||||
},
|
||||
findNumberingStyleById: function(styleId) {
|
||||
return numberingStyles[styleId];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Styles.EMPTY = new Styles({}, {}, {}, {});
|
||||
|
||||
function readStylesXml(root) {
|
||||
var paragraphStyles = Object.create(null);
|
||||
var characterStyles = Object.create(null);
|
||||
var tableStyles = Object.create(null);
|
||||
var numberingStyles = Object.create(null);
|
||||
|
||||
root.getElementsByTagName("w:style").forEach(function(styleElement) {
|
||||
var style = readStyleElement(styleElement);
|
||||
var styleSet;
|
||||
|
||||
switch (style.type) {
|
||||
case "paragraph":
|
||||
styleSet = paragraphStyles;
|
||||
break;
|
||||
|
||||
case "character":
|
||||
styleSet = characterStyles;
|
||||
break;
|
||||
|
||||
case "table":
|
||||
styleSet = tableStyles;
|
||||
break;
|
||||
|
||||
case "numbering":
|
||||
styleSet = numberingStyles;
|
||||
break;
|
||||
}
|
||||
|
||||
// Per 17.7.4.17 style (Style Definition) of ECMA-376 4th edition Part 1:
|
||||
//
|
||||
// > If multiple style definitions each declare the same value for their
|
||||
// > styleId, then the first such instance shall keep its current
|
||||
// > identifier with all other instances being reassigned in any manner
|
||||
// > desired.
|
||||
//
|
||||
// For the purpose of conversion, there's no point holding onto styles
|
||||
// with reassigned style IDs, so we ignore such style definitions.
|
||||
|
||||
if (styleSet && styleSet[style.styleId] === undefined) {
|
||||
styleSet[style.styleId] = style;
|
||||
}
|
||||
});
|
||||
|
||||
return new Styles(paragraphStyles, characterStyles, tableStyles, numberingStyles);
|
||||
}
|
||||
|
||||
function readStyleElement(styleElement) {
|
||||
var type = styleElement.attributes["w:type"];
|
||||
|
||||
if (type === "numbering") {
|
||||
return readNumberingStyleElement(type, styleElement);
|
||||
} else {
|
||||
var styleId = readStyleId(styleElement);
|
||||
var name = styleName(styleElement);
|
||||
return {type: type, styleId: styleId, name: name};
|
||||
}
|
||||
}
|
||||
|
||||
function styleName(styleElement) {
|
||||
var nameElement = styleElement.first("w:name");
|
||||
return nameElement ? nameElement.attributes["w:val"] : null;
|
||||
}
|
||||
|
||||
function readNumberingStyleElement(type, styleElement) {
|
||||
var styleId = readStyleId(styleElement);
|
||||
|
||||
var numId = styleElement
|
||||
.firstOrEmpty("w:pPr")
|
||||
.firstOrEmpty("w:numPr")
|
||||
.firstOrEmpty("w:numId")
|
||||
.attributes["w:val"];
|
||||
|
||||
return {type: type, numId: numId, styleId: styleId};
|
||||
}
|
||||
|
||||
function readStyleId(styleElement) {
|
||||
return styleElement.attributes["w:styleId"];
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
exports.uriToZipEntryName = uriToZipEntryName;
|
||||
exports.replaceFragment = replaceFragment;
|
||||
|
||||
function uriToZipEntryName(base, uri) {
|
||||
if (uri.charAt(0) === "/") {
|
||||
return uri.substr(1);
|
||||
} else {
|
||||
// In general, we should check first and second for trailing and leading slashes,
|
||||
// but in our specific case this seems to be sufficient
|
||||
return base + "/" + uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function replaceFragment(uri, fragment) {
|
||||
var hashIndex = uri.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
uri = uri.substring(0, hashIndex);
|
||||
}
|
||||
return uri + "#" + fragment;
|
||||
}
|
||||
Reference in new issue
Block a user