Skip to content

Commit b17eb94

Browse files
committed
fix(tui): don't italicize intraword underscores in markdown
The inline markdown regex matched `_..._` / `__...__` anywhere, so file paths like `browser_screenshot_ecc1c3feab.png` got mid-path italics. Require non-word flanking (`(?<!\w)` / `(?!\w)`) on underscore emphasis so snake_case identifiers and paths render literally, matching the CommonMark intraword rule. `*` / `**` keep intraword semantics.
1 parent 36e8435 commit b17eb94

2 files changed

Lines changed: 39 additions & 5 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { INLINE_RE, stripInlineMarkup } from '../components/markdown.js'
4+
5+
const matches = (text: string) => [...text.matchAll(INLINE_RE)].map(m => m[0])
6+
7+
describe('INLINE_RE emphasis', () => {
8+
it('matches word-boundary italic/bold', () => {
9+
expect(matches('say _hi_ there')).toEqual(['_hi_'])
10+
expect(matches('very __bold__ move')).toEqual(['__bold__'])
11+
expect(matches('(_paren_) and [_bracket_]')).toEqual(['_paren_', '_bracket_'])
12+
})
13+
14+
it('keeps intraword underscores literal', () => {
15+
const path = '/home/me/.hermes/cache/screenshots/browser_screenshot_ecc1c3feab.png'
16+
17+
expect(matches(path)).toEqual([])
18+
expect(matches('snake_case_var and MY_CONST')).toEqual([])
19+
expect(matches('foo__bar__baz')).toEqual([])
20+
})
21+
22+
it('still matches asterisk emphasis intraword', () => {
23+
expect(matches('a*b*c')).toEqual(['*b*'])
24+
expect(matches('a**bold**c')).toEqual(['**bold**'])
25+
})
26+
})
27+
28+
describe('stripInlineMarkup', () => {
29+
it('strips word-boundary emphasis only', () => {
30+
expect(stripInlineMarkup('say _hi_ there')).toBe('say hi there')
31+
expect(stripInlineMarkup('browser_screenshot_ecc.png')).toBe('browser_screenshot_ecc.png')
32+
expect(stripInlineMarkup('__bold__ and foo__bar__')).toBe('bold and foo__bar__')
33+
})
34+
})

ui-tui/src/components/markdown.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const DEF_RE = /^\s*:\s+(.+)$/
1212
const TABLE_DIVIDER_CELL_RE = /^:?-{3,}:?$/
1313
const MD_URL_RE = '((?:[^\\s()]|\\([^\\s()]*\\))+?)'
1414

15-
const INLINE_RE = new RegExp(
16-
`(!\\[(.*?)\\]\\(${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<]+))`,
15+
export const INLINE_RE = new RegExp(
16+
`(!\\[(.*?)\\]\\(${MD_URL_RE}\\)|\\[(.+?)\\]\\(${MD_URL_RE}\\)|<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>|~~(.+?)~~|\`([^\\\`]+)\`|\\*\\*(.+?)\\*\\*|(?<!\\w)__(.+?)__(?!\\w)|\\*(.+?)\\*|(?<!\\w)_(.+?)_(?!\\w)|==(.+?)==|\\[\\^([^\\]]+)\\]|\\^([^^\\s][^^]*?)\\^|~([^~\\s][^~]*?)~|(https?:\\/\\/[^\\s<]+))`,
1717
'g'
1818
)
1919

@@ -90,17 +90,17 @@ const isTableDivider = (row: string) => {
9090
return cells.length > 1 && cells.every(cell => TABLE_DIVIDER_CELL_RE.test(cell))
9191
}
9292

93-
const stripInlineMarkup = (value: string) =>
93+
export const stripInlineMarkup = (value: string) =>
9494
value
9595
.replace(/!\[(.*?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '[image: $1] $2')
9696
.replace(/\[(.+?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '$1')
9797
.replace(/<((?:https?:\/\/|mailto:)[^>\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/g, '$1')
9898
.replace(/~~(.+?)~~/g, '$1')
9999
.replace(/`([^`]+)`/g, '$1')
100100
.replace(/\*\*(.+?)\*\*/g, '$1')
101-
.replace(/__(.+?)__/g, '$1')
101+
.replace(/(?<!\w)__(.+?)__(?!\w)/g, '$1')
102102
.replace(/\*(.+?)\*/g, '$1')
103-
.replace(/_(.+?)_/g, '$1')
103+
.replace(/(?<!\w)_(.+?)_(?!\w)/g, '$1')
104104
.replace(/==(.+?)==/g, '$1')
105105
.replace(/\[\^([^\]]+)\]/g, '[$1]')
106106
.replace(/\^([^^\s][^^]*?)\^/g, '^$1')

0 commit comments

Comments
 (0)