/ src / scripts / markdown_parser.js
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
/*
 * Turns markdown into html in the browser, so a page written in markdown
 * shows a preview beside its source as a page written in wiki markup
 * already does. What it makes is meant to match what the site makes of
 * the same page when it saves it, so what a writer sees while writing is
 * what a reader will see.
 */
/*
 * How many spaces of indent begin a block of code, the count GitHub's
 * markdown uses. A leading tab counts as this much.
 */
var MARKDOWN_CODE_INDENT = 4;
/*
 * How many marks in a row open a fenced block of code.
 */
var MARKDOWN_FENCE_LEAST = 3;
/*
 * The notes a page sets down at its foot, by the name each is called by.
 */
var markdown_notes = {};
/*
 * The names of the notes in the order they were first pointed at, which
 * is the order they are numbered and listed in.
 */
var markdown_note_order = [];
/*
 * Turns a whole markdown document into html.
 *
 * @param String document what the writer has written
 * @return String the html it stands for
 */
function parseMarkdownContent(document_text, group_id, page_id,
    token_key, token_value, pretty, base)
{
    var text = String(document_text).replace(/\r\n/g, "\n")
        .replace(/\r/g, "\n");
    var references = {};
    markdown_notes = {};
    markdown_note_order = [];
    text = collectMarkdownReferences(text, references, markdown_notes);
    var lines = text.split("\n");
    var html = parseMarkdownBlocks(lines, {at: 0}, references).join("\n");
    html += markdownNotesSection();
    html = fillMarkdownResources(html, group_id, page_id, token_key,
        token_value, pretty, base);
    /* A preview shows what a reader would see, so the marks the site
       fills in when it shows a page are filled here too. */
    if (typeof fillWikiStandIns === "function") {
        html = fillWikiStandIns(html);
    }
    return html;
}
/*
 * Turns what names a file kept with the page into the picture, the
 * player, or the link that stands for it. The site does this after it has
 * read the page rather than while reading it, so it is done here after
 * the same fashion, and what comes out matches what the reader of a page
 * written in wiki markup already gets.
 *
 * @param String html the page as read so far
 * @param int group_id which group's files to ask for
 * @param int page_id which page's files to ask for
 * @param String token_key what the mark guarding a request is called
 * @param String token_value the mark itself
 * @param bool pretty whether the site serves pretty addresses
 * @param String base where the site is rooted
 * @return String the same, with each named file drawn
 */
function fillMarkdownResources(html, group_id, page_id, token_key,
    token_value, pretty, base)
{
    var group = (group_id === undefined) ? 0 : group_id;
    var page = (page_id === undefined) ? 0 : page_id;
    return String(html).replace(
        /\(\(resource(-nolink|-thumb|-link)?:(.+?)\|(.*?)\)\)/g,
        function (match, kind, resource_name, description)
        {
            var resource_url = wikiResourceUrl(group, page, resource_name,
                token_key, token_value, pretty, base);
            var plain = (kind === "-nolink");
            /* A file asked for as a thumb stands as the small picture
               kept beside it, inside a link to the file itself, so a
               document shows its front page rather than a bare link. */
            if (kind === "-thumb") {
                return '<a class="image-list" href="' + resource_url +
                    '" ><img src="' + wikiThumbUrl(resource_url, pretty) +
                    '" loading="lazy" alt="' + description + '" ></a>';
            }
            if (kind === "-link") {
                return '<a href="' + resource_url + '" title="' +
                    description + '" >' + description + "</a>";
            }
            if ((/\.(avif|bmp|gif|heic|heif|jpeg|jpg|png|svg|tif|tiff|webp)$/i
                ).test(resource_name)) {
                /* A picture asked for without a link is drawn on its
                   own, as the site draws it. */
                if (plain) {
                    return '<img src="' + resource_url +
                        '" loading="lazy" alt="' + description +
                        '" class="photo" >';
                }
                return '<img src="' + resource_url + '" alt="' +
                    description + '" class="wiki-resource-image" >';
            }
            /* A document is shown in a frame of its own, as the site
               shows one, with its description standing in where a
               browser will not draw it. */
            if ((/\.(pdf|html?)$/i).test(resource_name)) {
                return '<iframe class="wiki-resource-object" src="' +
                    resource_url + '" >' + description + "</iframe>";
            }
            if ((/\.(avi|flv|m4v|mov|mp4|mpg|ogg|webm|wmv)$/i).test(
                resource_name)) {
                return '<video style="width:100%" controls="controls">' +
                    '<source src="' + resource_url +
                    '" type="video/mp4">' + description + "</video>";
            }
            return '<a href="' + resource_url + '" alt="' + description +
                '" >' + description + "</a>";
        });
}
/*
 * Takes the link definitions out of a document and remembers them, so a
 * link written by name can be looked up when it is met.
 *
 * @param String text the whole document
 * @param Object references where the definitions are put, by name
 * @return String the document without its definitions
 */
function collectMarkdownReferences(text, references, notes)
{
    var kept = [];
    var lines = text.split("\n");
    var pattern =
        /^ {0,3}\[([^\]]+)\]:\s*(\S+)(?:\s+["'(](.*)["')])?\s*$/;
    var note_pattern = /^ {0,3}\[\^([^\]]+)\]:\s*(.*)$/;
    for (var index = 0; index < lines.length; index++) {
        var noted = lines[index].match(note_pattern);
        if (noted && notes) {
            notes[noted[1]] = noted[2];
            continue;
        }
        var found = lines[index].match(pattern);
        if (found) {
            references[found[1].toLowerCase()] =
                {target: found[2], title: found[3] || ""};
        } else {
            kept.push(lines[index]);
        }
    }
    return kept.join("\n");
}
/*
 * Reads one block after another until the lines run out.
 *
 * @param Array lines the document, a line to an entry
 * @param Object place where reading has got to, kept in one object so a
 *      reader can move it on
 * @param Object references the link definitions found earlier
 * @return Array the html of each block
 */
function parseMarkdownBlocks(lines, place, references)
{
    var blocks = [];
    while (place.at < lines.length) {
        if (lines[place.at].trim() === "") {
            place.at++;
            continue;
        }
        var made = parseMarkdownBlock(lines, place, references);
        if (made !== "") {
            blocks.push(made);
        }
    }
    return blocks;
}
/*
 * Reads the one block the reader is standing on.
 *
 * @param Array lines the document, a line to an entry
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of that block
 */
function parseMarkdownBlock(lines, place, references)
{
    var kind = markdownBlockKind(lines, place.at);
    if (kind === "fence") {
        return readMarkdownFence(lines, place);
    }
    if (kind === "indentcode") {
        return readMarkdownIndentedCode(lines, place);
    }
    if (kind === "heading") {
        return readMarkdownHeading(lines, place, references);
    }
    if (kind === "rule") {
        place.at++;
        return "<hr>";
    }
    if (kind === "quote") {
        return readMarkdownBlockQuote(lines, place, references);
    }
    if (kind === "list") {
        return readMarkdownList(lines, place, references);
    }
    if (kind === "table") {
        return readMarkdownTable(lines, place, references);
    }
    if (kind === "deflist") {
        return readMarkdownDefinitionList(lines, place, references);
    }
    if (kind === "wrapped") {
        return readMarkdownWrapped(lines, place, references);
    }
    if (kind === "template") {
        return readMarkdownTemplate(lines, place, references);
    }
    if (kind === "spread") {
        return readMarkdownSpreadTemplate(lines, place, references);
    }
    return readMarkdownParagraph(lines, place, references);
}
/*
 * Says what kind of block begins at a line.
 *
 * @param Array lines the document
 * @param int at which line to look at
 * @return String the kind of block
 */
function markdownBlockKind(lines, at)
{
    var line = lines[at];
    var indent = line.length - line.replace(/^ +/, "").length;
    if (indent >= MARKDOWN_CODE_INDENT || line.charAt(0) === "\t") {
        return "indentcode";
    }
    var trimmed = line.replace(/^\s+/, "");
    if (markdownFenceLength(trimmed) > 0) {
        return "fence";
    }
    if (markdownHeadingLevel(trimmed) > 0) {
        return "heading";
    }
    if (trimmed.charAt(0) === ">") {
        return "quote";
    }
    if (isMarkdownRule(trimmed)) {
        return "rule";
    }
    if (markdownListMarker(line) !== null) {
        return "list";
    }
    if (matchMarkdownWrappedOpen(trimmed) !== null) {
        return "wrapped";
    }
    if (readMarkdownBlockTemplate(trimmed) !== null) {
        return "template";
    }
    if (markdownOpensSpreadTemplate(lines, at)) {
        return "spread";
    }
    if (isMarkdownTableStart(lines, at)) {
        return "table";
    }
    /* A term with a colon line beneath it opens a definition list, which
       markdown's extended syntax writes that way round. */
    if (line.trim() !== "" && markdownDefinitionSays(line) === null &&
        markdownDefinitionSays(lines[at + 1]) !== null) {
        return "deflist";
    }
    return "paragraph";
}
/*
 * Gives what a markdown definition line says, where the line is one. A
 * definition is a line opening with a colon, the way markdown's extended
 * syntax writes one beneath the term it explains.
 *
 * @param String line the line to look at
 * @return String what the line says after its colon, or null where the
 *      line is not a definition
 */
function markdownDefinitionSays(line)
{
    if (line === undefined || line === null) {
        return null;
    }
    var trimmed = line.replace(/^\s+/, "");
    if (trimmed === "" || trimmed.charAt(0) !== ":") {
        return null;
    }
    return trim(trimmed.slice(1));
}
/*
 * Reads a markdown definition list: a term on a line of its own, then one
 * or more lines each opening with a colon giving a meaning. A meaning may
 * open a same-unit mark, in which case it holds whole blocks rather than
 * only what fits on the line.
 *
 * @param Array lines the document, split into lines
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the description-list html
 */
function readMarkdownDefinitionList(lines, place, references)
{
    var html = "<dl>\n";
    while (place.at < lines.length &&
        markdownDefinitionSays(lines[place.at]) === null &&
        markdownDefinitionSays(lines[place.at + 1]) !== null) {
        html += "<dt>" + renderMarkdownInline(trim(lines[place.at]),
            references) + "</dt>\n";
        place.at++;
        var says = markdownDefinitionSays(lines[place.at]);
        while (place.at < lines.length && says !== null) {
            place.at++;
            var together = readMarkdownSameUnit(says, lines, place,
                references);
            html += "<dd>" + ((together === null) ?
                renderMarkdownInline(says, references) : together) +
                "</dd>\n";
            says = markdownDefinitionSays(lines[place.at]);
        }
        while (place.at < lines.length && lines[place.at].trim() === "") {
            place.at++;
        }
    }
    return html + "</dl>\n";
}
/*
 * Reads what a same-unit mark holds, where a definition opens one. The
 * mark says the lines it holds are all the meaning of the term above,
 * however many there are, so what is inside is read as ordinary blocks.
 * Reading stops where the braces balance rather than at a line's end.
 *
 * @param String opening what the definition line said after its colon
 * @param Array lines the document, split into lines
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of what the mark held, or null where the
 *      definition did not open one
 */
function readMarkdownSameUnit(opening, lines, place, references)
{
    var mark = "{{same-unit";
    /* The mark may come after words of the writer's own, so it is looked
       for anywhere in the line rather than only at its start. What comes
       before it is kept and read as it would have been. */
    var at = opening.toLowerCase().indexOf(mark);
    if (at === -1) {
        return null;
    }
    var before = opening.slice(0, at);
    var gathered = opening.slice(at);
    var counted = function (text, brace) {
        return text.split(brace).length - 1;
    };
    while (counted(gathered, "{{") > counted(gathered, "}}") &&
        place.at < lines.length) {
        gathered += "\n" + lines[place.at];
        place.at++;
    }
    var inner = gathered.slice(mark.length);
    var close = inner.lastIndexOf("}}");
    if (close !== -1) {
        inner = inner.slice(0, close);
    }
    var inside = {"at": 0};
    /* The block reader hands back one entry per block, so they are joined
       rather than left to run together with a comma between them. */
    var html = parseMarkdownBlocks(trim(inner).split("\n"), inside,
        references).join("\n");
    if (trim(before) !== "") {
        html = renderMarkdownInline(trim(before), references) + " " + html;
    }
    return html;
}
/*
 * Says how many marks open a fenced block of code, or none.
 *
 * @param String line the line to look at
 * @return int how many marks, zero when the line opens nothing
 */
function markdownFenceLength(line)
{
    var found = line.match(/^(`{3,}|~{3,})/);
    return found ? found[1].length : 0;
}
/*
 * Says what level of heading a line marks with hashes, or none.
 *
 * @param String line the line to look at
 * @return int one through six, or zero
 */
function markdownHeadingLevel(line)
{
    var found = line.match(/^(#{1,6})(\s|$)/);
    return found ? found[1].length : 0;
}
/*
 * Says whether a line is a rule drawn across the page.
 *
 * @param String line the line to look at
 * @return bool true when it is
 */
function isMarkdownRule(line)
{
    var bare = line.replace(/\s+/g, "");
    return (/^(\*{3,}|-{3,}|_{3,})$/).test(bare);
}
/*
 * Says what marks a line as an item of a list, and which sort.
 *
 * @param String line the line to look at
 * @return Object the sort of list and how wide the marker is, or null
 */
function markdownListMarker(line)
{
    var found = line.match(/^(\s{0,3})([*+-])(\s+)/);
    if (found) {
        return {ordered: false,
            width: found[1].length + 1 + found[3].length};
    }
    found = line.match(/^(\s{0,3})(\d{1,9})([.)])(\s+)/);
    if (found) {
        return {ordered: true, start: parseInt(found[2], 10),
            width: found[1].length + found[2].length + 1 +
                found[4].length};
    }
    return null;
}
/*
 * Reads a fenced block of code.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @return String the html of the block
 */
function readMarkdownFence(lines, place)
{
    var opener = lines[place.at].replace(/^\s+/, "");
    var length = markdownFenceLength(opener);
    var mark = opener.charAt(0);
    var language = opener.substring(length).trim();
    place.at++;
    var body = [];
    while (place.at < lines.length) {
        var line = lines[place.at];
        var bare = line.replace(/^\s+/, "");
        if (bare.charAt(0) === mark &&
            markdownFenceLength(bare) >= length &&
            bare.replace(new RegExp("^\\" + mark + "+"), "").trim() === "") {
            place.at++;
            break;
        }
        body.push(line);
        place.at++;
    }
    var opening = language !== "" ?
        '<pre><code class="language-' + markdownEscape(language) + '">' :
        "<pre><code>";
    return opening + markdownEscape(body.join("\n")) + "</code></pre>";
}
/*
 * Reads a block of code marked by its indent.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @return String the html of the block
 */
function readMarkdownIndentedCode(lines, place)
{
    var body = [];
    while (place.at < lines.length) {
        var line = lines[place.at];
        if (line.trim() === "") {
            var ahead = place.at + 1;
            while (ahead < lines.length && lines[ahead].trim() === "") {
                ahead++;
            }
            if (ahead < lines.length &&
                markdownBlockKind(lines, ahead) === "indentcode") {
                body.push("");
                place.at++;
                continue;
            }
            break;
        }
        if (markdownBlockKind(lines, place.at) !== "indentcode") {
            break;
        }
        body.push(stripMarkdownCodeIndent(line));
        place.at++;
    }
    return "<pre><code>" + markdownEscape(body.join("\n")) +
        "</code></pre>";
}
/*
 * Takes the indent that marked a line as code off the front of it.
 *
 * @param String line the line to strip
 * @return String the line without its leading indent
 */
function stripMarkdownCodeIndent(line)
{
    if (line.charAt(0) === "\t") {
        return line.substring(1);
    }
    return line.substring(MARKDOWN_CODE_INDENT);
}
/*
 * Reads a heading, whether marked with hashes or underlined.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the heading
 */
function readMarkdownHeading(lines, place, references)
{
    var line = lines[place.at].replace(/^\s+/, "");
    var level = markdownHeadingLevel(line);
    place.at++;
    var text = line.substring(level).replace(/\s+#*\s*$/, "").trim();
    return markdownHeadingHtml(level, text, references);
}
/*
 * Builds the html of a heading of a given level, with the anchor the
 * site gives it so a contents entry and its heading agree.
 *
 * @param int level one through six
 * @param String text what the heading says
 * @param Object references the link definitions found earlier
 * @return String the html of the heading
 */
function markdownHeadingHtml(level, text, references)
{
    var inner = renderMarkdownInline(text, references);
    var name = inner.replace(/<[^>]*>/g, "").trim();
    return "<h" + level + ' id="' + name + '">' + inner + "</h" +
        level + ">";
}
/*
 * Reads a quotation, which may hold blocks of its own.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the quotation
 */
function readMarkdownBlockQuote(lines, place, references)
{
    var inner = [];
    while (place.at < lines.length) {
        var line = lines[place.at];
        var bare = line.replace(/^\s+/, "");
        if (bare.charAt(0) !== ">") {
            if (line.trim() === "") {
                break;
            }
            if (inner.length === 0) {
                break;
            }
            inner.push(line);
            place.at++;
            continue;
        }
        inner.push(bare.replace(/^>\s?/, ""));
        place.at++;
    }
    var within = {at: 0};
    var blocks = parseMarkdownBlocks(inner, within, references);
    return "<blockquote>" + blocks.join("\n") + "</blockquote>";
}
/*
 * Reads a list and the items under it.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the list
 */
function readMarkdownList(lines, place, references)
{
    var first = markdownListMarker(lines[place.at]);
    var ordered = first.ordered;
    var items = [];
    while (place.at < lines.length) {
        if (lines[place.at].trim() === "") {
            var ahead = place.at + 1;
            while (ahead < lines.length && lines[ahead].trim() === "") {
                ahead++;
            }
            if (ahead < lines.length &&
                markdownListMarker(lines[ahead]) !== null) {
                place.at = ahead;
                continue;
            }
            break;
        }
        var marker = markdownListMarker(lines[place.at]);
        if (marker === null || marker.ordered !== ordered) {
            break;
        }
        items.push(readMarkdownListItem(lines, place, marker, references));
    }
    var tag = ordered ? "ol" : "ul";
    return "<" + tag + ">\n" + items.join("\n") + "\n</" + tag + ">";
}
/*
 * Reads one item of a list, taking with it any lines that belong under
 * it by their indent.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object marker what marked the item
 * @param Object references the link definitions found earlier
 * @return String the html of the item
 */
function readMarkdownListItem(lines, place, marker, references)
{
    var gathered = [lines[place.at].substring(marker.width)];
    place.at++;
    while (place.at < lines.length) {
        var line = lines[place.at];
        if (line.trim() === "") {
            break;
        }
        var indent = line.length - line.replace(/^ +/, "").length;
        /* A line belongs to the item only where it stands under it. An
           unindented line ends the item, as it does on the site, rather
           than being folded into its words. */
        if (indent < marker.width) {
            break;
        }
        gathered.push(stripMarkdownIndent(line, marker.width));
        place.at++;
    }
    var within = {at: 0};
    var blocks = parseMarkdownBlocks(gathered, within, references);
    return "<li>" + tightenMarkdownItem(blocks.join("\n")) + "</li>";
}
/*
 * Takes up to a given number of spaces off the front of a line.
 *
 * @param String line the line to strip
 * @param int amount how many spaces to take at most
 * @return String the line with that much taken off
 */
function stripMarkdownIndent(line, amount)
{
    var taken = 0;
    var at = 0;
    while (at < line.length && taken < amount && line.charAt(at) === " ") {
        taken++;
        at++;
    }
    return line.substring(at);
}
/*
 * An item holding one paragraph shows its words alone, with no paragraph
 * marks around them, which is what a reader expects of a short list.
 *
 * @param String html what the item holds
 * @return String the same, without a lone paragraph's marks
 */
function tightenMarkdownItem(html)
{
    var found = html.match(/^<p>([\s\S]*?)<\/p>([\s\S]*)$/);
    if (found && found[1].indexOf("<p>") < 0) {
        return found[1] + found[2];
    }
    return html;
}
/*
 * Says whether a table begins at a line, which needs a row of headings
 * and a row of dashes under it.
 *
 * @param Array lines the document
 * @param int at which line to look at
 * @return bool true when a table begins there
 */
function isMarkdownTableStart(lines, at)
{
    if (at + 1 >= lines.length) {
        return false;
    }
    if (lines[at].indexOf("|") < 0) {
        return false;
    }
    return isMarkdownTableDivider(lines[at + 1]);
}
/*
 * Says whether a line is the row of dashes under a table's headings.
 *
 * @param String line the line to look at
 * @return bool true when it is
 */
function isMarkdownTableDivider(line)
{
    if (!line || line.indexOf("-") < 0) {
        return false;
    }
    var cells = markdownTableCells(line);
    if (cells.length === 0) {
        return false;
    }
    for (var index = 0; index < cells.length; index++) {
        if (!(/^:?-{1,}:?$/).test(cells[index].trim())) {
            return false;
        }
    }
    return true;
}
/*
 * Splits a table row into its cells.
 *
 * @param String line the row
 * @return Array the cells, without the bars between them
 */
function markdownTableCells(line)
{
    var trimmed = line.trim();
    if (trimmed.charAt(0) === "|") {
        trimmed = trimmed.substring(1);
    }
    if (trimmed.charAt(trimmed.length - 1) === "|") {
        trimmed = trimmed.substring(0, trimmed.length - 1);
    }
    return trimmed.split("|");
}
/*
 * Reads a table: a row of headings, a row of dashes, and the rows under
 * them.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the table
 */
function readMarkdownTable(lines, place, references)
{
    var headings = markdownTableCells(lines[place.at]);
    place.at += 2;
    var head = "<thead>\n<tr>";
    for (var index = 0; index < headings.length; index++) {
        head += "<th>" + renderMarkdownInline(headings[index].trim(),
            references) + "</th>";
    }
    head += "</tr>\n</thead>\n";
    var body = "<tbody>\n";
    while (place.at < lines.length && lines[place.at].indexOf("|") >= 0 &&
        lines[place.at].trim() !== "") {
        var cells = markdownTableCells(lines[place.at]);
        body += "<tr>";
        for (var cell = 0; cell < cells.length; cell++) {
            body += "<td>" + renderMarkdownInline(cells[cell].trim(),
                references) + "</td>";
        }
        body += "</tr>\n";
        place.at++;
    }
    body += "</tbody>\n";
    return "<table>\n" + head + body + "</table>";
}
/*
 * Says whether a line opens the form that wraps the lines under it up to
 * a closing marker, and what it asks for. The form is three parts
 * between double braces: the word block or inblock, a name, and a style.
 *
 * @param String line the line to look at
 * @return Object what it asks for, or null
 */
function matchMarkdownWrappedOpen(line)
{
    var trimmed = line.replace(/\s+$/, "");
    if (trimmed.substring(0, 2) !== "{{" ||
        trimmed.substring(trimmed.length - 2) !== "}}") {
        return null;
    }
    var inner = trimmed.substring(2, trimmed.length - 2);
    var parts = inner.split("|");
    if (parts.length !== 3) {
        return null;
    }
    if (parts[0] !== "block" && parts[0] !== "inblock") {
        return null;
    }
    if (parts[1] === "" || parts[1].indexOf("}") >= 0 ||
        parts[2].indexOf("}") >= 0) {
        return null;
    }
    return {keyword: parts[0], name: parts[1], style: parts[2]};
}
/*
 * Reads the form that wraps the lines under it up to a closing marker.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the wrapped block
 */
function readMarkdownWrapped(lines, place, references)
{
    var open = matchMarkdownWrappedOpen(lines[place.at].replace(/^\s+/, ""));
    place.at++;
    var closing = "{{end-" + open.keyword + "}}";
    var gathered = [];
    while (place.at < lines.length) {
        if (lines[place.at].trim() === closing) {
            place.at++;
            break;
        }
        gathered.push(lines[place.at]);
        place.at++;
    }
    var within = {at: 0};
    var inner = parseMarkdownBlocks(gathered, within, references).join("\n");
    var tag = (open.keyword === "inblock") ? "span" : "div";
    return "<" + tag + ' id="' + markdownEscape(open.name) + '" style="' +
        markdownEscape(open.style) + '">\n' + inner + "</" + tag + ">";
}
/*
 * Says whether a line is a template written on one line between double
 * braces, and what it asks for: a wrapping with a class, an id or a
 * style, or one of the words that stand for an alignment.
 *
 * @param String line the line to look at
 * @return Object the tag, what it carries, and its words, or null
 */
function readMarkdownBlockTemplate(line)
{
    var trimmed = line.replace(/\s+$/, "");
    if (trimmed.substring(0, 2) !== "{{") {
        return null;
    }
    /* Words may follow a field on the same line, as when a page puts a
       line break after one, and they are kept rather than making the
       whole line unreadable. */
    var closes = trimmed.lastIndexOf("}}");
    if (closes < 0) {
        return null;
    }
    var tail = trimmed.substring(closes + 2);
    var inner = trimmed.substring(2, closes);
    /* Standing on its own the mark has no enclosing thing to be the
       content of, so what it holds is simply read where it stands. It is
       matched on what it opens with rather than on a name, as what
       follows the mark is text rather than a value. */
    var mark = "same-unit";
    var opening = inner.replace(/^\s+/, "");
    if (opening.slice(0, mark.length).toLowerCase() === mark &&
        (opening.length == mark.length ||
        /\s/.test(opening.charAt(mark.length)))) {
        return {attributes: ' class="same-unit"',
            content: trim(opening.slice(mark.length))};
    }
    var alignments = {center: "center", left: "align-left",
        right: "align-right"};
    var bar = inner.indexOf("|");
    /* A field of a form written into a page is drawn as the site draws
       it, so a writer sees the form taking shape rather than the words
       that ask for it. */
    if (typeof wikiFormField === "function") {
        var named = (bar > 0) ? inner.substring(0, bar) : inner;
        var given = (bar > 0) ? inner.substring(bar + 1).split("|") : [];
        var drawn = wikiFormField(named, given);
        if (drawn !== "") {
            return {ready: drawn + tail};
        }
    }
    if (bar > 0) {
        var name = inner.substring(0, bar);
        if (alignments[name] !== undefined) {
            return {attributes: ' class="' + alignments[name] + '"',
                content: inner.substring(bar + 1)};
        }
    }
    /* The site stores a quote as the name that stands for it, so both
       the mark itself and that name open a value here. */
    inner = inner.replace(/&quot;/g, '"').replace(/&#0?39;|&apos;/g, "'");
    var named = inner.match(
        /^(block-)?(class|id|style)\s*[=:]\s*"([^"]*)"\s*([\s\S]*)$/);
    if (named) {
        return {attributes: " " + named[2] + '="' +
            markdownEscape(named[3]) + '"', content: named[4]};
    }
    return null;
}
/*
 * Reads a template written on one line, wrapping what it carries.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the wrapping
 */
function readMarkdownTemplate(lines, place, references)
{
    var asked = readMarkdownBlockTemplate(lines[place.at].replace(
        /^\s+/, ""));
    place.at++;
    if (asked.ready !== undefined) {
        return asked.ready;
    }
    var within = {at: 0};
    var inner = parseMarkdownBlocks(asked.content.split("\n"), within,
        references).join("\n");
    return "<div" + asked.attributes + ">\n" + inner + "</div>";
}
/*
 * Says whether a template opens here whose braces close on a later
 * line, as in a block-class laid out over several lines with its words
 * between.
 *
 * @param Array lines the document
 * @param int at which line to look at
 * @return bool true when a template opens here and closes later
 */
function markdownOpensSpreadTemplate(lines, at)
{
    var line = lines[at].replace(/\s+$/, "");
    if (line.substring(0, 2) !== "{{" ||
        line.substring(line.length - 2) === "}}") {
        return false;
    }
    var ahead = at + 1;
    while (ahead < lines.length) {
        if (lines[ahead].replace(/\s+$/, "") === "}}") {
            return readMarkdownBlockTemplate(line + "}}") !== null;
        }
        ahead++;
    }
    return false;
}
/*
 * Reads a template whose braces close on a later line, wrapping
 * everything between in what the template asks for.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the wrapping
 */
function readMarkdownSpreadTemplate(lines, place, references)
{
    var opener = lines[place.at].replace(/\s+$/, "");
    var asked = readMarkdownBlockTemplate(opener + "}}");
    place.at++;
    var gathered = [];
    while (place.at < lines.length) {
        if (lines[place.at].replace(/\s+$/, "") === "}}") {
            place.at++;
            break;
        }
        gathered.push(lines[place.at]);
        place.at++;
    }
    var within = {at: 0};
    var inner = parseMarkdownBlocks(gathered, within, references).join("\n");
    return "<div" + asked.attributes + ">\n" + inner + "</div>";
}
/*
 * Reads a paragraph, which runs to the next blank line or to the start
 * of another kind of block. A line of equals or dashes under it makes it
 * a heading instead.
 *
 * @param Array lines the document
 * @param Object place where reading has got to
 * @param Object references the link definitions found earlier
 * @return String the html of the paragraph
 */
function readMarkdownParagraph(lines, place, references)
{
    var gathered = [];
    while (place.at < lines.length) {
        var line = lines[place.at];
        if (line.trim() === "") {
            break;
        }
        var level = markdownSetextLevel(line);
        if (level > 0 && gathered.length > 0) {
            place.at++;
            return markdownHeadingHtml(level, gathered.join(" ").trim(),
                references);
        }
        if (gathered.length > 0 &&
            markdownBlockKind(lines, place.at) !== "paragraph") {
            break;
        }
        gathered.push(line.trim());
        place.at++;
    }
    return "<p>" + renderMarkdownInline(gathered.join("\n"), references) +
        "</p>";
}
/*
 * Says what level of heading a line of equals or dashes marks the line
 * above it as.
 *
 * @param String line the line to look at
 * @return int one, two, or zero for neither
 */
function markdownSetextLevel(line)
{
    var trimmed = line.trim();
    if ((/^=+$/).test(trimmed)) {
        return 1;
    }
    if ((/^-+$/).test(trimmed)) {
        return 2;
    }
    return 0;
}
/*
 * Turns the marks that work within a line into html: code spans, strong
 * and emphasis, links, images and plain addresses.
 *
 * @param String text one line or paragraph of markdown
 * @param Object references the link definitions found earlier
 * @return String the html it stands for
 */
function renderMarkdownInline(text, references)
{
    var out = "";
    var at = 0;
    while (at < text.length) {
        var here = text.charAt(at);
        if (here === "\\" && at + 1 < text.length) {
            out += markdownEscape(text.charAt(at + 1));
            at += 2;
            continue;
        }
        if (here === "`") {
            var code = readMarkdownCode(text, at);
            if (code !== null) {
                out += code.html;
                at = code.after;
                continue;
            }
        }
        if (here === "<") {
            var auto = readMarkdownAutolink(text, at);
            if (auto !== null) {
                out += auto.html;
                at = auto.after;
                continue;
            }
        }
        if (here === "!" && text.charAt(at + 1) === "[") {
            var picture = readMarkdownLink(text, at + 1, true, references);
            if (picture !== null) {
                out += picture.html;
                at = picture.after;
                continue;
            }
        }
        if (here === "[" && text.charAt(at + 1) === "^") {
            var note = readMarkdownNoteRef(text, at);
            if (note !== null) {
                out += note.html;
                at = note.after;
                continue;
            }
        }
        if (here === "[") {
            var link = readMarkdownLink(text, at, false, references);
            if (link !== null) {
                out += link.html;
                at = link.after;
                continue;
            }
        }
        if (here === "*" || here === "_") {
            var stressed = readMarkdownStress(text, at, references);
            if (stressed !== null) {
                out += stressed.html;
                at = stressed.after;
                continue;
            }
        }
        out += markdownEscape(here);
        at++;
    }
    return out;
}
/*
 * Gives the number a note is listed under, setting it down in the order
 * notes are first pointed at.
 *
 * @param String name what the note is called
 * @return int its number in the list at the foot of the page
 */
function markdownNoteNumber(name)
{
    var at = markdown_note_order.indexOf(name);
    if (at < 0) {
        markdown_note_order.push(name);
        at = markdown_note_order.length - 1;
    }
    return at + 1;
}
/*
 * Reads a pointer to a note, which stands in the words as a small
 * number leading to the note at the foot of the page.
 *
 * @param String text the line being read
 * @param int at where the pointer begins
 * @return Object the html and where reading got to, or null
 */
function readMarkdownNoteRef(text, at)
{
    var found = text.substring(at).match(/^\[\^([^\]]+)\]/);
    if (!found) {
        return null;
    }
    var name = found[1];
    if (markdown_notes[name] === undefined) {
        return null;
    }
    var number = markdownNoteNumber(name);
    return {html: '<sup class="footnote-ref"><a href="#fn-' +
        markdownEscape(name) + '" id="fnref-' + markdownEscape(name) +
        '">' + number + "</a></sup>", after: at + found[0].length};
}
/*
 * Builds the list of notes at the foot of the page, in the order they
 * were first pointed at. Nothing is built where a page has none.
 *
 * @return String the html of the list, or an empty string
 */
function markdownNotesSection()
{
    if (markdown_note_order.length === 0) {
        return "";
    }
    var items = "";
    for (var index = 0; index < markdown_note_order.length; index++) {
        var name = markdown_note_order[index];
        items += '<li id="fn-' + markdownEscape(name) + '">' +
            renderMarkdownInline(markdown_notes[name], {}) +
            ' <a href="#fnref-' + markdownEscape(name) +
            '">&#8617;</a></li>\n';
    }
    return '\n<section class="footnotes"><ol>\n' + items +
        "</ol></section>\n";
}
/*
 * Reads a span of code written between back marks.
 *
 * @param String text the line being read
 * @param int at where the span begins
 * @return Object the html and where reading got to, or null
 */
function readMarkdownCode(text, at)
{
    var opener = text.substring(at).match(/^`+/)[0];
    var closer = text.indexOf(opener, at + opener.length);
    if (closer < 0) {
        return null;
    }
    var body = text.substring(at + opener.length, closer);
    return {html: "<code>" + markdownEscape(body.trim()) + "</code>",
        after: closer + opener.length};
}
/*
 * Reads a plain address written between angle marks.
 *
 * @param String text the line being read
 * @param int at where it begins
 * @return Object the html and where reading got to, or null
 */
function readMarkdownAutolink(text, at)
{
    var found = text.substring(at).match(/^<((?:https?|mailto):[^>\s]+)>/);
    if (!found) {
        return null;
    }
    var target = found[1];
    return {html: '<a href="' + markdownEscape(target) + '">' +
        markdownEscape(target) + "</a>", after: at + found[0].length};
}
/*
 * Reads a link or a picture, whether its target is written beside it or
 * looked up by name.
 *
 * @param String text the line being read
 * @param int at where the label begins
 * @param bool is_image whether this is a picture rather than a link
 * @param Object references the link definitions found earlier
 * @return Object the html and where reading got to, or null
 */
function readMarkdownLink(text, at, is_image, references)
{
    var depth = 0;
    var close = -1;
    for (var index = at; index < text.length; index++) {
        if (text.charAt(index) === "[") {
            depth++;
        } else if (text.charAt(index) === "]") {
            depth--;
            if (depth === 0) {
                close = index;
                break;
            }
        }
    }
    if (close < 0) {
        return null;
    }
    var label = text.substring(at + 1, close);
    var after = close + 1;
    if (text.charAt(after) === "(") {
        var end = text.indexOf(")", after);
        if (end < 0) {
            return null;
        }
        var inside = text.substring(after + 1, end).trim();
        var parts = inside.match(/^(\S+)(?:\s+["'(](.*)["')])?$/);
        var target = parts ? parts[1] : inside;
        var title = parts && parts[2] ? parts[2] : "";
        return {html: buildMarkdownLink(label, target, title, is_image,
            references), after: end + 1};
    }
    var named = label;
    var reference_end = after;
    if (text.charAt(after) === "[") {
        var shut = text.indexOf("]", after);
        if (shut < 0) {
            return null;
        }
        var written = text.substring(after + 1, shut).trim();
        if (written !== "") {
            named = written;
        }
        reference_end = shut + 1;
    }
    var found = references[named.toLowerCase()];
    if (!found) {
        return null;
    }
    return {html: buildMarkdownLink(label, found.target, found.title,
        is_image, references), after: reference_end};
}
/*
 * Builds the html of a link or a picture.
 *
 * @param String label what stands between the square marks
 * @param String target where it points
 * @param String title what it is called, when it is given one
 * @param bool is_image whether this is a picture rather than a link
 * @param Object references the link definitions found earlier
 * @return String the html
 */
function buildMarkdownLink(label, target, title, is_image, references)
{
    var named = title !== "" ?
        ' title="' + markdownEscape(title) + '"' : "";
    if (is_image) {
        return '<img src="' + markdownEscape(target) + '" alt="' +
            markdownEscape(label) + '"' + named + ">";
    }
    return '<a href="' + markdownEscape(target) + '"' + named + ">" +
        renderMarkdownInline(label, references) + "</a>";
}
/*
 * Reads words made strong or emphatic by stars or underscores.
 *
 * @param String text the line being read
 * @param int at where the marks begin
 * @param Object references the link definitions found earlier
 * @return Object the html and where reading got to, or null
 */
function readMarkdownStress(text, at, references)
{
    var mark = text.charAt(at);
    var run = text.substring(at).match(new RegExp("^\\" + mark + "+"))[0];
    var width = run.length >= 2 ? 2 : 1;
    var opener = mark === "*" ? (width === 2 ? "**" : "*") :
        (width === 2 ? "__" : "_");
    var from = at + width;
    var closer = -1;
    for (var index = from; index < text.length; index++) {
        if (text.charAt(index) === "\\") {
            index++;
            continue;
        }
        if (text.substring(index, index + width) === opener &&
            text.charAt(index - 1) !== " ") {
            closer = index;
            break;
        }
    }
    if (closer < 0) {
        return null;
    }
    var inner = text.substring(from, closer);
    if (inner.trim() === "") {
        return null;
    }
    var tag = width === 2 ? "strong" : "em";
    return {html: "<" + tag + ">" + renderMarkdownInline(inner,
        references) + "</" + tag + ">", after: closer + width};
}
/*
 * Makes text safe to put in a page, so a writer's angle marks and
 * ampersands read as themselves rather than as markup.
 *
 * @param String text what to make safe
 * @return String the same, made safe
 */
function markdownEscape(text)
{
    return String(text).replace(/&/g, "&").replace(/</g, "&lt;")
        .replace(/>/g, "&gt;").replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}
if (typeof module !== "undefined" && module.exports) {
    module.exports = {parseMarkdownContent: parseMarkdownContent};
}
X