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

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 || "";
};