const noopTest = { exec: () => null } as unknown as RegExp; function cachedIndentRegex(createRegex: (indent: number) => RegExp) { const cache: RegExp[] = []; return (indent: number) => { const cacheIndex = Math.max(0, Math.min(3, indent - 1)); let regex = cache[cacheIndex]; if (!regex) { regex = createRegex(cacheIndex); cache[cacheIndex] = regex; } return regex; }; } function edit(regex: string | RegExp, opt = '') { let source = typeof regex === 'string' ? regex : regex.source; const obj = { replace: (name: string | RegExp, val: string | RegExp) => { let valSource = typeof val === 'string' ? val : val.source; valSource = valSource.replace(other.caret, '$1'); source = source.replace(name, valSource); return obj; }, getRegex: () => { return new RegExp(source, opt); }, }; return obj; } // use a default variable to ensure the regex is evaluated on client an not removed by minifiers const supportsLookbehind = ((a = '') => { try { return !!new RegExp('(?<=1)(?/, blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, listIsTask: /^\[[ xX]\] +\S/, listReplaceTask: /^\[[ xX]\] +/, listTaskCheckbox: /\[[ xX]\]/, anyLine: /\n.*\n/, hrefBrackets: /^<(.*)>$/, tableDelimiter: /[:|]/, tableAlignChars: /^\||\| *$/g, tableRowBlankLine: /\n[ \t]*$/, tableAlignRight: /^ *-+: *$/, tableAlignCenter: /^ *:-+: *$/, tableAlignLeft: /^ *:-+ *$/, startATag: /^/i, startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, startAngleBracket: /^$/, pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, unicodeAlphaNumeric: /[\p{L}\p{N}]/u, numericCharacterReference: /&#(?:(\d{1,7})|[Xx]([A-Fa-f0-9]{1,6}));/, numericCharacterReferenceOrAmpersand: /&#(?:(\d{1,7})|[Xx]([A-Fa-f0-9]{1,6}));|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, escapeTest: /[&<>"']/, escapeReplace: /[&<>"']/g, escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, caret: /(^|[^\[])\^/g, percentDecode: /%25/g, findPipe: /\|/g, splitPipe: / \|/, slashPipe: /\\\|/g, carriageReturn: /\r\n|\r/g, spaceLine: /^ +$/gm, notSpaceStart: /^\S*/, endingNewline: /\n$/, listItemRegex: (bull: string) => new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`), nextBulletRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`)), hrRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}((?:-[ \t]*){3,}|(?:_[ \t]*){3,}|(?:\\*[ \t]*){3,})(?:\\n+|$)`)), fencesBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}(?:\`\`\`|~~~)`)), headingBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}#`)), // a list item ends where a paragraph would be interrupted, so this mirrors the // html start conditions in the paragraph rule below; type 7 is excluded there htmlBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}(?:)|<(?:(?:script|pre|style|textarea)(?:[\\s>]|$)|!--))`, 'i')), blockquoteBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}>`)), }; /** * Block-Level Grammar */ const newline = /^(?:[ \t]*(?:\n|$))+/; const blockCode = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/; const fences = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/; const hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/; const heading = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/; const bullet = / {0,3}(?:[*+-]|\d{1,9}[.)])/; const lheadingCore = /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |fences|blockquote|heading|hr|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/; const lheading = edit(lheadingCore) .replace(/bull/g, bullet) // lists can interrupt .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code can start a block but cannot interrupt a paragraph .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt .replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/) // ATX heading can interrupt .replace(/hr/g, / {0,3}(?:(?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/) // thematic break can interrupt .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt .replace(/\|table/g, '') // table not in commonmark .getRegex(); const lheadingGfm = edit(lheadingCore) .replace(/bull/g, bullet) // lists can interrupt .replace(/blockCode/g, /(?: {4}| {0,3}\t)/) // indented code can start a block but cannot interrupt a paragraph .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt .replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/) // ATX heading can interrupt .replace(/hr/g, / {0,3}(?:(?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/) // thematic break can interrupt .replace(/html/g, / {0,3}<[^\n>]+>\n/) // block html can interrupt .replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/) // table can interrupt .getRegex(); const _paragraph = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]+\n)[^\n]+)*)/; const blockText = /^[^\n]+/; const _blockLabel = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/; const def = edit(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/) .replace('label', _blockLabel) .replace('title', /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/) .getRegex(); const list = edit(/^(bull)([ \t][^\n]*?)?(?:\n|$)/) .replace(/bull/g, bullet) .getRegex(); const _tag = '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|h[1-6]|head|header|hr|html|iframe' + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title' + '|tr|track|ul'; const _comment = /|$))/; const html = edit( '^ {0,3}(?:' // optional indentation + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n*|$)' // (1) + '|comment[^\\n]*(\\n+|$)' // (2) + '|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)' // (3) + '|[^\\n]*\\n*|$)' // (4) + '|[^\\n]*\\n*|$)' // (5) + '|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6) + '|<(?!(?:script|pre|style|textarea)(?![a-z0-9-]))([a-z][a-z0-9-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) closing tag + ')', 'i') .replace('comment', _comment) .replace('tag', _tag) .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) .getRegex(); const createParagraph = (listInterrupt: RegExp) => edit(_paragraph) .replace('hr', hr) .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs .replace('|table', '') .replace('blockquote', ' {0,3}>') .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*(?:\\n|$))|~~~)[^\\n]*(?:\\n|$)') .replace('list', listInterrupt) .replace('html', ')|<(?:(?:script|pre|style|textarea)(?:[\\s>]|$)|!--)') .replace('tag', _tag) // pars can be interrupted by type (6) html blocks .getRegex(); // only non-empty lists starting from 1 can interrupt paragraphs const paragraph = createParagraph(/ {0,3}(?:[*+-]|1[.)])[ \t]+[^ \t\n]/); // inside a blockquote a bare list marker (any number) starts a sibling list, // so it must not be lazily continued as paragraph text (unlike a top level // paragraph, where an empty list cannot interrupt) const blockquoteParagraph = createParagraph(/ {0,3}(?:[*+-]|\d{1,9}[.)])(?:[ \t]|\n|$)/); const blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/) .replace('paragraph', blockquoteParagraph) .getRegex(); /** * Normal Block Grammar */ const blockNormal = { blockquote, code: blockCode, def, fences, heading, hr, html, lheading, list, newline, paragraph, table: noopTest, text: blockText, }; type BlockKeys = keyof typeof blockNormal; /** * GFM Block Grammar */ const gfmTable = edit( '^ *([^\\n ].*)\\n' // Header + ' {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)' // Align + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)') // Cells .replace('hr', hr) .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') .replace('blockquote', ' {0,3}>') .replace('code', '(?: {4}| {0,3}\t)[^\\n]') .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*(?:\\n|$))|~~~)[^\\n]*(?:\\n|$)') .replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\t]') // any bullet ends the table rows .replace('html', ')|<(?:(?:script|pre|style|textarea)(?:[\\s>]|$)|!--)') .replace('tag', _tag) // tables can be interrupted by type (6) html blocks .getRegex(); const blockGfm: Record = { ...blockNormal, lheading: lheadingGfm, table: gfmTable, paragraph: edit(_paragraph) .replace('hr', hr) .replace('heading', ' {0,3}#{1,6}(?:\\s|$)') .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs .replace('table', gfmTable) // interrupt paragraphs with table .replace('blockquote', ' {0,3}>') .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*(?:\\n|$))|~~~)[^\\n]*(?:\\n|$)') .replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]') // only non-empty lists starting from 1 can interrupt .replace('html', ')|<(?:(?:script|pre|style|textarea)(?:[\\s>]|$)|!--)') .replace('tag', _tag) // pars can be interrupted by type (6) html blocks .getRegex(), }; /** * Pedantic grammar (original John Gruber's loose markdown specification) */ const blockPedantic: Record = { ...blockNormal, html: edit( '^ *(?:comment *(?:\\n|\\s*$)' + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') .replace('comment', _comment) .replace(/tag/g, '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') .getRegex(), def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, heading: /^(#{1,6})(.*)(?:\n+|$)/, fences: noopTest, // fences not supported lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, paragraph: edit(_paragraph) .replace('hr', hr) .replace('heading', ' *#{1,6} *[^\n]') .replace('lheading', lheading) .replace('|table', '') .replace('blockquote', ' {0,3}>') .replace('|fences', '') .replace('|list', '') .replace('|html', '') .replace('|tag', '') .getRegex(), }; /** * Inline-Level Grammar */ const escape = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; const br = /^( {2,}|\\)\n(?!\s*$)[ \t]*/; const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ const blockSkip = edit(/link|precode-code|html/, 'g') .replace('link', /\[(?:[^\[\]`]|(?`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/) .replace('precode-', supportsLookbehind ? '(?`+)[^`]+\k(?!`)/) .replace('html', /<(?! )[^<>]*?>/) .getRegex(); const emStrongLDelimCore = /^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/; const emStrongLDelim = edit(emStrongLDelimCore, 'u') .replace(/punct/g, _punctuation) .getRegex(); const emStrongLDelimGfm = edit(emStrongLDelimCore, 'u') .replace(/punct/g, _punctuationGfmStrongEm) .getRegex(); // Pedantic: do not treat opening punct/quotes as left-delim punct (e.g. **( / **"). const emStrongLDelimPedanticCore = /^(?:\*+(?:((?!\*)(?!openQuote)punct)|([^\s*]))?)|^_+(?:((?!_)(?!openQuote)punct)|([^\s_]))?/; const emStrongLDelimPedantic = edit(emStrongLDelimPedanticCore, 'u') .replace(/openQuote/g, _openQuote) .replace(/punct/g, _punctuation) .getRegex(); const emStrongRDelimAstCore = '^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + '|[^*]+(?=[^*])' // Consume to delim + '|(?!\\*)punct(\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + '|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter + '|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)' // (3) #***a, ***a can only be Left Delimiter + '|[\\s](\\*+)(?!\\*)(?=punct)' // (4) ***# can only be Left Delimiter + '|(?!\\*)punct(\\*+)(?!\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter + '|notPunctSpace(\\*+)(?=notPunctSpace)'; // (6) a***a can be either Left or Right Delimiter const emStrongRDelimAst = edit(emStrongRDelimAstCore, 'gu') .replace(/notPunctSpace/g, _notPunctuationOrSpace) .replace(/punctSpace/g, _punctuationOrSpace) .replace(/punct/g, _punctuation) .getRegex(); const emStrongRDelimAstGfm = edit(emStrongRDelimAstCore, 'gu') .replace(/notPunctSpace/g, _notPunctuationOrSpaceGfmStrongEm) .replace(/punctSpace/g, _punctuationOrSpaceGfmStrongEm) .replace(/punct/g, _punctuationGfmStrongEm) .getRegex(); // Pedantic: rule 3 requires whitespace before left delim; rule 6 allows punct before close (e.g. `:**bar`). const emStrongRDelimAstPedanticCore = '^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)' // Skip orphan inside strong + '|[^*]+(?=[^*])' // Consume to delim + '|(?!\\*)punct(\\*+)(?=[\\s]|$)' // (1) #*** can only be a Right Delimiter + '|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter + '|(?!\\*)[\\s](\\*+)(?=notPunctSpace)' // (3) ***a can only be Left Delimiter (whitespace required; not #***a) + '|[\\s](\\*+)(?!\\*)(?=punct)' // (4) ***# can only be Left Delimiter + '|(?!\\*)punct(\\*+)(?!\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter + '|(?:(?!\\*)punct|notPunctSpace)(\\*+)(?!\\*)(?=notPunctSpace)'; // (6) a***a, #***a, and :** (e.g. **foo:**bar) const emStrongRDelimAstPedantic = edit(emStrongRDelimAstPedanticCore, 'gu') .replace(/notPunctSpace/g, _notPunctuationOrSpace) .replace(/punctSpace/g, _punctuationOrSpace) .replace(/punct/g, _punctuation) .getRegex(); // Normal: no rule 6 for _ (a___a cannot close as emphasis). const emStrongRDelimUnd = edit( '^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + '|[^_]+(?=[^_])' // Consume to delim + '|(?!_)punct(_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter + '|(?!_)punctSpace(_+)(?=notPunctSpace)' // (3) #___a, ___a can only be Left Delimiter + '|[\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter + '|(?!_)punct(_+)(?!_)(?=punct)', 'gu') // (5) #___# can be either Left or Right Delimiter .replace(/notPunctSpace/g, _notPunctuationOrSpace) .replace(/punctSpace/g, _punctuationOrSpace) .replace(/punct/g, _punctuation) .getRegex(); // Pedantic: same rule 3/6 adjustments as emStrongRDelimAstPedantic (e.g. `_foo:_bar`). const emStrongRDelimUndPedanticCore = '^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)' // Skip orphan inside strong + '|[^_]+(?=[^_])' // Consume to delim + '|(?!_)punct(_+)(?=[\\s]|$)' // (1) #___ can only be a Right Delimiter + '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter + '|(?!_)[\\s](_+)(?=notPunctSpace)' // (3) ___a can only be Left Delimiter (whitespace required; not #___a) + '|[\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter + '|(?!_)punct(_+)(?!_)(?=punct)' // (5) #___# can be either Left or Right Delimiter + '|(?:(?!_)punct|notPunctSpace)(_+)(?!_)(?=notPunctSpace)'; // (6) a___a, #___a, and :__ (e.g. _foo:_bar) const emStrongRDelimUndPedantic = edit(emStrongRDelimUndPedanticCore, 'gu') .replace(/notPunctSpace/g, _notPunctuationOrSpace) .replace(/punctSpace/g, _punctuationOrSpace) .replace(/punct/g, _punctuation) .getRegex(); // Tilde left delimiter for strikethrough (similar to emStrongLDelim for asterisk) const delLDelim = edit(/^~~?(?:((?!~)punct)|[^\s~])/, 'u') .replace(/punct/g, _punctuation) .getRegex(); // Tilde delimiter patterns for strikethrough (similar to asterisk) const delRDelimCore = '^[^~]+(?=[^~])' // Consume to delim + '|(?!~)punct(~~?)(?=[\\s]|$)' // (1) #~~ can only be a Right Delimiter + '|notPunctSpace(~~?)(?!~)(?=punctSpace|$)' // (2) a~~#, a~~ can only be a Right Delimiter + '|(?!~)punctSpace(~~?)(?=notPunctSpace)' // (3) #~~a, ~~a can only be Left Delimiter + '|[\\s](~~?)(?!~)(?=punct)' // (4) ~~# can only be Left Delimiter + '|(?!~)punct(~~?)(?!~)(?=punct)' // (5) #~~# can be either Left or Right Delimiter + '|notPunctSpace(~~?)(?=notPunctSpace)'; // (6) a~~a can be either Left or Right Delimiter const delRDelim = edit(delRDelimCore, 'gu') .replace(/notPunctSpace/g, _notPunctuationOrSpace) .replace(/punctSpace/g, _punctuationOrSpace) .replace(/punct/g, _punctuation) .getRegex(); const anyPunctuation = edit(/\\(punct)/, 'gu') .replace(/punct/g, _punctuation) .getRegex(); const autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/) .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/) .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/) .getRegex(); const _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex(); const tag = edit( '^comment' + '|^' // self-closing tag + '|^<[a-zA-Z][a-zA-Z0-9-]*(?:attribute)*?\\s*/?>' // open tag + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + '|^' // declaration, e.g. + '|^') // CDATA section .replace('comment', _inlineComment) .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) .getRegex(); // One matched pair of brackets, holding no bracket of its own. const _inlineLabelBrackets = /\[(?:\\[\s\S]|[^\[\]\\])*\]/; // CommonMark lets the brackets in a link label nest to any depth, which a regex // cannot follow, so it stops at a fixed one. Two levels is what the spec suite // asks for: a third and a fourth flip no further example. const _inlineLabel = edit(/(?:\[(?:brackets|\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/) .replace('brackets', _inlineLabelBrackets) .getRegex(); const link = edit(/^!?\[(label)\]\([ \t\n]*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?[ \t\n]*\)/) .replace('label', _inlineLabel) .replace('href', /<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]+|(?=\))/) .replace('title', /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/) .getRegex(); const reflink = edit(/^!?\[(label)\]\[(ref)\]/) .replace('label', _inlineLabel) .replace('ref', _blockLabel) .getRegex(); const nolink = edit(/^!?\[(ref)\](?:\[\])?/) .replace('ref', _blockLabel) .getRegex(); // `reflink` and `nolink` are anchored, so the tokenizer tries each of them at a // single position. `reflinkSearch` drops the anchors and runs with the global // flag, which makes every '[' in the source a start position. A label crosses a // bracket only by escaping it, and nothing caps how often it does, so a // candidate that can never match still scans to the end of the source and the // whole pass costs O(n^2). // // The labels below bound that scan. `_boundedInlineLabel` limits how many // escapes, code spans and nested brackets a link text may hold, leaving runs of // ordinary characters unbounded so link text of any length is still found. // `_boundedBlockLabel` limits the label itself to 999 items, which is every // label CommonMark allows: "A link label can have at most 999 characters // inside the square brackets." const _boundedBlockLabel = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\]){1,999}/; const _boundedInlineLabel = edit(/(?:[^\[\]\\`]*(?:\[(?:brackets|\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\]))){0,999}?[^\[\]\\`]*?/) .replace('brackets', _inlineLabelBrackets) .getRegex(); const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') .replace('reflink', edit(/^!?\[(label)\]\[(ref)\]/) .replace('label', _boundedInlineLabel) .replace('ref', _boundedBlockLabel) .getRegex()) .replace('nolink', edit(/^!?\[(ref)\](?:\[\])?/) .replace('ref', _boundedBlockLabel) .getRegex()) .getRegex(); const _caseInsensitiveProtocol = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/; const _extendedEmail = /[A-Za-z0-9._+-]+@[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![\w-])/; const _extendedEmailProtocol = edit(/(?:mailto:email|xmpp:email(?:\/[A-Za-z0-9@.]+)?)/) .replace(/email/g, _extendedEmail) .getRegex(); /** * Normal Inline Grammar */ const inlineNormal = { _backpedal: noopTest, // only used for GFM url anyPunctuation, autolink, blockSkip, br, code: inlineCode, del: noopTest, delLDelim: noopTest, delRDelim: noopTest, emStrongLDelim, emStrongRDelimAst, emStrongRDelimUnd, escape, link, nolink, punctuation, reflink, reflinkSearch, tag, text: inlineText, url: noopTest, }; type InlineKeys = keyof typeof inlineNormal; /** * Pedantic Inline Grammar */ const inlinePedantic: Record = { ...inlineNormal, emStrongLDelim: emStrongLDelimPedantic, emStrongRDelimAst: emStrongRDelimAstPedantic, emStrongRDelimUnd: emStrongRDelimUndPedantic, link: edit(/^!?\[(label)\]\((.*?)\)/) .replace('label', _inlineLabel) .getRegex(), reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) .replace('label', _inlineLabel) .getRegex(), }; /** * GFM Inline Grammar */ const inlineGfm: Record = { ...inlineNormal, emStrongRDelimAst: emStrongRDelimAstGfm, emStrongLDelim: emStrongLDelimGfm, delLDelim, delRDelim, url: edit(/^emailProtocol|^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/) .replace('emailProtocol', _extendedEmailProtocol) .replace('protocol', _caseInsensitiveProtocol) .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![\w-])/) .getRegex(), _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/, text: edit(/^(?:[^a-zA-Z0-9](?=emailProtocol)|(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\ = { ...inlineGfm, br: edit(br).replace('{2,}', '*').getRegex(), text: edit(inlineGfm.text) .replace('\\b_', '\\b_| {2,}\\n') .replace(/\{2,\}/g, '*') .getRegex(), }; /** * exports */ export const block = { normal: blockNormal, gfm: blockGfm, pedantic: blockPedantic, }; export const inline = { normal: inlineNormal, gfm: inlineGfm, breaks: inlineBreaks, pedantic: inlinePedantic, }; export interface Rules { other: typeof other block: Record inline: Record }