13
\$\begingroup\$

I have this plain JavaScript library for typesetting pseudocode in HTML documents. (See the documentation for details.) (See the GitHub repository - broken as of now.)

Source code

algotype.js

//// ////////////////////////////////////////////////////// ////
 // Algotype.js, version 1.6 by Rodion "(code)rodde" Efremov // 
////////////////////////////////////////////////////////////////

var Algotype = {};

// The string beginning the comments of the algorithm declaration.
Algotype.ALGORITHM_HEADER_COMMENT_TAG = "#";

// The string beginning the step comments. 
Algotype.ALGORITHM_STEP_COMMENT_TAG = "#";

// The width of code line numbers. This default works well. If you, however, 
// need to typeset an algorithm with at least 100 rows (in which case the space
// is tight) just increase this constant.
Algotype.LINE_NUMBER_WIDTH = 25;

// The indentation in pixels.
Algotype.INDENTATION_WIDTH = 30;

// Number of pixels between the line number span and the pseudocode span.
Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE = 8;

// The URL from which to download the MathJax math typesetting facilities.
Algotype.MATHJAX_SCRIPT_URL = 
    "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_CHTML";

// Configuration for MathJax. This provides in the pseudocode macros for such
// keywords as 'and', 'or', 'not', and others, with the font that matches the 
// actual keywords, such as 'for each', 'repeat ... until', etc.
Algotype.MATHJAX_CONFIG =
        'MathJax.Hub.Config({' + 
        "tex2jax: {inlineMath: [['$','$']]}," +
        'TeX: {' + 
            'Macros: {' +
                'And:     "\\\\mathbf{and}",'    +
                'Or:      "\\\\mathbf{or}",'     +
                'Not:     "\\\\mathbf{not}",'    +
                'Is:      "\\\\mathbf{is}",'     +
                'In:      "\\\\mathbf{in}",'     +
                'Mapped:  "\\\\mathbf{mapped}",' +
                'Nil:     "\\\\mathbf{nil}"'     +
            '}' +
        '}' +
    '});';

Algotype.MATHJAX_CONFIG_MIME_TYPE = "text/x-mathjax-config";

Algotype.UNNAMED_ALGORITHM = "UnnamedAlgorithm";

Algotype.loadMathJax = function() {
    // Load the MathJax.
    var importedScript = document.createElement("script");
    importedScript.async = "true";
    importedScript.src = Algotype.MATHJAX_SCRIPT_URL;
    document.head.appendChild(importedScript);

    // Make MathJax process the configuration.
    var mathJaxSettingsScript = document.createElement("script");
    mathJaxSettingsScript.type = Algotype.MATHJAX_CONFIG_MIME_TYPE;
    mathJaxSettingsScript.innerHTML = Algotype.MATHJAX_CONFIG;
    document.head.appendChild(mathJaxSettingsScript);
};

Algotype.getAlgorithmHeaderComment = function (algorithmElement) {
    var algorithmHeaderComment = 
            algorithmElement.getAttribute("comment");

    if (!algorithmHeaderComment) {
        return "";
    }

    return " " +
            Algotype.ALGORITHM_HEADER_COMMENT_TAG + 
            " " + algorithmHeaderComment;
};

Algotype.getAlgorithmParameterList = function(algorithmElement) {
    var algorithmParameterList = 
            algorithmElement.getAttribute("parameters") || "";

    algorithmParameterList = algorithmParameterList.trim();

    if (!algorithmParameterList) {
        return "$()$";
    }

    // Remove the beginning parenthesis, if present.
    if (algorithmParameterList[0] === "(") {
        algorithmParameterList = 
                algorithmParameterList.substring(1, 
                                                 algorithmParameterList.length);
    }

    // Remove the ending parenthesis, if present.
    if (algorithmParameterList[algorithmParameterList.length - 1] === ")") {
        algorithmParameterList =
                algorithmParameterList
                .substring(0, algorithmParameterList.length - 1);
    }

    // Remove possible leading and trailing space within the parentheses.
    algorithmParameterList = algorithmParameterList.trim();

    // Split the string into parameter tokens.
    var algorithmParameters = algorithmParameterList.split(/\s*,\s*|\s+/);

    // Construct the TeX for the algorithm parameter list.
    var tex = "$(";
    var separator = "";

    for (var i = 0; i < algorithmParameters.length; ++i) {
        tex += separator;
        tex += algorithmParameters[i];
        separator = ", ";
    }

    return tex + ")$";
};

Algotype.getLabelHtml = function(state, label) {
    return "<table class='algotype-code-row-table'>\n" +
           "  <tbody class='algotype-code-row-tbody'\n" +
           "    <tr class='algotype-algorithm-line'>\n" +
           "      <td class='algotype-algorithm-line-number'></td>\n" +
           "      <td class='algotype-line-number-space' width='" + 
           (Algotype.INDENTATION_WIDTH * state["indentation"] +
            Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) +
           "px'></td>\n" + 
           "      <td class='algotype-label algotype-text'>" + label + 
           "</td>\n" +
           "    </tr>\n" +
           "  </tbody>\n" +
           "</table>\n";
};

Algotype.typesetIf = function(ifElement, state) {
    var conditionTeX = (ifElement.getAttribute("condition") || "").trim();
    conditionTeX = addTeXDelimeters(conditionTeX);

    var htmlText = "";
    var comment = ifElement.getAttribute("comment");
    var commentId = (ifElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    var ifId = (ifElement.getAttribute("id") || "").trim();
    var ifIdTextBegin = "";
    var ifIdTextEnd = "";

    if (ifId) {
        ifIdTextBegin = "<span id='" + ifId + "'>";
        ifIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                ifIdTextBegin + "if " + 
                conditionTeX + ":" + ifIdTextEnd + 
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = ifElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

Algotype.typesetElseIf = function(elseIfElement, state) {
    var conditionTeX = (elseIfElement.getAttribute("condition") || "").trim();
    conditionTeX = addTeXDelimeters(conditionTeX);

    var htmlText = "";
    var comment = elseIfElement.getAttribute("comment");
    var commentId = (elseIfElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    var elseIfId = (elseIfElement.getAttribute("id") || "").trim();
    var elseIfIdTextBegin = "";
    var elseIfIdTextEnd = "";

    if (elseIfId) {
        elseIfIdTextBegin = "<span id='" + elseIfId + "'>";
        elseIfIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                elseIfIdTextBegin + "else if " + 
                conditionTeX + ":" + elseIfIdTextEnd + 
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = elseIfElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

Algotype.typesetElse = function(elseElement, state) {
    var htmlText = "";
    var comment = elseElement.getAttribute("comment");
    var commentId = (elseElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    var elseId = (elseElement.getAttribute("id") || "").trim();
    var elseIdTextBegin = "";
    var elseIdTextEnd = "";

    if (elseId) {
        elseIdTextBegin = "<span id='" + elseId + "'>";
        elseIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                elseIdTextBegin + "else:" + elseIdTextEnd + 
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;


    state["indentation"]++;

    var childElements = elseElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

Algotype.typesetStep = function(stepElement, state, isReturn) {
    var stepText = (stepElement.innerHTML || "").trim();
    var inTeX = false;
    var htmlText = "";
    var call = "";

    for (var i = 0; i < stepText.length; ++i) {
        var character = stepText[i];

        switch (character) {
            case '$':
                if (!inTeX) {
                    if (call) {
                        // Dump the current call.
                        htmlText += 
                            " <span " + 
                            "class='algotype-text algotype-algorithm-name'>" + 
                            call + 
                            "</span>";

                        call = "";
                    }

                    inTeX = true;
                } else {
                    inTeX = false;
                }

                htmlText += "$";
                break;

            default:

                if (inTeX) {
                    htmlText += character;
                } else {
                    call += character;
                }
        }
    }

    if (call) {
        // Handling the trailing call sequence.
        htmlText += " <span " + 
                    "class='algotype-text algotype-algorithm-name'>" + 
                    call + 
                    "</span>";
    }

    var comment = stepElement.getAttribute("comment") || "";
    var commentId = (stepElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "'";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment'" + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    var returnHtml = "";

    if (isReturn === true) {
        returnHtml = "<span class='algotype-text algotype-keyword'>" +
                     "return</span> ";
    }

    var stepId = (stepElement.getAttribute("id") || "").trim();
    var stepIdTextBegin = "";
    var stepIdTextEnd = "";

    if (stepId) {
        stepIdTextBegin = "<span id='" + stepId + "'>";
        stepIdTextEnd = "</span>";
    }

    htmlText = "<table class='algotype-code-row-table'>\n" + 
               "  <tbody class='algotype-code-row-tbody'>\n" +
               "    <tr class='algotype-algorithm-line'>\n" +
               "      <td class='algotype-algorithm-line-number'>" +
               state["lineNumber"] +
               "</td> " +
               "<td class='algotype-line-number-space' width='" +
               (Algotype.INDENTATION_WIDTH * state["indentation"] +
                Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) +
               "px'></td>" +
               "<td class='algotype-text'>" + 
               stepIdTextBegin + 
               returnHtml + 
               htmlText + stepIdTextEnd + comment +
               "</td>\n" +
               "    </tr>\n" +
               "  </tbody>\n" +
               "</table>\n";

    state["lineNumber"]++;
    return htmlText;
};

Algotype.typesetReturn = function(returnElement, state) {
    return Algotype.typesetStep(returnElement, state, true);
};

Algotype.typesetBreak = function(breakElement, state) {
    var comment = breakElement.getAttribute("comment") || "";
    var commentId = (breakElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "'";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment'" + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    var label = breakElement.innerHTML;
    var breakId = (breakElement.getAttribute("id") || "").trim();
    var breakIdTextBegin = "";
    var breakIdTextEnd = "";

    if (breakId) {
        breakIdTextBegin = "<span id='" + breakId + "'>";
        breakIdTextEnd = "</span>";
    }

    var htmlText = 
            "<table class='algotype-code-row-table'>\n" +
            "  <tbody class='algotype-code-row-tbody'>\n" +
            "    <tr class='algotype-algorithm-line'>\n" +
            "      <td class='algotype-algorithm-line-number'>" +
            state["lineNumber"] + "</td>\n" +
            "      <td class='algotype-line-number-space' width='" +
            (Algotype.INDENTATION_WIDTH * state["indentation"] +
             Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) +
            "px'></td>\n" +
            "      <td class='algotype-text algotype-keyword'>" +
            breakIdTextBegin + "break " +
            (label ? "<span class='algotype-label'>" + label + "</span>" : "") +
            breakIdTextEnd + comment +
            "</td>\n" +
            "    </tr>\n" +
            "  </tbody>\n" +
            "</table>\n";


    state["lineNumber"]++;
    return htmlText;
};

Algotype.typesetContinue = function(continueElement, state) {
    var comment = continueElement.getAttribute("comment") || "";
    var commentId = (continueElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "'";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment'" + idText + ">" + 
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " + 
                  comment.trim() + "</span>";
    }

    var label = continueElement.innerHTML;
    var continueId = (continueElement.getAttribute("id") || "").trim();
    var continueIdTextBegin = "";
    var continueIdTextEnd = "";

    if (continueId) {
        continueIdTextBegin = "<span id='" + continueId + "'>";
        continueIdTextEnd = "</span>";
    }

    var htmlText = "<table class='algotype-code-row-table'>\n" +
            "  <tbody class='algotype-code-row-tbody'>\n" +
            "    <tr class='algotype-algorithm-line'>\n" +
            "      <td class='algotype-algorithm-line-number'>" +
            state["lineNumber"] + "</td>\n" +
            "      <td class='algotype-line-number-space' width='" +
            (Algotype.INDENTATION_WIDTH * state["indentation"] +
             Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) +
            "px'></td>\n" +
            "      <td class='algotype-text algotype-keyword'>" +
            continueIdTextBegin + "continue " +
            (label ? "<span class='algotype-label'>" + label + "</span>" : "") +
            continueIdTextEnd + comment +
            "</td>\n" +
            "    </tr>\n" +
            "  </tbody>\n" +
            "</table>\n";


    state["lineNumber"]++;
    return htmlText;
};

Algotype.typesetForEach = function(forEachElement, state) {
    var conditionTeX = forEachElement.getAttribute("condition") || "";
    conditionTeX = conditionTeX.trim();

    if (conditionTeX[0] !== "$") {
        conditionTeX = "$" + conditionTeX;
    }

    if (conditionTeX[conditionTeX.length - 1] !== "$") {
        conditionTeX += "$";
    }

    var label = forEachElement.getAttribute("label");
    var htmlText = "";
    var comment = forEachElement.getAttribute("comment");
    var commentId = (forEachElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" + 
                Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                comment.trim() + "</span>";
    }

    if (label) {
        label = label.trim();

        if (label[label.length - 1] !== ":") {
            label += ":";
        }

        htmlText += Algotype.getLabelHtml(state, label);
    }

    var forEachId = (forEachElement.getAttribute("id") || "").trim();
    var forEachIdTextBegin = "";
    var forEachIdTextEnd = "";

    if (forEachId) {
        forEachIdTextBegin = "<span id='" + forEachId +"'>";
        forEachIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                forEachIdTextBegin + "for each " + 
                conditionTeX + ":" + forEachIdTextEnd + 
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = forEachElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

function addTeXDelimeters(code) {
    if (!code) {
        return "";
    }

    code = code.trim();

    if (code[0] !== "$") {
        code = "$" + code;
    }

    if (code[code.length - 1] !== "$") {
        code += "$";
    }

    return code;
}

Algotype.typesetFor = function(forElement, state) {
    var initConditionTeX = forElement.getAttribute("init") || "";
    var toConditionTeX = forElement.getAttribute("to") || "";
    var stepConditionTeX = forElement.getAttribute("step") || "";

    initConditionTeX = addTeXDelimeters(initConditionTeX);
    toConditionTeX = addTeXDelimeters(toConditionTeX);

    if (stepConditionTeX) {
        stepConditionTeX = addTeXDelimeters(stepConditionTeX);
    }

    var label = forElement.getAttribute("label");
    var htmlText = "";
    var comment = forElement.getAttribute("comment");
    var commentId = (forElement.getAttribute("comment-id") || "").trim();
    var idText = "";
    var stepText = "";

    if (stepConditionTeX) {
        stepText = " step " + stepConditionTeX;
    }

    if (commentId) {
        idText = "id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                comment.trim() + "</span>";
    }

    if (label) {
        label = label.trim();

        if (label[label.length - 1] !== ":") {
            label += ":";
        }

        htmlText += Algotype.getLabelHtml(state, label);
    }

    var forId = (forElement.getAttribute("id") || "").trim();
    var forIdTextBegin = "";
    var forIdTextEnd = "";

    if (forId) {
        forIdTextBegin = "<span id='" + forId + "'>";
        forIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                forIdTextBegin + "for " + 
                initConditionTeX + " to " + toConditionTeX + stepText + ":" + 
                forIdTextEnd +
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = forElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

Algotype.typesetForDownto = function(forDowntoElement, state) {
    var initConditionTeX = forDowntoElement.getAttribute("init") || "";
    var toConditionTeX   = forDowntoElement.getAttribute("to")   || "";
    var stepConditionTeX = forDowntoElement.getAttribute("step") || "";

    initConditionTeX = addTeXDelimeters(initConditionTeX);
    toConditionTeX   = addTeXDelimeters(toConditionTeX);

    if (stepConditionTeX) {
        stepConditionTeX = addTeXDelimeters(stepConditionTeX);
    }

    var label = forDowntoElement.getAttribute("label");
    var htmlText = "";
    var comment = forDowntoElement.getAttribute("comment");
    var commentId = (forDowntoElement.getAttribute("comment-id") || "").trim();
    var idText = "";
    var stepText = "";

    if (stepConditionTeX) {
        stepText = " step " + stepConditionTeX;
    }

    if (commentId) {
        idText = "id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    if (label) {
        label = label.trim();

        if (label[label.length - 1] !== ":") {
            label += ":";
        }

        htmlText += Algotype.getLabelHtml(state, label);
    }

    var forDowntoId = (forDowntoElement.getAttribute("id") || "").trim();
    var forDowntoTextBegin = "";
    var forDowntoTextEnd = "";

    if (forDowntoId) {
        forDowntoTextBegin = "<span id='" + forDowntoId + "'>";
        forDowntoTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" + 
                forDowntoTextBegin + "for " +
                initConditionTeX + " downto " + toConditionTeX + stepText + 
                ":" + 
                forDowntoTextEnd +
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;
    var childElements = forDowntoElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

Algotype.typesetForever = function(foreverElement, state) {
    var label = foreverElement.getAttribute("label");
    var htmlText = "";
    var comment = foreverElement.getAttribute("comment");
    var commentId = (foreverElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = "id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                comment.trim() + "</span>";
    }

    if (label) {
        label = label.trim();

        if (label[label.length - 1] !== ":") {
            label += ":";
        }

        htmlText += Algotype.getLabelHtml(state, label);
    }

    var foreverId = (foreverElement.getAttribute("id") || "").trim();
    var foreverIdTextBegin = "";
    var foreverIdTextEnd = "";

    if (foreverId) {
        foreverIdTextBegin = "<span id='" + foreverId + "'>";
        foreverIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                foreverIdTextBegin + "forever:" + foreverIdTextEnd +
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = foreverElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;   
    return htmlText;
};

Algotype.typesetWhile = function(whileElement, state) {
    var conditionTeX = (whileElement.getAttribute("condition") || "").trim();

    conditionTeX = addTeXDelimeters(conditionTeX);

    var label = whileElement.getAttribute("label");
    var htmlText = "";
    var comment = whileElement.getAttribute("comment");
    var commentId = (whileElement.getAttribute("comment-id") || "").trim();
    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " + 
                  comment.trim() + "</span>";
    }

    if (label) {
        label = label.trim();

        if (label[label.length - 1] !== ":") {
            label += ":";
        }

        htmlText += Algotype.getLabelHtml(state, label);
    }

    var whileId = (whileElement.getAttribute("id") || "").trim();
    var whileIdTextBegin = "";
    var whileIdTextEnd = "";

    if (whileId) {
        whileIdTextBegin = "<span id='" + whileId + "'>";
        whileIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                whileIdTextBegin + "while " + 
                conditionTeX + ":" + whileIdTextEnd + 
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = whileElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;
    return htmlText;
};

Algotype.typesetRepeatUntil = function(repeatUntilElement, state) {
    var conditionTeX = 
            (repeatUntilElement.getAttribute("condition") || "").trim();

    conditionTeX = addTeXDelimeters(conditionTeX);

    var label = repeatUntilElement.getAttribute("label");
    var htmlText = "";
    var comment = repeatUntilElement.getAttribute("comment");
    var commentId = 
            (repeatUntilElement.getAttribute("comment-id") || "").trim();

    var idText = "";

    if (commentId) {
        idText = " id='" + commentId + "' ";
    }

    if (comment) {
        comment = " <span class='algotype-step-comment' " + idText + ">" +
                  Algotype.ALGORITHM_STEP_COMMENT_TAG + " " +
                  comment.trim() + "</span>";
    }

    if (label) {
        label = label.trim();

        if (label[label.length - 1] !== ":") {
            label += ":";
        }

        htmlText += Algotype.getLabelHtml(state, label);
    }

    var repeatUntilId = (repeatUntilElement.getAttribute("id") || "").trim();
    var repeatUntilIdTextBegin = "";
    var repeatUntilIdTextEnd = "";

    if (repeatUntilId) {
        repeatUntilIdTextBegin = "<span id='" + repeatUntilId + "'>";
        repeatUntilIdTextEnd = "</span>";
    }

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                repeatUntilIdTextBegin + "repeat" + repeatUntilIdTextEnd + 
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    var saveIndentation = state["indentation"];

    state["lineNumber"]++;
    state["indentation"]++;

    var childElements = repeatUntilElement.children;

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    // Reset the indentation counter.
    state["indentation"] = saveIndentation;

    htmlText += "<table class='algotype-code-row-table'>\n" +
                "  <tbody class='algotype-code-row-tbody'>\n" +
                "    <tr class='algotype-algorithm-line'>\n" +
                "      <td class='algotype-algorithm-line-number'>" +
                state["lineNumber"] +
                "      </td> " +
                "      <td class='algotype-line-number-space' width='" + 
                (Algotype.INDENTATION_WIDTH * state["indentation"] + 
                 Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
                "px'></td>\n" +
                "      <td class='algotype-text algotype-keyword'>" +
                repeatUntilIdTextBegin + "until " + 
                conditionTeX + repeatUntilIdTextEnd + 
                (comment ? comment : "") +
                "      </td> " +
                "    </tr>\n" +
                "  </tbody>\n" +
                "</table>\n";

    state["lineNumber"]++;
    return htmlText;
};

Algotype.typesetAlgorithm = function(algorithmElement) {
    var algorithmName =
            algorithmElement.getAttribute("name") || Algotype.UNNAMED_ALGORITHM;

    var algorithmParameterList = 
            Algotype.getAlgorithmParameterList(algorithmElement);

    var commentText = Algotype.getAlgorithmHeaderComment(algorithmElement);

    var parentNode = algorithmElement.parentNode;

    var htmlText = 
            "<span class='algotype-text algotype-algorithm-name'>" +
            algorithmName +
            "</span><span class='algotype-text'>" + algorithmParameterList + 
            commentText +
            "</span><br/>";

    var childElements = algorithmElement.children;

    var state = {
        lineNumber: 1,
        indentation: 0
    };

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];

        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }

    var paragraphElement = document.createElement("p");
    paragraphElement.style.textAlign = "left";
    paragraphElement.innerHTML = htmlText;
    parentNode.appendChild(paragraphElement);
};

Algotype.setup = function() {
    // Load MathJax.
    Algotype.loadMathJax();

    // Typeset all algorithms present in the DOM.
    var algorithmList = document.getElementsByTagName("alg-algorithm");

    for (var i = 0; i < algorithmList.length; ++i) {
        Algotype.typesetAlgorithm(algorithmList[i]);
    }
};

Algotype.dispatchTable = {};

Algotype.dispatchTable["alg-foreach"]      = Algotype.typesetForEach;
Algotype.dispatchTable["alg-for"]          = Algotype.typesetFor;
Algotype.dispatchTable["alg-for-downto"]   = Algotype.typesetForDownto;
Algotype.dispatchTable["alg-forever"]      = Algotype.typesetForever;
Algotype.dispatchTable["alg-while"]        = Algotype.typesetWhile;
Algotype.dispatchTable["alg-repeat-until"] = Algotype.typesetRepeatUntil;
Algotype.dispatchTable["alg-if"]           = Algotype.typesetIf;
Algotype.dispatchTable["alg-else-if"]      = Algotype.typesetElseIf;
Algotype.dispatchTable["alg-else"]         = Algotype.typesetElse;
Algotype.dispatchTable["alg-step"]         = Algotype.typesetStep;
Algotype.dispatchTable["alg-return"]       = Algotype.typesetReturn;
Algotype.dispatchTable["alg-break"]        = Algotype.typesetBreak;
Algotype.dispatchTable["alg-continue"]     = Algotype.typesetContinue;

var oldOnloadHandler = window.onload;

window.onload = function() {
    if (oldOnloadHandler) {
        oldOnloadHandler();
    }

    Algotype.setup();
};

algotype.css

/*

//// ////////////////////////////////////////////////////// ////
 // Algotype.js, version 1.6 by Rodion "(code)rodde" Efremov // 
////////////////////////////////////////////////////////////////  */

alg-algorithm {
    display: none;
}

.algotype-text {
    padding-bottom: 2px;
    font-family: Times New Roman;
    font-size: 18px;
}

.algotype-keyword {
    font-weight: bold;
}

table.algotype-code-row-table {
    padding: 0;
    margin: 0;
    border-collapse: collapse;
    margin-bottom: -3px;
}

tbody.algotype-code-row-tbody {
    padding: 0;
    margin: 0;
    margin-bottom: -3px;
}

tr.algotype-algorithm-line {
    padding: 0;
    margin: 0;
    margin-bottom: -3px;
}

td.algotype-algorithm-line-number {
    padding: 0;
    margin: 0;
    font-family: Times New Roman;
    font-size: 16px;
    font-weight: bold;
    width: 20px;
    text-align: right;
    margin-bottom: 0px;
}

td.algotype-line-number-space {
    padding: 0;
    margin: 0;
    margin-bottom: -3px;
}

.algotype-algorithm-name {
    font-variant: small-caps;
    font-weight: bolder;
}

.algotype-label {
    font-size: 14px;
    font-family: monospace;
    font-weight: normal;
}

.algotype-step-comment {
    font-family: Times New Roman;
    font-size: 18px;
    font-weight: normal;
    font-variant: normal;
}

How it looks

Algotype.js in action

Critique request

At the very least, my code is more or less DRY. I am not a professional JavaScript developer, so if you are, please tell me anything I could improve.

\$\endgroup\$
1
\$\begingroup\$

Looks like no one has yet read through this code yet, so I guess I'll give it a try. Here are my initial observations, as well as some general suggestions:

  1. You may want to take a look at ES6 classes.

  2. This is entirely up to you, but some developers will do var Element = document.createElement to cut down on the verbosity of the code. This allows you to type less and increase readability.

  3. Use of ES6 template literals and arrow functions may help shorten some of your code and increase readability. Instead of this:

    return " " +
                Algotype.ALGORITHM_HEADER_COMMENT_TAG + 
                " " + algorithmHeaderComment;
    

    You can do something like this:

    return ` ${Algotype.ALGORITHM_HEADER_COMMENT_TAG} ${algorithmHeaderComment}`;
    
  4. Instead of this:

    Algotype.getAlgorithmParameterList = function(algorithmElement) {
        var algorithmParameterList = 
                algorithmElement.getAttribute("parameters") || "";
    
        algorithmParameterList = algorithmParameterList.trim();
    ...
    

    Why not this?

    Algotype.getAlgorithmParameterList = algorithmElement => {
        var parameterList = (algorithmElement.getAttribute("parameters") || "").trim();
        ...
    }
    

    As an additional comment on your variable names, long and descriptive variable names are good, but you should strike a balance. Variable names that are too long or convey unneeded information can and should be shortened. For example, shortening algorithmElement to element, since one can infer that your functions are supposed to operate on algorithm elements.

  5. Why not regex?

    // Remove the beginning parenthesis, if present.
    if (algorithmParameterList[0] === "(") {
        algorithmParameterList = 
                algorithmParameterList.substring(1, 
                                                 algorithmParameterList.length);
    }
    
    // Remove the ending parenthesis, if present.
    if (algorithmParameterList[algorithmParameterList.length - 1] === ")") {
        algorithmParameterList =
                algorithmParameterList
                .substring(0, algorithmParameterList.length - 1);
    }
    
    // Remove possible leading and trailing space within the parentheses.
    algorithmParameterList = algorithmParameterList.trim();
    

    You used a regex to split the string, why not use one for removing the parentheses and spaces? You can use a regex similar to this to accomplish the same task as the code block above:

    var parameterList = /^\(*\s*([\w]*)\s*\)*$/.exec(parameterList)[1];
    

    Make sure you thoroughly document this in your code though, because this sacrifices readability for brevity.

At the very least, my code is more or less DRY.

  1. Not really, the code is still a little wet. Could use a little more DRY. It seems like code that is very similar to this is repeated a lot, but with slightly different parameterization.

    htmlText += "<table class='algotype-code-row-table'>\n" +
        "  <tbody class='algotype-code-row-tbody'>\n" +
        "    <tr class='algotype-algorithm-line'>\n" +
        "      <td class='algotype-algorithm-line-number'>" +
        state["lineNumber"] +
        "      </td> " +
        "      <td class='algotype-line-number-space' width='" + 
        (Algotype.INDENTATION_WIDTH * state["indentation"] + 
         Algotype.DISTANCE_BETWEEN_LINE_NUMBER_AND_CODE) + 
        "px'></td>\n" +
        "      <td class='algotype-text algotype-keyword'>" +
        ifIdTextBegin + "if " + 
        conditionTeX + ":" + ifIdTextEnd + 
        (comment ? comment : "") +
        "      </td> " +
        "    </tr>\n" +
        "  </tbody>\n" +
        "</table>\n";
    

    Why don't you just write a helper method for generating this? You have Algotype.getLabelHTML already, just modify it slightly to look something like this:

    Algotype.getLabelHtml = (lineNumber, width, content) => {
        return "<table class='algotype-code-row-table'>\n" +
               "  <tbody class='algotype-code-row-tbody'\n" +
               "    <tr class='algotype-algorithm-line'>\n" +
               "      <td class='algotype-algorithm-line-number'>" +
               `        ${lineNumber}\n` +
               "      </td>\n" +
               `      <td class='algotype-line-number-space' width='${width}px'></td>\n` +
               `      ${content}\n` +
               "    </tr>\n" +
               "  </tbody>\n" +
               "</table>\n";
    };
    
  2. Refactor common elements. This chunk seems to be in every one of the typeset* functions.

    for (var i = 0; i < childElements.length; ++i) {
        var elementName = childElements[i].tagName.toLowerCase();
        var handlerFunction = Algotype.dispatchTable[elementName];
    
        if (handlerFunction) {
            htmlText += handlerFunction(childElements[i], state);
        } else {
            throw new Error("Unknown element: '" + elementName + "'.");
        }
    }
    

    You could probably put that in a helper function somewhere.

  3. Have you considered using an XML parser to tokenize this? Or using the native JS DOM to store your generated HTML?

Other than that, the code is mostly understandable and readable. It is a bit monolithic however, which I suspect is why no one has reviewed it. 1200~ lines of code is a bit of a daunting task to read through. I suggest you break it up into submodules. Put all your utility/formatting functions in a file, and put all the typesetting functions in another. That's one way to go about doing it.

Happy coding!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.