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