class MDState { /** @var {object} */ abbreviations = {}; /** @var {object} */ footnotes = {}; } class MDConfig { } class MDBlock { /** * @returns {String} HTML */ toHTML(config, state) { throw new Error("toHTML not implemented"); } } class MDHTMLBlock extends MDBlock { /** @var {String} html */ html; /** * @param {String} html */ constructor(html) { super(); this.html = html; } toHTML = (config, state) => this.html; } class MDHTMLWrappedBlock extends MDBlock { /** @var {String} */ #beforeHTML; /** @var {MDBlock} */ #content; /** @var {String} */ #afterHTML; /** * @param {String} beforeHTML * @param {MDBlock} content * @param {String} afterHTML */ constructor(beforeHTML, content, afterHTML) { super(); if (!(content instanceof MDBlock)) { throw new Error("content is of type " + typeof(content) + " instead of MDBlock"); } this.#beforeHTML = beforeHTML; this.#content = content; this.#afterHTML = afterHTML; } toHTML(config, state) { return this.#beforeHTML + this.#content.toHTML() + this.#afterHTML; } } class MDInlineBlock extends MDBlock { /** @var {String} */ #html; /** * @param {String} line */ constructor(line) { super(); this.#html = line; } toHTML = (config, state) => "inline:" + this.#html; } class MDUnprocessedLinesBlock extends MDBlock { /** @var {MDBlock[]} */ #blocks = []; /** * @param {String[]} lines */ constructor(lines) { super(); // Find markers that always start a new block let blockQuoteBlocks = MDUnprocessedLinesBlock.findBlockQuote(lines); if (blockQuoteBlocks) { this.#blocks = blockQuoteBlocks; return; } let headerBlocks = MDUnprocessedLinesBlock.findHeader(lines); if (headerBlocks) { this.#blocks = headerBlocks; return; } let codeLines = MDUnprocessedLinesBlock.findTickCodeBlock(lines); if (codeLines) { this.#blocks = codeLines; return; } let codeLines0 = MDUnprocessedLinesBlock.findIndentCodeBlock(lines); if (codeLines0) { this.#blocks = codeLines0; return; } // Find runs of contiguous non-blank lines var contiguousLines = []; let blankRegex = /^\s*$/; for (const line of lines) { if (blankRegex.exec(line)) { if (contiguousLines.length > 0) { this.#blocks.push(new MDContiguousUnprocessedLinesBlock(contiguousLines)); } contiguousLines = []; } else { contiguousLines.push(line); } } if (contiguousLines.length > 0) { this.#blocks.push(new MDContiguousUnprocessedLinesBlock(contiguousLines)); } } /** * @param {String[]} lines * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the blockquoted content, or null if not found */ static findBlockQuote(lines) { var portion = 0; var beforeLines = []; var blockQuoteLines = []; var afterLines = []; for (const line of lines) { switch (portion) { case 0: if (line.startsWith(">")) { blockQuoteLines.push(line.substring(1)); portion = 1; } else { beforeLines.push(line); } break; case 1: if (line.startsWith(">")) { blockQuoteLines.push(line.substring(1)); } else { afterLines.push(line); portion = 2; } break; case 2: afterLines.push(line); break; } } if (blockQuoteLines.length == 0) { return null; } var blocks = []; if (beforeLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(beforeLines)); } blocks.push(new MDHTMLWrappedBlock( "
\n", new MDUnprocessedLinesBlock(Markdown.trimEvenly(blockQuoteLines)), "
\n")); if (afterLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(afterLines)); } return blocks; } /** * @param {String[]} lines * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the header content, or null if not found */ static findHeader(lines) { var portion = 0; var beforeLines = []; var headerBlock = null; var afterLines = []; for (const line of lines) { let hashMatch = /^\s*(#{1,6})\s*(.*)$/.exec(line); let dashMatch = /^(-+|=+)$/.exec(line); switch (portion) { case 0: if (hashMatch) { let headerLevel = hashMatch[1].length; let contentMarkdown = hashMatch[2]; headerBlock = new MDHTMLWrappedBlock('', new MDInlineBlock(contentMarkdown), '\n"); portion = 1; } else if (dashMatch && beforeLines.length > 1) { let contentMarkdown = beforeLines.pop(); let headerLevel = dashMatch[1].startsWith("=") ? 1 : 2; headerBlock = new MDHTMLWrappedBlock('', new MDInlineBlock(contentMarkdown), '\n"); portion = 1; } else { beforeLines.push(line); } break; case 1: afterLines.push(line); break; } } if (headerBlock == null) { return null; } var blocks = []; if (beforeLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(beforeLines)); } blocks.push(headerBlock); if (afterLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(afterLines)); } return blocks; } /** * @param {String[]} lines * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the code content, or null if not found */ static findTickCodeBlock(lines) { var portion = 0; var beforeLines = []; var codeLines = []; var afterLines = []; for (const line of lines) { switch (portion) { case 0: if (/^\s*```\s*$/.exec(line)) { portion = 1; } else { beforeLines.push(line); } break; case 1: if (/^\s*```\s*$/.exec(line)) { portion = 2; } else { codeLines.push(line); } break; case 2: afterLines.push(line); break; } } if (codeLines.length == 0) return null; var blocks = []; if (beforeLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(beforeLines)); } blocks.push(new MDHTMLWrappedBlock("
", MDHTMLBlock(codeLines.join("\n")), "
\n")); if (afterLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(afterLines)); } return blocks; } /** * @param {String[]} lines * @returns {MDBlock[]} up to 3 blocks for the unprocessed lines and the code content, or null if not found */ static findIndentCodeBlock(lines) { var portion = 0; var beforeLines = []; var codeLines = []; var afterLines = []; let regex = /^(\s{4,})(.*)$/; var minIndent = 999999; for (const line of lines) { let indentMatch = regex.exec(line); switch (portion) { case 0: if (indentMatch) { minIndent = Math.min(minIndent, indentMatch[1].length); codeLines.push(line); portion = 1; } else { beforeLines.push(line); } break; case 1: if (indentMatch) { minIndent = Math.min(minIndent, indentMatch[1].length); codeLines.push(line); } else { afterLines.push(line); portion = 2; } break; case 2: afterLines.push(line); break; } } if (codeLines.length == 0) return null; var blocks = []; if (beforeLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(beforeLines)); } blocks.push(new MDHTMLWrappedBlock("
", new MDHTMLBlock(codeLines.map((l) => l.substring(minIndent)).join("\n")), "
\n")); if (afterLines.length > 0) { blocks.push(new MDUnprocessedLinesBlock(afterLines)); } return blocks; } toHTML(config, state) { var html = ""; for (const block of this.#blocks) { html += block.toHTML() + "\n"; } return html; } } class MDContiguousUnprocessedLinesBlock extends MDBlock { /** @var {MDBlock[]} */ #blocks; /** * @param {String[]} lines */ constructor(lines) { super(); this.#blocks = [ new MDHTMLWrappedBlock('

contiguous: ', new MDInlineBlock(lines.join(' ')), "

\n\n") ]; } toHTML(config, state) { var html = ""; for (const block of this.#blocks) { html += block.toHTML() + "\n"; } return html; } } class Markdown { /** * @param {String} markdown * @returns {String} HTML */ static toHTML(markdown, config=new MDConfig()) { // Blocks that immediately start a new block // - Headers // - Blockquote // - Code block ```\ncode\n``` // Blocks that need blank line first // - HR --- - - - *** * * * * * * // - Lists // - Table // - Code block [4+spaces]code // - Definition list term\n: definition\n: alternate def // Unknown blocks // - Footnotes some text[^1] [^1]: first footnote content // - Abbreviations *[HTML]: Hyper Text // Inline styles // - Links // - Italic // - Bold // - `code` // - Strikethrough // - Images ![alt text](url){.cssclass} // - Literals \* let state = new MDState(); let lines = markdown.trim().replace("\r", "").split("\n"); return new MDUnprocessedLinesBlock(lines).toHTML(config, state); } /** * @param {String[]} lines * @returns {String[]} */ static trimEvenly(lines) { var minIndent = 999999; let regex = /^(\s*)($|\S.*$)/; for (const line of lines) { let groups = regex.exec(line); let indent = groups[1].length; if (groups[2].trim().length > 0 && indent < 4) { minIndent = Math.min(minIndent, indent); } } if (minIndent == 0) return lines; var trimmed = []; let trimRegex = new RegExp(`^\s{${minIndent}}`); for (const line of lines) { trimmed.push(line.replace(trimRegex, '')); } return trimmed; } }