From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 30 Oct 2023 05:58:47 +0000 (+1100) Subject: Timeline Perf Improvement (#1521) X-Git-Tag: v3.2.0~4 X-Git-Url: https://git.wafflesoft.org/?a=commitdiff_plain;h=c854c7f9d2167e1e38baf90d17afa0ee1f77033b;p=rainny.git Timeline Perf Improvement (#1521) * emojify msg txt find&replace instead of recursion * move findAndReplace func in its own file * improve find and replace * move markdown file to plugins * make find and replace work without g flag regex * fix pagination stop on msg arrive * render blurhash in small size --- diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index 9a03604..53ee6dd 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -3,7 +3,8 @@ import { Descendant, Text } from 'slate'; import { sanitizeText } from '../../utils/sanitize'; import { BlockType } from './types'; import { CustomElement } from './slate'; -import { parseBlockMD, parseInlineMD, replaceMatch } from '../../utils/markdown'; +import { parseBlockMD, parseInlineMD } from '../../plugins/markdown'; +import { findAndReplace } from '../../utils/findAndReplace'; export type OutputOptions = { allowTextFormatting?: boolean; @@ -69,14 +70,14 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => { } }; -const HTML_TAG_REG = /<([\w-]+)(?: [^>]*)?(?:(?:\/>)|(?:>.*?<\/\1>))/; -const ignoreHTMLParseInlineMD = (text: string): string => { - if (text === '') return text; - const match = text.match(HTML_TAG_REG); - if (!match) return parseInlineMD(text); - const [matchedTxt] = match; - return replaceMatch((txt) => [ignoreHTMLParseInlineMD(txt)], text, match, matchedTxt).join(''); -}; +const HTML_TAG_REG_G = /<([\w-]+)(?: [^>]*)?(?:(?:\/>)|(?:>.*?<\/\1>))/g; +const ignoreHTMLParseInlineMD = (text: string): string => + findAndReplace( + text, + HTML_TAG_REG_G, + (match) => match[0], + (txt) => parseInlineMD(txt) + ).join(''); export const toMatrixCustomHTML = ( node: Descendant | Descendant[], diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 9603209..0c74de5 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -345,7 +345,6 @@ const useTimelinePagination = ( return async (backwards: boolean) => { if (fetching) return; - const targetTimeline = timelineRef.current; const { linkedTimelines: lTimelines } = timelineRef.current; const timelinesEventsCount = lTimelines.map(timelineToEventsCount); @@ -385,7 +384,6 @@ const useTimelinePagination = ( } fetching = false; - if (targetTimeline !== timelineRef.current) return; if (alive()) { recalibratePagination(lTimelines, timelinesEventsCount, backwards); } diff --git a/src/app/organisms/room/message/ImageContent.tsx b/src/app/organisms/room/message/ImageContent.tsx index c8b32cc..6e28802 100644 --- a/src/app/organisms/room/message/ImageContent.tsx +++ b/src/app/organisms/room/message/ImageContent.tsx @@ -98,7 +98,13 @@ export const ImageContent = as<'div', ImageContentProps>( )} {typeof blurHash === 'string' && !load && ( - + )} {!autoPlay && srcState.status === AsyncStatus.Idle && ( diff --git a/src/app/organisms/room/message/VideoContent.tsx b/src/app/organisms/room/message/VideoContent.tsx index 107d5f9..8b3bd34 100644 --- a/src/app/organisms/room/message/VideoContent.tsx +++ b/src/app/organisms/room/message/VideoContent.tsx @@ -88,7 +88,13 @@ export const VideoContent = as<'div', VideoContentProps>( return ( {typeof blurHash === 'string' && !load && ( - + )} {thumbSrcState.status === AsyncStatus.Success && !load && ( diff --git a/src/app/plugins/markdown.ts b/src/app/plugins/markdown.ts new file mode 100644 index 0000000..c6bb391 --- /dev/null +++ b/src/app/plugins/markdown.ts @@ -0,0 +1,368 @@ +export type MatchResult = RegExpMatchArray | RegExpExecArray; +export type RuleMatch = (text: string) => MatchResult | null; + +export const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => + text.slice(0, match.index); +export const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => + text.slice((match.index ?? 0) + match[0].length); + +export const replaceMatch = ( + convertPart: (txt: string) => Array, + text: string, + match: MatchResult, + content: C +): Array => [ + ...convertPart(beforeMatch(text, match)), + content, + ...convertPart(afterMatch(text, match)), +]; + +/* + ***************** + * INLINE PARSER * + ***************** + */ + +export type InlineMDParser = (text: string) => string; + +export type InlineMatchConverter = (parse: InlineMDParser, match: MatchResult) => string; + +export type InlineMDRule = { + match: RuleMatch; + html: InlineMatchConverter; +}; + +export type InlineRuleRunner = ( + parse: InlineMDParser, + text: string, + rule: InlineMDRule +) => string | undefined; +export type InlineRulesRunner = ( + parse: InlineMDParser, + text: string, + rules: InlineMDRule[] +) => string | undefined; + +const MIN_ANY = '(.+?)'; +const URL_NEG_LB = '(? text.match(BOLD_REG_1), + html: (parse, match) => { + const [, , g2] = match; + return `${parse(g2)}`; + }, +}; + +const ITALIC_MD_1 = '*'; +const ITALIC_PREFIX_1 = '\\*'; +const ITALIC_NEG_LA_1 = '(?!\\*)'; +const ITALIC_REG_1 = new RegExp( + `${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}` +); +const ItalicRule1: InlineMDRule = { + match: (text) => text.match(ITALIC_REG_1), + html: (parse, match) => { + const [, , g2] = match; + return `${parse(g2)}`; + }, +}; + +const ITALIC_MD_2 = '_'; +const ITALIC_PREFIX_2 = '_'; +const ITALIC_NEG_LA_2 = '(?!_)'; +const ITALIC_REG_2 = new RegExp( + `${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}` +); +const ItalicRule2: InlineMDRule = { + match: (text) => text.match(ITALIC_REG_2), + html: (parse, match) => { + const [, , g2] = match; + return `${parse(g2)}`; + }, +}; + +const UNDERLINE_MD_1 = '__'; +const UNDERLINE_PREFIX_1 = '_{2}'; +const UNDERLINE_NEG_LA_1 = '(?!_)'; +const UNDERLINE_REG_1 = new RegExp( + `${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` +); +const UnderlineRule: InlineMDRule = { + match: (text) => text.match(UNDERLINE_REG_1), + html: (parse, match) => { + const [, , g2] = match; + return `${parse(g2)}`; + }, +}; + +const STRIKE_MD_1 = '~~'; +const STRIKE_PREFIX_1 = '~{2}'; +const STRIKE_NEG_LA_1 = '(?!~)'; +const STRIKE_REG_1 = new RegExp( + `${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}` +); +const StrikeRule: InlineMDRule = { + match: (text) => text.match(STRIKE_REG_1), + html: (parse, match) => { + const [, , g2] = match; + return `${parse(g2)}`; + }, +}; + +const CODE_MD_1 = '`'; +const CODE_PREFIX_1 = '`'; +const CODE_NEG_LA_1 = '(?!`)'; +const CODE_REG_1 = new RegExp(`${URL_NEG_LB}${CODE_PREFIX_1}(.+?)${CODE_PREFIX_1}${CODE_NEG_LA_1}`); +const CodeRule: InlineMDRule = { + match: (text) => text.match(CODE_REG_1), + html: (parse, match) => { + const [, , g2] = match; + return `${g2}`; + }, +}; + +const SPOILER_MD_1 = '||'; +const SPOILER_PREFIX_1 = '\\|{2}'; +const SPOILER_NEG_LA_1 = '(?!\\|)'; +const SPOILER_REG_1 = new RegExp( + `${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}` +); +const SpoilerRule: InlineMDRule = { + match: (text) => text.match(SPOILER_REG_1), + html: (parse, match) => { + const [, , g2] = match; + return `${parse(g2)}`; + }, +}; + +const LINK_ALT = `\\[${MIN_ANY}\\]`; +const LINK_URL = `\\((https?:\\/\\/.+?)\\)`; +const LINK_REG_1 = new RegExp(`${LINK_ALT}${LINK_URL}`); +const LinkRule: InlineMDRule = { + match: (text) => text.match(LINK_REG_1), + html: (parse, match) => { + const [, g1, g2] = match; + return `${parse(g1)}`; + }, +}; + +const runInlineRule: InlineRuleRunner = (parse, text, rule) => { + const matchResult = rule.match(text); + if (matchResult) { + const content = rule.html(parse, matchResult); + return replaceMatch((txt) => [parse(txt)], text, matchResult, content).join(''); + } + return undefined; +}; + +/** + * Runs multiple rules at the same time to better handle nested rules. + * Rules will be run in the order they appear. + */ +const runInlineRules: InlineRulesRunner = (parse, text, rules) => { + const matchResults = rules.map((rule) => rule.match(text)); + + let targetRule: InlineMDRule | undefined; + let targetResult: MatchResult | undefined; + + for (let i = 0; i < matchResults.length; i += 1) { + const currentResult = matchResults[i]; + if (currentResult && typeof currentResult.index === 'number') { + if ( + !targetResult || + (typeof targetResult?.index === 'number' && currentResult.index < targetResult.index) + ) { + targetResult = currentResult; + targetRule = rules[i]; + } + } + } + + if (targetRule && targetResult) { + const content = targetRule.html(parse, targetResult); + return replaceMatch((txt) => [parse(txt)], text, targetResult, content).join(''); + } + return undefined; +}; + +const LeveledRules = [ + BoldRule, + ItalicRule1, + UnderlineRule, + ItalicRule2, + StrikeRule, + SpoilerRule, + LinkRule, +]; + +export const parseInlineMD: InlineMDParser = (text) => { + if (text === '') return text; + let result: string | undefined; + if (!result) result = runInlineRule(parseInlineMD, text, CodeRule); + + if (!result) result = runInlineRules(parseInlineMD, text, LeveledRules); + + return result ?? text; +}; + +/* + **************** + * BLOCK PARSER * + **************** + */ + +export type BlockMDParser = (test: string, parseInline?: (txt: string) => string) => string; + +export type BlockMatchConverter = ( + match: MatchResult, + parseInline?: (txt: string) => string +) => string; + +export type BlockMDRule = { + match: RuleMatch; + html: BlockMatchConverter; +}; + +export type BlockRuleRunner = ( + parse: BlockMDParser, + text: string, + rule: BlockMDRule, + parseInline?: (txt: string) => string +) => string | undefined; + +const HEADING_REG_1 = /^(#{1,6}) +(.+)\n?/m; +const HeadingRule: BlockMDRule = { + match: (text) => text.match(HEADING_REG_1), + html: (match, parseInline) => { + const [, g1, g2] = match; + const level = g1.length; + return `${parseInline ? parseInline(g2) : g2}`; + }, +}; + +const CODEBLOCK_MD_1 = '```'; +const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((.+\n)+)`{3} *(?!.)\n?/m; +const CodeBlockRule: BlockMDRule = { + match: (text) => text.match(CODEBLOCK_REG_1), + html: (match) => { + const [, g1, g2] = match; + const classNameAtt = g1 ? ` class="language-${g1}"` : ''; + return `
${g2}
`; + }, +}; + +const BLOCKQUOTE_MD_1 = '>'; +const QUOTE_LINE_PREFIX = /^> */; +const BLOCKQUOTE_TRAILING_NEWLINE = /\n$/; +const BLOCKQUOTE_REG_1 = /(^>.*\n?)+/m; +const BlockQuoteRule: BlockMDRule = { + match: (text) => text.match(BLOCKQUOTE_REG_1), + html: (match, parseInline) => { + const [blockquoteText] = match; + + const lines = blockquoteText + .replace(BLOCKQUOTE_TRAILING_NEWLINE, '') + .split('\n') + .map((lineText) => { + const line = lineText.replace(QUOTE_LINE_PREFIX, ''); + if (parseInline) return `${parseInline(line)}
`; + return `${line}
`; + }) + .join(''); + return `
${lines}
`; + }, +}; + +const ORDERED_LIST_MD_1 = '-'; +const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */; +const O_LIST_START = /^([\d])\./; +const O_LIST_TYPE = /^([aAiI])\./; +const O_LIST_TRAILING_NEWLINE = /\n$/; +const ORDERED_LIST_REG_1 = /(^(-|[\da-zA-Z]\.) +.+\n?)+/m; +const OrderedListRule: BlockMDRule = { + match: (text) => text.match(ORDERED_LIST_REG_1), + html: (match, parseInline) => { + const [listText] = match; + const [, listStart] = listText.match(O_LIST_START) ?? []; + const [, listType] = listText.match(O_LIST_TYPE) ?? []; + + const lines = listText + .replace(O_LIST_TRAILING_NEWLINE, '') + .split('\n') + .map((lineText) => { + const line = lineText.replace(O_LIST_ITEM_PREFIX, ''); + const txt = parseInline ? parseInline(line) : line; + return `
  • ${txt}

  • `; + }) + .join(''); + + const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`; + const startAtt = listStart ? ` start="${listStart}"` : ''; + const typeAtt = listType ? ` type="${listType}"` : ''; + return `
      ${lines}
    `; + }, +}; + +const UNORDERED_LIST_MD_1 = '*'; +const U_LIST_ITEM_PREFIX = /^\* */; +const U_LIST_TRAILING_NEWLINE = /\n$/; +const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m; +const UnorderedListRule: BlockMDRule = { + match: (text) => text.match(UNORDERED_LIST_REG_1), + html: (match, parseInline) => { + const [listText] = match; + + const lines = listText + .replace(U_LIST_TRAILING_NEWLINE, '') + .split('\n') + .map((lineText) => { + const line = lineText.replace(U_LIST_ITEM_PREFIX, ''); + const txt = parseInline ? parseInline(line) : line; + return `
  • ${txt}

  • `; + }) + .join(''); + + return `
      ${lines}
    `; + }, +}; + +const runBlockRule: BlockRuleRunner = (parse, text, rule, parseInline) => { + const matchResult = rule.match(text); + if (matchResult) { + const content = rule.html(matchResult, parseInline); + return replaceMatch((txt) => [parse(txt, parseInline)], text, matchResult, content).join(''); + } + return undefined; +}; + +export const parseBlockMD: BlockMDParser = (text, parseInline) => { + if (text === '') return text; + let result: string | undefined; + + if (!result) result = runBlockRule(parseBlockMD, text, CodeBlockRule, parseInline); + if (!result) result = runBlockRule(parseBlockMD, text, BlockQuoteRule, parseInline); + if (!result) result = runBlockRule(parseBlockMD, text, OrderedListRule, parseInline); + if (!result) result = runBlockRule(parseBlockMD, text, UnorderedListRule, parseInline); + if (!result) result = runBlockRule(parseBlockMD, text, HeadingRule, parseInline); + + // replace \n with
    because want to preserve empty lines + if (!result) { + if (parseInline) { + result = text + .split('\n') + .map((lineText) => parseInline(lineText)) + .join('
    '); + } else { + result = text.replace(/\n/g, '
    '); + } + } + + return result ?? text; +}; diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 928419c..ee41687 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -18,11 +18,11 @@ import { getMxIdLocalPart, getRoomWithCanonicalAlias } from '../utils/matrix'; import { getMemberDisplayName } from '../utils/room'; import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex'; import { getHexcodeForEmoji, getShortcodeFor } from './emoji'; -import { replaceMatch } from '../utils/markdown'; +import { findAndReplace } from '../utils/findAndReplace'; const ReactPrism = lazy(() => import('./react-prism/ReactPrism')); -const EMOJI_REG = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`); +const EMOJI_REG_G = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`, 'g'); export const LINKIFY_OPTS: LinkifyOpts = { attributes: { @@ -35,26 +35,22 @@ export const LINKIFY_OPTS: LinkifyOpts = { ignoreTags: ['span'], }; -const stringToEmojifyJSX = (text: string): (string | JSX.Element)[] => { - const match = text.match(EMOJI_REG); - if (!match) return [text]; - - const [emoji] = match; - - return replaceMatch( - stringToEmojifyJSX, +const textToEmojifyJSX = (text: string): (string | JSX.Element)[] => + findAndReplace( text, - match, - - - {emoji} + EMOJI_REG_G, + (match, pushIndex) => ( + + + {match[0]} + - + ), + (txt) => txt ); -}; export const emojifyAndLinkify = (text: string, linkify?: boolean) => { - const emojifyJSX = stringToEmojifyJSX(text); + const emojifyJSX = textToEmojifyJSX(text); if (linkify) { return {emojifyJSX}; diff --git a/src/app/utils/findAndReplace.ts b/src/app/utils/findAndReplace.ts new file mode 100644 index 0000000..a4bd1ed --- /dev/null +++ b/src/app/utils/findAndReplace.ts @@ -0,0 +1,28 @@ +export type ReplaceCallback = ( + match: RegExpExecArray | RegExpMatchArray, + pushIndex: number +) => R; +export type ConvertPartCallback = (text: string, pushIndex: number) => R; + +export const findAndReplace = ( + text: string, + regex: RegExp, + replace: ReplaceCallback, + convertPart: ConvertPartCallback +): Array => { + const result: Array = []; + let lastEnd = 0; + + let match: RegExpExecArray | RegExpMatchArray | null = regex.exec(text); + while (match !== null && typeof match.index === 'number') { + result.push(convertPart(text.slice(lastEnd, match.index), result.length)); + result.push(replace(match, result.length)); + + lastEnd = match.index + match[0].length; + if (regex.global) match = regex.exec(text); + } + + result.push(convertPart(text.slice(lastEnd), result.length)); + + return result; +}; diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts deleted file mode 100644 index c6bb391..0000000 --- a/src/app/utils/markdown.ts +++ /dev/null @@ -1,368 +0,0 @@ -export type MatchResult = RegExpMatchArray | RegExpExecArray; -export type RuleMatch = (text: string) => MatchResult | null; - -export const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => - text.slice(0, match.index); -export const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => - text.slice((match.index ?? 0) + match[0].length); - -export const replaceMatch = ( - convertPart: (txt: string) => Array, - text: string, - match: MatchResult, - content: C -): Array => [ - ...convertPart(beforeMatch(text, match)), - content, - ...convertPart(afterMatch(text, match)), -]; - -/* - ***************** - * INLINE PARSER * - ***************** - */ - -export type InlineMDParser = (text: string) => string; - -export type InlineMatchConverter = (parse: InlineMDParser, match: MatchResult) => string; - -export type InlineMDRule = { - match: RuleMatch; - html: InlineMatchConverter; -}; - -export type InlineRuleRunner = ( - parse: InlineMDParser, - text: string, - rule: InlineMDRule -) => string | undefined; -export type InlineRulesRunner = ( - parse: InlineMDParser, - text: string, - rules: InlineMDRule[] -) => string | undefined; - -const MIN_ANY = '(.+?)'; -const URL_NEG_LB = '(? text.match(BOLD_REG_1), - html: (parse, match) => { - const [, , g2] = match; - return `${parse(g2)}`; - }, -}; - -const ITALIC_MD_1 = '*'; -const ITALIC_PREFIX_1 = '\\*'; -const ITALIC_NEG_LA_1 = '(?!\\*)'; -const ITALIC_REG_1 = new RegExp( - `${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}` -); -const ItalicRule1: InlineMDRule = { - match: (text) => text.match(ITALIC_REG_1), - html: (parse, match) => { - const [, , g2] = match; - return `${parse(g2)}`; - }, -}; - -const ITALIC_MD_2 = '_'; -const ITALIC_PREFIX_2 = '_'; -const ITALIC_NEG_LA_2 = '(?!_)'; -const ITALIC_REG_2 = new RegExp( - `${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}` -); -const ItalicRule2: InlineMDRule = { - match: (text) => text.match(ITALIC_REG_2), - html: (parse, match) => { - const [, , g2] = match; - return `${parse(g2)}`; - }, -}; - -const UNDERLINE_MD_1 = '__'; -const UNDERLINE_PREFIX_1 = '_{2}'; -const UNDERLINE_NEG_LA_1 = '(?!_)'; -const UNDERLINE_REG_1 = new RegExp( - `${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` -); -const UnderlineRule: InlineMDRule = { - match: (text) => text.match(UNDERLINE_REG_1), - html: (parse, match) => { - const [, , g2] = match; - return `${parse(g2)}`; - }, -}; - -const STRIKE_MD_1 = '~~'; -const STRIKE_PREFIX_1 = '~{2}'; -const STRIKE_NEG_LA_1 = '(?!~)'; -const STRIKE_REG_1 = new RegExp( - `${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}` -); -const StrikeRule: InlineMDRule = { - match: (text) => text.match(STRIKE_REG_1), - html: (parse, match) => { - const [, , g2] = match; - return `${parse(g2)}`; - }, -}; - -const CODE_MD_1 = '`'; -const CODE_PREFIX_1 = '`'; -const CODE_NEG_LA_1 = '(?!`)'; -const CODE_REG_1 = new RegExp(`${URL_NEG_LB}${CODE_PREFIX_1}(.+?)${CODE_PREFIX_1}${CODE_NEG_LA_1}`); -const CodeRule: InlineMDRule = { - match: (text) => text.match(CODE_REG_1), - html: (parse, match) => { - const [, , g2] = match; - return `${g2}`; - }, -}; - -const SPOILER_MD_1 = '||'; -const SPOILER_PREFIX_1 = '\\|{2}'; -const SPOILER_NEG_LA_1 = '(?!\\|)'; -const SPOILER_REG_1 = new RegExp( - `${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}` -); -const SpoilerRule: InlineMDRule = { - match: (text) => text.match(SPOILER_REG_1), - html: (parse, match) => { - const [, , g2] = match; - return `${parse(g2)}`; - }, -}; - -const LINK_ALT = `\\[${MIN_ANY}\\]`; -const LINK_URL = `\\((https?:\\/\\/.+?)\\)`; -const LINK_REG_1 = new RegExp(`${LINK_ALT}${LINK_URL}`); -const LinkRule: InlineMDRule = { - match: (text) => text.match(LINK_REG_1), - html: (parse, match) => { - const [, g1, g2] = match; - return `${parse(g1)}`; - }, -}; - -const runInlineRule: InlineRuleRunner = (parse, text, rule) => { - const matchResult = rule.match(text); - if (matchResult) { - const content = rule.html(parse, matchResult); - return replaceMatch((txt) => [parse(txt)], text, matchResult, content).join(''); - } - return undefined; -}; - -/** - * Runs multiple rules at the same time to better handle nested rules. - * Rules will be run in the order they appear. - */ -const runInlineRules: InlineRulesRunner = (parse, text, rules) => { - const matchResults = rules.map((rule) => rule.match(text)); - - let targetRule: InlineMDRule | undefined; - let targetResult: MatchResult | undefined; - - for (let i = 0; i < matchResults.length; i += 1) { - const currentResult = matchResults[i]; - if (currentResult && typeof currentResult.index === 'number') { - if ( - !targetResult || - (typeof targetResult?.index === 'number' && currentResult.index < targetResult.index) - ) { - targetResult = currentResult; - targetRule = rules[i]; - } - } - } - - if (targetRule && targetResult) { - const content = targetRule.html(parse, targetResult); - return replaceMatch((txt) => [parse(txt)], text, targetResult, content).join(''); - } - return undefined; -}; - -const LeveledRules = [ - BoldRule, - ItalicRule1, - UnderlineRule, - ItalicRule2, - StrikeRule, - SpoilerRule, - LinkRule, -]; - -export const parseInlineMD: InlineMDParser = (text) => { - if (text === '') return text; - let result: string | undefined; - if (!result) result = runInlineRule(parseInlineMD, text, CodeRule); - - if (!result) result = runInlineRules(parseInlineMD, text, LeveledRules); - - return result ?? text; -}; - -/* - **************** - * BLOCK PARSER * - **************** - */ - -export type BlockMDParser = (test: string, parseInline?: (txt: string) => string) => string; - -export type BlockMatchConverter = ( - match: MatchResult, - parseInline?: (txt: string) => string -) => string; - -export type BlockMDRule = { - match: RuleMatch; - html: BlockMatchConverter; -}; - -export type BlockRuleRunner = ( - parse: BlockMDParser, - text: string, - rule: BlockMDRule, - parseInline?: (txt: string) => string -) => string | undefined; - -const HEADING_REG_1 = /^(#{1,6}) +(.+)\n?/m; -const HeadingRule: BlockMDRule = { - match: (text) => text.match(HEADING_REG_1), - html: (match, parseInline) => { - const [, g1, g2] = match; - const level = g1.length; - return `${parseInline ? parseInline(g2) : g2}`; - }, -}; - -const CODEBLOCK_MD_1 = '```'; -const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((.+\n)+)`{3} *(?!.)\n?/m; -const CodeBlockRule: BlockMDRule = { - match: (text) => text.match(CODEBLOCK_REG_1), - html: (match) => { - const [, g1, g2] = match; - const classNameAtt = g1 ? ` class="language-${g1}"` : ''; - return `
    ${g2}
    `; - }, -}; - -const BLOCKQUOTE_MD_1 = '>'; -const QUOTE_LINE_PREFIX = /^> */; -const BLOCKQUOTE_TRAILING_NEWLINE = /\n$/; -const BLOCKQUOTE_REG_1 = /(^>.*\n?)+/m; -const BlockQuoteRule: BlockMDRule = { - match: (text) => text.match(BLOCKQUOTE_REG_1), - html: (match, parseInline) => { - const [blockquoteText] = match; - - const lines = blockquoteText - .replace(BLOCKQUOTE_TRAILING_NEWLINE, '') - .split('\n') - .map((lineText) => { - const line = lineText.replace(QUOTE_LINE_PREFIX, ''); - if (parseInline) return `${parseInline(line)}
    `; - return `${line}
    `; - }) - .join(''); - return `
    ${lines}
    `; - }, -}; - -const ORDERED_LIST_MD_1 = '-'; -const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */; -const O_LIST_START = /^([\d])\./; -const O_LIST_TYPE = /^([aAiI])\./; -const O_LIST_TRAILING_NEWLINE = /\n$/; -const ORDERED_LIST_REG_1 = /(^(-|[\da-zA-Z]\.) +.+\n?)+/m; -const OrderedListRule: BlockMDRule = { - match: (text) => text.match(ORDERED_LIST_REG_1), - html: (match, parseInline) => { - const [listText] = match; - const [, listStart] = listText.match(O_LIST_START) ?? []; - const [, listType] = listText.match(O_LIST_TYPE) ?? []; - - const lines = listText - .replace(O_LIST_TRAILING_NEWLINE, '') - .split('\n') - .map((lineText) => { - const line = lineText.replace(O_LIST_ITEM_PREFIX, ''); - const txt = parseInline ? parseInline(line) : line; - return `
  • ${txt}

  • `; - }) - .join(''); - - const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`; - const startAtt = listStart ? ` start="${listStart}"` : ''; - const typeAtt = listType ? ` type="${listType}"` : ''; - return `
      ${lines}
    `; - }, -}; - -const UNORDERED_LIST_MD_1 = '*'; -const U_LIST_ITEM_PREFIX = /^\* */; -const U_LIST_TRAILING_NEWLINE = /\n$/; -const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m; -const UnorderedListRule: BlockMDRule = { - match: (text) => text.match(UNORDERED_LIST_REG_1), - html: (match, parseInline) => { - const [listText] = match; - - const lines = listText - .replace(U_LIST_TRAILING_NEWLINE, '') - .split('\n') - .map((lineText) => { - const line = lineText.replace(U_LIST_ITEM_PREFIX, ''); - const txt = parseInline ? parseInline(line) : line; - return `
  • ${txt}

  • `; - }) - .join(''); - - return `
      ${lines}
    `; - }, -}; - -const runBlockRule: BlockRuleRunner = (parse, text, rule, parseInline) => { - const matchResult = rule.match(text); - if (matchResult) { - const content = rule.html(matchResult, parseInline); - return replaceMatch((txt) => [parse(txt, parseInline)], text, matchResult, content).join(''); - } - return undefined; -}; - -export const parseBlockMD: BlockMDParser = (text, parseInline) => { - if (text === '') return text; - let result: string | undefined; - - if (!result) result = runBlockRule(parseBlockMD, text, CodeBlockRule, parseInline); - if (!result) result = runBlockRule(parseBlockMD, text, BlockQuoteRule, parseInline); - if (!result) result = runBlockRule(parseBlockMD, text, OrderedListRule, parseInline); - if (!result) result = runBlockRule(parseBlockMD, text, UnorderedListRule, parseInline); - if (!result) result = runBlockRule(parseBlockMD, text, HeadingRule, parseInline); - - // replace \n with
    because want to preserve empty lines - if (!result) { - if (parseInline) { - result = text - .split('\n') - .map((lineText) => parseInline(lineText)) - .join('
    '); - } else { - result = text.replace(/\n/g, '
    '); - } - } - - return result ?? text; -};