alle new
This commit is contained in:
erik committed 2026-10-01 06:46:51 +02:00
1 parent da64472251
commit 5b20adee48
1605 files changed
+396517 -1674

No files matched your search

+469
View File
@@ -0,0 +1,469 @@
var _ = require("underscore");
var promises = require("./promises");
var documents = require("./documents");
var htmlPaths = require("./styles/html-paths");
var results = require("./results");
var images = require("./images");
var Html = require("./html");
var writers = require("./writers");
exports.DocumentConverter = DocumentConverter;
function DocumentConverter(options) {
return {
convertToHtml: function(element) {
var comments = _.indexBy(
element.type === documents.types.document ? element.comments : [],
"commentId"
);
var conversion = new DocumentConversion(options, comments);
return conversion.convertToHtml(element);
}
};
}
function DocumentConversion(options, comments) {
var noteNumber = 1;
var noteReferences = [];
var referencedComments = [];
options = _.extend({ignoreEmptyParagraphs: true}, options);
var idPrefix = options.idPrefix === undefined ? "" : options.idPrefix;
var ignoreEmptyParagraphs = options.ignoreEmptyParagraphs;
var defaultParagraphStyle = htmlPaths.topLevelElement("p");
var styleMap = options.styleMap || [];
function convertToHtml(document) {
var messages = [];
var html = elementToHtml(document, messages, Object.create(null));
var deferredNodes = [];
walkHtml(html, function(node) {
if (node.type === "deferred") {
deferredNodes.push(node);
}
});
var deferredValues = Object.create(null);
return promises.forEachSeries(deferredNodes, function(deferred) {
return deferred.value().then(function(value) {
deferredValues[deferred.id] = value;
});
}).then(function() {
function replaceDeferred(nodes) {
return flatMap(nodes, function(node) {
if (node.type === "deferred") {
return deferredValues[node.id];
} else if (node.children) {
return [
_.extend({}, node, {
children: replaceDeferred(node.children)
})
];
} else {
return [node];
}
});
}
var writer = writers.writer({
prettyPrint: options.prettyPrint,
outputFormat: options.outputFormat
});
Html.write(writer, Html.simplify(replaceDeferred(html)));
return new results.Result(writer.asString(), messages);
});
}
function convertElements(elements, messages, context) {
return flatMap(elements, function(element) {
return elementToHtml(element, messages, context);
});
}
function elementToHtml(element, messages, context) {
if (!context) {
throw new Error("context not set");
}
var handler = elementConverters[element.type];
if (handler) {
return handler(element, messages, context);
} else {
return [];
}
}
function convertParagraph(element, messages, context) {
return htmlPathForParagraph(element, messages).wrap(function() {
var content = convertElements(element.children, messages, context);
if (ignoreEmptyParagraphs) {
return content;
} else {
return [Html.forceWrite].concat(content);
}
});
}
function htmlPathForParagraph(element, messages) {
var style = findStyle(element);
if (style) {
return style.to;
} else {
if (element.styleId) {
messages.push(unrecognisedStyleWarning("paragraph", element));
}
return defaultParagraphStyle;
}
}
function convertRun(run, messages, context) {
var nodes = function() {
return convertElements(run.children, messages, context);
};
var paths = [];
if (run.highlight !== null) {
var path = findHtmlPath({type: "highlight", color: run.highlight});
if (path) {
paths.push(path);
}
}
if (run.isSmallCaps) {
paths.push(findHtmlPathForRunProperty("smallCaps"));
}
if (run.isAllCaps) {
paths.push(findHtmlPathForRunProperty("allCaps"));
}
if (run.isStrikethrough) {
paths.push(findHtmlPathForRunProperty("strikethrough", "s"));
}
if (run.isUnderline) {
paths.push(findHtmlPathForRunProperty("underline"));
}
if (run.verticalAlignment === documents.verticalAlignment.subscript) {
paths.push(htmlPaths.element("sub", {}, {fresh: false}));
}
if (run.verticalAlignment === documents.verticalAlignment.superscript) {
paths.push(htmlPaths.element("sup", {}, {fresh: false}));
}
if (run.isItalic) {
paths.push(findHtmlPathForRunProperty("italic", "em"));
}
if (run.isBold) {
paths.push(findHtmlPathForRunProperty("bold", "strong"));
}
var stylePath = htmlPaths.empty;
var style = findStyle(run);
if (style) {
stylePath = style.to;
} else if (run.styleId) {
messages.push(unrecognisedStyleWarning("run", run));
}
paths.push(stylePath);
paths.forEach(function(path) {
nodes = path.wrap.bind(path, nodes);
});
return nodes();
}
function findHtmlPathForRunProperty(elementType, defaultTagName) {
var path = findHtmlPath({type: elementType});
if (path) {
return path;
} else if (defaultTagName) {
return htmlPaths.element(defaultTagName, {}, {fresh: false});
} else {
return htmlPaths.empty;
}
}
function findHtmlPath(element, defaultPath) {
var style = findStyle(element);
return style ? style.to : defaultPath;
}
function findStyle(element) {
for (var i = 0; i < styleMap.length; i++) {
if (styleMap[i].from.matches(element)) {
return styleMap[i];
}
}
}
function recoveringConvertImage(convertImage) {
return function(image, messages) {
return promises.try(function() {
return convertImage(image, messages);
}).catch(function(error) {
messages.push(results.error(error));
return [];
});
};
}
function noteHtmlId(note) {
return referentHtmlId(note.noteType, note.noteId);
}
function noteRefHtmlId(note) {
return referenceHtmlId(note.noteType, note.noteId);
}
function referentHtmlId(referenceType, referenceId) {
return htmlId(referenceType + "-" + referenceId);
}
function referenceHtmlId(referenceType, referenceId) {
return htmlId(referenceType + "-ref-" + referenceId);
}
function htmlId(suffix) {
return idPrefix + suffix;
}
var defaultTablePath = htmlPaths.elements([
htmlPaths.element("table", {}, {fresh: true})
]);
function convertTable(element, messages, context) {
return findHtmlPath(element, defaultTablePath).wrap(function() {
return convertTableChildren(element, messages, context);
});
}
function convertTableChildren(element, messages, context) {
var bodyIndex = _.findIndex(element.children, function(child) {
return !child.type === documents.types.tableRow || !child.isHeader;
});
if (bodyIndex === -1) {
bodyIndex = element.children.length;
}
var children;
if (bodyIndex === 0) {
children = convertElements(
element.children,
messages,
_.extend({}, context, {isTableHeader: false})
);
} else {
var headRows = convertElements(
element.children.slice(0, bodyIndex),
messages,
_.extend({}, context, {isTableHeader: true})
);
var bodyRows = convertElements(
element.children.slice(bodyIndex),
messages,
_.extend({}, context, {isTableHeader: false})
);
children = [
Html.freshElement("thead", {}, headRows),
Html.freshElement("tbody", {}, bodyRows)
];
}
return [Html.forceWrite].concat(children);
}
function convertTableRow(element, messages, context) {
var children = convertElements(element.children, messages, context);
return [
Html.freshElement("tr", {}, [Html.forceWrite].concat(children))
];
}
function convertTableCell(element, messages, context) {
var tagName = context.isTableHeader ? "th" : "td";
var children = convertElements(element.children, messages, context);
var attributes = {};
if (element.colSpan !== 1) {
attributes.colspan = element.colSpan.toString();
}
if (element.rowSpan !== 1) {
attributes.rowspan = element.rowSpan.toString();
}
return [
Html.freshElement(tagName, attributes, [Html.forceWrite].concat(children))
];
}
function convertCommentReference(reference, messages, context) {
return findHtmlPath(reference, htmlPaths.ignore).wrap(function() {
var comment = comments[reference.commentId];
var count = referencedComments.length + 1;
var label = "[" + commentAuthorLabel(comment) + count + "]";
referencedComments.push({label: label, comment: comment});
// TODO: remove duplication with note references
return [
Html.freshElement("a", {
href: "#" + referentHtmlId("comment", reference.commentId),
id: referenceHtmlId("comment", reference.commentId)
}, [Html.text(label)])
];
});
}
function convertComment(referencedComment, messages, context) {
// TODO: remove duplication with note references
var label = referencedComment.label;
var comment = referencedComment.comment;
var body = convertElements(comment.body, messages, context).concat([
Html.nonFreshElement("p", {}, [
Html.text(" "),
Html.freshElement("a", {"href": "#" + referenceHtmlId("comment", comment.commentId)}, [
Html.text("↑")
])
])
]);
return [
Html.freshElement(
"dt",
{"id": referentHtmlId("comment", comment.commentId)},
[Html.text("Comment " + label)]
),
Html.freshElement("dd", {}, body)
];
}
function convertBreak(element, messages, context) {
return htmlPathForBreak(element).wrap(function() {
return [];
});
}
function htmlPathForBreak(element) {
var style = findStyle(element);
if (style) {
return style.to;
} else if (element.breakType === "line") {
return htmlPaths.topLevelElement("br");
} else {
return htmlPaths.empty;
}
}
var elementConverters = {
"document": function(document, messages, context) {
var children = convertElements(document.children, messages, context);
var notes = noteReferences.map(function(noteReference) {
return document.notes.resolve(noteReference);
});
var notesNodes = convertElements(notes, messages, context);
return children.concat([
Html.freshElement("ol", {}, notesNodes),
Html.freshElement("dl", {}, flatMap(referencedComments, function(referencedComment) {
return convertComment(referencedComment, messages, context);
}))
]);
},
"paragraph": convertParagraph,
"run": convertRun,
"text": function(element, messages, context) {
return [Html.text(element.value)];
},
"tab": function(element, messages, context) {
return [Html.text("\t")];
},
"hyperlink": function(element, messages, context) {
var href = element.anchor ? "#" + htmlId(element.anchor) : element.href;
var attributes = {href: href};
if (element.targetFrame != null) {
attributes.target = element.targetFrame;
}
var children = convertElements(element.children, messages, context);
return [Html.nonFreshElement("a", attributes, children)];
},
"checkbox": function(element) {
var attributes = {type: "checkbox"};
if (element.checked) {
attributes["checked"] = "checked";
}
return [Html.freshElement("input", attributes)];
},
"bookmarkStart": function(element, messages, context) {
var anchor = Html.freshElement("a", {
id: htmlId(element.name)
}, [Html.forceWrite]);
return [anchor];
},
"noteReference": function(element, messages, context) {
noteReferences.push(element);
var anchor = Html.freshElement("a", {
href: "#" + noteHtmlId(element),
id: noteRefHtmlId(element)
}, [Html.text("[" + (noteNumber++) + "]")]);
return [Html.freshElement("sup", {}, [anchor])];
},
"note": function(element, messages, context) {
var children = convertElements(element.body, messages, context);
var backLink = Html.elementWithTag(htmlPaths.element("p", {}, {fresh: false}), [
Html.text(" "),
Html.freshElement("a", {href: "#" + noteRefHtmlId(element)}, [Html.text("↑")])
]);
var body = children.concat([backLink]);
return Html.freshElement("li", {id: noteHtmlId(element)}, body);
},
"commentReference": convertCommentReference,
"comment": convertComment,
"image": deferredConversion(recoveringConvertImage(options.convertImage || images.dataUri)),
"table": convertTable,
"tableRow": convertTableRow,
"tableCell": convertTableCell,
"break": convertBreak
};
return {
convertToHtml: convertToHtml
};
}
var deferredId = 1;
function deferredConversion(func) {
return function(element, messages, context) {
return [
{
type: "deferred",
id: deferredId++,
value: function() {
return func(element, messages, context);
}
}
];
};
}
function unrecognisedStyleWarning(type, element) {
return results.warning(
"Unrecognised " + type + " style: '" + element.styleName + "'" +
" (Style ID: " + element.styleId + ")"
);
}
function flatMap(values, func) {
return _.flatten(values.map(func), true);
}
function walkHtml(nodes, callback) {
nodes.forEach(function(node) {
callback(node);
if (node.children) {
walkHtml(node.children, callback);
}
});
}
var commentAuthorLabel = exports.commentAuthorLabel = function commentAuthorLabel(comment) {
return comment.authorInitials || "";
};
+254
View File
@@ -0,0 +1,254 @@
var _ = require("underscore");
var types = exports.types = {
document: "document",
paragraph: "paragraph",
run: "run",
text: "text",
tab: "tab",
checkbox: "checkbox",
hyperlink: "hyperlink",
noteReference: "noteReference",
image: "image",
note: "note",
commentReference: "commentReference",
comment: "comment",
table: "table",
tableRow: "tableRow",
tableCell: "tableCell",
"break": "break",
bookmarkStart: "bookmarkStart"
};
function Document(children, options) {
options = options || {};
return {
type: types.document,
children: children,
notes: options.notes || new Notes({}),
comments: options.comments || []
};
}
function Paragraph(children, properties) {
properties = properties || {};
var indent = properties.indent || {};
return {
type: types.paragraph,
children: children,
styleId: properties.styleId || null,
styleName: properties.styleName || null,
numbering: properties.numbering || null,
alignment: properties.alignment || null,
indent: {
start: indent.start || null,
end: indent.end || null,
firstLine: indent.firstLine || null,
hanging: indent.hanging || null
}
};
}
function Run(children, properties) {
properties = properties || {};
return {
type: types.run,
children: children,
styleId: properties.styleId || null,
styleName: properties.styleName || null,
isBold: !!properties.isBold,
isUnderline: !!properties.isUnderline,
isItalic: !!properties.isItalic,
isStrikethrough: !!properties.isStrikethrough,
isAllCaps: !!properties.isAllCaps,
isSmallCaps: !!properties.isSmallCaps,
verticalAlignment: properties.verticalAlignment || verticalAlignment.baseline,
font: properties.font || null,
fontSize: properties.fontSize || null,
highlight: properties.highlight || null
};
}
var verticalAlignment = {
baseline: "baseline",
superscript: "superscript",
subscript: "subscript"
};
function Text(value) {
return {
type: types.text,
value: value
};
}
function Tab() {
return {
type: types.tab
};
}
function Checkbox(options) {
return {
type: types.checkbox,
checked: options.checked
};
}
function Hyperlink(children, options) {
return {
type: types.hyperlink,
children: children,
href: options.href,
anchor: options.anchor,
targetFrame: options.targetFrame
};
}
function NoteReference(options) {
return {
type: types.noteReference,
noteType: options.noteType,
noteId: options.noteId
};
}
function Notes(notes) {
this._notes = _.indexBy(notes, function(note) {
return noteKey(note.noteType, note.noteId);
});
}
Notes.prototype.resolve = function(reference) {
return this.findNoteByKey(noteKey(reference.noteType, reference.noteId));
};
Notes.prototype.findNoteByKey = function(key) {
return this._notes[key] || null;
};
function Note(options) {
return {
type: types.note,
noteType: options.noteType,
noteId: options.noteId,
body: options.body
};
}
function commentReference(options) {
return {
type: types.commentReference,
commentId: options.commentId
};
}
function comment(options) {
return {
type: types.comment,
commentId: options.commentId,
body: options.body,
authorName: options.authorName,
authorInitials: options.authorInitials
};
}
function noteKey(noteType, id) {
return noteType + "-" + id;
}
function Image(options) {
return {
type: types.image,
// `read` is retained for backwards compatibility, but other read
// methods should be preferred.
read: function(encoding) {
if (encoding) {
return options.readImage(encoding);
} else {
return options.readImage().then(function(arrayBuffer) {
return Buffer.from(arrayBuffer);
});
}
},
readAsArrayBuffer: function() {
return options.readImage();
},
readAsBase64String: function() {
return options.readImage("base64");
},
readAsBuffer: function() {
return options.readImage().then(function(arrayBuffer) {
return Buffer.from(arrayBuffer);
});
},
altText: options.altText,
contentType: options.contentType
};
}
function Table(children, properties) {
properties = properties || {};
return {
type: types.table,
children: children,
styleId: properties.styleId || null,
styleName: properties.styleName || null
};
}
function TableRow(children, options) {
options = options || {};
return {
type: types.tableRow,
children: children,
isHeader: options.isHeader || false
};
}
function TableCell(children, options) {
options = options || {};
return {
type: types.tableCell,
children: children,
colSpan: options.colSpan == null ? 1 : options.colSpan,
rowSpan: options.rowSpan == null ? 1 : options.rowSpan
};
}
function Break(breakType) {
return {
type: types["break"],
breakType: breakType
};
}
function BookmarkStart(options) {
return {
type: types.bookmarkStart,
name: options.name
};
}
exports.document = exports.Document = Document;
exports.paragraph = exports.Paragraph = Paragraph;
exports.run = exports.Run = Run;
exports.text = exports.Text = Text;
exports.tab = exports.Tab = Tab;
exports.checkbox = exports.Checkbox = Checkbox;
exports.Hyperlink = Hyperlink;
exports.noteReference = exports.NoteReference = NoteReference;
exports.Notes = Notes;
exports.Note = Note;
exports.commentReference = commentReference;
exports.comment = comment;
exports.Image = Image;
exports.Table = Table;
exports.TableRow = TableRow;
exports.TableCell = TableCell;
exports.lineBreak = Break("line");
exports.pageBreak = Break("page");
exports.columnBreak = Break("column");
exports.BookmarkStart = BookmarkStart;
exports.verticalAlignment = verticalAlignment;
+817
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+16
View File
@@ -0,0 +1,16 @@
var fs = require("fs");
var promises = require("./promises");
function readFile(path, options) {
return promises.resolve(fs.promises.readFile(path, options));
}
function writeFile(path, data, options) {
return promises.resolve(fs.promises.writeFile(path, data, options));
}
exports.createWriteStream = fs.createWriteStream.bind(fs);
exports.readFile = readFile;
exports.readFileSync = fs.readFileSync.bind(fs);
exports.writeFile = writeFile;
+51
View File
@@ -0,0 +1,51 @@
var htmlPaths = require("../styles/html-paths");
function nonFreshElement(tagName, attributes, children) {
return elementWithTag(
htmlPaths.element(tagName, attributes, {fresh: false}),
children);
}
function freshElement(tagName, attributes, children) {
var tag = htmlPaths.element(tagName, attributes, {fresh: true});
return elementWithTag(tag, children);
}
function elementWithTag(tag, children) {
return {
type: "element",
tag: tag,
children: children || []
};
}
function text(value) {
return {
type: "text",
value: value
};
}
var forceWrite = {
type: "forceWrite"
};
exports.freshElement = freshElement;
exports.nonFreshElement = nonFreshElement;
exports.elementWithTag = elementWithTag;
exports.text = text;
exports.forceWrite = forceWrite;
var voidTagNames = {
"br": true,
"hr": true,
"img": true,
"input": true
};
function isVoidElement(node) {
return (node.children.length === 0) && voidTagNames[node.tag.tagName];
}
exports.isVoidElement = isVoidElement;
+41
View File
@@ -0,0 +1,41 @@
var ast = require("./ast");
exports.freshElement = ast.freshElement;
exports.nonFreshElement = ast.nonFreshElement;
exports.elementWithTag = ast.elementWithTag;
exports.text = ast.text;
exports.forceWrite = ast.forceWrite;
exports.simplify = require("./simplify");
function write(writer, nodes) {
nodes.forEach(function(node) {
writeNode(writer, node);
});
}
function writeNode(writer, node) {
toStrings[node.type](writer, node);
}
var toStrings = {
element: generateElementString,
text: generateTextString,
forceWrite: function() { }
};
function generateElementString(writer, node) {
if (ast.isVoidElement(node)) {
writer.selfClosing(node.tag.tagName, node.tag.attributes);
} else {
writer.open(node.tag.tagName, node.tag.attributes);
write(writer, node.children);
writer.close(node.tag.tagName);
}
}
function generateTextString(writer, node) {
writer.text(node.value);
}
exports.write = write;
+88
View File
@@ -0,0 +1,88 @@
var _ = require("underscore");
var ast = require("./ast");
function simplify(nodes) {
return collapse(removeEmpty(nodes));
}
function collapse(nodes) {
var children = [];
nodes.map(collapseNode).forEach(function(child) {
appendChild(children, child);
});
return children;
}
function collapseNode(node) {
return collapsers[node.type](node);
}
var collapsers = {
element: collapseElement,
text: identity,
forceWrite: identity
};
function collapseElement(node) {
return ast.elementWithTag(node.tag, collapse(node.children));
}
function identity(value) {
return value;
}
function appendChild(children, child) {
var lastChild = children[children.length - 1];
if (child.type === "element" && !child.tag.fresh && lastChild && lastChild.type === "element" && child.tag.matchesElement(lastChild.tag)) {
if (child.tag.separator) {
appendChild(lastChild.children, ast.text(child.tag.separator));
}
child.children.forEach(function(grandChild) {
// Mutation is fine since simplifying elements create a copy of the children.
appendChild(lastChild.children, grandChild);
});
} else {
children.push(child);
}
}
function removeEmpty(nodes) {
return flatMap(nodes, function(node) {
return emptiers[node.type](node);
});
}
function flatMap(values, func) {
return _.flatten(_.map(values, func), true);
}
var emptiers = {
element: elementEmptier,
text: textEmptier,
forceWrite: neverEmpty
};
function neverEmpty(node) {
return [node];
}
function elementEmptier(element) {
var children = removeEmpty(element.children);
if (children.length === 0 && !ast.isVoidElement(element)) {
return [];
} else {
return [ast.elementWithTag(element.tag, children)];
}
}
function textEmptier(node) {
if (node.value.length === 0) {
return [];
} else {
return [node];
}
}
module.exports = simplify;
+37
View File
@@ -0,0 +1,37 @@
var _ = require("underscore");
var promises = require("./promises");
var Html = require("./html");
exports.imgElement = imgElement;
function imgElement(func) {
return function(element, messages) {
return promises.resolve(func(element)).then(function(result) {
var attributes = {};
if (element.altText) {
attributes.alt = element.altText;
}
_.extend(attributes, result);
return [Html.freshElement("img", attributes)];
});
};
}
// Undocumented, but retained for backwards-compatibility with 0.3.x
exports.inline = exports.imgElement;
exports.dataUri = imgElement(function(element) {
return element.readAsBase64String().then(function(imageBuffer) {
return {
src: "data:" + element.contentType + ";base64," + imageBuffer
};
});
});
function imageFilenameExtension(image) {
return image.contentType.split(/\/|\\/)[1];
}
exports.imageFilenameExtension = imageFilenameExtension;
+86
View File
@@ -0,0 +1,86 @@
interface Mammoth {
convertToHtml: (input: Input, options?: Options) => Promise<Result>;
extractRawText: (input: Input) => Promise<Result>;
embedStyleMap: (input: Input, styleMap: string) => Promise<{
toArrayBuffer: () => ArrayBuffer,
toBuffer: () => Buffer,
}>;
images: Images;
}
type Input = NodeJsInput | BrowserInput;
type NodeJsInput = PathInput | BufferInput;
interface PathInput {
path: string;
}
interface BufferInput {
buffer: Buffer;
}
type BrowserInput = ArrayBufferInput;
interface ArrayBufferInput {
arrayBuffer: ArrayBuffer;
}
interface Options {
styleMap?: string | Array<string>;
includeEmbeddedStyleMap?: boolean;
includeDefaultStyleMap?: boolean;
convertImage?: ImageConverter;
ignoreEmptyParagraphs?: boolean;
idPrefix?: string;
externalFileAccess?: boolean;
transformDocument?: (element: any) => any;
}
interface ImageConverter {
__mammothBrand: "ImageConverter";
}
interface Image {
contentType: string;
readAsArrayBuffer: () => Promise<ArrayBuffer>;
readAsBase64String: () => Promise<string>;
readAsBuffer: () => Promise<Buffer>;
read: ImageRead;
}
interface ImageRead {
(): Promise<Buffer>;
(encoding: string): Promise<string>;
}
interface ImageAttributes {
src: string;
}
interface Images {
dataUri: ImageConverter;
imgElement: (f: (image: Image) => Promise<ImageAttributes>) => ImageConverter;
}
interface Result {
value: string;
messages: Array<Message>;
}
type Message = Warning | Error;
interface Warning {
type: "warning";
message: string;
}
interface Error {
type: "error";
message: string;
error: unknown;
}
declare const mammoth: Mammoth;
export = mammoth;
+124
View File
@@ -0,0 +1,124 @@
var _ = require("underscore");
var docxReader = require("./docx/docx-reader");
var docxStyleMap = require("./docx/style-map");
var DocumentConverter = require("./document-to-html").DocumentConverter;
var convertElementToRawText = require("./raw-text").convertElementToRawText;
var readStyle = require("./style-reader").readStyle;
var readOptions = require("./options-reader").readOptions;
var promises = require("./promises");
var unzip = require("./unzip");
var Result = require("./results").Result;
exports.convertToHtml = convertToHtml;
exports.convertToMarkdown = convertToMarkdown;
exports.convert = convert;
exports.extractRawText = extractRawText;
exports.images = require("./images");
exports.transforms = require("./transforms");
exports.underline = require("./underline");
exports.embedStyleMap = embedStyleMap;
exports.readEmbeddedStyleMap = readEmbeddedStyleMap;
function convertToHtml(input, options) {
return convert(input, options);
}
function convertToMarkdown(input, options) {
var markdownOptions = Object.create(options || {});
markdownOptions.outputFormat = "markdown";
return convert(input, markdownOptions);
}
function convert(input, options) {
options = readOptions(options);
var result = unzip.openZip(input)
.then(function(docxFile) {
return docxStyleMap.readStyleMap(docxFile).then(function(styleMap) {
options.embeddedStyleMap = styleMap;
return docxFile;
});
})
.then(function(docxFile) {
return docxReader.read(docxFile, input, options)
.then(function(documentResult) {
return documentResult.map(options.transformDocument);
})
.then(function(documentResult) {
return convertDocumentToHtml(documentResult, options);
});
});
return promises.toExternalPromise(result);
}
function readEmbeddedStyleMap(input) {
var result = unzip.openZip(input)
.then(docxStyleMap.readStyleMap);
return promises.toExternalPromise(result);
}
function convertDocumentToHtml(documentResult, options) {
var styleMapResult = parseStyleMap(options.readStyleMap());
var parsedOptions = _.extend({}, options, {
styleMap: styleMapResult.value
});
var documentConverter = new DocumentConverter(parsedOptions);
return documentResult.flatMapThen(function(document) {
return styleMapResult.flatMapThen(function(styleMap) {
return documentConverter.convertToHtml(document);
});
});
}
function parseStyleMap(styleMap) {
return Result.combine((styleMap || []).map(readStyle))
.map(function(styleMap) {
return styleMap.filter(function(styleMapping) {
return !!styleMapping;
});
});
}
function extractRawText(input) {
var result = unzip.openZip(input)
.then(docxReader.read)
.then(function(documentResult) {
return documentResult.map(convertElementToRawText);
});
return promises.toExternalPromise(result);
}
function embedStyleMap(input, styleMap) {
var result = unzip.openZip(input)
.then(function(docxFile) {
return docxStyleMap.writeStyleMap(docxFile, styleMap)
.then(function() {
return docxFile;
});
})
.then(function(docxFile) {
return docxFile.toArrayBuffer();
})
.then(function(arrayBuffer) {
return {
toArrayBuffer: function() {
return arrayBuffer;
},
toBuffer: function() {
return Buffer.from(arrayBuffer);
}
};
});
return promises.toExternalPromise(result);
}
exports.styleMapping = function() {
throw new Error('Use a raw string instead of mammoth.styleMapping e.g. "p[style-name=\'Title\'] => h1" instead of mammoth.styleMapping("p[style-name=\'Title\'] => h1")');
};
+65
View File
@@ -0,0 +1,65 @@
/* global process */
var path = require("path");
var mammoth = require("./");
var fs = require("./fs");
var images = require("./images");
var promises = require("./promises");
function main(argv) {
var docxPath = argv["docx-path"];
var outputPath = argv["output-path"];
var outputDir = argv.output_dir;
var outputFormat = argv.output_format;
var styleMapPath = argv.style_map;
var result = readStyleMap(styleMapPath).then(function(styleMap) {
var options = {
styleMap: styleMap,
outputFormat: outputFormat
};
if (outputDir) {
var basename = path.basename(docxPath, ".docx");
outputPath = path.join(outputDir, basename + ".html");
var imageIndex = 0;
options.convertImage = images.imgElement(function(element) {
imageIndex++;
var extension = images.imageFilenameExtension(element);
var filename = imageIndex + (extension === undefined ? "" : "." + extension);
return element.read().then(function(imageBuffer) {
var imagePath = path.join(outputDir, filename);
return fs.writeFile(imagePath, imageBuffer);
}).then(function() {
return {src: filename};
});
});
}
return mammoth.convert({path: docxPath}, options)
.then(function(result) {
result.messages.forEach(function(message) {
process.stderr.write(message.message);
process.stderr.write("\n");
});
var outputStream = outputPath ? fs.createWriteStream(outputPath) : process.stdout;
outputStream.write(result.value);
});
});
promises.toExternalPromise(result).done();
}
function readStyleMap(styleMapPath) {
if (styleMapPath) {
return fs.readFile(styleMapPath, "utf8");
} else {
return promises.resolve(null);
}
}
module.exports = main;
+107
View File
@@ -0,0 +1,107 @@
exports.readOptions = readOptions;
var _ = require("underscore");
var defaultStyleMap = exports._defaultStyleMap = [
"p.Heading1 => h1:fresh",
"p.Heading2 => h2:fresh",
"p.Heading3 => h3:fresh",
"p.Heading4 => h4:fresh",
"p.Heading5 => h5:fresh",
"p.Heading6 => h6:fresh",
"p[style-name='Heading 1'] => h1:fresh",
"p[style-name='Heading 2'] => h2:fresh",
"p[style-name='Heading 3'] => h3:fresh",
"p[style-name='Heading 4'] => h4:fresh",
"p[style-name='Heading 5'] => h5:fresh",
"p[style-name='Heading 6'] => h6:fresh",
"p[style-name='heading 1'] => h1:fresh",
"p[style-name='heading 2'] => h2:fresh",
"p[style-name='heading 3'] => h3:fresh",
"p[style-name='heading 4'] => h4:fresh",
"p[style-name='heading 5'] => h5:fresh",
"p[style-name='heading 6'] => h6:fresh",
// Apple Pages
"p.Heading => h1:fresh",
"p[style-name='Heading'] => h1:fresh",
"r[style-name='Strong'] => strong",
"p[style-name='footnote text'] => p:fresh",
"r[style-name='footnote reference'] =>",
"p[style-name='endnote text'] => p:fresh",
"r[style-name='endnote reference'] =>",
"p[style-name='annotation text'] => p:fresh",
"r[style-name='annotation reference'] =>",
// LibreOffice
"p[style-name='Footnote'] => p:fresh",
"r[style-name='Footnote anchor'] =>",
"p[style-name='Endnote'] => p:fresh",
"r[style-name='Endnote anchor'] =>",
"p:unordered-list(1) => ul > li:fresh",
"p:unordered-list(2) => ul|ol > li > ul > li:fresh",
"p:unordered-list(3) => ul|ol > li > ul|ol > li > ul > li:fresh",
"p:unordered-list(4) => ul|ol > li > ul|ol > li > ul|ol > li > ul > li:fresh",
"p:unordered-list(5) => ul|ol > li > ul|ol > li > ul|ol > li > ul|ol > li > ul > li:fresh",
"p:ordered-list(1) => ol > li:fresh",
"p:ordered-list(2) => ul|ol > li > ol > li:fresh",
"p:ordered-list(3) => ul|ol > li > ul|ol > li > ol > li:fresh",
"p:ordered-list(4) => ul|ol > li > ul|ol > li > ul|ol > li > ol > li:fresh",
"p:ordered-list(5) => ul|ol > li > ul|ol > li > ul|ol > li > ul|ol > li > ol > li:fresh",
"r[style-name='Hyperlink'] =>",
"p[style-name='Normal'] => p:fresh",
// Apple Pages
"p.Body => p:fresh",
"p[style-name='Body'] => p:fresh"
];
var standardOptions = exports._standardOptions = {
externalFileAccess: false,
transformDocument: identity,
includeDefaultStyleMap: true,
includeEmbeddedStyleMap: true
};
function readOptions(options) {
options = options || {};
return _.extend({}, standardOptions, options, {
customStyleMap: readStyleMap(options.styleMap),
readStyleMap: function() {
var styleMap = this.customStyleMap;
if (this.includeEmbeddedStyleMap) {
styleMap = styleMap.concat(readStyleMap(this.embeddedStyleMap));
}
if (this.includeDefaultStyleMap) {
styleMap = styleMap.concat(defaultStyleMap);
}
return styleMap;
}
});
}
function readStyleMap(styleMap) {
if (!styleMap) {
return [];
} else if (_.isString(styleMap)) {
return styleMap.split("\n")
.map(function(line) {
return line.trim();
})
.filter(function(line) {
return line !== "" && line.charAt(0) !== "#";
});
} else {
return styleMap;
}
}
function identity(value) {
return value;
}
+135
View File
@@ -0,0 +1,135 @@
var _ = require("underscore");
exports.defer = defer;
exports.Promise = Promise;
exports.resolve = Promise.resolve.bind(Promise);
exports.reject = Promise.reject.bind(Promise);
exports.forEachSeries = function(array, func) {
var arrayIndex = 0;
var deferred = defer();
var next = function() {
if (arrayIndex < array.length) {
var element = array[arrayIndex];
arrayIndex++;
func(element).then(next, deferred.reject);
} else {
deferred.resolve();
}
};
next();
return deferred.promise;
};
var props = exports.props = function(obj) {
// We rely on .keys() and .values() returning properties in the same order.
var keys = Object.keys(obj);
var values = Object.values(obj);
var result = Promise.all(values).then(function(resolvedValues) {
var resolvedObj = {};
for (var index = 0; index < keys.length; index++) {
resolvedObj[keys[index]] = resolvedValues[index];
}
return resolvedObj;
});
return addAlsoToPromise(result);
};
var alsoMethod = function(func) {
var result = this.then(function(value) {
var returnValue = _.extend({}, value, func(value));
return props(returnValue);
});
return addAlsoToPromise(result);
};
function addAlsoToPromise(promise) {
promise.also = alsoMethod;
return promise;
}
exports.try = function(func) {
try {
return Promise.resolve(func());
} catch (error) {
return Promise.reject(error);
}
};
function defer() {
var resolve;
var reject;
var promise = new Promise(function(resolveArg, rejectArg) {
resolve = resolveArg;
reject = rejectArg;
});
return {
resolve: resolve,
reject: reject,
promise: promise
};
}
function ExternalPromise(executor) {
var promise = new Promise(executor);
promise.__proto__ = ExternalPromise.prototype;
return promise;
}
ExternalPromise.__proto__ = Promise;
ExternalPromise.prototype.__proto__ = Promise.prototype;
ExternalPromise.prototype.done = function(resolve, reject) {
return this.then(resolve, reject).catch(function(error) {
if (isNode()) {
var stack = error instanceof Error ? error.stack : error;
process.stderr.write("Fatal " + stack + "\n");
process.exit(2);
} else {
setTimeout(function() {
throw error;
}, 0);
}
});
};
/**
* Previous versions of Mammoth used bluebird instead of native promises. While
* internal usage can be replaced easily enough, external users of the library
* may have relied on values returned from function calls being a bluebird
* promise.
*
* However, since the return value was only documented as a promise, not as a
* bluebird promise, and it's relatively straightforward to wrap the return
* value in a bluebird promise if necessary, changing the API to return a native
* promise should be relatively safe.
*
* The only exception is that the docs previously used `.done()`: therefore,
* when returning promises from the API to external users, a `done` method is
* added.
*/
function toExternalPromise(promise) {
return ExternalPromise.resolve(promise);
}
exports.toExternalPromise = toExternalPromise;
/**
* Detect whether we're running in node.js. This uses the same logic as in
* bluebird for consistency with previous versions of Mammoth.
*/
function isNode() {
if (typeof process === "undefined") {
return false;
}
return {}.toString.call(process).toLowerCase() === "[object process]";
}
+14
View File
@@ -0,0 +1,14 @@
var documents = require("./documents");
function convertElementToRawText(element) {
if (element.type === "text") {
return element.value;
} else if (element.type === documents.types.tab) {
return "\t";
} else {
var tail = element.type === "paragraph" ? "\n\n" : "";
return (element.children || []).map(convertElementToRawText).join("") + tail;
}
}
exports.convertElementToRawText = convertElementToRawText;
+72
View File
@@ -0,0 +1,72 @@
var _ = require("underscore");
exports.Result = Result;
exports.success = success;
exports.warning = warning;
exports.error = error;
function Result(value, messages) {
this.value = value;
this.messages = messages || [];
}
Result.prototype.map = function(func) {
return new Result(func(this.value), this.messages);
};
Result.prototype.flatMap = function(func) {
var funcResult = func(this.value);
return new Result(funcResult.value, combineMessages([this, funcResult]));
};
Result.prototype.flatMapThen = function(func) {
var that = this;
return func(this.value).then(function(otherResult) {
return new Result(otherResult.value, combineMessages([that, otherResult]));
});
};
Result.combine = function(results) {
var values = _.flatten(_.pluck(results, "value"));
var messages = combineMessages(results);
return new Result(values, messages);
};
function success(value) {
return new Result(value, []);
}
function warning(message) {
return {
type: "warning",
message: message
};
}
function error(exception) {
return {
type: "error",
message: exception.message,
error: exception
};
}
function combineMessages(results) {
var messages = [];
_.flatten(_.pluck(results, "messages"), true).forEach(function(message) {
if (!containsMessage(messages, message)) {
messages.push(message);
}
});
return messages;
}
function containsMessage(messages, message) {
return _.find(messages, isSameMessage.bind(null, message)) !== undefined;
}
function isSameMessage(first, second) {
return first.type === second.type && first.message === second.message;
}
+365
View File
@@ -0,0 +1,365 @@
var _ = require("underscore");
var lop = require("lop");
var documentMatchers = require("./styles/document-matchers");
var htmlPaths = require("./styles/html-paths");
var tokenise = require("./styles/parser/tokeniser").tokenise;
var results = require("./results");
exports.readHtmlPath = readHtmlPath;
exports.readDocumentMatcher = readDocumentMatcher;
exports.readStyle = readStyle;
function readStyle(string) {
return parseString(styleRule, string);
}
function createStyleRule() {
return lop.rules.sequence(
lop.rules.sequence.capture(documentMatcherRule()),
lop.rules.tokenOfType("whitespace"),
lop.rules.tokenOfType("arrow"),
lop.rules.sequence.capture(lop.rules.optional(lop.rules.sequence(
lop.rules.tokenOfType("whitespace"),
lop.rules.sequence.capture(htmlPathRule())
).head())),
lop.rules.tokenOfType("end")
).map(function(documentMatcher, htmlPath) {
return {
from: documentMatcher,
to: htmlPath.valueOrElse(htmlPaths.empty)
};
});
}
function readDocumentMatcher(string) {
return parseString(documentMatcherRule(), string);
}
function documentMatcherRule() {
var sequence = lop.rules.sequence;
var identifierToConstant = function(identifier, constant) {
return lop.rules.then(
lop.rules.token("identifier", identifier),
function() {
return constant;
}
);
};
var paragraphRule = identifierToConstant("p", documentMatchers.paragraph);
var runRule = identifierToConstant("r", documentMatchers.run);
var elementTypeRule = lop.rules.firstOf("p or r or table",
paragraphRule,
runRule
);
var styleIdRule = lop.rules.sequence(
lop.rules.tokenOfType("dot"),
lop.rules.sequence.cut(),
lop.rules.sequence.capture(identifierRule)
).map(function(styleId) {
return {styleId: styleId};
});
var styleNameMatcherRule = lop.rules.firstOf("style name matcher",
lop.rules.then(
lop.rules.sequence(
lop.rules.tokenOfType("equals"),
lop.rules.sequence.cut(),
lop.rules.sequence.capture(stringRule)
).head(),
function(styleName) {
return {styleName: documentMatchers.equalTo(styleName)};
}
),
lop.rules.then(
lop.rules.sequence(
lop.rules.tokenOfType("startsWith"),
lop.rules.sequence.cut(),
lop.rules.sequence.capture(stringRule)
).head(),
function(styleName) {
return {styleName: documentMatchers.startsWith(styleName)};
}
)
);
var styleNameRule = lop.rules.sequence(
lop.rules.tokenOfType("open-square-bracket"),
lop.rules.sequence.cut(),
lop.rules.token("identifier", "style-name"),
lop.rules.sequence.capture(styleNameMatcherRule),
lop.rules.tokenOfType("close-square-bracket")
).head();
var listTypeRule = lop.rules.firstOf("list type",
identifierToConstant("ordered-list", {isOrdered: true}),
identifierToConstant("unordered-list", {isOrdered: false})
);
var listRule = sequence(
lop.rules.tokenOfType("colon"),
sequence.capture(listTypeRule),
sequence.cut(),
lop.rules.tokenOfType("open-paren"),
sequence.capture(integerRule),
lop.rules.tokenOfType("close-paren")
).map(function(listType, levelNumber) {
return {
list: {
isOrdered: listType.isOrdered,
levelIndex: levelNumber - 1
}
};
});
function createMatcherSuffixesRule(rules) {
var matcherSuffix = lop.rules.firstOf.apply(
lop.rules.firstOf,
["matcher suffix"].concat(rules)
);
var matcherSuffixes = lop.rules.zeroOrMore(matcherSuffix);
return lop.rules.then(matcherSuffixes, function(suffixes) {
var matcherOptions = {};
suffixes.forEach(function(suffix) {
_.extend(matcherOptions, suffix);
});
return matcherOptions;
});
}
var paragraphOrRun = sequence(
sequence.capture(elementTypeRule),
sequence.capture(createMatcherSuffixesRule([
styleIdRule,
styleNameRule,
listRule
]))
).map(function(createMatcher, matcherOptions) {
return createMatcher(matcherOptions);
});
var table = sequence(
lop.rules.token("identifier", "table"),
sequence.capture(createMatcherSuffixesRule([
styleIdRule,
styleNameRule
]))
).map(function(options) {
return documentMatchers.table(options);
});
var bold = identifierToConstant("b", documentMatchers.bold);
var italic = identifierToConstant("i", documentMatchers.italic);
var underline = identifierToConstant("u", documentMatchers.underline);
var strikethrough = identifierToConstant("strike", documentMatchers.strikethrough);
var allCaps = identifierToConstant("all-caps", documentMatchers.allCaps);
var smallCaps = identifierToConstant("small-caps", documentMatchers.smallCaps);
var highlight = sequence(
lop.rules.token("identifier", "highlight"),
lop.rules.sequence.capture(lop.rules.optional(lop.rules.sequence(
lop.rules.tokenOfType("open-square-bracket"),
lop.rules.sequence.cut(),
lop.rules.token("identifier", "color"),
lop.rules.tokenOfType("equals"),
lop.rules.sequence.capture(stringRule),
lop.rules.tokenOfType("close-square-bracket")
).head()))
).map(function(color) {
return documentMatchers.highlight({
color: color.valueOrElse(undefined)
});
});
var commentReference = identifierToConstant("comment-reference", documentMatchers.commentReference);
var breakMatcher = sequence(
lop.rules.token("identifier", "br"),
sequence.cut(),
lop.rules.tokenOfType("open-square-bracket"),
lop.rules.token("identifier", "type"),
lop.rules.tokenOfType("equals"),
sequence.capture(stringRule),
lop.rules.tokenOfType("close-square-bracket")
).map(function(breakType) {
switch (breakType) {
case "line":
return documentMatchers.lineBreak;
case "page":
return documentMatchers.pageBreak;
case "column":
return documentMatchers.columnBreak;
default:
// TODO: handle unknown document matchers
}
});
return lop.rules.firstOf("element type",
paragraphOrRun,
table,
bold,
italic,
underline,
strikethrough,
allCaps,
smallCaps,
highlight,
commentReference,
breakMatcher
);
}
function readHtmlPath(string) {
return parseString(htmlPathRule(), string);
}
function htmlPathRule() {
var capture = lop.rules.sequence.capture;
var whitespaceRule = lop.rules.tokenOfType("whitespace");
var freshRule = lop.rules.then(
lop.rules.optional(lop.rules.sequence(
lop.rules.tokenOfType("colon"),
lop.rules.token("identifier", "fresh")
)),
function(option) {
return option.map(function() {
return true;
}).valueOrElse(false);
}
);
var separatorRule = lop.rules.then(
lop.rules.optional(lop.rules.sequence(
lop.rules.tokenOfType("colon"),
lop.rules.token("identifier", "separator"),
lop.rules.tokenOfType("open-paren"),
capture(stringRule),
lop.rules.tokenOfType("close-paren")
).head()),
function(option) {
return option.valueOrElse("");
}
);
var tagNamesRule = lop.rules.oneOrMoreWithSeparator(
identifierRule,
lop.rules.tokenOfType("choice")
);
var styleElementRule = lop.rules.sequence(
capture(tagNamesRule),
capture(lop.rules.zeroOrMore(attributeOrClassRule)),
capture(freshRule),
capture(separatorRule)
).map(function(tagName, attributesList, fresh, separator) {
var attributes = Object.create(null);
var options = {};
attributesList.forEach(function(attribute) {
if (attribute.append && attributes[attribute.name]) {
attributes[attribute.name] += " " + attribute.value;
} else {
attributes[attribute.name] = attribute.value;
}
});
if (fresh) {
options.fresh = true;
}
if (separator) {
options.separator = separator;
}
return htmlPaths.element(tagName, attributes, options);
});
return lop.rules.firstOf("html path",
lop.rules.then(lop.rules.tokenOfType("bang"), function() {
return htmlPaths.ignore;
}),
lop.rules.then(
lop.rules.zeroOrMoreWithSeparator(
styleElementRule,
lop.rules.sequence(
whitespaceRule,
lop.rules.tokenOfType("gt"),
whitespaceRule
)
),
htmlPaths.elements
)
);
}
var identifierRule = lop.rules.then(
lop.rules.tokenOfType("identifier"),
decodeEscapeSequences
);
var integerRule = lop.rules.tokenOfType("integer");
var stringRule = lop.rules.then(
lop.rules.tokenOfType("string"),
decodeEscapeSequences
);
var escapeSequences = {
"n": "\n",
"r": "\r",
"t": "\t"
};
function decodeEscapeSequences(value) {
return value.replace(/\\(.)/g, function(match, code) {
return escapeSequences[code] || code;
});
}
var attributeRule = lop.rules.sequence(
lop.rules.tokenOfType("open-square-bracket"),
lop.rules.sequence.cut(),
lop.rules.sequence.capture(identifierRule),
lop.rules.tokenOfType("equals"),
lop.rules.sequence.capture(stringRule),
lop.rules.tokenOfType("close-square-bracket")
).map(function(name, value) {
return {name: name, value: value, append: false};
});
var classRule = lop.rules.sequence(
lop.rules.tokenOfType("dot"),
lop.rules.sequence.cut(),
lop.rules.sequence.capture(identifierRule)
).map(function(className) {
return {name: "class", value: className, append: true};
});
var attributeOrClassRule = lop.rules.firstOf(
"attribute or class",
attributeRule,
classRule
);
function parseString(rule, string) {
var tokens = tokenise(string);
var parser = lop.Parser();
var parseResult = parser.parseTokens(rule, tokens);
if (parseResult.isSuccess()) {
return results.success(parseResult.value());
} else {
return new results.Result(null, [results.warning(describeFailure(string, parseResult))]);
}
}
function describeFailure(input, parseResult) {
return "Did not understand this style mapping, so ignored it: " + input + "\n" +
parseResult.errors().map(describeError).join("\n");
}
function describeError(error) {
return "Error was at character number " + error.characterNumber() + ": " +
"Expected " + error.expected + " but got " + error.actual;
}
var styleRule = createStyleRule();
+100
View File
@@ -0,0 +1,100 @@
exports.paragraph = paragraph;
exports.run = run;
exports.table = table;
exports.bold = new Matcher("bold");
exports.italic = new Matcher("italic");
exports.underline = new Matcher("underline");
exports.strikethrough = new Matcher("strikethrough");
exports.allCaps = new Matcher("allCaps");
exports.smallCaps = new Matcher("smallCaps");
exports.highlight = highlight;
exports.commentReference = new Matcher("commentReference");
exports.lineBreak = new BreakMatcher({breakType: "line"});
exports.pageBreak = new BreakMatcher({breakType: "page"});
exports.columnBreak = new BreakMatcher({breakType: "column"});
exports.equalTo = equalTo;
exports.startsWith = startsWith;
function paragraph(options) {
return new Matcher("paragraph", options);
}
function run(options) {
return new Matcher("run", options);
}
function table(options) {
return new Matcher("table", options);
}
function highlight(options) {
return new HighlightMatcher(options);
}
function Matcher(elementType, options) {
options = options || {};
this._elementType = elementType;
this._styleId = options.styleId;
this._styleName = options.styleName;
if (options.list) {
this._listIndex = options.list.levelIndex;
this._listIsOrdered = options.list.isOrdered;
}
}
Matcher.prototype.matches = function(element) {
return element.type === this._elementType &&
(this._styleId === undefined || element.styleId === this._styleId) &&
(this._styleName === undefined || (element.styleName && this._styleName.operator(this._styleName.operand, element.styleName))) &&
(this._listIndex === undefined || isList(element, this._listIndex, this._listIsOrdered)) &&
(this._breakType === undefined || this._breakType === element.breakType);
};
function HighlightMatcher(options) {
options = options || {};
this._color = options.color;
}
HighlightMatcher.prototype.matches = function(element) {
return element.type === "highlight" &&
(this._color === undefined || element.color === this._color);
};
function BreakMatcher(options) {
options = options || {};
this._breakType = options.breakType;
}
BreakMatcher.prototype.matches = function(element) {
return element.type === "break" &&
(this._breakType === undefined || element.breakType === this._breakType);
};
function isList(element, levelIndex, isOrdered) {
return element.numbering &&
element.numbering.level == levelIndex &&
element.numbering.isOrdered == isOrdered;
}
function equalTo(value) {
return {
operator: operatorEqualTo,
operand: value
};
}
function startsWith(value) {
return {
operator: operatorStartsWith,
operand: value
};
}
function operatorEqualTo(first, second) {
return first.toUpperCase() === second.toUpperCase();
}
function operatorStartsWith(first, second) {
return second.toUpperCase().indexOf(first.toUpperCase()) === 0;
}
+75
View File
@@ -0,0 +1,75 @@
var _ = require("underscore");
var html = require("../html");
exports.topLevelElement = topLevelElement;
exports.elements = elements;
exports.element = element;
function topLevelElement(tagName, attributes) {
return elements([element(tagName, attributes, {fresh: true})]);
}
function elements(elementStyles) {
return new HtmlPath(elementStyles.map(function(elementStyle) {
if (_.isString(elementStyle)) {
return element(elementStyle);
} else {
return elementStyle;
}
}));
}
function HtmlPath(elements) {
this._elements = elements;
}
HtmlPath.prototype.wrap = function wrap(children) {
var result = children();
for (var index = this._elements.length - 1; index >= 0; index--) {
result = this._elements[index].wrapNodes(result);
}
return result;
};
function element(tagName, attributes, options) {
options = options || {};
return new Element(tagName, attributes, options);
}
function Element(tagName, attributes, options) {
var tagNames = Object.create(null);
if (_.isArray(tagName)) {
tagName.forEach(function(tagName) {
tagNames[tagName] = true;
});
tagName = tagName[0];
} else {
tagNames[tagName] = true;
}
this.tagName = tagName;
this.tagNames = tagNames;
this.attributes = attributes || {};
this.fresh = options.fresh;
this.separator = options.separator;
}
Element.prototype.matchesElement = function(element) {
return this.tagNames[element.tagName] && _.isEqual(this.attributes || {}, element.attributes || {});
};
Element.prototype.wrap = function wrap(generateNodes) {
return this.wrapNodes(generateNodes());
};
Element.prototype.wrapNodes = function wrapNodes(nodes) {
return [html.elementWithTag(this, nodes)];
};
exports.empty = elements([]);
exports.ignore = {
wrap: function() {
return [];
}
};
+30
View File
@@ -0,0 +1,30 @@
var lop = require("lop");
var RegexTokeniser = lop.RegexTokeniser;
exports.tokenise = tokenise;
var stringPrefix = "'((?:\\\\(?:.|$)|[^'\\\\])*)";
function tokenise(string) {
var identifierCharacter = "(?:[a-zA-Z\\-_]|\\\\.)";
var tokeniser = new RegexTokeniser([
{name: "identifier", regex: new RegExp("(" + identifierCharacter + "(?:" + identifierCharacter + "|[0-9])*)")},
{name: "dot", regex: /\./},
{name: "colon", regex: /:/},
{name: "gt", regex: />/},
{name: "whitespace", regex: /\s+/},
{name: "arrow", regex: /=>/},
{name: "equals", regex: /=/},
{name: "startsWith", regex: /\^=/},
{name: "open-paren", regex: /\(/},
{name: "close-paren", regex: /\)/},
{name: "open-square-bracket", regex: /\[/},
{name: "close-square-bracket", regex: /\]/},
{name: "string", regex: new RegExp(stringPrefix + "'")},
{name: "unterminated-string", regex: new RegExp(stringPrefix)},
{name: "integer", regex: /([0-9]+)/},
{name: "choice", regex: /\|/},
{name: "bang", regex: /(!)/}
]);
return tokeniser.tokenise(string);
}
+62
View File
@@ -0,0 +1,62 @@
var _ = require("underscore");
exports.paragraph = paragraph;
exports.run = run;
exports._elements = elements;
exports._elementsOfType = elementsOfType;
exports.getDescendantsOfType = getDescendantsOfType;
exports.getDescendants = getDescendants;
function paragraph(transform) {
return elementsOfType("paragraph", transform);
}
function run(transform) {
return elementsOfType("run", transform);
}
function elementsOfType(elementType, transform) {
return elements(function(element) {
if (element.type === elementType) {
return transform(element);
} else {
return element;
}
});
}
function elements(transform) {
return function transformElement(element) {
if (element.children) {
var children = _.map(element.children, transformElement);
element = _.extend(element, {children: children});
}
return transform(element);
};
}
function getDescendantsOfType(element, type) {
return getDescendants(element).filter(function(descendant) {
return descendant.type === type;
});
}
function getDescendants(element) {
var descendants = [];
visitDescendants(element, function(descendant) {
descendants.push(descendant);
});
return descendants;
}
function visitDescendants(element, visit) {
if (element.children) {
element.children.forEach(function(child) {
visitDescendants(child, visit);
visit(child);
});
}
}
+11
View File
@@ -0,0 +1,11 @@
var htmlPaths = require("./styles/html-paths");
var Html = require("./html");
exports.element = element;
function element(name) {
return function(html) {
return Html.elementWithTag(htmlPaths.element(name), [html]);
};
}
+17
View File
@@ -0,0 +1,17 @@
var fs = require("./fs");
var promises = require("./promises");
var zipfile = require("./zipfile");
exports.openZip = openZip;
function openZip(options) {
if (options.path) {
return fs.readFile(options.path).then(zipfile.openArrayBuffer);
} else if (options.buffer) {
return promises.resolve(zipfile.openArrayBuffer(options.buffer));
} else if (options.file) {
return promises.resolve(options.file);
} else {
return promises.reject(new Error("Could not find file in options"));
}
}
+159
View File
@@ -0,0 +1,159 @@
var _ = require("underscore");
exports.writer = writer;
function writer(options) {
options = options || {};
if (options.prettyPrint) {
return prettyWriter();
} else {
return simpleWriter();
}
}
var indentedElements = {
div: true,
p: true,
ul: true,
li: true
};
function prettyWriter() {
var indentationLevel = 0;
var indentation = " ";
var stack = [];
var start = true;
var inText = false;
var writer = simpleWriter();
function open(tagName, attributes) {
if (indentedElements[tagName]) {
indent();
}
stack.push(tagName);
writer.open(tagName, attributes);
if (indentedElements[tagName]) {
indentationLevel++;
}
start = false;
}
function close(tagName) {
if (indentedElements[tagName]) {
indentationLevel--;
indent();
}
stack.pop();
writer.close(tagName);
}
function text(value) {
startText();
var text = isInPre() ? value : value.replace("\n", "\n" + indentation);
writer.text(text);
}
function selfClosing(tagName, attributes) {
indent();
writer.selfClosing(tagName, attributes);
}
function insideIndentedElement() {
return stack.length === 0 || indentedElements[stack[stack.length - 1]];
}
function startText() {
if (!inText) {
indent();
inText = true;
}
}
function indent() {
inText = false;
if (!start && insideIndentedElement() && !isInPre()) {
writer._append("\n");
for (var i = 0; i < indentationLevel; i++) {
writer._append(indentation);
}
}
}
function isInPre() {
return _.some(stack, function(tagName) {
return tagName === "pre";
});
}
return {
asString: writer.asString,
open: open,
close: close,
text: text,
selfClosing: selfClosing
};
}
function simpleWriter() {
var fragments = [];
function open(tagName, attributes) {
var attributeString = generateAttributeString(attributes);
fragments.push("<" + tagName + attributeString + ">");
}
function close(tagName) {
fragments.push("</" + tagName + ">");
}
function selfClosing(tagName, attributes) {
var attributeString = generateAttributeString(attributes);
fragments.push("<" + tagName + attributeString + " />");
}
function generateAttributeString(attributes) {
return _.map(attributes, function(value, key) {
return " " + key + '="' + escapeHtmlAttribute(value) + '"';
}).join("");
}
function text(value) {
fragments.push(escapeHtmlText(value));
}
function append(html) {
fragments.push(html);
}
function asString() {
return fragments.join("");
}
return {
asString: asString,
open: open,
close: close,
text: text,
selfClosing: selfClosing,
_append: append
};
}
function escapeHtmlText(value) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function escapeHtmlAttribute(value) {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
+14
View File
@@ -0,0 +1,14 @@
var htmlWriter = require("./html-writer");
var markdownWriter = require("./markdown-writer");
exports.writer = writer;
function writer(options) {
options = options || {};
if (options.outputFormat === "markdown") {
return markdownWriter.writer();
} else {
return htmlWriter.writer(options);
}
}
+167
View File
@@ -0,0 +1,167 @@
var _ = require("underscore");
var htmlWriter = require("./html-writer");
function symmetricMarkdownElement(end) {
return markdownElement(end, end);
}
function markdownElement(start, end) {
return function() {
return {start: start, end: end};
};
}
function markdownLink(attributes) {
var href = attributes.href || "";
if (href) {
return {
start: "[",
end: "](" + escapeMarkdown(href) + ")",
anchorPosition: "before"
};
} else {
return {};
}
}
function markdownImage(attributes) {
var src = attributes.src || "";
var altText = attributes.alt || "";
if (src || altText) {
return {start: "![" + escapeMarkdown(altText) + "](" + escapeMarkdown(src) + ")"};
} else {
return {};
}
}
function markdownList(options) {
return function(attributes, list) {
return {
start: list ? "\n" : "",
end: list ? "" : "\n",
list: {
isOrdered: options.isOrdered,
indent: list ? list.indent + 1 : 0,
count: 0
}
};
};
}
function markdownListItem(attributes, list, listItem) {
list = list || {indent: 0, isOrdered: false, count: 0};
list.count++;
listItem.hasClosed = false;
var bullet = list.isOrdered ? list.count + "." : "-";
var start = repeatString("\t", list.indent) + bullet + " ";
return {
start: start,
end: function() {
if (!listItem.hasClosed) {
listItem.hasClosed = true;
return "\n";
}
}
};
}
var htmlToMarkdown = {
"p": markdownElement("", "\n\n"),
"br": markdownElement("", " \n"),
"ul": markdownList({isOrdered: false}),
"ol": markdownList({isOrdered: true}),
"li": markdownListItem,
"strong": symmetricMarkdownElement("__"),
"em": symmetricMarkdownElement("*"),
"a": markdownLink,
"img": markdownImage
};
(function() {
for (var i = 1; i <= 6; i++) {
htmlToMarkdown["h" + i] = markdownElement(repeatString("#", i) + " ", "\n\n");
}
})();
function repeatString(value, count) {
return new Array(count + 1).join(value);
}
function markdownWriter() {
var fragments = [];
var elementStack = [];
var list = null;
var listItem = {};
function open(tagName, attributes) {
attributes = attributes || {};
var createElement = htmlToMarkdown[tagName] || function() {
return {};
};
var element = createElement(attributes, list, listItem);
elementStack.push({end: element.end, list: list});
if (element.list) {
list = element.list;
}
var anchorBeforeStart = element.anchorPosition === "before";
if (anchorBeforeStart) {
writeAnchor(attributes);
}
fragments.push(element.start || "");
if (!anchorBeforeStart) {
writeAnchor(attributes);
}
}
function writeAnchor(attributes) {
if (attributes.id) {
var writer = htmlWriter.writer();
writer.open("a", {id: attributes.id});
writer.close("a");
fragments.push(writer.asString());
}
}
function close(tagName) {
var element = elementStack.pop();
list = element.list;
var end = _.isFunction(element.end) ? element.end() : element.end;
fragments.push(end || "");
}
function selfClosing(tagName, attributes) {
open(tagName, attributes);
close(tagName);
}
function text(value) {
fragments.push(escapeMarkdown(value));
}
function asString() {
return fragments.join("");
}
return {
asString: asString,
open: open,
close: close,
text: text,
selfClosing: selfClosing
};
}
exports.writer = markdownWriter;
function escapeMarkdown(value) {
return value
.replace(/\\/g, '\\\\')
.replace(/([\`\*_\{\}\[\]\(\)\#\+\-\.\!])/g, '\\$1');
}
+8
View File
@@ -0,0 +1,8 @@
var nodes = require("./nodes");
exports.Element = nodes.Element;
exports.element = nodes.element;
exports.emptyElement = nodes.emptyElement;
exports.text = nodes.text;
exports.readString = require("./reader").readString;
exports.writeString = require("./writer").writeString;
+70
View File
@@ -0,0 +1,70 @@
var _ = require("underscore");
exports.Element = Element;
exports.element = function(name, attributes, children) {
return new Element(name, attributes, children);
};
exports.text = function(value) {
return {
type: "text",
value: value
};
};
var emptyElement = exports.emptyElement = {
first: function() {
return null;
},
firstOrEmpty: function() {
return emptyElement;
},
attributes: {},
children: []
};
function Element(name, attributes, children) {
this.type = "element";
this.name = name;
this.attributes = attributes || {};
this.children = children || [];
}
Element.prototype.first = function(name) {
return _.find(this.children, function(child) {
return child.name === name;
});
};
Element.prototype.firstOrEmpty = function(name) {
return this.first(name) || emptyElement;
};
Element.prototype.getElementsByTagName = function(name) {
var elements = _.filter(this.children, function(child) {
return child.name === name;
});
return toElementList(elements);
};
Element.prototype.text = function() {
if (this.children.length === 0) {
return "";
} else if (this.children.length !== 1 || this.children[0].type !== "text") {
throw new Error("Not implemented");
}
return this.children[0].value;
};
var elementListPrototype = {
getElementsByTagName: function(name) {
return toElementList(_.flatten(this.map(function(element) {
return element.getElementsByTagName(name);
}, true)));
}
};
function toElementList(array) {
return _.extend(array, elementListPrototype);
}
+69
View File
@@ -0,0 +1,69 @@
var promises = require("../promises");
var _ = require("underscore");
var xmldom = require("./xmldom");
var nodes = require("./nodes");
var Element = nodes.Element;
exports.readString = readString;
var Node = xmldom.Node;
function readString(xmlString, namespaceMap) {
namespaceMap = namespaceMap || {};
try {
var document = xmldom.parseFromString(xmlString, "text/xml");
} catch (error) {
return promises.reject(error);
}
if (document.documentElement.tagName === "parsererror") {
return promises.resolve(new Error(document.documentElement.textContent));
}
function convertNode(node) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
return convertElement(node);
case Node.TEXT_NODE:
return nodes.text(node.nodeValue);
}
}
function convertElement(element) {
var convertedName = convertName(element);
var convertedChildren = [];
_.forEach(element.childNodes, function(childNode) {
var convertedNode = convertNode(childNode);
if (convertedNode) {
convertedChildren.push(convertedNode);
}
});
var convertedAttributes = Object.create(null);
_.forEach(element.attributes, function(attribute) {
convertedAttributes[convertName(attribute)] = attribute.value;
});
return new Element(convertedName, convertedAttributes, convertedChildren);
}
function convertName(node) {
if (node.namespaceURI) {
var mappedPrefix = namespaceMap[node.namespaceURI];
var prefix;
if (mappedPrefix) {
prefix = mappedPrefix + ":";
} else {
prefix = "{" + node.namespaceURI + "}";
}
return prefix + node.localName;
} else {
return node.localName;
}
}
return promises.resolve(convertNode(document.documentElement));
}
+61
View File
@@ -0,0 +1,61 @@
var _ = require("underscore");
var xmlbuilder = require("xmlbuilder");
exports.writeString = writeString;
function writeString(root, namespaces) {
var uriToPrefix = _.invert(namespaces);
var nodeWriters = {
element: writeElement,
text: writeTextNode
};
function writeNode(builder, node) {
return nodeWriters[node.type](builder, node);
}
function writeElement(builder, element) {
var elementBuilder = builder.element(mapElementName(element.name), element.attributes);
element.children.forEach(function(child) {
writeNode(elementBuilder, child);
});
}
function mapElementName(name) {
var longFormMatch = /^\{(.*)\}(.*)$/.exec(name);
if (longFormMatch) {
var prefix = uriToPrefix[longFormMatch[1]];
return prefix + (prefix === "" ? "" : ":") + longFormMatch[2];
} else {
return name;
}
}
function writeDocument(root) {
var builder = xmlbuilder
.create(mapElementName(root.name), {
version: '1.0',
encoding: 'UTF-8',
standalone: true
});
_.forEach(namespaces, function(uri, prefix) {
var key = "xmlns" + (prefix === "" ? "" : ":" + prefix);
builder.attribute(key, uri);
});
root.children.forEach(function(child) {
writeNode(builder, child);
});
return builder.end();
}
return writeDocument(root);
}
function writeTextNode(builder, node) {
builder.text(node.value);
}
+23
View File
@@ -0,0 +1,23 @@
var xmldom = require("@xmldom/xmldom");
var dom = require("@xmldom/xmldom/lib/dom");
function parseFromString(string) {
var error = null;
var domParser = new xmldom.DOMParser({
errorHandler: function(level, message) {
error = {level: level, message: message};
}
});
var document = domParser.parseFromString(string);
if (error === null) {
return document;
} else {
throw new Error(error.level + ": " + error.message);
}
}
exports.parseFromString = parseFromString;
exports.Node = dom.Node;
+72
View File
@@ -0,0 +1,72 @@
var base64js = require("base64-js");
var JSZip = require("jszip");
exports.openArrayBuffer = openArrayBuffer;
exports.splitPath = splitPath;
exports.joinPath = joinPath;
function openArrayBuffer(arrayBuffer) {
return JSZip.loadAsync(arrayBuffer).then(function(zipFile) {
function exists(name) {
return zipFile.file(name) !== null;
}
function read(name, encoding) {
return zipFile.file(name).async("uint8array").then(function(array) {
if (encoding === "base64") {
return base64js.fromByteArray(array);
} else if (encoding) {
var decoder = new TextDecoder(encoding);
return decoder.decode(array);
} else {
return array;
}
});
}
function write(name, contents) {
zipFile.file(name, contents);
}
function toArrayBuffer() {
return zipFile.generateAsync({type: "arraybuffer"});
}
return {
exists: exists,
read: read,
write: write,
toArrayBuffer: toArrayBuffer
};
});
}
function splitPath(path) {
var lastIndex = path.lastIndexOf("/");
if (lastIndex === -1) {
return {dirname: "", basename: path};
} else {
return {
dirname: path.substring(0, lastIndex),
basename: path.substring(lastIndex + 1)
};
}
}
function joinPath() {
var nonEmptyPaths = Array.prototype.filter.call(arguments, function(path) {
return path;
});
var relevantPaths = [];
nonEmptyPaths.forEach(function(path) {
if (/^\//.test(path)) {
relevantPaths = [path];
} else {
relevantPaths.push(path);
}
});
return relevantPaths.join("/");
}