From 4e18e54fdb8cc93348e92f479b4b75fd415506f2 Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 7 Oct 2026 20:14:16 +0200 Subject: [PATCH] Upstream markedjs/marked state of the touched files before c8795a47c1 --- src/rules.ts | 614 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 src/rules.ts diff --git a/src/rules.ts b/src/rules.ts new file mode 100644 index 0000000..81da76a --- /dev/null +++ b/src/rules.ts @@ -0,0 +1,614 @@ +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 +}