| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- // Blocks
- // - Paragraph
- // - Header 1-6 # ## ### #### ##### ###### or === ---
- // - Blockquote (nestable) >
- // - Unordered list (nestable) *_
- // - Ordered list (nestable) 1._
- // - Code block ```\ncode\n``` or 4 spaces/tab indent
- // - Horizontal rule --- - - - * * * etc
- // - Table -|-
- // - Definition list term\n: definition\n: alternate definition
- // - Footnote (bottom) citation[^1]
- // - Abbreviation (definition) *[ABC]: Abbrev Blah Cat
- // Inline
- // - Link [text](https://url)
- // - Emphasis *emphasized*
- // - Strong **bold**
- // - Inline code `code`
- // - Strikethrough ~strike~
- // - Image {.cssclass}
- // - Footnote (inline) [^1]: footnote text
- // - Abbreviation (inline)
-
- class _MDBlock {
- toHTML(config) {
- throw new Error(self.constructor.name + ".toHTML not implemented");
- }
-
- /**
- * @param {_MDBlock[]} blocks
- * @returns {String}
- */
- static toHTML(blocks, config) {
- return blocks.map((block) => block.toHTML(config)).join("\n");
- }
- }
-
- class _MDMultiBlock extends _MDBlock {
- /** @var {_MDBlock[]} */
- #blocks;
- /**
- * @param {_MDBlock[]} blocks
- */
- constructor(blocks) {
- super();
- this.#blocks = blocks;
- }
- toHTML(config) {
- return _MDBlock.toHTML(this.#blocks, config);
- }
- }
-
- class _MDParagraph extends _MDBlock {
- /** @var {_MDBlock} */
- content;
-
- /**
- * @param {_MDBlock} content
- */
- constructor(content) {
- super();
- this.content = content;
- }
-
- toHTML(config) {
- let contentHTML = this.content.toHTML(config);
- return `<p>${contentHTML}</p>\n`;
- }
- }
-
- class _MDHeader extends _MDBlock {
- /** @var {number} */
- level;
- /** @var {_MDBlock} */
- content;
-
- /**
- * @param {number} level
- * @param {_MDBlock} content
- */
- constructor(level, content) {
- super();
- this.level = level;
- this.content = content;
- }
-
- toHTML(config) {
- let contentHTML = this.content.toHTML(config);
- return `<h${this.level}>${contentHTML}</h${this.level}>\n`;
- }
- }
-
- class _MDBlockquote extends _MDBlock {
- /** @var {_MDBlock[]} */
- content;
- /**
- * @param {_MDBlock[]} content
- */
- constructor(content) {
- super();
- this.content = content;
- }
- toHTML(config) {
- let contentHTML = _MDBlock.toHTML(this.content, config);
- return `<blockquote>\n${contentHTML}\n</blockquote>`;
- }
- }
-
- class _MDUnorderedList extends _MDBlock {
- /** @var {_MDListItem[]} */
- items;
- /**
- * @param {_MDListItem[]} items
- */
- constructor(items) {
- super();
- this.items = items;
- }
- toHTML(config) {
- let contentHTML = _MDBlock.toHTML(this.items);
- return `<ul>\n${contentHTML}\n</ul>`;
- }
- }
-
- class _MDOrderedList extends _MDBlock {
- /** @var {_MDListItem[]} */
- items;
- /**
- * @param {_MDListItem[]} items
- */
- constructor(items) {
- super();
- this.items = items;
- }
- toHTML(config) {
- let contentHTML = _MDBlock.toHTML(this.items);
- return `<ol>\n${contentHTML}\n</ol>`;
- }
- }
-
- class _MDListItem extends _MDBlock {
- /** @var {_MDBlock} */
- content;
-
- /**
- * @param {_MDBlock} content
- */
- constructor(content) {
- super();
- this.content = content;
- }
-
- toHTML(config) {
- let contentHTML = this.content.toHTML(config);
- return `<li>${contentHTML}</li>`;
- }
- }
-
- class _MDCodeBlock extends _MDBlock {
- /** @var {String} */
- #code;
- /**
- * @param {String} code
- */
- constructor(code) {
- super();
- this.#code = code;
- }
- toHTML(config) {
- return `<pre><code>${this.#code}</code></pre>`;
- }
- }
-
- class _MDHorizontalRule extends _MDBlock {
- toHTML(config) {
- return "<hr>\n";
- }
- }
-
- class _MDTableHeaderCell extends _MDBlock {
- /** @var {_MDBlock} */
- #content;
- /**
- * @param {_MDBlock} content
- */
- constructor(content) {
- super();
- this.#content = content;
- }
- toHTML(config) {
- let contentHTML = this.#content.toHTML(config);
- return `<th>${contentHTML}</th>`;
- }
- }
-
- class _MDTableCell extends _MDBlock {
- /** @var {_MDBlock} */
- #content;
- /**
- * @param {_MDBlock} content
- */
- constructor(content) {
- super();
- this.#content = content;
- }
- toHTML(config) {
- let contentHTML = this.#content.toHTML(config);
- return `<th>${contentHTML}</th>`;
- }
- }
-
- class _MDTableRow extends _MDBlock {
- /** @var {_MDTableCell[]|_MDTableHeaderCell[]} */
- #cells;
- /**
- * @param {_MDTableCell[]|_MDTableHeaderCell[]} cells
- */
- constructor(cells) {
- super();
- this.#cells = cells;
- }
- toHTML(config) {
- cellsHTML = _MDBlock.toHTML(this.#cells, config);
- return `<tr>\n${cellsHTML}\n</tr>`;
- }
- }
-
- class _MDTable extends _MDBlock {
- /** @var {_MDTableRow} */
- #headerRow;
- /** @var {_MDTableRow[]} */
- #bodyRows;
- toHTML(config) {
- let headerRowHTML = this.#headerRow.toHTML(config);
- let bodyRowsHTML = _MDBlock.toHTML(this.#bodyRows);
- return `<table>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
- }
- }
-
- class _MDDefinitionList extends _MDBlock {
- /** @var {_MDBlock[]} */
- #content;
- /**
- * @param {_MDBlock[]} content
- */
- constructor(content) {
- super();
- this.#content = content;
- }
- toHTML(config) {
- let contentHTML = _MDBlock.toHTML(this.#content);
- return `<dl>\n${contentHTML}\n</dl>`;
- }
- }
-
- class _MDDefinitionTerm extends _MDBlock {
- /** @var {_MDBlock} */
- #content;
- /**
- * @param {_MDBlock} content
- */
- constructor(content) {
- super();
- this.#content = content;
- }
- toHTML(config) {
- let contentHTML = this.#content.toHTML(config);
- return `<dt>${contentHTML}</dt>`;
- }
- }
-
- class _MDDefinitionDefinition extends _MDBlock {
- /** @var {_MDBlock} */
- #content;
- /**
- * @param {_MDBlock} content
- */
- constructor(content) {
- super();
- this.#content = content;
- }
- toHTML(config) {
- let contentHTML = this.#content.toHTML(config);
- return `<dd>${contentHTML}</dd>`;
- }
- }
-
- class _MDFootnoteReference extends _MDBlock {
- /** @var {String} */
- #id;
- /**
- * @param {String} id
- */
- constructor(id) {
- super();
- this.#id = id;
- }
- toHTML(config) {
- return `<sup><a href="#footnote${this.#id}">${this.#id}</a></sup>`;
- }
- }
-
- class _MDFootnoteContent extends _MDBlock {
- /** @var {String} */
- #id;
- /** @var {_MDBlock} */
- #content;
- /**
- * @param {String} id
- * @param {_MDBlock} content
- */
- constructor(id, content) {
- super();
- this.#id = id;
- this.#content = content;
- }
- toHTML(config) {
- // TODO: Forward and back links
- // TODO: Deferring footnotes to end of document
- //<ol>
- //<li id="fn:1" role="doc-endnote">
- //<p>Footnote <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
- //</li>
- //</ol>
- return '';
- }
- }
-
- class _MDAbbreviationOccurrence extends _MDBlock {
- /** @var {String} */
- #label;
- /** @var {String} */
- #definition;
-
- /**
- * @param {String} label
- * @param {String} definition
- */
- constructor(label, definition) {
- super();
- this.#label = label;
- this.#definition = definition;
- }
-
- toHTML(config) {
- return `<abbr title="${this.#definition.replace('"', '"')}">${this.#label}</abbr>`;
- }
- }
-
- class _MDInline extends _MDBlock {
- /** @var {String} */
- #raw;
- /**
- * @param {String} raw
- */
- constructor(raw) {
- super();
- this.#raw = raw;
- }
-
- toHTML(config) {
- return this.#raw;
- }
- }
-
- // 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 \*
- class _MDState {
- /** @var {String[]} */
- lines = [];
-
- /** @var {object} */
- abbreviations = {};
-
- /** @var {object} */
- footnotes = {};
-
- /** @var {number} */
- p = 0;
-
- copy() {
- let cp = new _MDState();
- cp.abbreviations = this.abbreviations;
- cp.footnotes = this.footnotes;
- cp.p = this.p;
- return cp;
- }
-
- /** @param {_MDState} other */
- apply(other) {
- this.abbreviations = other.abbreviations;
- this.footnotes = other.footnotes;
- this.p = other.p;
- }
-
- hasLines(minCount) {
- return this.p + minCount <= this.lines.length;
- }
- }
-
- class MDConfig {
-
- }
-
- class Markdown {
- /**
- * @param {String} line
- */
- static #stripIndent(line) {
- return line.replace(/^(?: {1,4}|\t)/, '');
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock[]}
- */
- static #readBlocks(state) {
- var blocks = [];
- while (state.hasLines(1)) {
- let block = this.#readNextBlock(state);
- if (block) {
- blocks.push(block);
- } else {
- break;
- }
- }
- return blocks;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock}
- */
- static #readNextBlock(state) {
- while (state.hasLines(1) && state.lines[state.p].trim().length == 0) {
- console.info("Skipping blank line " + state.p);
- state.p++;
- }
- var block;
- block = this.#readUnderlineHeader(state); if (block) return block;
- block = this.#readHashHeader(state); if (block) return block;
- block = this.#readBlockQuote(state); if (block) return block;
- block = this.#readUnorderedList(state); if (block) return block;
- block = this.#readOrderedList(state); if (block) return block;
- block = this.#readFencedCodeBlock(state); if (block) return block;
- block = this.#readIndentedCodeBlock(state); if (block) return block;
- block = this.#readHorizontalRule(state); if (block) return block;
- block = this.#readTable(state); if (block) return block;
- block = this.#readDefinitionList(state); if (block) return block;
- block = this.#readFootnoteDef(state); if (block) return block;
- block = this.#readAbbreviationDef(state); if (block) return block;
- block = this.#readParagraph(state); if (block) return block;
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readInline(state, line) {
- return new _MDInline(line);
- }
-
- /**
- * Reads the contents of something like a list item
- * @param {_MDState} state
- * @param {number} firstLineStartPos
- * @param {RegExp} stopRegex
- * @returns {_MDBlock}
- */
- static #readInteriorContent(state, firstLineStartPos, stopRegex) {
- var p = state.p;
- var seenBlankLine = false;
- var needsBlocks = false;
- var lines = [];
- while (p < state.lines.length) {
- let line = state.lines[p++];
- if (p == state.p + 1) {
- line = line.substring(firstLineStartPos);
- }
- let isBlank = line.trim().length == 0;
- let isIndented = /^\s+/.exec(line) !== null;
- if (isBlank) {
- seenBlankLine = true;
- lines.push(line.trim());
- } else if (stopRegex && stopRegex.exec(line)) {
- p--;
- break;
- } else if (isIndented) {
- if (seenBlankLine) {
- needsBlocks = true;
- }
- lines.push(this.#stripIndent(line));
- } else {
- if (seenBlankLine) {
- p--;
- break;
- }
- lines.push(this.#stripIndent(line));
- }
- }
- while (lines.length > 0 && lines[lines.length - 1].trim().length == 0) {
- lines.pop();
- }
- if (needsBlocks) {
- let substate = new _MDState();
- substate.lines = lines;
- substate.abbreviations = state.abbreviations;
- substate.footnotes = state.footnotes;
- let blocks = this.#readBlocks(substate);
- state.p = p;
- return new _MDMultiBlock(blocks);
- } else {
- state.p = p;
- return this.#readInline(state, lines.join("\n"));
- }
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readUnderlineHeader(state) {
- var p = state.p;
- if (!state.hasLines(2)) return null;
- let contentLine = state.lines[p++].trim();
- let underLine = state.lines[p++].trim();
- if (contentLine == '') return null;
- if (/^=+$/.exec(underLine)) {
- state.p = p;
- return new _MDHeader(1, this.#readInline(state, contentLine));
- }
- if (/^\-+$/.exec(underLine)) {
- state.p = p;
- return new _MDHeader(2, this.#readInline(state, contentLine));
- }
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readHashHeader(state) {
- var p = state.p;
- var groups = /^(#{1,6})\s*([^#].*)\s*$/.exec(state.lines[p++]);
- if (groups === null) return null;
- state.p = p;
- return new _MDHeader(groups[1].length, this.#readInline(state, groups[2]));
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readBlockQuote(state) {
- var blockquoteLines = [];
- var p = state.p;
- while (p < state.lines.length) {
- let line = state.lines[p++];
- if (line.startsWith(">")) {
- blockquoteLines.push(line);
- } else {
- break;
- }
- }
- if (blockquoteLines.length > 0) {
- let contentLines = blockquoteLines.map(function(line) {
- return line.substring(1).replace(/^ {0,3}\t?/, '');
- });
- let substate = new _MDState();
- substate.lines = contentLines;
- substate.abbreviations = state.abbreviations;
- substate.footnotes = state.footnotes;
- let quotedBlocks = this.#readBlocks(substate);
- state.p = p;
- return new _MDBlockquote(quotedBlocks);
- }
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDListItem|null}
- */
- static #readUnorderedListItem(state) {
- var p = state.p;
- let line = state.lines[p];
- let groups = /^([\*\+\-]\s+)(.*)$/.exec(line);
- if (groups === null) return null;
- return new _MDListItem(this.#readInteriorContent(state, groups[1].length, /^[\*\+\-]\s+/));
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readUnorderedList(state) {
- var p = state.p;
- var items = [];
- var item = null;
- do {
- item = this.#readUnorderedListItem(state);
- if (item) items.push(item);
- } while (item);
- if (items.length == 0) return null;
- return new _MDUnorderedList(items);
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readOrderedList(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readFencedCodeBlock(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readIndentedCodeBlock(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readHorizontalRule(state) {
- var p = state.p;
- let line = state.lines[p++];
- if (/^\s*(?:\-(?:\s*\-){2,}|\*(?:\s*\*){2,})\s*$/.exec(line)) {
- state.p = p;
- return new _MDHorizontalRule();
- }
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readTable(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readDefinitionList(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readFootnoteDef(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readAbbreviationDef(state) {
- return null;
- }
-
- /**
- * @param {_MDState} state
- * @returns {_MDBlock|null}
- */
- static #readParagraph(state) {
- if (!state.hasLines(1)) return null;
- var paragraphLines = [];
- var p = state.p;
- while (p < state.lines.length) {
- let line = state.lines[p++];
- if (line.trim().length == 0) {
- break;
- }
- paragraphLines.push(line);
- }
- if (paragraphLines.length > 0) {
- state.p = p;
- let content = paragraphLines.join("\n");
- return new _MDParagraph(this.#readInline(state, content));
- }
- return null;
- }
-
- /**
- * @param {String} markdown
- * @returns {String} HTML
- */
- static toHTML(markdown, config=new MDConfig()) {
- var state = new _MDState();
- let lines = markdown.replace("\r", "").split("\n");
- state.lines = lines;
- let blocks = this.#readBlocks(state);
- let html = _MDBlock.toHTML(blocks);
- return html;
- }
- }
|