From 6cac56f3142dfdc569a17b4f5639c7d5fbda515d Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Tue, 19 May 2026 00:05:59 -0700 Subject: [PATCH] fix(tui): preserve dunder identifiers in markdown --- ui-tui/src/__tests__/markdown.test.ts | 33 +++++++++++++++++++++++++-- ui-tui/src/components/markdown.tsx | 14 ++++++++---- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ui-tui/src/__tests__/markdown.test.ts b/ui-tui/src/__tests__/markdown.test.ts index b2fab923271..0c2b2c5d28e 100644 --- a/ui-tui/src/__tests__/markdown.test.ts +++ b/ui-tui/src/__tests__/markdown.test.ts @@ -46,7 +46,7 @@ const renderPlain = (node: React.ReactNode) => { 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('very __bold move__ today')).toEqual(['__bold move__']) expect(matches('(_paren_) and [_bracket_]')).toEqual(['_paren_', '_bracket_']) }) @@ -58,6 +58,12 @@ describe('INLINE_RE emphasis', () => { expect(matches('foo__bar__baz')).toEqual([]) }) + it('keeps Python dunder identifiers literal', () => { + expect(matches('if __name__ == "__main__":')).toEqual([]) + expect(matches('def __init__(self):')).toEqual([]) + expect(matches('print(__file__)')).toEqual([]) + }) + it('still matches asterisk emphasis intraword', () => { expect(matches('a*b*c')).toEqual(['*b*']) expect(matches('a**bold**c')).toEqual(['**bold**']) @@ -93,7 +99,12 @@ 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__') + expect(stripInlineMarkup('__bold move__ and foo__bar__')).toBe('bold move and foo__bar__') + }) + + it('preserves Python dunder identifiers', () => { + expect(stripInlineMarkup('if __name__ == "__main__":')).toBe('if __name__ == "__main__":') + expect(stripInlineMarkup('class X: def __init__(self): pass')).toBe('class X: def __init__(self): pass') }) it('leaves ~!/~? kaomoji alone and still handles real subscript', () => { @@ -216,6 +227,24 @@ describe('Md wrapping', () => { expect(lines.some(line => line.startsWith(' hi ok'))).toBe(true) }) + + it('renders Python dunder identifiers literally outside code fences', () => { + const lines = renderPlain( + React.createElement( + Box, + { width: 80 }, + React.createElement(Md, { + t: DEFAULT_THEME, + text: 'if __name__ == "__main__":\n obj.__init__()' + }) + ) + ) + + const rendered = lines.join('\n') + + expect(rendered).toContain('if __name__ == "__main__":') + expect(rendered).toContain('obj.__init__()') + }) }) describe('Md link labels', () => { diff --git a/ui-tui/src/components/markdown.tsx b/ui-tui/src/components/markdown.tsx index c215cd811bf..3e48c82b0c7 100644 --- a/ui-tui/src/components/markdown.tsx +++ b/ui-tui/src/components/markdown.tsx @@ -70,6 +70,12 @@ const NUMBERED_RE = /^(\s*)(\d+)[.)]\s+(.*)$/ const QUOTE_RE = /^\s*(?:>\s*)+/ const TABLE_DIVIDER_CELL_RE = /^:?-{3,}:?$/ const MD_URL_RE = '((?:[^\\s()]|\\([^\\s()]*\\))+?)' +const MD_IDENTIFIER_RE = '[A-Za-z_][A-Za-z0-9_]*' +const MD_DUNDER_IDENTIFIER_RE = `(?:${MD_IDENTIFIER_RE}__(?!\\w))` +const MD_UNDERSCORE_BOLD_RE = `(? .replace(/~~(.+?)~~/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/(?