From 3b2ff786195c1364b51ea6f4c1ee84846e7708ae Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:30:15 +0530 Subject: [PATCH 1/9] add inline markdown in editor --- .vscode/settings.json | 2 +- src/app/components/editor/output.ts | 19 ++-- src/app/organisms/room/RoomInput.tsx | 5 +- src/app/organisms/settings/Settings.jsx | 11 +-- src/app/utils/markdown.ts | 115 ++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 src/app/utils/markdown.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 8272ea1e42..88a83d6e95 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "editor.formatOnSave": true, + "editor.formatOnSave": false, "editor.defaultFormatter": "esbenp.prettier-vscode", "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index 5d0443fa84..cec49aaff5 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -2,8 +2,9 @@ import { Descendant, Text } from 'slate'; import { sanitizeText } from '../../utils/sanitize'; import { BlockType } from './Elements'; import { CustomElement, FormattedText } from './slate'; +import { parseInlineMD } from '../../utils/markdown'; -const textToCustomHtml = (node: FormattedText): string => { +const textToCustomHtml = (node: FormattedText, allowMarkdown?: boolean): string => { let string = sanitizeText(node.text); if (node.bold) string = `${string}`; if (node.italic) string = `${string}`; @@ -11,6 +12,11 @@ const textToCustomHtml = (node: FormattedText): string => { if (node.strikeThrough) string = `${string}`; if (node.code) string = `${string}`; if (node.spoiler) string = `${string}`; + + if (allowMarkdown && string === sanitizeText(node.text)) { + string = parseInlineMD(string); + } + return string; }; @@ -47,11 +53,14 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => { } }; -export const toMatrixCustomHTML = (node: Descendant | Descendant[]): string => { - if (Array.isArray(node)) return node.map((n) => toMatrixCustomHTML(n)).join(''); - if (Text.isText(node)) return textToCustomHtml(node); +export const toMatrixCustomHTML = ( + node: Descendant | Descendant[], + allowMarkdown?: boolean +): string => { + if (Array.isArray(node)) return node.map((n) => toMatrixCustomHTML(n, allowMarkdown)).join(''); + if (Text.isText(node)) return textToCustomHtml(node, allowMarkdown); - const children = node.children.map((n) => toMatrixCustomHTML(n)).join(''); + const children = node.children.map((n) => toMatrixCustomHTML(n, allowMarkdown)).join(''); return elementToCustomHtml(node, children); }; diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index efef03a270..06ff8ce51c 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -108,6 +108,7 @@ export const RoomInput = forwardRef( ({ editor, roomViewRef, roomId }, ref) => { const mx = useMatrixClient(); const room = mx.getRoom(roomId); + const [isMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId)); @@ -251,7 +252,7 @@ export const RoomInput = forwardRef( uploadBoardHandlers.current?.handleSend(); const plainText = toPlainText(editor.children).trim(); - const customHtml = trimCustomHtml(toMatrixCustomHTML(editor.children)); + const customHtml = trimCustomHtml(toMatrixCustomHTML(editor.children, isMarkdown)); if (plainText === '') return; @@ -288,7 +289,7 @@ export const RoomInput = forwardRef( resetEditorHistory(editor); setReplyDraft(); sendTypingStatus(false); - }, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft]); + }, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown]); const handleKeyDown: KeyboardEventHandler = useCallback( (evt) => { diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index fef158675d..bd9ce0441c 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -6,7 +6,7 @@ import cons from '../../../client/state/cons'; import settings from '../../../client/state/settings'; import navigation from '../../../client/state/navigation'; import { - toggleSystemTheme, toggleMarkdown, + toggleSystemTheme, toggleNotifications, toggleNotificationSounds, } from '../../../client/action/settings'; import { usePermission } from '../../hooks/usePermission'; @@ -52,6 +52,7 @@ function AppearanceSection() { const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing'); const [useSystemEmoji, setUseSystemEmoji] = useSetting(settingsAtom, 'useSystemEmoji'); + const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [hideMembershipEvents, setHideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents'); const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents'); const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); @@ -138,14 +139,14 @@ function AppearanceSection() { } /> { toggleMarkdown(); updateState({}); }} + isActive={isMarkdown} + onToggle={() => setIsMarkdown(!isMarkdown) } /> )} - content={Format messages with markdown syntax before sending.} + content={Format messages with inline markdown syntax before sending.} /> + text.slice(0, match.index); +const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => + text.slice((match.index ?? 0) + match[0].length); + +export const parseInlineMD = (text: string): string => { + const linkMatch = text.match(LINK_REG_1); + if (linkMatch) { + const [, g1, g2] = linkMatch; + const before = parseInlineMD(beforeMatch(text, linkMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, linkMatch)); + + return `${before}${child}${after}`; + } + + const boldMatch = text.match(BOLD_REG_1); + if (boldMatch) { + const [, g1] = boldMatch; + const before = parseInlineMD(beforeMatch(text, boldMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, boldMatch)); + + return `${before}${child}${after}`; + } + + const underlineMatch = text.match(UNDERLINE_REG_1); + if (underlineMatch) { + const [, g1] = underlineMatch; + const before = parseInlineMD(beforeMatch(text, underlineMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, underlineMatch)); + + return `${before}${child}${after}`; + } + + const italicMatch = text.match(ITALIC_REG_1) ?? text.match(ITALIC_REG_2); + if (italicMatch) { + const [, g1] = italicMatch; + const before = parseInlineMD(beforeMatch(text, italicMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, italicMatch)); + + return `${before}${child}${after}`; + } + + const strikeMatch = text.match(STRIKE_REG_1); + if (strikeMatch) { + const [, g1] = strikeMatch; + const before = parseInlineMD(beforeMatch(text, strikeMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, strikeMatch)); + + return `${before}${child}${after}`; + } + + const codeMatch = text.match(CODE_REG_1); + if (codeMatch) { + const [, g1] = codeMatch; + const before = parseInlineMD(beforeMatch(text, codeMatch)); + const child = g1; + const after = parseInlineMD(afterMatch(text, codeMatch)); + + return `${before}${child}${after}`; + } + + const spoilerMatch = text.match(SPOILER_REG_1); + if (spoilerMatch) { + const [, g1] = spoilerMatch; + const before = parseInlineMD(beforeMatch(text, spoilerMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, spoilerMatch)); + + return `${before}${child}${after}`; + } + + return text; +}; From c64d0526d266a7a1105adcddcb91af5a90e6ad59 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:44:40 +0530 Subject: [PATCH 2/9] send markdown re-generative data in tags --- src/app/utils/markdown.ts | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts index 0036725f49..77567d472b 100644 --- a/src/app/utils/markdown.ts +++ b/src/app/utils/markdown.ts @@ -1,30 +1,37 @@ const MIN_ANY = '(.+?)'; +const BOLD_MD_1 = '**' const BOLD_PREFIX_1 = '\\*{2}'; const BOLD_NEG_LA_1 = '(?!\\*)'; const BOLD_REG_1 = new RegExp(`${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`); +const ITALIC_MD_1 = '*' const ITALIC_PREFIX_1 = '\\*'; const ITALIC_NEG_LA_1 = '(?!\\*)'; const ITALIC_REG_1 = new RegExp(`${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`); +const ITALIC_MD_2 = '_' const ITALIC_PREFIX_2 = '_'; const ITALIC_NEG_LA_2 = '(?!_)'; const ITALIC_REG_2 = new RegExp(`${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`); +const UNDERLINE_MD_1 = '__'; const UNDERLINE_PREFIX_1 = '_{2}'; const UNDERLINE_NEG_LA_1 = '(?!_)'; const UNDERLINE_REG_1 = new RegExp( `${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` ); +const STRIKE_MD_1 = '~~'; const STRIKE_PREFIX_1 = '~{2}'; const STRIKE_NEG_LA_1 = '(?!~)'; const STRIKE_REG_1 = new RegExp(`${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`); +const CODE_MD_1 = '`'; const CODE_PREFIX_1 = '`'; const CODE_NEG_LA_1 = '(?!`)'; const CODE_REG_1 = new RegExp(`${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}`); +const SPOILER_MD_1 = '||'; const SPOILER_PREFIX_1 = '\\|{2}'; const SPOILER_NEG_LA_1 = '(?!\\|)'; const SPOILER_REG_1 = new RegExp( @@ -41,16 +48,6 @@ const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): st text.slice((match.index ?? 0) + match[0].length); export const parseInlineMD = (text: string): string => { - const linkMatch = text.match(LINK_REG_1); - if (linkMatch) { - const [, g1, g2] = linkMatch; - const before = parseInlineMD(beforeMatch(text, linkMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, linkMatch)); - - return `${before}${child}${after}`; - } - const boldMatch = text.match(BOLD_REG_1); if (boldMatch) { const [, g1] = boldMatch; @@ -58,7 +55,7 @@ export const parseInlineMD = (text: string): string => { const child = parseInlineMD(g1); const after = parseInlineMD(afterMatch(text, boldMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const underlineMatch = text.match(UNDERLINE_REG_1); @@ -68,7 +65,7 @@ export const parseInlineMD = (text: string): string => { const child = parseInlineMD(g1); const after = parseInlineMD(afterMatch(text, underlineMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const italicMatch = text.match(ITALIC_REG_1) ?? text.match(ITALIC_REG_2); @@ -78,7 +75,7 @@ export const parseInlineMD = (text: string): string => { const child = parseInlineMD(g1); const after = parseInlineMD(afterMatch(text, italicMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const strikeMatch = text.match(STRIKE_REG_1); @@ -88,7 +85,7 @@ export const parseInlineMD = (text: string): string => { const child = parseInlineMD(g1); const after = parseInlineMD(afterMatch(text, strikeMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const codeMatch = text.match(CODE_REG_1); @@ -98,7 +95,7 @@ export const parseInlineMD = (text: string): string => { const child = g1; const after = parseInlineMD(afterMatch(text, codeMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const spoilerMatch = text.match(SPOILER_REG_1); @@ -108,8 +105,18 @@ export const parseInlineMD = (text: string): string => { const child = parseInlineMD(g1); const after = parseInlineMD(afterMatch(text, spoilerMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } + const linkMatch = text.match(LINK_REG_1); + if (linkMatch) { + const [, g1, g2] = linkMatch; + const before = parseInlineMD(beforeMatch(text, linkMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, linkMatch)); + + return `${before}${child}${after}`; + } + return text; }; From ccfa4fc78daddb75a405895dd852436878935f20 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:03:30 +0530 Subject: [PATCH 3/9] enable vscode format on save --- .vscode/settings.json | 2 +- src/app/utils/markdown.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 88a83d6e95..8272ea1e42 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "editor.formatOnSave": false, + "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts index 77567d472b..7b12a111d9 100644 --- a/src/app/utils/markdown.ts +++ b/src/app/utils/markdown.ts @@ -1,15 +1,15 @@ const MIN_ANY = '(.+?)'; -const BOLD_MD_1 = '**' +const BOLD_MD_1 = '**'; const BOLD_PREFIX_1 = '\\*{2}'; const BOLD_NEG_LA_1 = '(?!\\*)'; const BOLD_REG_1 = new RegExp(`${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`); -const ITALIC_MD_1 = '*' +const ITALIC_MD_1 = '*'; const ITALIC_PREFIX_1 = '\\*'; const ITALIC_NEG_LA_1 = '(?!\\*)'; const ITALIC_REG_1 = new RegExp(`${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`); -const ITALIC_MD_2 = '_' +const ITALIC_MD_2 = '_'; const ITALIC_PREFIX_2 = '_'; const ITALIC_NEG_LA_2 = '(?!_)'; const ITALIC_REG_2 = new RegExp(`${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`); @@ -75,7 +75,9 @@ export const parseInlineMD = (text: string): string => { const child = parseInlineMD(g1); const after = parseInlineMD(afterMatch(text, italicMatch)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const strikeMatch = text.match(STRIKE_REG_1); @@ -117,6 +119,6 @@ export const parseInlineMD = (text: string): string => { return `${before}${child}${after}`; } - + return text; }; From 4decaeca1b0f7faddd3797cdaa2a46df01ac7c38 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:10:18 +0530 Subject: [PATCH 4/9] fix match italic and diff order --- src/app/utils/markdown.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts index 7b12a111d9..becc0183e9 100644 --- a/src/app/utils/markdown.ts +++ b/src/app/utils/markdown.ts @@ -58,6 +58,16 @@ export const parseInlineMD = (text: string): string => { return `${before}${child}${after}`; } + const italicMatch = text.match(ITALIC_REG_1); + if (italicMatch) { + const [, g1] = italicMatch; + const before = parseInlineMD(beforeMatch(text, italicMatch)); + const child = parseInlineMD(g1); + const after = parseInlineMD(afterMatch(text, italicMatch)); + + return `${before}${child}${after}`; + } + const underlineMatch = text.match(UNDERLINE_REG_1); if (underlineMatch) { const [, g1] = underlineMatch; @@ -68,16 +78,14 @@ export const parseInlineMD = (text: string): string => { return `${before}${child}${after}`; } - const italicMatch = text.match(ITALIC_REG_1) ?? text.match(ITALIC_REG_2); - if (italicMatch) { - const [, g1] = italicMatch; - const before = parseInlineMD(beforeMatch(text, italicMatch)); + const italicMatch2 = text.match(ITALIC_REG_2); + if (italicMatch2) { + const [, g1] = italicMatch2; + const before = parseInlineMD(beforeMatch(text, italicMatch2)); const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, italicMatch)); + const after = parseInlineMD(afterMatch(text, italicMatch2)); - return `${before}${child}${after}`; + return `${before}${child}${after}`; } const strikeMatch = text.match(STRIKE_REG_1); From a32a663841361158e92d2b4304bb43b30ed93e0d Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:35:56 +0530 Subject: [PATCH 5/9] prevent formatting in code block --- src/app/components/editor/output.ts | 34 ++++++++++++++++++---------- src/app/organisms/room/RoomInput.tsx | 7 +++++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index cec49aaff5..e7bc3ab9ae 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -4,16 +4,21 @@ import { BlockType } from './Elements'; import { CustomElement, FormattedText } from './slate'; import { parseInlineMD } from '../../utils/markdown'; -const textToCustomHtml = (node: FormattedText, allowMarkdown?: boolean): string => { +export type OutputOptions = { + allowTextFormatting?: boolean; + allowMarkdown?: boolean; +}; + +const textToCustomHtml = (node: FormattedText, opts: OutputOptions): string => { let string = sanitizeText(node.text); - if (node.bold) string = `${string}`; - if (node.italic) string = `${string}`; - if (node.underline) string = `${string}`; - if (node.strikeThrough) string = `${string}`; - if (node.code) string = `${string}`; - if (node.spoiler) string = `${string}`; + if (opts.allowTextFormatting && node.bold) string = `${string}`; + if (opts.allowTextFormatting && node.italic) string = `${string}`; + if (opts.allowTextFormatting && node.underline) string = `${string}`; + if (opts.allowTextFormatting && node.strikeThrough) string = `${string}`; + if (opts.allowTextFormatting && node.code) string = `${string}`; + if (opts.allowTextFormatting && node.spoiler) string = `${string}`; - if (allowMarkdown && string === sanitizeText(node.text)) { + if (opts.allowMarkdown && string === sanitizeText(node.text)) { string = parseInlineMD(string); } @@ -55,12 +60,17 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => { export const toMatrixCustomHTML = ( node: Descendant | Descendant[], - allowMarkdown?: boolean + opts: OutputOptions ): string => { - if (Array.isArray(node)) return node.map((n) => toMatrixCustomHTML(n, allowMarkdown)).join(''); - if (Text.isText(node)) return textToCustomHtml(node, allowMarkdown); + const parseNode = (n: Descendant) => { + const isCodeLine = 'type' in n && n.type === BlockType.CodeLine; + if (isCodeLine) return toMatrixCustomHTML(n, {}); + return toMatrixCustomHTML(n, opts); + }; + if (Array.isArray(node)) return node.map(parseNode).join(''); + if (Text.isText(node)) return textToCustomHtml(node, opts); - const children = node.children.map((n) => toMatrixCustomHTML(n, allowMarkdown)).join(''); + const children = node.children.map(parseNode).join(''); return elementToCustomHtml(node, children); }; diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index 06ff8ce51c..7564d5f440 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -252,7 +252,12 @@ export const RoomInput = forwardRef( uploadBoardHandlers.current?.handleSend(); const plainText = toPlainText(editor.children).trim(); - const customHtml = trimCustomHtml(toMatrixCustomHTML(editor.children, isMarkdown)); + const customHtml = trimCustomHtml( + toMatrixCustomHTML(editor.children, { + allowTextFormatting: true, + allowMarkdown: isMarkdown, + }) + ); if (plainText === '') return; From 83002214742ff272aa683d341e35c677f735d7c0 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:51:21 +0530 Subject: [PATCH 6/9] make code md rule highest --- src/app/utils/markdown.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts index becc0183e9..e386bf7dd5 100644 --- a/src/app/utils/markdown.ts +++ b/src/app/utils/markdown.ts @@ -48,6 +48,16 @@ const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): st text.slice((match.index ?? 0) + match[0].length); export const parseInlineMD = (text: string): string => { + const codeMatch = text.match(CODE_REG_1); + if (codeMatch) { + const [, g1] = codeMatch; + const before = parseInlineMD(beforeMatch(text, codeMatch)); + const child = g1; + const after = parseInlineMD(afterMatch(text, codeMatch)); + + return `${before}${child}${after}`; + } + const boldMatch = text.match(BOLD_REG_1); if (boldMatch) { const [, g1] = boldMatch; @@ -98,16 +108,6 @@ export const parseInlineMD = (text: string): string => { return `${before}${child}${after}`; } - const codeMatch = text.match(CODE_REG_1); - if (codeMatch) { - const [, g1] = codeMatch; - const before = parseInlineMD(beforeMatch(text, codeMatch)); - const child = g1; - const after = parseInlineMD(afterMatch(text, codeMatch)); - - return `${before}${child}${after}`; - } - const spoilerMatch = text.match(SPOILER_REG_1); if (spoilerMatch) { const [, g1] = spoilerMatch; From 2d001cb05872ca8a46981d9bd1743fa74ac3188e Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:11:26 +0530 Subject: [PATCH 7/9] improve inline markdown parsing --- src/app/utils/markdown.ts | 199 ++++++++++++++++++++++++-------------- 1 file changed, 127 insertions(+), 72 deletions(-) diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts index e386bf7dd5..ea4b968bdf 100644 --- a/src/app/utils/markdown.ts +++ b/src/app/utils/markdown.ts @@ -1,18 +1,65 @@ +export type PlainMDParser = (text: string) => string; +export type MatchResult = RegExpMatchArray | RegExpExecArray; +export type RuleMatch = (text: string) => MatchResult | null; +export type MatchConverter = (parse: PlainMDParser, match: MatchResult) => string; + +export type MDRule = { + match: RuleMatch; + html: MatchConverter; +}; + +export type MatchReplacer = ( + parse: PlainMDParser, + text: string, + match: MatchResult, + content: string +) => string; + +export type RuleRunner = (parse: PlainMDParser, text: string, rule: MDRule) => string | undefined; +export type RulesRunner = ( + parse: PlainMDParser, + text: string, + rules: MDRule[] +) => string | undefined; + const MIN_ANY = '(.+?)'; const BOLD_MD_1 = '**'; const BOLD_PREFIX_1 = '\\*{2}'; const BOLD_NEG_LA_1 = '(?!\\*)'; const BOLD_REG_1 = new RegExp(`${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`); +const BoldRule: MDRule = { + match: (text) => text.match(BOLD_REG_1), + html: (parse, match) => { + const [, g1] = match; + const child = parse(g1); + return `${child}`; + }, +}; const ITALIC_MD_1 = '*'; const ITALIC_PREFIX_1 = '\\*'; const ITALIC_NEG_LA_1 = '(?!\\*)'; const ITALIC_REG_1 = new RegExp(`${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`); +const ItalicRule1: MDRule = { + match: (text) => text.match(ITALIC_REG_1), + html: (parse, match) => { + const [, g1] = match; + return `${parse(g1)}`; + }, +}; + const ITALIC_MD_2 = '_'; const ITALIC_PREFIX_2 = '_'; const ITALIC_NEG_LA_2 = '(?!_)'; const ITALIC_REG_2 = new RegExp(`${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`); +const ItalicRule2: MDRule = { + match: (text) => text.match(ITALIC_REG_2), + html: (parse, match) => { + const [, g1] = match; + return `${parse(g1)}`; + }, +}; const UNDERLINE_MD_1 = '__'; const UNDERLINE_PREFIX_1 = '_{2}'; @@ -20,16 +67,37 @@ const UNDERLINE_NEG_LA_1 = '(?!_)'; const UNDERLINE_REG_1 = new RegExp( `${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` ); +const UnderlineRule: MDRule = { + match: (text) => text.match(UNDERLINE_REG_1), + html: (parse, match) => { + const [, g1] = match; + return `${parse(g1)}`; + }, +}; const STRIKE_MD_1 = '~~'; const STRIKE_PREFIX_1 = '~{2}'; const STRIKE_NEG_LA_1 = '(?!~)'; const STRIKE_REG_1 = new RegExp(`${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`); +const StrikeRule: MDRule = { + match: (text) => text.match(STRIKE_REG_1), + html: (parse, match) => { + const [, g1] = match; + return `${parse(g1)}`; + }, +}; const CODE_MD_1 = '`'; const CODE_PREFIX_1 = '`'; const CODE_NEG_LA_1 = '(?!`)'; const CODE_REG_1 = new RegExp(`${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}`); +const CodeRule: MDRule = { + match: (text) => text.match(CODE_REG_1), + html: (parse, match) => { + const [, g1] = match; + return `${g1}`; + }, +}; const SPOILER_MD_1 = '||'; const SPOILER_PREFIX_1 = '\\|{2}'; @@ -37,96 +105,83 @@ const SPOILER_NEG_LA_1 = '(?!\\|)'; const SPOILER_REG_1 = new RegExp( `${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}` ); +const SpoilerRule: MDRule = { + match: (text) => text.match(SPOILER_REG_1), + html: (parse, match) => { + const [, g1] = match; + return `${parse(g1)}`; + }, +}; const LINK_ALT = `\\[${MIN_ANY}\\]`; const LINK_URL = `\\((https?:\\/\\/.+?)\\)`; const LINK_REG_1 = new RegExp(`${LINK_ALT}${LINK_URL}`); +const LinkRule: MDRule = { + match: (text) => text.match(LINK_REG_1), + html: (parse, match) => { + const [, g1, g2] = match; + return `${parse(g1)}`; + }, +}; const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => text.slice(0, match.index); const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string => text.slice((match.index ?? 0) + match[0].length); -export const parseInlineMD = (text: string): string => { - const codeMatch = text.match(CODE_REG_1); - if (codeMatch) { - const [, g1] = codeMatch; - const before = parseInlineMD(beforeMatch(text, codeMatch)); - const child = g1; - const after = parseInlineMD(afterMatch(text, codeMatch)); - - return `${before}${child}${after}`; - } - - const boldMatch = text.match(BOLD_REG_1); - if (boldMatch) { - const [, g1] = boldMatch; - const before = parseInlineMD(beforeMatch(text, boldMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, boldMatch)); - - return `${before}${child}${after}`; - } - - const italicMatch = text.match(ITALIC_REG_1); - if (italicMatch) { - const [, g1] = italicMatch; - const before = parseInlineMD(beforeMatch(text, italicMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, italicMatch)); +const replaceMatch: MatchReplacer = (parse, text, match, content) => + `${parse(beforeMatch(text, match))}${content}${parse(afterMatch(text, match))}`; - return `${before}${child}${after}`; +const runRule: RuleRunner = (parse, text, rule) => { + const matchResult = rule.match(text); + if (matchResult) { + const content = rule.html(parse, matchResult); + return replaceMatch(parse, text, matchResult, content); } + return undefined; +}; - const underlineMatch = text.match(UNDERLINE_REG_1); - if (underlineMatch) { - const [, g1] = underlineMatch; - const before = parseInlineMD(beforeMatch(text, underlineMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, underlineMatch)); - - return `${before}${child}${after}`; - } - - const italicMatch2 = text.match(ITALIC_REG_2); - if (italicMatch2) { - const [, g1] = italicMatch2; - const before = parseInlineMD(beforeMatch(text, italicMatch2)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, italicMatch2)); - - return `${before}${child}${after}`; +const runRules: RulesRunner = (parse, text, rules) => { + const matchResults = rules.map((rule) => rule.match(text)); + + let targetRule: MDRule | 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]; + } + } } - const strikeMatch = text.match(STRIKE_REG_1); - if (strikeMatch) { - const [, g1] = strikeMatch; - const before = parseInlineMD(beforeMatch(text, strikeMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, strikeMatch)); - - return `${before}${child}${after}`; + if (targetRule && targetResult) { + const content = targetRule.html(parse, targetResult); + return replaceMatch(parse, text, targetResult, content); } + return undefined; +}; - const spoilerMatch = text.match(SPOILER_REG_1); - if (spoilerMatch) { - const [, g1] = spoilerMatch; - const before = parseInlineMD(beforeMatch(text, spoilerMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, spoilerMatch)); +const LeveledRules = [ + BoldRule, + ItalicRule1, + UnderlineRule, + ItalicRule2, + StrikeRule, + SpoilerRule, + LinkRule, +]; - return `${before}${child}${after}`; - } - - const linkMatch = text.match(LINK_REG_1); - if (linkMatch) { - const [, g1, g2] = linkMatch; - const before = parseInlineMD(beforeMatch(text, linkMatch)); - const child = parseInlineMD(g1); - const after = parseInlineMD(afterMatch(text, linkMatch)); +export const parseInlineMD = (text: string): string => { + let result: string | undefined; + if (!result) result = runRule(parseInlineMD, text, CodeRule); - return `${before}${child}${after}`; - } + if (!result) result = runRules(parseInlineMD, text, LeveledRules); - return text; + return result ?? text; }; From bc5a9f5c0a4f567f8f366bbaff6aed603afde0c5 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:18:12 +0530 Subject: [PATCH 8/9] add comment --- src/app/utils/markdown.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/utils/markdown.ts b/src/app/utils/markdown.ts index ea4b968bdf..e4294d7d6c 100644 --- a/src/app/utils/markdown.ts +++ b/src/app/utils/markdown.ts @@ -141,6 +141,10 @@ const runRule: RuleRunner = (parse, text, rule) => { return undefined; }; +/** + * Runs multiple rules at the same time to better handle nested rules. + * Rules will be run in the order they appear. + */ const runRules: RulesRunner = (parse, text, rules) => { const matchResults = rules.map((rule) => rule.match(text)); From bcb8d6aea05e0c556530227d3f41616ab2f737ff Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:42:12 +0530 Subject: [PATCH 9/9] improve code logic --- src/app/components/editor/output.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index e7bc3ab9ae..92c86dd86b 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -11,12 +11,14 @@ export type OutputOptions = { const textToCustomHtml = (node: FormattedText, opts: OutputOptions): string => { let string = sanitizeText(node.text); - if (opts.allowTextFormatting && node.bold) string = `${string}`; - if (opts.allowTextFormatting && node.italic) string = `${string}`; - if (opts.allowTextFormatting && node.underline) string = `${string}`; - if (opts.allowTextFormatting && node.strikeThrough) string = `${string}`; - if (opts.allowTextFormatting && node.code) string = `${string}`; - if (opts.allowTextFormatting && node.spoiler) string = `${string}`; + if (opts.allowTextFormatting) { + if (node.bold) string = `${string}`; + if (node.italic) string = `${string}`; + if (node.underline) string = `${string}`; + if (node.strikeThrough) string = `${string}`; + if (node.code) string = `${string}`; + if (node.spoiler) string = `${string}`; + } if (opts.allowMarkdown && string === sanitizeText(node.text)) { string = parseInlineMD(string);