// 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 ![alt text](https://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 `

${contentHTML}

\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 `${contentHTML}\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 `
\n${contentHTML}\n
`; } } 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 ``; } } 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 `
    \n${contentHTML}\n
`; } } 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 `
  • ${contentHTML}
  • `; } } class _MDCodeBlock extends _MDBlock { /** @var {String} */ #code; /** * @param {String} code */ constructor(code) { super(); this.#code = code; } toHTML(config) { return `
    ${this.#code}
    `; } } class _MDHorizontalRule extends _MDBlock { toHTML(config) { return "
    \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 `${contentHTML}`; } } 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 `${contentHTML}`; } } 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 `\n${cellsHTML}\n`; } } 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 `\n\n${headerRowHTML}\n\n\n${bodyRowsHTML}\n\n
    `; } } 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 `
    \n${contentHTML}\n
    `; } } 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 `
    ${contentHTML}
    `; } } 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 `
    ${contentHTML}
    `; } } class _MDFootnoteReference extends _MDBlock { /** @var {String} */ #id; /** * @param {String} id */ constructor(id) { super(); this.#id = id; } toHTML(config) { return `${this.#id}`; } } 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 //
      //
    1. //

      Footnote ↩︎

      //
    2. //
    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 `${this.#label}`; } } 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 ![alt text](url){.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; } }