You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
15 KiB
456 lines
15 KiB
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, {});
|
|
|
|
var deferredNodes = [];
|
|
walkHtml(html, function(node) {
|
|
if (node.type === "deferred") {
|
|
deferredNodes.push(node);
|
|
}
|
|
});
|
|
var deferredValues = {};
|
|
return promises.mapSeries(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, options) {
|
|
return flatMap(elements, function(element) {
|
|
return elementToHtml(element, messages, options);
|
|
});
|
|
}
|
|
|
|
function elementToHtml(element, messages, options) {
|
|
if (!options) {
|
|
throw new Error("options not set");
|
|
}
|
|
var handler = elementConverters[element.type];
|
|
if (handler) {
|
|
return handler(element, messages, options);
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function convertParagraph(element, messages, options) {
|
|
return htmlPathForParagraph(element, messages).wrap(function() {
|
|
var content = convertElements(element.children, messages, options);
|
|
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, options) {
|
|
var nodes = function() {
|
|
return convertElements(run.children, messages, options);
|
|
};
|
|
var paths = [];
|
|
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.attempt(function() {
|
|
return convertImage(image, messages);
|
|
}).caught(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, options) {
|
|
return findHtmlPath(element, defaultTablePath).wrap(function() {
|
|
return convertTableChildren(element, messages, options);
|
|
});
|
|
}
|
|
|
|
function convertTableChildren(element, messages, options) {
|
|
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({}, options, {isTableHeader: false})
|
|
);
|
|
} else {
|
|
var headRows = convertElements(
|
|
element.children.slice(0, bodyIndex),
|
|
messages,
|
|
_.extend({}, options, {isTableHeader: true})
|
|
);
|
|
var bodyRows = convertElements(
|
|
element.children.slice(bodyIndex),
|
|
messages,
|
|
_.extend({}, options, {isTableHeader: false})
|
|
);
|
|
children = [
|
|
Html.freshElement("thead", {}, headRows),
|
|
Html.freshElement("tbody", {}, bodyRows)
|
|
];
|
|
}
|
|
return [Html.forceWrite].concat(children);
|
|
}
|
|
|
|
function convertTableRow(element, messages, options) {
|
|
var children = convertElements(element.children, messages, options);
|
|
return [
|
|
Html.freshElement("tr", {}, [Html.forceWrite].concat(children))
|
|
];
|
|
}
|
|
|
|
function convertTableCell(element, messages, options) {
|
|
var tagName = options.isTableHeader ? "th" : "td";
|
|
var children = convertElements(element.children, messages, options);
|
|
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, options) {
|
|
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, options) {
|
|
// TODO: remove duplication with note references
|
|
|
|
var label = referencedComment.label;
|
|
var comment = referencedComment.comment;
|
|
var body = convertElements(comment.body, messages, options).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, options) {
|
|
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, options) {
|
|
var children = convertElements(document.children, messages, options);
|
|
var notes = noteReferences.map(function(noteReference) {
|
|
return document.notes.resolve(noteReference);
|
|
});
|
|
var notesNodes = convertElements(notes, messages, options);
|
|
return children.concat([
|
|
Html.freshElement("ol", {}, notesNodes),
|
|
Html.freshElement("dl", {}, flatMap(referencedComments, function(referencedComment) {
|
|
return convertComment(referencedComment, messages, options);
|
|
}))
|
|
]);
|
|
},
|
|
"paragraph": convertParagraph,
|
|
"run": convertRun,
|
|
"text": function(element, messages, options) {
|
|
return [Html.text(element.value)];
|
|
},
|
|
"tab": function(element, messages, options) {
|
|
return [Html.text("\t")];
|
|
},
|
|
"hyperlink": function(element, messages, options) {
|
|
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, options);
|
|
return [Html.nonFreshElement("a", attributes, children)];
|
|
},
|
|
"bookmarkStart": function(element, messages, options) {
|
|
var anchor = Html.freshElement("a", {
|
|
id: htmlId(element.name)
|
|
}, [Html.forceWrite]);
|
|
return [anchor];
|
|
},
|
|
"noteReference": function(element, messages, options) {
|
|
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, options) {
|
|
var children = convertElements(element.body, messages, options);
|
|
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, options) {
|
|
return [
|
|
{
|
|
type: "deferred",
|
|
id: deferredId++,
|
|
value: function() {
|
|
return func(element, messages, options);
|
|
}
|
|
}
|
|
];
|
|
};
|
|
}
|
|
|
|
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 || "";
|
|
};
|
|
|