import type {Token, TokenFn} from './parser.js'; import CaseFold from './casefold.js'; import {text2DOM} from './misc.js'; import {TokenDone, Tokeniser} from './parser.js'; import Parser from './parser.js'; /** * This module contains a full CommonMark parser with several optional (enabled by default) extensions. * * @module markdown * @requires module:casefold * @requires module:misc * @requires module:parser */ /** */ /** * This type allows for the overriding of default processing behaviour. * * Most of the fields simply allow for alternate Node creation behaviour and custom processing. */ type Tags = { /** This field allows the whitelisting of raw HTML elements. Takes an array of tuples, of which the first element is the HTML element name, and the remaining elements are allowed attributes names. */ allowedHTML: null | [keyof HTMLElementTagNameMap, ...string[]][]; blockquote: (c: DocumentFragment) => Element | DocumentFragment; code: (info: string, text: string) => Element | DocumentFragment; heading1: (c: DocumentFragment) => Element | DocumentFragment; heading2: (c: DocumentFragment) => Element | DocumentFragment; heading3: (c: DocumentFragment) => Element | DocumentFragment; heading4: (c: DocumentFragment) => Element | DocumentFragment; heading5: (c: DocumentFragment) => Element | DocumentFragment; heading6: (c: DocumentFragment) => Element | DocumentFragment; paragraphs: (c: DocumentFragment) => Element | DocumentFragment; unorderedList: (c: DocumentFragment) => Element | DocumentFragment; orderedList: (start: string, c: DocumentFragment) => Element | DocumentFragment; listItem: (c: DocumentFragment) => Element | DocumentFragment; checkbox: (checked: boolean) => Element | DocumentFragment; thematicBreaks: () => Element | DocumentFragment; link: (href: string, title: string, c: DocumentFragment) => Element | DocumentFragment; image: (src: string, title: string, alt: string) => Element | DocumentFragment; inlineCode: (c: DocumentFragment) => Element | DocumentFragment; italic: (c: DocumentFragment) => Element | DocumentFragment; bold: (c: DocumentFragment) => Element | DocumentFragment; underline: (c: DocumentFragment) => Element | DocumentFragment; subscript: (c: DocumentFragment) => Element | DocumentFragment; superscript: (c: DocumentFragment) => Element | DocumentFragment; strikethrough: (c: DocumentFragment) => Element | DocumentFragment; insert: (c: DocumentFragment) => Element | DocumentFragment; highlight: (c: DocumentFragment) => Element | DocumentFragment; table: (c: DocumentFragment) => Element | DocumentFragment; thead: (c: DocumentFragment) => Element | DocumentFragment; tbody: (c: DocumentFragment) => Element | DocumentFragment; tr: (c: DocumentFragment) => Element | DocumentFragment; th: (alignment: string, c: DocumentFragment) => Element | DocumentFragment; td: (alignment: string, c: DocumentFragment) => Element | DocumentFragment; break: () => Element | DocumentFragment; } /** * This type allows for the disabling of various Markdown extensions. */ type UserTags = Tags & { /** Set to null to disable the Task List Item extension. */ checkbox: null | ((checked: boolean) => Element | DocumentFragment); /** Set to null to disable the underline extension. When enabled, will replace single underscore emphasis with underline tags. */ underline: null | ((c: DocumentFragment) => Element | DocumentFragment); /** Set to null to disable the subscript extension.*/ subscript: null | ((c: DocumentFragment) => Element | DocumentFragment); /** Set to null to disable the superscript extension.*/ superscript: null | ((c: DocumentFragment) => Element | DocumentFragment); /** Set to null to disable the strikethrough extension.*/ strikethrough: null | ((c: DocumentFragment) => Element | DocumentFragment); /** Set to null to disable the insert extension.*/ insert: null | ((c: DocumentFragment) => Element | DocumentFragment); /** Set to null to disable the highlight extension.*/ highlight: null | ((c: DocumentFragment) => Element | DocumentFragment); /** Set to null to disable the table extension.*/ table: null | ((c: DocumentFragment) => Element | DocumentFragment); } let inlineStarts = "", taskListItems = false; const makeNode = (nodeName: NodeName, params: Record = {}, children: string | DocumentFragment = "") => { const node = document.createElement(nodeName) as HTMLElementTagNameMap[NodeName]; for(const key in params) { node.setAttribute(key, params[key]); } if (typeof children === "string") { node.innerText = children; } else { node.append(children); } return node; }, setText = (node: N, text: string) => { node.textContent = text; return node; }, tags: Tags = ([ ["blockquote", "blockquote"], ["paragraphs", "p"], ["unorderedList", "ul"], ["listItem", "li"], ["inlineCode", "code"], ["italic", "em"], ["bold", "strong"], ["underline", "u"], ["subscript", "sub"], ["superscript", "sup"], ["strikethrough", "s"], ["insert", "ins"], ["highlight", "mark"], ["table", "table"], ["thead", "thead"], ["tbody", "tbody"], ["tr", "tr"], ...Array.from({"length": 6}, (_, n) => [`heading${n+1}`, `h${n+1}`] as [`heading${1 | 2 | 3 | 4 | 5 | 6}`, `h${1 | 2 | 3 | 4 | 5 | 6}`]) ] as const).reduce((o, [key, tag]) => (o[key] = (c: DocumentFragment) => makeNode(tag, {}, c), o), { "code": (_info: string, text: string) => makeNode("pre", {}, text), "orderedList": (start: string, c: DocumentFragment) => makeNode("ol", start ? {start} : {}, c), "allowedHTML": null, "checkbox": (checked: boolean) => makeNode("input", checked ? {"checked": "", "disabled": "", "type": "checkbox"} : {"disabled": "", "type": "checkbox"}), "thematicBreaks": () => makeNode("hr"), "link": (href: string, title: string, c: DocumentFragment) => makeNode("a", title ? {href, title} : {href}, c), "image": (src: string, title: string, alt: string) => makeNode("img", title ? {src, alt, title} : {src, alt}), "th": (alignment: string, c: DocumentFragment) => makeNode("th", alignment ? {"style": "text-align:"+alignment} : {}, c), "td": (alignment: string, c: DocumentFragment) => makeNode("td", alignment ? {"style": "text-align:"+alignment} : {}, c), "break": () => makeNode("br") } as any as Tags), whiteSpace = " ", nl = "\n", whiteSpaceNL = whiteSpace + nl, letter = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", number = "0123456789", scheme = letter + number + "+.-", control = Array.from({"length": 31}).reduce((t, _, n) => t + String.fromCharCode(n), String.fromCharCode(127)), emailStart = letter + number + "!#$&&'*+-/=?^_`{|}~.", emailLabelStr = letter + number + "-", htmlElements = ["pre", "script", "style", "textarea", "address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p", "param", "search", "section", "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul"], type1Elements = htmlElements.slice(0, 4), notTable = () => null, parseTable = (tk: Tokeniser) => { while (true) { switch (tk.exceptRun("|\\\n")) { default: return null; case '|': tk.next(); if (tk.acceptRun(whiteSpace) === nl || !tk.peek()) { return null; } return new TableBlock(tk); case '\\': tk.next(); tk.next(); } } }, parseIndentedCodeBlockStart = (tk: Tokeniser, inParagraph: boolean) => { if (!inParagraph && tk.accept(" ") && tk.length() === 4) { return new IndentedCodeBlock(tk); } return null; }, parseBlockQuoteStart = (tk: Tokeniser) => { if (tk.accept(">")) { return new BlockQuote(tk); } return null; }, parseThematicBreak = (tk: Tokeniser) => { const tbChar = tk.next(); switch (tbChar) { case '*': case '-': case '_': tk.acceptRun(whiteSpace); if (tk.accept(tbChar)) { tk.acceptRun(whiteSpace); if (tk.accept(tbChar)) { tk.acceptRun(whiteSpace + tbChar); if (tk.accept(nl) || !tk.peek()) { return new ThematicBreakBlock(tk); } } } } return null; }, parseListBlockStart = (tk: Tokeniser, inParagraph: boolean) => { const lbChar = tk.next(); switch (lbChar) { case '*': case '-': case '+': if (tk.accept(inParagraph ? whiteSpace : whiteSpaceNL)) { tk.backup(); return new ListBlock(tk); } else if (!inParagraph && !tk.peek()) { return new ListBlock(tk); } break; case '0': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if (inParagraph) { break; } case '1': const l = tk.length(); if (!inParagraph) { tk.acceptRun(number); } if (tk.length() - l < 9 && tk.accept(".)")) { if (tk.accept(inParagraph ? whiteSpace : whiteSpaceNL)) { tk.backup(); return new ListBlock(tk); } else if (!inParagraph && tk.peek() === "") { return new ListBlock(tk); } } } return null; }, parseATXHeader = (tk: Tokeniser) => { const level = tk.acceptString("######") as 1 | 2 | 3 | 4 | 5 | 6; if (level > 0 && (tk.accept(whiteSpace) || tk.peek() === nl || !tk.peek())) { return new ATXHeadingBlock(tk, level); } return null; }, parseFencedCodeBlockStart = (tk: Tokeniser) => { const fcbChar = tk.next(); switch (fcbChar) { case '`': case '~': if (tk.accept(fcbChar) && tk.accept(fcbChar)) { tk.acceptRun(fcbChar); if (tk.exceptRun(nl + (fcbChar === '`' ? '`' : "")) !== fcbChar) { tk.accept(nl); return new FencedCodeBlock(tk, fcbChar); } } } return null; }, parseHTML1 = (tk: Tokeniser) => { if (tk.accept("<")) { tk.accept("/"); if (type1Elements.indexOf(tk.acceptWord(type1Elements, false).toLowerCase()) >= 0 && (tk.accept(whiteSpace + ">") || tk.peek() === nl)) { return new HTMLBlock(tk, 1); } } return null; }, parseHTML2 = (tk: Tokeniser) => { if (tk.acceptString("