| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- 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(
- "<blockquote>\n",
- new MDUnprocessedLinesBlock(Markdown.trimEvenly(blockQuoteLines)),
- "</blockquote>\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('<h' + headerLevel + '>', new MDInlineBlock(contentMarkdown), '</h' + headerLevel + ">\n");
- portion = 1;
- } else if (dashMatch && beforeLines.length > 1) {
- let contentMarkdown = beforeLines.pop();
- let headerLevel = dashMatch[1].startsWith("=") ? 1 : 2;
- headerBlock = new MDHTMLWrappedBlock('<h' + headerLevel + '>', new MDInlineBlock(contentMarkdown), '</h' + headerLevel + ">\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("<pre>", MDHTMLBlock(codeLines.join("\n")), "</pre>\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("<pre>", new MDHTMLBlock(codeLines.map((l) => l.substring(minIndent)).join("\n")), "</pre>\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('<p>contiguous: ', new MDInlineBlock(lines.join(' ')), "</p>\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 {.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;
- }
- }
|