diff --git a/lib/shared/markdown.js b/lib/shared/markdown.js --- a/lib/shared/markdown.js +++ b/lib/shared/markdown.js @@ -85,9 +85,6 @@ const codeBlockRegex: RegExp = /^(?: {4}[^\n]*\n*?)+(?!\n* {4}[^\n])(?:\n|$)/; const codeBlockStripTrailingNewlineRegex: RegExp = /^((?: {4}[^\n]*\n*?)+)(?!\n* {4}[^\n])(?:\n|$)/; -const blockQuoteRegex: RegExp = /^( *>[^\n]+(?:\n[^\n]+)*)(?:\n|$)/; -const blockQuoteStripFollowingNewlineRegex: RegExp = /^( *>[^\n]+(?:\n[^\n]+)*)(?:\n|$){2}/; - const urlRegex: RegExp = /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/i; const mentionRegex = new RegExp(`^(@(${oldValidUsernameRegexString}))\\b`); @@ -219,6 +216,40 @@ return match; } +const blockQuoteRegex: RegExp = /^( *>[^\n]+(?:\n[^\n]+)*)(?:\n|$)/; +const blockQuoteStripFollowingNewlineRegex: RegExp = /^( *>[^\n]+(?:\n[^\n]+)*)(?:\n|$){2}/; +const maxNestedQuotations = 5; + +// Custom match and parse functions implementation for block quotes +// to allow us to specify quotes parsing depth +// to avoid too many recursive calls and e.g. app crash +function matchBlockQuote(quoteRegex: RegExp): MatchFunction { + return (source: string, state: State) => { + if ( + state.inline || + (state?.quotationsDepth && state.quotationsDepth >= maxNestedQuotations) + ) { + return null; + } + return quoteRegex.exec(source); + }; +} + +function parseBlockQuote( + capture: Capture, + parse: Parser, + state: State, +): UnTypedASTNode { + const content = capture[1].replace(/^ *> ?/gm, ''); + const currentQuotationsDepth = state?.quotationsDepth ?? 0; + return { + content: parse(content, { + ...state, + quotationsDepth: currentQuotationsDepth + 1, + }), + }; +} + export { paragraphRegex, paragraphStripTrailingNewlineRegex, @@ -231,6 +262,8 @@ codeBlockStripTrailingNewlineRegex, fenceRegex, fenceStripTrailingNewlineRegex, + matchBlockQuote, + parseBlockQuote, jsonMatch, jsonPrint, matchList, diff --git a/native/markdown/rules.react.js b/native/markdown/rules.react.js --- a/native/markdown/rules.react.js +++ b/native/markdown/rules.react.js @@ -215,19 +215,10 @@ blockQuote: { ...SimpleMarkdown.defaultRules.blockQuote, // match end of blockQuote by either \n\n or end of string - match: SimpleMarkdown.blockRegex( + match: SharedMarkdown.matchBlockQuote( SharedMarkdown.blockQuoteStripFollowingNewlineRegex, ), - parse( - capture: SharedMarkdown.Capture, - parse: SharedMarkdown.Parser, - state: SharedMarkdown.State, - ) { - const content = capture[1].replace(/^ *> ?/gm, ''); - return { - content: parse(content, state), - }; - }, + parse: SharedMarkdown.parseBlockQuote, // eslint-disable-next-line react/display-name react: ( node: SharedMarkdown.SingleASTNode, diff --git a/web/markdown/rules.react.js b/web/markdown/rules.react.js --- a/web/markdown/rules.react.js +++ b/web/markdown/rules.react.js @@ -78,17 +78,8 @@ blockQuote: { ...SimpleMarkdown.defaultRules.blockQuote, // match end of blockQuote by either \n\n or end of string - match: SimpleMarkdown.blockRegex(SharedMarkdown.blockQuoteRegex), - parse( - capture: SharedMarkdown.Capture, - parse: SharedMarkdown.Parser, - state: SharedMarkdown.State, - ) { - const content = capture[1].replace(/^ *> ?/gm, ''); - return { - content: parse(content, state), - }; - }, + match: SharedMarkdown.matchBlockQuote(SharedMarkdown.blockQuoteRegex), + parse: SharedMarkdown.parseBlockQuote, }, inlineCode: SimpleMarkdown.defaultRules.inlineCode, em: SimpleMarkdown.defaultRules.em,