// 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 _MDHAlign { static Left = new _MDHAlign('Left'); static Center = new _MDHAlign('Center'); static Right = new _MDHAlign('Right'); constructor(name) { this.name = name; } toString() { return `_MDHAlign.${this.name}`; } static toHTMLAttribute(align) { switch (align) { case _MDHAlign.Left: return ' align="left"'; case _MDHAlign.Center: return ' align="center"'; case _MDHAlign.Right: return ' align="right"'; } return ''; } } class _MDSpan { toHTML(config) { throw new Error(self.constructor.name + ".toHTML not implemented"); } static toHTML(spans, config) { return spans.map((span) => span.toHTML(config)).join(""); } } class _MDMultiSpan extends _MDSpan { /** @var {_MDSpan[]} */ content; /** * @param {_MDSpan[]} content */ constructor(content) { super(); this.content = content; } toHTML() { return _MDSpan.toHTML(this.content); } } class _MDTextSpan extends _MDSpan { /** @param {String} text */ text; /** * @param {String} text */ constructor(text) { super(); this.text = text; } toHTML(config) { return this.text.replace('<', '<'); } } class _MDHTMLSpan extends _MDSpan { /** @param {String} html */ html; /** * @param {String} html */ constructor(html) { super(); this.html = html; } toHTML(config) { return this.html; } } class _MDLink extends _MDSpan { /** @var {String} */ link; /** @var {String|null} */ target = null; /** @var {_MDSpan} */ content; /** * @param {String} link * @param {_MDSpan} content */ constructor(link, content) { super(); this.link = link; this.content = content; } toHTML(config) { let escapedLink = this.link.replace('"', '"'); var html = `'; return html; } } class _MDReferencedLink extends _MDLink { /** @var {String} id */ id; constructor(id, content) { super(null, content); this.id = id; } toHTML(config) { if (this.link) { return super.toHTML(config); } else { let contentHTML = this.content.toHTML(config); return `[${contentHTML}][${this.id}]`; } } } class _MDEmphasis extends _MDSpan { /** @var {_MDSpan} content */ #content; /** * @param {_MDSpan} content */ constructor(content) { super(); this.#content = content; } toHTML(config) { let contentHTML = this.#content.toHTML(config); return `${contentHTML}`; } } class _MDStrong extends _MDSpan { /** @var {_MDSpan} content */ #content; /** * @param {_MDSpan} content */ constructor(content) { super(); this.#content = content; } toHTML(config) { let contentHTML = this.#content.toHTML(config); return `${contentHTML}`; } } class _MDStrikethrough extends _MDSpan { /** @var {_MDSpan} content */ #content; /** * @param {_MDSpan} content */ constructor(content) { super(); this.#content = content; } toHTML(config) { let contentHTML = this.#content.toHTML(config); return `${contentHTML}`; } } class _MDInlineCode extends _MDSpan { /** @var {_MDSpan} content */ #content; /** * @param {_MDSpan} content */ constructor(content) { super(); this.#content = content; } toHTML(config) { let contentHTML = this.#content.toHTML(config); return `${contentHTML}`; } } class _MDImage extends _MDSpan { /** @var {String} */ source; /** @var {String|null} */ alt; /** * @param {String} source */ constructor(source, alt) { super(); this.source = source; this.alt = alt; } toHTML(config) { let escapedSource = this.source.replace('"', '"'); let html = `${altEscaped}${this.symbol}`; } } class _MDAbbreviationReference extends _MDSpan { /** @var {_MDSpan} content */ #content; /** @var {String} definition */ #definition; /** * @param {_MDSpan} content */ constructor(content, definition) { super(); this.#content = content; this.#definition = definition; } toHTML(config) { let contentHTML = this.#content.toHTML(config); let definitionEscaped = this.#definition.replace('"', '"'); return `${contentHTML}`; } } 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 _MDTableCell extends _MDBlock { /** @var {_MDBlock} */ #content; /** @var {_MDHAlign|null} */ align = null; /** * @param {_MDBlock} content */ constructor(content) { super(); this.#content = content; } toHTML(config) { let contentHTML = this.#content.toHTML(config); let alignAttribute = _MDHAlign.toHTMLAttribute(this.align); return `${contentHTML}`; } } class _MDTableHeaderCell extends _MDTableCell { toHTML(config) { let html = super.toHTML(config); let groups = /^$/.exec(html); return ``; } } class _MDTableRow extends _MDBlock { /** @var {_MDTableCell[]|_MDTableHeaderCell[]} */ #cells; /** * @param {_MDTableCell[]|_MDTableHeaderCell[]} cells */ constructor(cells) { super(); this.#cells = cells; } /** * @param {_MDHAlign[]} alignments */ applyAlignments(alignments) { for (var i = 0; i < this.#cells.length; i++) { let cell = this.#cells[i]; let align = i < alignments.length ? alignments[i] : null; cell.align = align; } } toHTML(config) { let cellsHTML = _MDBlock.toHTML(this.#cells, config); return `\n${cellsHTML}\n`; } } class _MDTable extends _MDBlock { /** @var {_MDTableRow} */ #headerRow; /** @var {_MDTableRow[]} */ #bodyRows; /** * @param {_MDTableRow} headerRow * @param {_MDTableRow[]} bodyRows */ constructor(headerRow, bodyRows) { super(); this.#headerRow = headerRow; this.#bodyRows = 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 _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, p=-1) { let relativeTo = (p < 0) ? this.p : p; return relativeTo + minCount <= this.lines.length; } } class MDConfig { } class Markdown { /** * @param {String} line */ static #stripIndent(line, count=1) { let regex = new RegExp(`^(: {1,4}|\\t){${count}}`); return line.replace(regex, ''); } /** * @param {String} line * @param {Boolean} fullIndentsOnly * @returns {Number} indent count */ static #countIndents(line, fullIndentsOnly=false) { var count = 0; var lastLine = line; while (line.length > 0) { line = (fullIndentsOnly) ? line.replace(/^(?: {4}|\t)/, '') : line.replace(/^(?: {1,4}|\t)/, ''); if (line != lastLine) { count++; } else { break; } lastLine = line; } return count; } /** * @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) { // FIXME: When reading
  • content need to detect nested list without // a blank line 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 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 {_MDListItem|null} */ static #readOrderedListItem(state) { var p = state.p; let line = state.lines[p]; let groups = /^(\d+\.\s+)(.*)$/.exec(line); if (groups === null) return null; return new _MDListItem(this.#readInteriorContent(state, groups[1].length, /^\d+\.\s+/)); } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readOrderedList(state) { var items = []; var item = null; do { item = this.#readOrderedListItem(state); if (item) items.push(item); } while (item); if (items.length == 0) return null; return new _MDOrderedList(items); } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readFencedCodeBlock(state) { var p = state.p; if (state.lines[p++].trim() != '```') return null; var codeLines = []; while (state.hasLines(1, p)) { let line = state.lines[p++]; if (line.trim() == '```') { state.p = p; return new _MDCodeBlock(codeLines.join("\n")); } codeLines.push(line); } return null; } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readIndentedCodeBlock(state) { var p = state.p; var codeLines = []; while (state.hasLines(1, p)) { let line = state.lines[p++]; if (this.#countIndents(line, true) < 1) { p--; break; } codeLines.push(this.#stripIndent(line)); } if (codeLines.length == 0) return null; state.p = p; return new _MDCodeBlock(codeLines.join("\n")); } /** * @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 * @param {Boolean} isHeader * @return {_MDTableRow|null} */ static #readTableRow(state, isHeader) { if (!state.hasLines(1)) return null; var p = state.p; let line = state.lines[p++].trim(); if (/.*\|.*/.exec(line) === null) return null; if (line.startsWith('|')) line = line.substring(1); if (line.endsWith('|')) line = line.substring(0, line.length - 1); let cellTokens = line.split('|'); let cells = cellTokens.map(function(token) { let content = Markdown.#readInline(state, token); return isHeader ? new _MDTableHeaderCell(content) : new _MDTableCell(content); }); state.p = p; return new _MDTableRow(cells); } /** * @param {String} line * @returns {_MDHAlign[]} */ static #parseColumnAlignments(line) { line = line.trim(); if (line.startsWith('|')) line = line.substring(1); if (line.endsWith('|')) line = line.substring(0, line.length - 1); return line.split('|').map(function(token) { token = token.trim(); if (token.startsWith(':')) { if (token.endsWith(':')) { return _MDHAlign.Center; } return _MDHAlign.Left; } else if (token.endsWith(':')) { return _MDHAlign.Right; } return null; }); } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readTable(state) { if (!state.hasLines(2)) return null; let startP = state.p; let headerRow = this.#readTableRow(state, true); if (headerRow === null) { state.p = startP; return null; } let dividerLine = state.lines[state.p++]; let dividerGroups = /^\s*[|]?(?:\s*[:]?-+[:]?\s*\|)(?:\s*[:]?-+[:]?\s*)[|]?\s*$/.exec(dividerLine); if (dividerGroups === null) { state.p = startP; return null; } let columnAlignments = this.#parseColumnAlignments(dividerLine); headerRow.applyAlignments(columnAlignments); var bodyRows = []; while (state.hasLines(1)) { let row = this.#readTableRow(state, false); if (row === null) break; row.applyAlignments(columnAlignments); bodyRows.push(row); } return new _MDTable(headerRow, bodyRows); } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readDefinitionList(state) { // TODO return null; } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readFootnoteDef(state) { // TODO return null; } /** * @param {_MDState} state * @returns {_MDBlock|null} */ static #readAbbreviationDef(state) { // TODO 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; } }