From a283fbbdefce2b0602e4822876b53889fd53e06a Mon Sep 17 00:00:00 2001 From: "Issa Nimaga (via MelvinBot)" Date: Wed, 6 May 2026 11:22:47 +0000 Subject: [PATCH 1/5] Strip incomplete markdown from Concierge streaming drafts During Concierge streaming, partial markdown (e.g. unclosed bold, links, code blocks) was being rendered as raw syntax by ExpensiMark. Add stripIncompleteMarkdown() to sanitize the tail of bodyMarkdown before parsing, removing unclosed constructs so they don't flash briefly in the UI. Co-authored-by: Issa Nimaga --- src/pages/inbox/conciergeDraftState.ts | 78 ++++++++++++++++- .../pages/inbox/conciergeDraftState.test.ts | 83 ++++++++++++++++++- 2 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/conciergeDraftState.ts b/src/pages/inbox/conciergeDraftState.ts index 70c07518f8ec..79bddac2728e 100644 --- a/src/pages/inbox/conciergeDraftState.ts +++ b/src/pages/inbox/conciergeDraftState.ts @@ -20,8 +20,82 @@ type BuildConciergeDraftReportActionParams = { reportID: string; }; +/** + * Count non-overlapping occurrences of `needle` in `haystack`. + */ +function countOccurrences(haystack: string, needle: string): number { + let count = 0; + let pos = 0; + while (true) { + const idx = haystack.indexOf(needle, pos); + if (idx === -1) { + break; + } + count++; + pos = idx + needle.length; + } + return count; +} + +/** + * If the last line of `text` contains an odd number of `delimiter` occurrences, + * the final one opened a construct that was never closed. Strip from that + * opening delimiter to the end of the string. + */ +function stripUnpairedLastLineDelimiter(text: string, delimiter: string): string { + const lastNewline = text.lastIndexOf('\n'); + const lastLine = text.substring(lastNewline + 1); + const count = countOccurrences(lastLine, delimiter); + + if (count > 0 && count % 2 !== 0) { + return text.substring(0, text.lastIndexOf(delimiter)); + } + return text; +} + +/** + * Strips incomplete markdown constructs from the tail of a streaming markdown + * string so that ExpensiMark doesn't render raw syntax for half-finished + * links, bold, strikethrough, or code blocks. + */ +function stripIncompleteMarkdown(markdown: string): string { + if (!markdown) { + return markdown; + } + + let result = markdown; + + // 1. Incomplete link/image: find the last '[' and check whether a + // complete [text](url) follows it. If not, strip from '[' (or '!['). + const lastOpenBracket = result.lastIndexOf('['); + if (lastOpenBracket !== -1) { + const tail = result.substring(lastOpenBracket); + if (!/^\[[^\]]*\]\([^)]*\)/.test(tail)) { + const stripFrom = lastOpenBracket > 0 && result[lastOpenBracket - 1] === '!' ? lastOpenBracket - 1 : lastOpenBracket; + result = result.substring(0, stripFrom); + } + } + + // 2. Unclosed bold (**) on the last line. + result = stripUnpairedLastLineDelimiter(result, '**'); + + // 3. Unclosed strikethrough (~~) on the last line. + result = stripUnpairedLastLineDelimiter(result, '~~'); + + // 4. Unclosed code block (``` spans multiple lines). + const codeBlockCount = countOccurrences(result, '```'); + if (codeBlockCount % 2 !== 0) { + result = result.substring(0, result.lastIndexOf('```')); + } + + // 5. Unclosed inline code (`) on the last line (after code-block handling). + result = stripUnpairedLastLineDelimiter(result, '`'); + + return result; +} + function buildConciergeDraftReportAction({bodyMarkdown, created, finalRenderedHTML, reportActionID, reportID}: BuildConciergeDraftReportActionParams): ReportAction | null { - const html = finalRenderedHTML ?? (bodyMarkdown ? getParsedComment(bodyMarkdown, {reportID}) : ''); + const html = finalRenderedHTML ?? (bodyMarkdown ? getParsedComment(stripIncompleteMarkdown(bodyMarkdown), {reportID}) : ''); if (!html) { return null; @@ -101,5 +175,5 @@ function applyConciergeDraftEvent(currentDraft: ConciergeDraft | null, event: Co }; } -export {applyConciergeDraftEvent, buildConciergeDraftReportAction, getCachedDraft, setCachedDraft}; +export {applyConciergeDraftEvent, buildConciergeDraftReportAction, getCachedDraft, setCachedDraft, stripIncompleteMarkdown}; export type {ConciergeDraft}; diff --git a/tests/unit/pages/inbox/conciergeDraftState.test.ts b/tests/unit/pages/inbox/conciergeDraftState.test.ts index 3f3a5aeb0c46..0d76d9896fcb 100644 --- a/tests/unit/pages/inbox/conciergeDraftState.test.ts +++ b/tests/unit/pages/inbox/conciergeDraftState.test.ts @@ -1,4 +1,4 @@ -import {applyConciergeDraftEvent, getCachedDraft, setCachedDraft} from '@pages/inbox/conciergeDraftState'; +import {applyConciergeDraftEvent, getCachedDraft, setCachedDraft, stripIncompleteMarkdown} from '@pages/inbox/conciergeDraftState'; import CONST from '@src/CONST'; const REPORT_ID = '123'; @@ -132,6 +132,87 @@ describe('conciergeDraftState', () => { expect(otherReportDraft).toBe(initialDraft); }); + describe('stripIncompleteMarkdown', () => { + it('returns empty/falsy values unchanged', () => { + expect(stripIncompleteMarkdown('')).toBe(''); + }); + + it('does not alter complete markdown', () => { + const complete = 'Hello **bold** and [link](https://example.com) and `code`'; + expect(stripIncompleteMarkdown(complete)).toBe(complete); + }); + + // --- Links / Images --- + it('strips an incomplete link with only opening bracket', () => { + expect(stripIncompleteMarkdown('Check out [')).toBe('Check out '); + }); + + it('strips an incomplete link with text but no closing bracket', () => { + expect(stripIncompleteMarkdown('Check out [this page')).toBe('Check out '); + }); + + it('strips an incomplete link with bracket closed but no URL', () => { + expect(stripIncompleteMarkdown('Check out [link](')).toBe('Check out '); + }); + + it('strips an incomplete link with partial URL', () => { + expect(stripIncompleteMarkdown('Check out [link](https://example')).toBe('Check out '); + }); + + it('preserves a complete link followed by an incomplete one', () => { + expect(stripIncompleteMarkdown('[done](https://a.com) and [broken')).toBe('[done](https://a.com) and '); + }); + + it('strips an incomplete image syntax', () => { + expect(stripIncompleteMarkdown('Here is ![alt')).toBe('Here is '); + }); + + // --- Bold (**) --- + it('strips trailing unclosed bold', () => { + expect(stripIncompleteMarkdown('Hello **world')).toBe('Hello '); + }); + + it('strips bare trailing ** delimiter', () => { + expect(stripIncompleteMarkdown('Hello **')).toBe('Hello '); + }); + + it('preserves complete bold and strips only the unclosed one', () => { + expect(stripIncompleteMarkdown('**done** and **broken')).toBe('**done** and '); + }); + + // --- Strikethrough (~~) --- + it('strips trailing unclosed strikethrough', () => { + expect(stripIncompleteMarkdown('Hello ~~strike')).toBe('Hello '); + }); + + // --- Code blocks (```) --- + it('strips an unclosed code block', () => { + expect(stripIncompleteMarkdown('Here:\n```\ncode')).toBe('Here:\n'); + }); + + it('preserves a complete code block', () => { + const complete = 'Before\n```\ncode\n```\nAfter'; + expect(stripIncompleteMarkdown(complete)).toBe(complete); + }); + + // --- Inline code (`) --- + it('strips trailing unclosed inline code', () => { + expect(stripIncompleteMarkdown('Run `command')).toBe('Run '); + }); + + it('preserves complete inline code', () => { + const complete = 'Run `command` now'; + expect(stripIncompleteMarkdown(complete)).toBe(complete); + }); + + // --- Streaming integration --- + it('strips incomplete markdown during a streaming draft event', () => { + const draft = applyConciergeDraftEvent(null, createDraftEvent({bodyMarkdown: 'Check [this link'}), REPORT_ID); + // The raw '[this link' syntax should NOT appear in the rendered HTML + expect(getFirstMessageHTML(draft)).not.toContain('[this link'); + }); + }); + describe('draftCache', () => { // Always start clean so tests don't leak state into each other. beforeEach(() => { From 8a6bfef1d128cb300307b2d1420661bf6d7a9eef Mon Sep 17 00:00:00 2001 From: Issa Nimaga Date: Wed, 6 May 2026 13:52:12 +0100 Subject: [PATCH 2/5] Fix streaming markdown sanitization edge cases --- src/pages/inbox/conciergeDraftState.ts | 146 ++++++++++++------ .../pages/inbox/conciergeDraftState.test.ts | 15 ++ 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/src/pages/inbox/conciergeDraftState.ts b/src/pages/inbox/conciergeDraftState.ts index 79bddac2728e..2687ee62584d 100644 --- a/src/pages/inbox/conciergeDraftState.ts +++ b/src/pages/inbox/conciergeDraftState.ts @@ -20,36 +20,93 @@ type BuildConciergeDraftReportActionParams = { reportID: string; }; -/** - * Count non-overlapping occurrences of `needle` in `haystack`. - */ -function countOccurrences(haystack: string, needle: string): number { - let count = 0; - let pos = 0; - while (true) { - const idx = haystack.indexOf(needle, pos); - if (idx === -1) { +type MarkdownRange = { + start: number; + end: number; +}; + +const CODE_BLOCK_DELIMITER = '```'; +const INLINE_CODE_DELIMITER = '`'; + +function isEscaped(text: string, index: number): boolean { + let slashCount = 0; + let pos = index - 1; + + while (pos >= 0 && text[pos] === '\\') { + slashCount++; + pos--; + } + + return slashCount % 2 !== 0; +} + +function getCodeRanges(text: string): {ranges: MarkdownRange[]; unclosedCodeBlockStart: number | null} { + const ranges: MarkdownRange[] = []; + let unclosedCodeBlockStart: number | null = null; + + for (let pos = 0; pos <= text.length - CODE_BLOCK_DELIMITER.length; pos++) { + if (!text.startsWith(CODE_BLOCK_DELIMITER, pos) || isEscaped(text, pos)) { + continue; + } + + if (unclosedCodeBlockStart === null) { + unclosedCodeBlockStart = pos; + } else { + ranges.push({start: unclosedCodeBlockStart, end: pos + CODE_BLOCK_DELIMITER.length}); + unclosedCodeBlockStart = null; + } + pos += CODE_BLOCK_DELIMITER.length - 1; + } + + let lineStart = 0; + + while (lineStart <= text.length) { + const nextNewline = text.indexOf('\n', lineStart); + const lineEnd = nextNewline === -1 ? text.length : nextNewline; + let openingDelimiterIndex: number | null = null; + + for (let pos = lineStart; pos < lineEnd; pos++) { + const isInCodeRange = ranges.some((range) => pos >= range.start && pos < range.end); + if (text[pos] !== INLINE_CODE_DELIMITER || isEscaped(text, pos) || isInCodeRange) { + continue; + } + + if (openingDelimiterIndex === null) { + openingDelimiterIndex = pos; + } else { + ranges.push({start: openingDelimiterIndex, end: pos + INLINE_CODE_DELIMITER.length}); + openingDelimiterIndex = null; + } + } + + if (nextNewline === -1) { break; } - count++; - pos = idx + needle.length; + lineStart = nextNewline + 1; } - return count; + + return {ranges, unclosedCodeBlockStart}; } -/** - * If the last line of `text` contains an odd number of `delimiter` occurrences, - * the final one opened a construct that was never closed. Strip from that - * opening delimiter to the end of the string. - */ -function stripUnpairedLastLineDelimiter(text: string, delimiter: string): string { +function stripUnpairedLastLineDelimiter(text: string, delimiter: string, ignoredRanges: MarkdownRange[] = []): string { const lastNewline = text.lastIndexOf('\n'); - const lastLine = text.substring(lastNewline + 1); - const count = countOccurrences(lastLine, delimiter); + const lastLineStart = lastNewline + 1; + const delimiterIndexes: number[] = []; + + for (let pos = lastLineStart; pos <= text.length - delimiter.length; pos++) { + const isInIgnoredRange = ignoredRanges.some((range) => pos >= range.start && pos < range.end); + if (!text.startsWith(delimiter, pos) || isEscaped(text, pos) || isInIgnoredRange) { + continue; + } + + delimiterIndexes.push(pos); + pos += delimiter.length - 1; + } - if (count > 0 && count % 2 !== 0) { - return text.substring(0, text.lastIndexOf(delimiter)); + if (delimiterIndexes.length > 0 && delimiterIndexes.length % 2 !== 0) { + return text.substring(0, delimiterIndexes.at(-1)); } + return text; } @@ -63,33 +120,36 @@ function stripIncompleteMarkdown(markdown: string): string { return markdown; } - let result = markdown; + const initialCodeState = getCodeRanges(markdown); + let codeRanges = initialCodeState.ranges; + let result = initialCodeState.unclosedCodeBlockStart === null ? markdown : markdown.substring(0, initialCodeState.unclosedCodeBlockStart); - // 1. Incomplete link/image: find the last '[' and check whether a - // complete [text](url) follows it. If not, strip from '[' (or '!['). - const lastOpenBracket = result.lastIndexOf('['); - if (lastOpenBracket !== -1) { - const tail = result.substring(lastOpenBracket); - if (!/^\[[^\]]*\]\([^)]*\)/.test(tail)) { - const stripFrom = lastOpenBracket > 0 && result[lastOpenBracket - 1] === '!' ? lastOpenBracket - 1 : lastOpenBracket; - result = result.substring(0, stripFrom); - } - } + // Strip incomplete inline code before looking for other markdown so code + // contents don't look like unfinished links or emphasis. + codeRanges = codeRanges.filter((range) => range.end <= result.length); + result = stripUnpairedLastLineDelimiter(result, INLINE_CODE_DELIMITER, codeRanges); - // 2. Unclosed bold (**) on the last line. - result = stripUnpairedLastLineDelimiter(result, '**'); + codeRanges = getCodeRanges(result).ranges; + for (let openBracketIndex = result.length - 1; openBracketIndex >= 0; openBracketIndex--) { + const isInCodeRange = codeRanges.some((range) => openBracketIndex >= range.start && openBracketIndex < range.end); + if (result[openBracketIndex] !== '[' || isEscaped(result, openBracketIndex) || isInCodeRange) { + continue; + } - // 3. Unclosed strikethrough (~~) on the last line. - result = stripUnpairedLastLineDelimiter(result, '~~'); + const closeBracketIndex = result.indexOf(']', openBracketIndex + 1); + const stripFrom = openBracketIndex > 0 && result[openBracketIndex - 1] === '!' && !isEscaped(result, openBracketIndex - 1) ? openBracketIndex - 1 : openBracketIndex; - // 4. Unclosed code block (``` spans multiple lines). - const codeBlockCount = countOccurrences(result, '```'); - if (codeBlockCount % 2 !== 0) { - result = result.substring(0, result.lastIndexOf('```')); + if (closeBracketIndex === -1) { + result = result.substring(0, stripFrom); + } else if (result[closeBracketIndex + 1] === '(' && result.indexOf(')', closeBracketIndex + 2) === -1) { + result = result.substring(0, stripFrom); + } + break; } - // 5. Unclosed inline code (`) on the last line (after code-block handling). - result = stripUnpairedLastLineDelimiter(result, '`'); + codeRanges = getCodeRanges(result).ranges; + result = stripUnpairedLastLineDelimiter(result, '**', codeRanges); + result = stripUnpairedLastLineDelimiter(result, '~~', codeRanges); return result; } diff --git a/tests/unit/pages/inbox/conciergeDraftState.test.ts b/tests/unit/pages/inbox/conciergeDraftState.test.ts index 0d76d9896fcb..43491dcf97b6 100644 --- a/tests/unit/pages/inbox/conciergeDraftState.test.ts +++ b/tests/unit/pages/inbox/conciergeDraftState.test.ts @@ -163,6 +163,11 @@ describe('conciergeDraftState', () => { expect(stripIncompleteMarkdown('[done](https://a.com) and [broken')).toBe('[done](https://a.com) and '); }); + it('preserves bracketed text that is not a link', () => { + const complete = 'The accepted values are [yes/no] for this setting'; + expect(stripIncompleteMarkdown(complete)).toBe(complete); + }); + it('strips an incomplete image syntax', () => { expect(stripIncompleteMarkdown('Here is ![alt')).toBe('Here is '); }); @@ -195,6 +200,11 @@ describe('conciergeDraftState', () => { expect(stripIncompleteMarkdown(complete)).toBe(complete); }); + it('preserves a complete code block ending at the closing fence', () => { + const complete = 'Before\n```\ncode\n```'; + expect(stripIncompleteMarkdown(complete)).toBe(complete); + }); + // --- Inline code (`) --- it('strips trailing unclosed inline code', () => { expect(stripIncompleteMarkdown('Run `command')).toBe('Run '); @@ -205,6 +215,11 @@ describe('conciergeDraftState', () => { expect(stripIncompleteMarkdown(complete)).toBe(complete); }); + it('preserves markdown-looking text inside complete inline code', () => { + const complete = 'Use `[accountID]` and `**not bold` in the payload'; + expect(stripIncompleteMarkdown(complete)).toBe(complete); + }); + // --- Streaming integration --- it('strips incomplete markdown during a streaming draft event', () => { const draft = applyConciergeDraftEvent(null, createDraftEvent({bodyMarkdown: 'Check [this link'}), REPORT_ID); From f99a09a748ebba99c977d4274bb1ee63c98688de Mon Sep 17 00:00:00 2001 From: Issa Nimaga Date: Wed, 6 May 2026 14:13:24 +0100 Subject: [PATCH 3/5] Normalize streamed Concierge markdown delimiters --- src/pages/inbox/conciergeDraftState.ts | 28 ++++++++++++++++++- .../pages/inbox/conciergeDraftState.test.ts | 19 ++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/pages/inbox/conciergeDraftState.ts b/src/pages/inbox/conciergeDraftState.ts index 2687ee62584d..e09f3bdb6f37 100644 --- a/src/pages/inbox/conciergeDraftState.ts +++ b/src/pages/inbox/conciergeDraftState.ts @@ -110,10 +110,30 @@ function stripUnpairedLastLineDelimiter(text: string, delimiter: string, ignored return text; } +function normalizeDelimiterForExpensiMark(text: string, delimiter: string, replacement: string, ignoredRanges: MarkdownRange[] = []): string { + let result = ''; + + for (let pos = 0; pos < text.length; pos++) { + const isInIgnoredRange = ignoredRanges.some((range) => pos >= range.start && pos < range.end); + if (text.startsWith(delimiter, pos) && !isEscaped(text, pos) && !isInIgnoredRange) { + result += replacement; + pos += delimiter.length - 1; + continue; + } + + result += text[pos]; + } + + return result; +} + /** * Strips incomplete markdown constructs from the tail of a streaming markdown * string so that ExpensiMark doesn't render raw syntax for half-finished - * links, bold, strikethrough, or code blocks. + * links, bold, strikethrough, or code blocks. Completed double-delimiter + * emphasis is normalized to ExpensiMark's single-delimiter syntax so the text + * stays styled without leaking raw delimiters while the server-rendered HTML is + * still pending. */ function stripIncompleteMarkdown(markdown: string): string { if (!markdown) { @@ -149,7 +169,13 @@ function stripIncompleteMarkdown(markdown: string): string { codeRanges = getCodeRanges(result).ranges; result = stripUnpairedLastLineDelimiter(result, '**', codeRanges); + codeRanges = getCodeRanges(result).ranges; + result = normalizeDelimiterForExpensiMark(result, '**', '*', codeRanges); + + codeRanges = getCodeRanges(result).ranges; result = stripUnpairedLastLineDelimiter(result, '~~', codeRanges); + codeRanges = getCodeRanges(result).ranges; + result = normalizeDelimiterForExpensiMark(result, '~~', '~', codeRanges); return result; } diff --git a/tests/unit/pages/inbox/conciergeDraftState.test.ts b/tests/unit/pages/inbox/conciergeDraftState.test.ts index 43491dcf97b6..9b090e293478 100644 --- a/tests/unit/pages/inbox/conciergeDraftState.test.ts +++ b/tests/unit/pages/inbox/conciergeDraftState.test.ts @@ -47,7 +47,7 @@ describe('conciergeDraftState', () => { expect(draft?.reportAction.actorAccountID).toBe(CONST.ACCOUNT_ID.CONCIERGE); expect(draft?.reportAction.created).toBe(CREATED); expect(getFirstMessageHTML(draft)).toContain('world'); - expect(getFirstMessageText(draft)).toBe('Hello, *world*!'); + expect(getFirstMessageText(draft)).toBe('Hello, world!'); }); it('should update the same draft session when a newer sequence arrives', () => { @@ -79,7 +79,7 @@ describe('conciergeDraftState', () => { ); expect(staleDraft).toBe(initialDraft); - expect(getFirstMessageText(staleDraft)).toBe('Hello, *world*!'); + expect(getFirstMessageText(staleDraft)).toBe('Hello, world!'); }); it('should keep the draft visible through completion and prefer finalRenderedHTML when provided', () => { @@ -138,10 +138,14 @@ describe('conciergeDraftState', () => { }); it('does not alter complete markdown', () => { - const complete = 'Hello **bold** and [link](https://example.com) and `code`'; + const complete = 'Hello *bold* and [link](https://example.com) and `code`'; expect(stripIncompleteMarkdown(complete)).toBe(complete); }); + it('normalizes complete double-delimiter emphasis for ExpensiMark', () => { + expect(stripIncompleteMarkdown('Hello **bold** and ~~strike~~')).toBe('Hello *bold* and ~strike~'); + }); + // --- Links / Images --- it('strips an incomplete link with only opening bracket', () => { expect(stripIncompleteMarkdown('Check out [')).toBe('Check out '); @@ -182,7 +186,7 @@ describe('conciergeDraftState', () => { }); it('preserves complete bold and strips only the unclosed one', () => { - expect(stripIncompleteMarkdown('**done** and **broken')).toBe('**done** and '); + expect(stripIncompleteMarkdown('**done** and **broken')).toBe('*done* and '); }); // --- Strikethrough (~~) --- @@ -221,6 +225,13 @@ describe('conciergeDraftState', () => { }); // --- Streaming integration --- + it('keeps complete double-delimiter bold styled without showing raw delimiters during streaming', () => { + const draft = applyConciergeDraftEvent(null, createDraftEvent({bodyMarkdown: 'Hello **bold**!'}), REPORT_ID); + + expect(getFirstMessageHTML(draft)).toContain('bold'); + expect(getFirstMessageHTML(draft)).not.toContain('*'); + }); + it('strips incomplete markdown during a streaming draft event', () => { const draft = applyConciergeDraftEvent(null, createDraftEvent({bodyMarkdown: 'Check [this link'}), REPORT_ID); // The raw '[this link' syntax should NOT appear in the rendered HTML From d5828001066ecd81f7d0ab967166f33f70ab4d7f Mon Sep 17 00:00:00 2001 From: Issa Nimaga Date: Mon, 11 May 2026 16:55:51 +0100 Subject: [PATCH 4/5] Cleanups and review comment addressing --- src/pages/inbox/conciergeDraftState.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pages/inbox/conciergeDraftState.ts b/src/pages/inbox/conciergeDraftState.ts index e09f3bdb6f37..a122a9171ed5 100644 --- a/src/pages/inbox/conciergeDraftState.ts +++ b/src/pages/inbox/conciergeDraftState.ts @@ -20,13 +20,15 @@ type BuildConciergeDraftReportActionParams = { reportID: string; }; -type MarkdownRange = { +type TextRange = { start: number; end: number; }; const CODE_BLOCK_DELIMITER = '```'; const INLINE_CODE_DELIMITER = '`'; +const BOLD_DELIMITER = '**'; +const STRIKETHROUGH_DELIMITER = '~~'; function isEscaped(text: string, index: number): boolean { let slashCount = 0; @@ -40,8 +42,8 @@ function isEscaped(text: string, index: number): boolean { return slashCount % 2 !== 0; } -function getCodeRanges(text: string): {ranges: MarkdownRange[]; unclosedCodeBlockStart: number | null} { - const ranges: MarkdownRange[] = []; +function getCodeRanges(text: string): {ranges: TextRange[]; unclosedCodeBlockStart: number | null} { + const ranges: TextRange[] = []; let unclosedCodeBlockStart: number | null = null; for (let pos = 0; pos <= text.length - CODE_BLOCK_DELIMITER.length; pos++) { @@ -88,7 +90,7 @@ function getCodeRanges(text: string): {ranges: MarkdownRange[]; unclosedCodeBloc return {ranges, unclosedCodeBlockStart}; } -function stripUnpairedLastLineDelimiter(text: string, delimiter: string, ignoredRanges: MarkdownRange[] = []): string { +function stripUnpairedLastLineDelimiter(text: string, delimiter: string, ignoredRanges: TextRange[] = []): string { const lastNewline = text.lastIndexOf('\n'); const lastLineStart = lastNewline + 1; const delimiterIndexes: number[] = []; @@ -110,7 +112,7 @@ function stripUnpairedLastLineDelimiter(text: string, delimiter: string, ignored return text; } -function normalizeDelimiterForExpensiMark(text: string, delimiter: string, replacement: string, ignoredRanges: MarkdownRange[] = []): string { +function normalizeDelimiterForExpensiMark(text: string, delimiter: string, replacement: string, ignoredRanges: TextRange[] = []): string { let result = ''; for (let pos = 0; pos < text.length; pos++) { @@ -168,14 +170,14 @@ function stripIncompleteMarkdown(markdown: string): string { } codeRanges = getCodeRanges(result).ranges; - result = stripUnpairedLastLineDelimiter(result, '**', codeRanges); + result = stripUnpairedLastLineDelimiter(result, BOLD_DELIMITER, codeRanges); codeRanges = getCodeRanges(result).ranges; - result = normalizeDelimiterForExpensiMark(result, '**', '*', codeRanges); + result = normalizeDelimiterForExpensiMark(result, BOLD_DELIMITER, '*', codeRanges); codeRanges = getCodeRanges(result).ranges; - result = stripUnpairedLastLineDelimiter(result, '~~', codeRanges); + result = stripUnpairedLastLineDelimiter(result, STRIKETHROUGH_DELIMITER, codeRanges); codeRanges = getCodeRanges(result).ranges; - result = normalizeDelimiterForExpensiMark(result, '~~', '~', codeRanges); + result = normalizeDelimiterForExpensiMark(result, STRIKETHROUGH_DELIMITER, '~', codeRanges); return result; } From 583714beb872f5919eb4886d738ed77f1b2a9929 Mon Sep 17 00:00:00 2001 From: "Issa Nimaga (via MelvinBot)" Date: Tue, 19 May 2026 12:06:31 +0000 Subject: [PATCH 5/5] Extract isInAnyRange helper to deduplicate range-membership checks Co-authored-by: Issa Nimaga --- src/pages/inbox/conciergeDraftState.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/inbox/conciergeDraftState.ts b/src/pages/inbox/conciergeDraftState.ts index 8e88906872e6..7f175baf1b52 100644 --- a/src/pages/inbox/conciergeDraftState.ts +++ b/src/pages/inbox/conciergeDraftState.ts @@ -30,6 +30,10 @@ const INLINE_CODE_DELIMITER = '`'; const BOLD_DELIMITER = '**'; const STRIKETHROUGH_DELIMITER = '~~'; +function isInAnyRange(position: number, ranges: TextRange[]): boolean { + return ranges.some((range) => position >= range.start && position < range.end); +} + function isEscaped(text: string, index: number): boolean { let slashCount = 0; let pos = index - 1; @@ -68,8 +72,7 @@ function getCodeRanges(text: string): {ranges: TextRange[]; unclosedCodeBlockSta let openingDelimiterIndex: number | null = null; for (let pos = lineStart; pos < lineEnd; pos++) { - const isInCodeRange = ranges.some((range) => pos >= range.start && pos < range.end); - if (text[pos] !== INLINE_CODE_DELIMITER || isEscaped(text, pos) || isInCodeRange) { + if (text[pos] !== INLINE_CODE_DELIMITER || isEscaped(text, pos) || isInAnyRange(pos, ranges)) { continue; } @@ -96,8 +99,7 @@ function stripUnpairedLastLineDelimiter(text: string, delimiter: string, ignored const delimiterIndexes: number[] = []; for (let pos = lastLineStart; pos <= text.length - delimiter.length; pos++) { - const isInIgnoredRange = ignoredRanges.some((range) => pos >= range.start && pos < range.end); - if (!text.startsWith(delimiter, pos) || isEscaped(text, pos) || isInIgnoredRange) { + if (!text.startsWith(delimiter, pos) || isEscaped(text, pos) || isInAnyRange(pos, ignoredRanges)) { continue; } @@ -116,8 +118,7 @@ function normalizeDelimiterForExpensiMark(text: string, delimiter: string, repla let result = ''; for (let pos = 0; pos < text.length; pos++) { - const isInIgnoredRange = ignoredRanges.some((range) => pos >= range.start && pos < range.end); - if (text.startsWith(delimiter, pos) && !isEscaped(text, pos) && !isInIgnoredRange) { + if (text.startsWith(delimiter, pos) && !isEscaped(text, pos) && !isInAnyRange(pos, ignoredRanges)) { result += replacement; pos += delimiter.length - 1; continue; @@ -153,8 +154,7 @@ function stripIncompleteMarkdown(markdown: string): string { codeRanges = getCodeRanges(result).ranges; for (let openBracketIndex = result.length - 1; openBracketIndex >= 0; openBracketIndex--) { - const isInCodeRange = codeRanges.some((range) => openBracketIndex >= range.start && openBracketIndex < range.end); - if (result[openBracketIndex] !== '[' || isEscaped(result, openBracketIndex) || isInCodeRange) { + if (result[openBracketIndex] !== '[' || isEscaped(result, openBracketIndex) || isInAnyRange(openBracketIndex, codeRanges)) { continue; }