<commit_before>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 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) {
        return flatMap(elements, function(element) {
            return elementToHtml(element, messages);
        });
    }

    function elementToHtml(element, messages) {
        var handler = elementConverters[element.type];
        if (handler) {
            return handler(element, messages);
        } else {
            return [];
        }
    }

    function convertParagraph(element, messages) {
        return htmlPathForParagraph(element, messages).wrap(function() {
            var content = convertElements(element.children, messages);
            if (options.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) {
        var nodes = function() {
            return convertElements(run.children, messages);
        };
        var paths = [];
        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;
        if (run.styleId) {
            var style = findStyle(run);
            if (style) {
                stylePath = style.to;
            } else {
                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];
            }
        }
    }

    var defaultConvertImage = images.imgElement(function(element) {
        return element.read("base64").then(function(imageBuffer) {
            return {
                src: "data:" + element.contentType + ";base64," + imageBuffer
            };
        });
    });

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

    function convertTable(element, messages) {
        return wrapChildrenInFreshElement(element, "table", messages);
    }

    function convertTableRow(element, messages) {
        return wrapChildrenInFreshElement(element, "tr", messages);
    }

    function convertTableCell(element, messages) {
        var children = convertElements(element.children, messages);
        var attributes = {};
        if (element.colSpan !== 1) {
            attributes.colspan = element.colSpan.toString();
        }
        if (element.rowSpan !== 1) {
            attributes.rowspan = element.rowSpan.toString();
        }

        return [
            Html.freshElement("td", attributes, [Html.forceWrite].concat(children))
        ];
    }

    function convertCommentReference(reference, messages) {
        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) {
        // TODO: remove duplication with note references

        var label = referencedComment.label;
        var comment = referencedComment.comment;
        var body = convertElements(comment.body).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 wrapChildrenInFreshElement(element, wrapElementName, messages) {
        var children = convertElements(element.children, messages);
        return [
            Html.freshElement(wrapElementName, {}, [Html.forceWrite].concat(children))
        ];
    }

    var elementConverters = {
        "document": function(document, messages) {
            var children = convertElements(document.children, messages);
            var notes = noteReferences.map(function(noteReference) {
                return document.notes.resolve(noteReference);
            });
            var notesNodes = convertElements(notes, messages);
            return children.concat([
                Html.freshElement("ol", {}, notesNodes),
                Html.freshElement("dl", {}, flatMap(referencedComments, function(referencedComment) {
                    return convertComment(referencedComment, messages);
                }))
            ]);
        },
        "paragraph": convertParagraph,
        "run": convertRun,
        "text": function(element, messages) {
            return [Html.text(element.value)];
        },
        "tab": function(element, messages) {
            return [Html.text("\t")];
        },
        "hyperlink": function(element, messages) {
            var href = element.anchor ? "#" + htmlId(element.anchor) : element.href;

            var children = convertElements(element.children, messages);
            return [Html.freshElement("a", {href: href}, children)];
        },
        "bookmarkStart": function(element, messages) {
            var anchor = Html.freshElement("a", {
                id: htmlId(element.name)
            }, [Html.forceWrite]);
            return [anchor];
        },
        "noteReference": function(element, messages) {
            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) {
            var children = convertElements(element.body, messages);
            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 || defaultConvertImage)),
        "table": convertTable,
        "tableRow": convertTableRow,
        "tableCell": convertTableCell,
        "lineBreak": function(element, messages) {
            return [Html.selfClosingElement("br")];
        }
    };
    return {
        convertToHtml: convertToHtml
    };
}

var deferredId = 1;

function deferredConversion(func) {
    return function(element, messages) {
        return [
            {
                type: "deferred",
                id: deferredId++,
                value: function() {
                    return func(element, messages);
                }
            }
        ];
    };
}

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 || "";
};
<commit_msg>Allow matching against all runs<commit_after>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 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) {
        return flatMap(elements, function(element) {
            return elementToHtml(element, messages);
        });
    }

    function elementToHtml(element, messages) {
        var handler = elementConverters[element.type];
        if (handler) {
            return handler(element, messages);
        } else {
            return [];
        }
    }

    function convertParagraph(element, messages) {
        return htmlPathForParagraph(element, messages).wrap(function() {
            var content = convertElements(element.children, messages);
            if (options.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) {
        var nodes = function() {
            return convertElements(run.children, messages);
        };
        var paths = [];
        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];
            }
        }
    }

    var defaultConvertImage = images.imgElement(function(element) {
        return element.read("base64").then(function(imageBuffer) {
            return {
                src: "data:" + element.contentType + ";base64," + imageBuffer
            };
        });
    });

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

    function convertTable(element, messages) {
        return wrapChildrenInFreshElement(element, "table", messages);
    }

    function convertTableRow(element, messages) {
        return wrapChildrenInFreshElement(element, "tr", messages);
    }

    function convertTableCell(element, messages) {
        var children = convertElements(element.children, messages);
        var attributes = {};
        if (element.colSpan !== 1) {
            attributes.colspan = element.colSpan.toString();
        }
        if (element.rowSpan !== 1) {
            attributes.rowspan = element.rowSpan.toString();
        }

        return [
            Html.freshElement("td", attributes, [Html.forceWrite].concat(children))
        ];
    }

    function convertCommentReference(reference, messages) {
        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) {
        // TODO: remove duplication with note references

        var label = referencedComment.label;
        var comment = referencedComment.comment;
        var body = convertElements(comment.body).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 wrapChildrenInFreshElement(element, wrapElementName, messages) {
        var children = convertElements(element.children, messages);
        return [
            Html.freshElement(wrapElementName, {}, [Html.forceWrite].concat(children))
        ];
    }

    var elementConverters = {
        "document": function(document, messages) {
            var children = convertElements(document.children, messages);
            var notes = noteReferences.map(function(noteReference) {
                return document.notes.resolve(noteReference);
            });
            var notesNodes = convertElements(notes, messages);
            return children.concat([
                Html.freshElement("ol", {}, notesNodes),
                Html.freshElement("dl", {}, flatMap(referencedComments, function(referencedComment) {
                    return convertComment(referencedComment, messages);
                }))
            ]);
        },
        "paragraph": convertParagraph,
        "run": convertRun,
        "text": function(element, messages) {
            return [Html.text(element.value)];
        },
        "tab": function(element, messages) {
            return [Html.text("\t")];
        },
        "hyperlink": function(element, messages) {
            var href = element.anchor ? "#" + htmlId(element.anchor) : element.href;

            var children = convertElements(element.children, messages);
            return [Html.freshElement("a", {href: href}, children)];
        },
        "bookmarkStart": function(element, messages) {
            var anchor = Html.freshElement("a", {
                id: htmlId(element.name)
            }, [Html.forceWrite]);
            return [anchor];
        },
        "noteReference": function(element, messages) {
            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) {
            var children = convertElements(element.body, messages);
            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 || defaultConvertImage)),
        "table": convertTable,
        "tableRow": convertTableRow,
        "tableCell": convertTableCell,
        "lineBreak": function(element, messages) {
            return [Html.selfClosingElement("br")];
        }
    };
    return {
        convertToHtml: convertToHtml
    };
}

var deferredId = 1;

function deferredConversion(func) {
    return function(element, messages) {
        return [
            {
                type: "deferred",
                id: deferredId++,
                value: function() {
                    return func(element, messages);
                }
            }
        ];
    };
}

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


<commit_before>exports.BodyReader = BodyReader;

var _ = require("underscore");

var documents = require("../documents");
var Result = require("../results").Result;
var warning = require("../results").warning;

function BodyReader(options) {
    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 readRunProperties(element) {
        return readRunStyle(element).map(function(style) {
            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"],
                isBold: readBooleanElement(element.first("w:b")),
                isUnderline: readBooleanElement(element.first("w:u")),
                isItalic: readBooleanElement(element.first("w:i")),
                isStrikethrough: readBooleanElement(element.first("w:strike"))
            };
        });
    }

    function readBooleanElement(element) {
        if (element) {
            var value = element.attributes["w:val"];
            return value !== "false" && value !== "0";
        } else {
            return false;
        }
    }

    function readParagraphStyle(element) {
        return readStyle(element, "w:pStyle", "Paragraph", styles.findParagraphStyleById);
    }

    function readRunStyle(element) {
        return readStyle(element, "w:rStyle", "Run", styles.findCharacterStyleById);
    }

    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 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) {
            return readXmlElements(element.children)
                .map(function(children) {
                    var properties = _.find(children, isParagraphProperties);
                    return new documents.Paragraph(
                        children.filter(negate(isParagraphProperties)),
                        properties
                    );
                })
                .insertExtra();
        },
        "w:pPr": function(element) {
            var properties = {
                type: "paragraphProperties"
            };

            var alignElement = element.first("w:jc");
            if (alignElement) {
                properties.alignment = alignElement.attributes["w:val"];
            }
            properties.numbering = readNumberingProperties(element.firstOrEmpty("w:numPr"));

            return readParagraphStyle(element).map(function(style) {
                properties.styleId = style.styleId;
                properties.styleName = style.name;
                return properties;
            });
        },
        "w:r": function(element) {
            return readXmlElements(element.children)
                .map(function(children) {
                    var properties = _.find(children, isRunProperties);

                    return new documents.Run(
                        children.filter(negate(isRunProperties)),
                        properties
                    );
                });
        },
        "w:rPr": readRunProperties,
        "w:t": function(element) {
            return elementResult(new documents.Text(element.text()));
        },
        "w:tab": function(element) {
            return elementResult(new documents.Tab());
        },
        "w:hyperlink": function(element) {
            var relationshipId = element.attributes["r:id"];
            var anchor = element.attributes["w:anchor"];
            return readXmlElements(element.children).map(function(children) {
                if (relationshipId) {
                    var href = relationships[relationshipId].target;
                    return new documents.Hyperlink(children, {href: href});
                } else if (anchor) {
                    return new documents.Hyperlink(children, {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.first("mc:Fallback"));
        },

        "w:sdt": function(element) {
            return readXmlElements(element.firstOrEmpty("w:sdtContent").children);
        },

        "w:ins": 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
    };
    return {
        readXmlElement: readXmlElement,
        readXmlElements: readXmlElements,
        _readNumberingProperties: readNumberingProperties
    };

    function readNumberingProperties(element) {
        var level = element.firstOrEmpty("w:ilvl").attributes["w:val"];
        var numId = element.firstOrEmpty("w:numId").attributes["w:val"];
        if (level === undefined || numId === undefined) {
            return null;
        } else {
            return numbering.findLevel(numId, level);
        }
    }

    function readTable(element) {
        return readXmlElements(element.children)
            .flatMap(calculateRowSpans)
            .map(documents.Table);
    }

    function readTableRow(element) {
        return readXmlElements(element.children).map(documents.TableRow);
    }

    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) {
            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) {
            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 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 properties = element.first("wp:docPr").attributes;
        var altText = isBlank(properties.descr) ? properties.title : properties.descr;
        return readImage(findBlipImageFile(blip), altText);
    }

    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 {
            var imagePath = relationships[linkRelationshipid].target;
            return {
                path: imagePath,
                read: files.read.bind(files, imagePath)
            };
        }
    }

    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 = joinZipPath("word", relationships[relationshipId].target);
        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");
    }
}

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:tblPr": true,
    "w:tblGrid": true,
    "w:tcPr": true
};

function isParagraphProperties(element) {
    return element.type === "paragraphProperties";
}

function isRunProperties(element) {
    return element.type === "runProperties";
}

function negate(predicate) {
    return function(value) {
        return !predicate(value);
    };
}

function joinZipPath(first, second) {
    // In general, we should check first and second for trailing and leading slashes,
    // but in our specific case this seems to be sufficient
    return first + "/" + second;
}

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

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;
}
<commit_msg>Simplify reading of paragraph properties<commit_after>exports.BodyReader = BodyReader;

var _ = require("underscore");

var documents = require("../documents");
var Result = require("../results").Result;
var warning = require("../results").warning;

function BodyReader(options) {
    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 readRunProperties(element) {
        return readRunStyle(element).map(function(style) {
            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"],
                isBold: readBooleanElement(element.first("w:b")),
                isUnderline: readBooleanElement(element.first("w:u")),
                isItalic: readBooleanElement(element.first("w:i")),
                isStrikethrough: readBooleanElement(element.first("w:strike"))
            };
        });
    }

    function readBooleanElement(element) {
        if (element) {
            var value = element.attributes["w:val"];
            return value !== "false" && value !== "0";
        } else {
            return false;
        }
    }

    function readParagraphStyle(element) {
        return readStyle(element, "w:pStyle", "Paragraph", styles.findParagraphStyleById);
    }

    function readRunStyle(element) {
        return readStyle(element, "w:rStyle", "Run", styles.findCharacterStyleById);
    }

    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 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) {
            return readXmlElements(element.children)
                .map(function(children) {
                    var properties = _.find(children, isParagraphProperties);
                    return new documents.Paragraph(
                        children.filter(negate(isParagraphProperties)),
                        properties
                    );
                })
                .insertExtra();
        },
        "w:pPr": function(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(element.firstOrEmpty("w:numPr"))
                };
            });
        },
        "w:r": function(element) {
            return readXmlElements(element.children)
                .map(function(children) {
                    var properties = _.find(children, isRunProperties);

                    return new documents.Run(
                        children.filter(negate(isRunProperties)),
                        properties
                    );
                });
        },
        "w:rPr": readRunProperties,
        "w:t": function(element) {
            return elementResult(new documents.Text(element.text()));
        },
        "w:tab": function(element) {
            return elementResult(new documents.Tab());
        },
        "w:hyperlink": function(element) {
            var relationshipId = element.attributes["r:id"];
            var anchor = element.attributes["w:anchor"];
            return readXmlElements(element.children).map(function(children) {
                if (relationshipId) {
                    var href = relationships[relationshipId].target;
                    return new documents.Hyperlink(children, {href: href});
                } else if (anchor) {
                    return new documents.Hyperlink(children, {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.first("mc:Fallback"));
        },

        "w:sdt": function(element) {
            return readXmlElements(element.firstOrEmpty("w:sdtContent").children);
        },

        "w:ins": 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
    };
    return {
        readXmlElement: readXmlElement,
        readXmlElements: readXmlElements,
        _readNumberingProperties: readNumberingProperties
    };

    function readNumberingProperties(element) {
        var level = element.firstOrEmpty("w:ilvl").attributes["w:val"];
        var numId = element.firstOrEmpty("w:numId").attributes["w:val"];
        if (level === undefined || numId === undefined) {
            return null;
        } else {
            return numbering.findLevel(numId, level);
        }
    }

    function readTable(element) {
        return readXmlElements(element.children)
            .flatMap(calculateRowSpans)
            .map(documents.Table);
    }

    function readTableRow(element) {
        return readXmlElements(element.children).map(documents.TableRow);
    }

    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) {
            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) {
            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 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 properties = element.first("wp:docPr").attributes;
        var altText = isBlank(properties.descr) ? properties.title : properties.descr;
        return readImage(findBlipImageFile(blip), altText);
    }

    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 {
            var imagePath = relationships[linkRelationshipid].target;
            return {
                path: imagePath,
                read: files.read.bind(files, imagePath)
            };
        }
    }

    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 = joinZipPath("word", relationships[relationshipId].target);
        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");
    }
}

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:tblPr": true,
    "w:tblGrid": true,
    "w:tcPr": true
};

function isParagraphProperties(element) {
    return element.type === "paragraphProperties";
}

function isRunProperties(element) {
    return element.type === "runProperties";
}

function negate(predicate) {
    return function(value) {
        return !predicate(value);
    };
}

function joinZipPath(first, second) {
    // In general, we should check first and second for trailing and leading slashes,
    // but in our specific case this seems to be sufficient
    return first + "/" + second;
}

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

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


<commit_before>// @flow

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React from 'react';
import {Select, Icon, Button} from 'antd';
import _ from 'lodash';

import HelpTooltip from './HelpTooltip';

export const AGGREGATION_FUNCTIONS = [
  'Min',
  'Max',
  'Mean',
  'Median',
  'Count',
  'Sum',
  'SumSq',
  'Stdev',
  'P10',
  'P20',
  'P30',
  'P40',
  'P50',
  'P60',
  'P70',
  'P80',
  'P90',
  'P95',
  'P99',
  'P999',
  'P9999',
];

export const POLICIES = [
  '1s:1h',
  '10s:6h',
  '10s:12h',
  '10s:1d',
  '10s:2d',
  '1m:2d',
  '1m:7d',
  '1m:14d',
  '1m:40d',
  '10m:2d',
  '10m:7d',
  '10m:14d',
  '10m:40d',
];

const Option = Select.Option;

type Props = {
  value: any,
  onChange: e => any,
  showDelete: boolean,
  onDeleteClicked: () => any,
  typeTag: string,
};

type PolicyObject = {
  timePolicy: string,
  aggFunctions: array<string>,
};

export function parsePolicy(policy: string): PolicyObject {
  const policyParsed = policy.split('|');
  return {
    timePolicy: _.first(policyParsed),
    aggFunctions: _.chain(policyParsed)
      .nth(1)
      .thru(value => {
        if (_.isEmpty(value)) {
          return [];
        }
        return value.split(',');
      })
      .value(),
  };
}

export function stringifyPolicy(policy: PolicyObject): string {
  return `${policy.timePolicy}${_.isEmpty(policy.aggFunctions)
    ? ''
    : `|${policy.aggFunctions.join(',')}`}`;
}

export function PolicyEditor(props: Props) {
  const {
    value = _.first(POLICIES),
    onDeleteClicked = noop => noop,
    showDelete = false,
    onChange = noop => noop,
    typeTag = '',
    policyList = POLICIES,
  } = props;
  const policy = parsePolicy(value);
  const handleChange = (field, newValue) => {
    onChange(
      stringifyPolicy({
        ...policy,
        [field]: newValue,
      }),
    );
  };
  const showAggs = typeTag === 'timer';
  let policies = policyList;
  if (!_.includes(['gauge', 'counter'], typeTag)) {
    policies = policies.filter(p => !p.startsWith('10m'));
  }

  return (

      {showDelete && (

          <Icon type="close-circle-o" />

      )}

          Resolution:Retention Period{' '}
          <HelpTooltip helpTextKey="resolution:retention-period" />

        <Select
          placeholder="Select a policy"
          value={policy.timePolicy}
          autoComplete="off"
          onChange={e => {
            handleChange('timePolicy', e);
          }}
          style={{width: '100%'}}
          notFoundContent={false}>
          {policies.map(opt => <Option key={opt}>{opt}</Option>)}
        </Select>
        {typeTag === 'timer' && (

            Resolutions {'>'} 1 minute are not supported for timers

        )}

          Aggregation Functions{' '}
          <HelpTooltip helpTextKey="aggregation-function" />

        {showAggs ? (
          <Select
            placeholder="Use default if none selected"
            value={policy.aggFunctions}
            onChange={e => {
              handleChange('aggFunctions', e);
            }}
            autoComplete="off"
            mode="multiple"
            style={{width: '100%'}}
            tokenSeparators={[',']}
            notFoundContent={false}>
            {AGGREGATION_FUNCTIONS.map(opt => <Option key={opt}>{opt}</Option>)}
          </Select>
        ) : (
          Only supported for timers
        )}

  );
}

function PoliciesEditor(props) {
  const {value: policies, onChange, typeTag} = props;
  return (

      {_.map(policies, (p, i) => {
        return (

            <PolicyEditor
              onDeleteClicked={() => {
                onChange(policies.filter((__, idx) => i !== idx));
              }}
              showDelete={_.size(policies) > 1}
              value={p}
              typeTag={typeTag}
              onChange={e => {
                const newPolicy = policies.slice();
                newPolicy[i] = e;
                onChange(newPolicy);
              }}
            />

        );
      })}

        <Button
          className="right"
          size="small"
          type="dashed"
          onClick={() => {
            onChange((policies || []).concat(_.first(POLICIES)));
          }}>
          Add Policy
        </Button>

  );
}

export default PoliciesEditor;
<commit_msg>Update policy list (#56)<commit_after>// @flow

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React from 'react';
import {Select, Icon, Button} from 'antd';
import _ from 'lodash';

import HelpTooltip from './HelpTooltip';

export const AGGREGATION_FUNCTIONS = [
  'Min',
  'Max',
  'Mean',
  'Median',
  'Count',
  'Sum',
  'SumSq',
  'Stdev',
  'P10',
  'P20',
  'P30',
  'P40',
  'P50',
  'P60',
  'P70',
  'P80',
  'P90',
  'P95',
  'P99',
  'P999',
  'P9999',
];

export const POLICIES = [
  '10s:2d',
  '1m:2d',
  '1m:40d',
  '10m:180d',
  '10m:1y',
  '10m:3y',
  '10m:5y',
  '1h:1y',
  '1h:3y',
  '1h:5y',
];

const Option = Select.Option;

type Props = {
  value: any,
  onChange: e => any,
  showDelete: boolean,
  onDeleteClicked: () => any,
  typeTag: string,
};

type PolicyObject = {
  timePolicy: string,
  aggFunctions: array<string>,
};

export function parsePolicy(policy: string): PolicyObject {
  const policyParsed = policy.split('|');
  return {
    timePolicy: _.first(policyParsed),
    aggFunctions: _.chain(policyParsed)
      .nth(1)
      .thru(value => {
        if (_.isEmpty(value)) {
          return [];
        }
        return value.split(',');
      })
      .value(),
  };
}

export function stringifyPolicy(policy: PolicyObject): string {
  return `${policy.timePolicy}${_.isEmpty(policy.aggFunctions)
    ? ''
    : `|${policy.aggFunctions.join(',')}`}`;
}

export function PolicyEditor(props: Props) {
  const {
    value = _.first(POLICIES),
    onDeleteClicked = noop => noop,
    showDelete = false,
    onChange = noop => noop,
    typeTag = '',
    policyList = POLICIES,
  } = props;
  const policy = parsePolicy(value);
  const handleChange = (field, newValue) => {
    onChange(
      stringifyPolicy({
        ...policy,
        [field]: newValue,
      }),
    );
  };
  const showAggs = typeTag === 'timer';
  let policies = policyList;
  if (!_.includes(['gauge', 'counter'], typeTag)) {
    policies = policies.filter(p => !p.startsWith('1h'));
  }

  return (

      {showDelete && (

          <Icon type="close-circle-o" />

      )}

          Resolution:Retention Period{' '}
          <HelpTooltip helpTextKey="resolution:retention-period" />

        <Select
          placeholder="Select a policy"
          value={policy.timePolicy}
          autoComplete="off"
          onChange={e => {
            handleChange('timePolicy', e);
          }}
          style={{width: '100%'}}
          notFoundContent={false}>
          {policies.map(opt => <Option key={opt}>{opt}</Option>)}
        </Select>
        {typeTag === 'timer' && (

            Resolutions {'>'} 1 minute are not supported for timers

        )}

          Aggregation Functions{' '}
          <HelpTooltip helpTextKey="aggregation-function" />

        {showAggs ? (
          <Select
            placeholder="Use default if none selected"
            value={policy.aggFunctions}
            onChange={e => {
              handleChange('aggFunctions', e);
            }}
            autoComplete="off"
            mode="multiple"
            style={{width: '100%'}}
            tokenSeparators={[',']}
            notFoundContent={false}>
            {AGGREGATION_FUNCTIONS.map(opt => <Option key={opt}>{opt}</Option>)}
          </Select>
        ) : (
          Only supported for timers
        )}

  );
}

function PoliciesEditor(props) {
  const {value: policies, onChange, typeTag} = props;
  return (

      {_.map(policies, (p, i) => {
        return (

            <PolicyEditor
              onDeleteClicked={() => {
                onChange(policies.filter((__, idx) => i !== idx));
              }}
              showDelete={_.size(policies) > 1}
              value={p}
              typeTag={typeTag}
              onChange={e => {
                const newPolicy = policies.slice();
                newPolicy[i] = e;
                onChange(newPolicy);
              }}
            />

        );
      })}

        <Button
          className="right"
          size="small"
          type="dashed"
          onClick={() => {
            onChange((policies || []).concat(_.first(POLICIES)));
          }}>
          Add Policy
        </Button>

  );
}

export default PoliciesEditor;
<|endoftext|>


<commit_before>import _ from 'lodash'

import db from './gitGraphAst'
import gitGraphParser from './parser/gitGraph'
import d3 from '../../d3'
import { logger } from '../../logger'

let allCommitsDict = {}
let branchNum
let config = {
  nodeSpacing: 150,
  nodeFillColor: 'yellow',
  nodeStrokeWidth: 2,
  nodeStrokeColor: 'grey',
  lineStrokeWidth: 4,
  branchOffset: 50,
  lineColor: 'grey',
  leftMargin: 50,
  branchColors: ['#442f74', '#983351', '#609732', '#AA9A39'],
  nodeRadius: 10,
  nodeLabel: {
    width: 75,
    height: 100,
    x: -25,
    y: 0
  }
}
let apiConfig = {}
export const setConf = function (c) {
  apiConfig = c
}

function svgCreateDefs (svg) {
  svg
    .append('defs')
    .append('g')
    .attr('id', 'def-commit')
    .append('circle')
    .attr('r', config.nodeRadius)
    .attr('cx', 0)
    .attr('cy', 0)
  svg.select('#def-commit')
    .append('foreignObject')
    .attr('width', config.nodeLabel.width)
    .attr('height', config.nodeLabel.height)
    .attr('x', config.nodeLabel.x)
    .attr('y', config.nodeLabel.y)
    .attr('class', 'node-label')
    .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
    .append('xhtml:p')
    .html('')
}

function svgDrawLine (svg, points, colorIdx, interpolate) {
  interpolate = interpolate || 'basis'
  const color = config.branchColors[colorIdx % config.branchColors.length]
  const lineGen = d3.svg.line()
    .x(function (d) {
      return Math.round(d.x)
    })
    .y(function (d) {
      return Math.round(d.y)
    })
    .interpolate(interpolate)

  svg
    .append('svg:path')
    .attr('d', lineGen(points))
    .style('stroke', color)
    .style('stroke-width', config.lineStrokeWidth)
    .style('fill', 'none')
}
// Pass in the element and its pre-transform coords
function getElementCoords (element, coords) {
  coords = coords || element.node().getBBox()
  const ctm = element.node().getCTM()
  const xn = ctm.e + coords.x * ctm.a
  const yn = ctm.f + coords.y * ctm.d
  return {
    left: xn,
    top: yn,
    width: coords.width,
    height: coords.height
  }
}

function svgDrawLineForCommits (svg, fromId, toId, direction, color) {
  logger.debug('svgDrawLineForCommits: ', fromId, toId)
  const fromBbox = getElementCoords(svg.select('#node-' + fromId + ' circle'))
  const toBbox = getElementCoords(svg.select('#node-' + toId + ' circle'))
  switch (direction) {
    case 'LR':
      // (toBbox)
      //  +--------
      //          + (fromBbox)
      if (fromBbox.left - toBbox.left > config.nodeSpacing) {
        const lineStart = { x: fromBbox.left - config.nodeSpacing, y: toBbox.top + toBbox.height / 2 }
        const lineEnd = { x: toBbox.left + toBbox.width, y: toBbox.top + toBbox.height / 2 }
        svgDrawLine(svg, [lineStart, lineEnd], color, 'linear')
        svgDrawLine(svg, [
          { x: fromBbox.left, y: fromBbox.top + fromBbox.height / 2 },
          { x: fromBbox.left - config.nodeSpacing / 2, y: fromBbox.top + fromBbox.height / 2 },
          { x: fromBbox.left - config.nodeSpacing / 2, y: lineStart.y },
          lineStart], color)
      } else {
        svgDrawLine(svg, [{
          'x': fromBbox.left,
          'y': fromBbox.top + fromBbox.height / 2
        }, {
          'x': fromBbox.left - config.nodeSpacing / 2,
          'y': fromBbox.top + fromBbox.height / 2
        }, {
          'x': fromBbox.left - config.nodeSpacing / 2,
          'y': toBbox.top + toBbox.height / 2
        }, {
          'x': toBbox.left + toBbox.width,
          'y': toBbox.top + toBbox.height / 2
        }], color)
      }
      break
    case 'BT':
      //      +           (fromBbox)
      //      |
      //      |
      //              +   (toBbox)
      if (toBbox.top - fromBbox.top > config.nodeSpacing) {
        const lineStart = { x: toBbox.left + toBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing }
        const lineEnd = { x: toBbox.left + toBbox.width / 2, y: toBbox.top }
        svgDrawLine(svg, [lineStart, lineEnd], color, 'linear')
        svgDrawLine(svg, [
          { x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height },
          { x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing / 2 },
          { x: toBbox.left + toBbox.width / 2, y: lineStart.y - config.nodeSpacing / 2 },
          lineStart], color)
      } else {
        svgDrawLine(svg, [{
          'x': fromBbox.left + fromBbox.width / 2,
          'y': fromBbox.top + fromBbox.height
        }, {
          'x': fromBbox.left + fromBbox.width / 2,
          'y': fromBbox.top + config.nodeSpacing / 2
        }, {
          'x': toBbox.left + toBbox.width / 2,
          'y': toBbox.top - config.nodeSpacing / 2
        }, {
          'x': toBbox.left + toBbox.width / 2,
          'y': toBbox.top
        }], color)
      }
      break
  }
}

function cloneNode (svg, selector) {
  return svg.select(selector).node().cloneNode(true)
}

function renderCommitHistory (svg, commitid, branches, direction) {
  let commit
  const numCommits = Object.keys(allCommitsDict).length
  if (_.isString(commitid)) {
    do {
      commit = allCommitsDict[commitid]
      logger.debug('in renderCommitHistory', commit.id, commit.seq)
      if (svg.select('#node-' + commitid).size() > 0) {
        return
      }
      svg
        .append(function () {
          return cloneNode(svg, '#def-commit')
        })
        .attr('class', 'commit')
        .attr('id', function () {
          return 'node-' + commit.id
        })
        .attr('transform', function () {
          switch (direction) {
            case 'LR':
              return 'translate(' + (commit.seq * config.nodeSpacing + config.leftMargin) + ', ' +
                (branchNum * config.branchOffset) + ')'
            case 'BT':
              return 'translate(' + (branchNum * config.branchOffset + config.leftMargin) + ', ' +
                ((numCommits - commit.seq) * config.nodeSpacing) + ')'
          }
        })
        .attr('fill', config.nodeFillColor)
        .attr('stroke', config.nodeStrokeColor)
        .attr('stroke-width', config.nodeStrokeWidth)

      const branch = _.find(branches, ['commit', commit])
      if (branch) {
        logger.debug('found branch ', branch.name)
        svg.select('#node-' + commit.id + ' p')
          .append('xhtml:span')
          .attr('class', 'branch-label')
          .text(branch.name + ', ')
      }
      svg.select('#node-' + commit.id + ' p')
        .append('xhtml:span')
        .attr('class', 'commit-id')
        .text(commit.id)
      if (commit.message !== '' && direction === 'BT') {
        svg.select('#node-' + commit.id + ' p')
          .append('xhtml:span')
          .attr('class', 'commit-msg')
          .text(', ' + commit.message)
      }
      commitid = commit.parent
    } while (commitid && allCommitsDict[commitid])
  }

  if (_.isArray(commitid)) {
    logger.debug('found merge commmit', commitid)
    renderCommitHistory(svg, commitid[0], branches, direction)
    branchNum++
    renderCommitHistory(svg, commitid[1], branches, direction)
    branchNum--
  }
}

function renderLines (svg, commit, direction, branchColor) {
  branchColor = branchColor || 0
  while (commit.seq > 0 && !commit.lineDrawn) {
    if (_.isString(commit.parent)) {
      svgDrawLineForCommits(svg, commit.id, commit.parent, direction, branchColor)
      commit.lineDrawn = true
      commit = allCommitsDict[commit.parent]
    } else if (_.isArray(commit.parent)) {
      svgDrawLineForCommits(svg, commit.id, commit.parent[0], direction, branchColor)
      svgDrawLineForCommits(svg, commit.id, commit.parent[1], direction, branchColor + 1)
      renderLines(svg, allCommitsDict[commit.parent[1]], direction, branchColor + 1)
      commit.lineDrawn = true
      commit = allCommitsDict[commit.parent[0]]
    }
  }
}

export const draw = function (txt, id, ver) {
  try {
    const parser = gitGraphParser.parser
    parser.yy = db

    logger.debug('in gitgraph renderer', txt, id, ver)
    // Parse the graph definition
    parser.parse(txt + '\n')

    config = _.extend(config, apiConfig, db.getOptions())
    logger.debug('effective options', config)
    const direction = db.getDirection()
    allCommitsDict = db.getCommits()
    const branches = db.getBranchesAsObjArray()
    if (direction === 'BT') {
      config.nodeLabel.x = branches.length * config.branchOffset
      config.nodeLabel.width = '100%'
      config.nodeLabel.y = -1 * 2 * config.nodeRadius
    }
    const svg = d3.select('#' + id)
    svgCreateDefs(svg)
    branchNum = 1
    _.each(branches, function (v) {
      renderCommitHistory(svg, v.commit.id, branches, direction)
      renderLines(svg, v.commit, direction)
      branchNum++
    })
    svg.attr('height', function () {
      if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing
      return (branches.length + 1) * config.branchOffset
    })
  } catch (e) {
    logger.error('Error while rendering gitgraph')
    logger.error(e.message)
  }
}

export default {
  setConf,
  draw
}
<commit_msg>Fix git graph<commit_after>import _ from 'lodash'

import db from './gitGraphAst'
import gitGraphParser from './parser/gitGraph'
import d3 from '../../d3'
import { logger } from '../../logger'

let allCommitsDict = {}
let branchNum
let config = {
  nodeSpacing: 150,
  nodeFillColor: 'yellow',
  nodeStrokeWidth: 2,
  nodeStrokeColor: 'grey',
  lineStrokeWidth: 4,
  branchOffset: 50,
  lineColor: 'grey',
  leftMargin: 50,
  branchColors: ['#442f74', '#983351', '#609732', '#AA9A39'],
  nodeRadius: 10,
  nodeLabel: {
    width: 75,
    height: 100,
    x: -25,
    y: 0
  }
}
let apiConfig = {}
export const setConf = function (c) {
  apiConfig = c
}

function svgCreateDefs (svg) {
  svg
    .append('defs')
    .append('g')
    .attr('id', 'def-commit')
    .append('circle')
    .attr('r', config.nodeRadius)
    .attr('cx', 0)
    .attr('cy', 0)
  svg.select('#def-commit')
    .append('foreignObject')
    .attr('width', config.nodeLabel.width)
    .attr('height', config.nodeLabel.height)
    .attr('x', config.nodeLabel.x)
    .attr('y', config.nodeLabel.y)
    .attr('class', 'node-label')
    .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
    .append('xhtml:p')
    .html('')
}

function svgDrawLine (svg, points, colorIdx, interpolate) {
  let curve = d3.curveBasis
  if (interpolate === 'linear') {
    curve = d3.curveLinear
  }
  const color = config.branchColors[colorIdx % config.branchColors.length]
  const lineGen = d3.line()
    .x(function (d) {
      return Math.round(d.x)
    })
    .y(function (d) {
      return Math.round(d.y)
    })
    .curve(curve)

  svg
    .append('svg:path')
    .attr('d', lineGen(points))
    .style('stroke', color)
    .style('stroke-width', config.lineStrokeWidth)
    .style('fill', 'none')
}
// Pass in the element and its pre-transform coords
function getElementCoords (element, coords) {
  coords = coords || element.node().getBBox()
  const ctm = element.node().getCTM()
  const xn = ctm.e + coords.x * ctm.a
  const yn = ctm.f + coords.y * ctm.d
  return {
    left: xn,
    top: yn,
    width: coords.width,
    height: coords.height
  }
}

function svgDrawLineForCommits (svg, fromId, toId, direction, color) {
  logger.debug('svgDrawLineForCommits: ', fromId, toId)
  const fromBbox = getElementCoords(svg.select('#node-' + fromId + ' circle'))
  const toBbox = getElementCoords(svg.select('#node-' + toId + ' circle'))
  switch (direction) {
    case 'LR':
      // (toBbox)
      //  +--------
      //          + (fromBbox)
      if (fromBbox.left - toBbox.left > config.nodeSpacing) {
        const lineStart = { x: fromBbox.left - config.nodeSpacing, y: toBbox.top + toBbox.height / 2 }
        const lineEnd = { x: toBbox.left + toBbox.width, y: toBbox.top + toBbox.height / 2 }
        svgDrawLine(svg, [lineStart, lineEnd], color, 'linear')
        svgDrawLine(svg, [
          { x: fromBbox.left, y: fromBbox.top + fromBbox.height / 2 },
          { x: fromBbox.left - config.nodeSpacing / 2, y: fromBbox.top + fromBbox.height / 2 },
          { x: fromBbox.left - config.nodeSpacing / 2, y: lineStart.y },
          lineStart], color)
      } else {
        svgDrawLine(svg, [{
          'x': fromBbox.left,
          'y': fromBbox.top + fromBbox.height / 2
        }, {
          'x': fromBbox.left - config.nodeSpacing / 2,
          'y': fromBbox.top + fromBbox.height / 2
        }, {
          'x': fromBbox.left - config.nodeSpacing / 2,
          'y': toBbox.top + toBbox.height / 2
        }, {
          'x': toBbox.left + toBbox.width,
          'y': toBbox.top + toBbox.height / 2
        }], color)
      }
      break
    case 'BT':
      //      +           (fromBbox)
      //      |
      //      |
      //              +   (toBbox)
      if (toBbox.top - fromBbox.top > config.nodeSpacing) {
        const lineStart = { x: toBbox.left + toBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing }
        const lineEnd = { x: toBbox.left + toBbox.width / 2, y: toBbox.top }
        svgDrawLine(svg, [lineStart, lineEnd], color, 'linear')
        svgDrawLine(svg, [
          { x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height },
          { x: fromBbox.left + fromBbox.width / 2, y: fromBbox.top + fromBbox.height + config.nodeSpacing / 2 },
          { x: toBbox.left + toBbox.width / 2, y: lineStart.y - config.nodeSpacing / 2 },
          lineStart], color)
      } else {
        svgDrawLine(svg, [{
          'x': fromBbox.left + fromBbox.width / 2,
          'y': fromBbox.top + fromBbox.height
        }, {
          'x': fromBbox.left + fromBbox.width / 2,
          'y': fromBbox.top + config.nodeSpacing / 2
        }, {
          'x': toBbox.left + toBbox.width / 2,
          'y': toBbox.top - config.nodeSpacing / 2
        }, {
          'x': toBbox.left + toBbox.width / 2,
          'y': toBbox.top
        }], color)
      }
      break
  }
}

function cloneNode (svg, selector) {
  return svg.select(selector).node().cloneNode(true)
}

function renderCommitHistory (svg, commitid, branches, direction) {
  let commit
  const numCommits = Object.keys(allCommitsDict).length
  if (_.isString(commitid)) {
    do {
      commit = allCommitsDict[commitid]
      logger.debug('in renderCommitHistory', commit.id, commit.seq)
      if (svg.select('#node-' + commitid).size() > 0) {
        return
      }
      svg
        .append(function () {
          return cloneNode(svg, '#def-commit')
        })
        .attr('class', 'commit')
        .attr('id', function () {
          return 'node-' + commit.id
        })
        .attr('transform', function () {
          switch (direction) {
            case 'LR':
              return 'translate(' + (commit.seq * config.nodeSpacing + config.leftMargin) + ', ' +
                (branchNum * config.branchOffset) + ')'
            case 'BT':
              return 'translate(' + (branchNum * config.branchOffset + config.leftMargin) + ', ' +
                ((numCommits - commit.seq) * config.nodeSpacing) + ')'
          }
        })
        .attr('fill', config.nodeFillColor)
        .attr('stroke', config.nodeStrokeColor)
        .attr('stroke-width', config.nodeStrokeWidth)

      const branch = _.find(branches, ['commit', commit])
      if (branch) {
        logger.debug('found branch ', branch.name)
        svg.select('#node-' + commit.id + ' p')
          .append('xhtml:span')
          .attr('class', 'branch-label')
          .text(branch.name + ', ')
      }
      svg.select('#node-' + commit.id + ' p')
        .append('xhtml:span')
        .attr('class', 'commit-id')
        .text(commit.id)
      if (commit.message !== '' && direction === 'BT') {
        svg.select('#node-' + commit.id + ' p')
          .append('xhtml:span')
          .attr('class', 'commit-msg')
          .text(', ' + commit.message)
      }
      commitid = commit.parent
    } while (commitid && allCommitsDict[commitid])
  }

  if (_.isArray(commitid)) {
    logger.debug('found merge commmit', commitid)
    renderCommitHistory(svg, commitid[0], branches, direction)
    branchNum++
    renderCommitHistory(svg, commitid[1], branches, direction)
    branchNum--
  }
}

function renderLines (svg, commit, direction, branchColor) {
  branchColor = branchColor || 0
  while (commit.seq > 0 && !commit.lineDrawn) {
    if (_.isString(commit.parent)) {
      svgDrawLineForCommits(svg, commit.id, commit.parent, direction, branchColor)
      commit.lineDrawn = true
      commit = allCommitsDict[commit.parent]
    } else if (_.isArray(commit.parent)) {
      svgDrawLineForCommits(svg, commit.id, commit.parent[0], direction, branchColor)
      svgDrawLineForCommits(svg, commit.id, commit.parent[1], direction, branchColor + 1)
      renderLines(svg, allCommitsDict[commit.parent[1]], direction, branchColor + 1)
      commit.lineDrawn = true
      commit = allCommitsDict[commit.parent[0]]
    }
  }
}

export const draw = function (txt, id, ver) {
  try {
    const parser = gitGraphParser.parser
    parser.yy = db

    logger.debug('in gitgraph renderer', txt, id, ver)
    // Parse the graph definition
    parser.parse(txt + '\n')

    config = _.extend(config, apiConfig, db.getOptions())
    logger.debug('effective options', config)
    const direction = db.getDirection()
    allCommitsDict = db.getCommits()
    const branches = db.getBranchesAsObjArray()
    if (direction === 'BT') {
      config.nodeLabel.x = branches.length * config.branchOffset
      config.nodeLabel.width = '100%'
      config.nodeLabel.y = -1 * 2 * config.nodeRadius
    }
    const svg = d3.select('#' + id)
    svgCreateDefs(svg)
    branchNum = 1
    _.each(branches, function (v) {
      renderCommitHistory(svg, v.commit.id, branches, direction)
      renderLines(svg, v.commit, direction)
      branchNum++
    })
    svg.attr('height', function () {
      if (direction === 'BT') return Object.keys(allCommitsDict).length * config.nodeSpacing
      return (branches.length + 1) * config.branchOffset
    })
  } catch (e) {
    logger.error('Error while rendering gitgraph')
    logger.error(e.message)
  }
}

export default {
  setConf,
  draw
}
<|endoftext|>


<commit_before>const PouchDB = require('pouchdb-http')

const env = require('./env')

module.exports = async function () {
  // create /_users if not exists
  const host = env.COUCHDB_URL
  const dbName = '_users'
  const users = new PouchDB(`${host}/${dbName}`)
  return users.info()
}
<commit_msg>fix: more reliable db instantiation<commit_after>const { resolve } = require('url')
const PouchDB = require('pouchdb-http')

const env = require('./env')

module.exports = async function () {
  // create /_users if not exists
  const host = env.COUCH_URL
  const dbName = '_users'
  const dbUrl = resolve(host, dbName)
  const users = new PouchDB(dbUrl)
  return users.info()
}
<|endoftext|>