/
githubmirror
/
marked
Обзор
Документация
Войти
/
githubmirror
/
marked
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/rules.ts
570 строк
22 KB
deepakganesh78
fix: parse EOF backtick fences after paragraphs (#4039)
05 авг 2026, 07:18
Не верифицирован
05 авг 2026, 07:18
81bd750
Код
Авторство
О чём код?
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)(?<!1)' + a); } catch { // See browser support here: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Lookbehind_assertion return false; } })(); export const other = { codeRemoveIndent: /^(?: {1,4}| {0,3}\t)/gm, outputLinkReplace: /\\([\[\]])/g, indentCodeCompensation: /^(\s+)(?:```)/, beginningSpace: /^\s+/, endingHash: /#$/, startingSpaceChar: /^ /, endingSpaceChar: / $/, nonSpaceChar: /[^ ]/, newLineCharGlobal: /\n/g, tabCharGlobal: /\t/g, multipleSpaceGlobal: /\s+/g, blankLine: /^[ \t]*$/, doubleBlankLine: /\n[ \t]*\n[ \t]*$/, blockquoteStart: /^ {0,3}>/, 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: /^<a /i, endATag: /^<\/a>/i, startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, startAngleBracket: /^</, endAngleBracket: />$/, pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, unicodeAlphaNumeric: /[\p{L}\p{N}]/u, 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}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`)), fencesBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}(?:\`\`\`|~~~)`)), headingBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}#`)), htmlBeginRegex: cachedIndentRegex((indent: number) => new RegExp(`^ {0,${indent}}<(?:[a-z].*>|!--)`, '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 |blockCode|fences|blockquote|heading|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 blocks can interrupt .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(/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 blocks can interrupt .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(/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 = /<!--(?:-?>|[\s\S]*?(?:-->|$))/; const html = edit( '^ {0,3}(?:' // optional indentation + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n*|$)' // (1) + '|comment[^\\n]*(\\n+|$)' // (2) + '|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)' // (3) + '|<![A-Z][\\s\\S]*?(?:>[^\\n]*\\n*|$)' // (4) + '|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>[^\\n]*\\n*|$)' // (5) + '|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (6) + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)' // (7) open tag + '|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\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', '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)') .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', '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)') .replace('tag', _tag) // tables can be interrupted by type (6) html blocks .getRegex(); const blockGfm: Record<BlockKeys, RegExp> = { ...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', '</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)') .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<BlockKeys, RegExp> = { ...blockNormal, html: edit( '^ *(?:comment *(?:\\n|\\s*$)' + '|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)' // closed tag + '|<tag(?:"[^"]*"|\'[^\']*\'|\\s[^\'"/>\\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: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\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*$)/; const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/; // list of unicode punctuation marks, plus any missing characters from CommonMark spec const _punctuation = /[\p{P}\p{S}]/u; const _punctuationOrSpace = /[\s\p{P}\p{S}]/u; const _notPunctuationOrSpace = /[^\s\p{P}\p{S}]/u; const punctuation = edit(/^((?![*_])punctSpace)/, 'u') .replace(/punctSpace/g, _punctuationOrSpace).getRegex(); /** * Opening punct/quotes excluded from pedantic LDelim nextChar so * `aa**"A"**bb` / `aa**(A)**bb` can start strong (Pi + Ps + ASCII straight quotes). */ const _openQuote = /[\p{Pi}\p{Ps}"']/u; // GFM allows ~ inside strong and em for strikethrough const _punctuationGfmStrongEm = /(?!~)[\p{P}\p{S}]/u; const _punctuationOrSpaceGfmStrongEm = /(?!~)[\s\p{P}\p{S}]/u; const _notPunctuationOrSpaceGfmStrongEm = /(?:[^\s\p{P}\p{S}]|~)/u; // sequences em should skip over [title](link), `code`, <html> const blockSkip = edit(/link|precode-code|html/, 'g') .replace('link', /\[(?:[^\[\]`]|(?<a>`+)[^`]+\k<a>(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/) .replace('precode-', supportsLookbehind ? '(?<!`)()' : '(^^|[^`])') .replace('code', /(?<b>`+)[^`]+\k<b>(?!`)/) .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' + '|^</[a-zA-Z][\\w:-]*\\s*>' // self-closing tag + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. <?php ?> + '|^<![a-zA-Z]+\\s[\\s\\S]*?>' // declaration, e.g. <!DOCTYPE html> + '|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>') // CDATA section .replace('comment', _inlineComment) .replace('attribute', /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/) .getRegex(); const _inlineLabel = /(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/; const link = edit(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/) .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(); const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') .replace('reflink', reflink) .replace('nolink', nolink) .getRegex(); const _caseInsensitiveProtocol = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/; /** * 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<InlineKeys, RegExp> = { ...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<InlineKeys, RegExp> = { ...inlineNormal, emStrongRDelimAst: emStrongRDelimAstGfm, emStrongLDelim: emStrongLDelimGfm, delLDelim, delRDelim, url: edit(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/) .replace('protocol', _caseInsensitiveProtocol) .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) .getRegex(), _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/, text: edit(/^(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/) .replace('protocol', _caseInsensitiveProtocol) .getRegex(), }; /** * GFM + Line Breaks Inline Grammar */ const inlineBreaks: Record<InlineKeys, RegExp> = { ...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<BlockKeys, RegExp> inline: Record<InlineKeys, RegExp> }