diff --git a/ui-tui/src/__tests__/markdown.test.ts b/ui-tui/src/__tests__/markdown.test.ts new file mode 100644 index 0000000000..236b4f961c --- /dev/null +++ b/ui-tui/src/__tests__/markdown.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' + +import { INLINE_RE, stripInlineMarkup } from '../components/markdown.js' + +const matches = (text: string) => [...text.matchAll(INLINE_RE)].map(m => m[0]) + +describe('INLINE_RE emphasis', () => { + it('matches word-boundary italic/bold', () => { + expect(matches('say _hi_ there')).toEqual(['_hi_']) + expect(matches('very __bold__ move')).toEqual(['__bold__']) + expect(matches('(_paren_) and [_bracket_]')).toEqual(['_paren_', '_bracket_']) + }) + + it('keeps intraword underscores literal', () => { + const path = '/home/me/.hermes/cache/screenshots/browser_screenshot_ecc1c3feab.png' + + expect(matches(path)).toEqual([]) + expect(matches('snake_case_var and MY_CONST')).toEqual([]) + expect(matches('foo__bar__baz')).toEqual([]) + }) + + it('still matches asterisk emphasis intraword', () => { + expect(matches('a*b*c')).toEqual(['*b*']) + expect(matches('a**bold**c')).toEqual(['**bold**']) + }) +}) + +describe('stripInlineMarkup', () => { + it('strips word-boundary emphasis only', () => { + expect(stripInlineMarkup('say _hi_ there')).toBe('say hi there') + expect(stripInlineMarkup('browser_screenshot_ecc.png')).toBe('browser_screenshot_ecc.png') + expect(stripInlineMarkup('__bold__ and foo__bar__')).toBe('bold and foo__bar__') + }) +}) diff --git a/ui-tui/src/components/markdown.tsx b/ui-tui/src/components/markdown.tsx index 5e1063837b..cd0da465d9 100644 --- a/ui-tui/src/components/markdown.tsx +++ b/ui-tui/src/components/markdown.tsx @@ -12,8 +12,8 @@ const DEF_RE = /^\s*:\s+(.+)$/ const TABLE_DIVIDER_CELL_RE = /^:?-{3,}:?$/ const MD_URL_RE = '((?:[^\\s()]|\\([^\\s()]*\\))+?)' -const INLINE_RE = new RegExp( - `(!\\[(.*?)\\]\\(${MD_URL_RE}\\)|\\[(.+?)\\]\\(${MD_URL_RE}\\)|<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>|~~(.+?)~~|\`([^\\\`]+)\`|\\*\\*(.+?)\\*\\*|__(.+?)__|\\*(.+?)\\*|_(.+?)_|==(.+?)==|\\[\\^([^\\]]+)\\]|\\^([^^\\s][^^]*?)\\^|~([^~\\s][^~]*?)~|(https?:\\/\\/[^\\s<]+))`, +export const INLINE_RE = new RegExp( + `(!\\[(.*?)\\]\\(${MD_URL_RE}\\)|\\[(.+?)\\]\\(${MD_URL_RE}\\)|<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>|~~(.+?)~~|\`([^\\\`]+)\`|\\*\\*(.+?)\\*\\*|(? { return cells.length > 1 && cells.every(cell => TABLE_DIVIDER_CELL_RE.test(cell)) } -const stripInlineMarkup = (value: string) => +export const stripInlineMarkup = (value: string) => value .replace(/!\[(.*?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '[image: $1] $2') .replace(/\[(.+?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '$1') @@ -98,9 +98,9 @@ const stripInlineMarkup = (value: string) => .replace(/~~(.+?)~~/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/__(.+?)__/g, '$1') + .replace(/(?