fix(tui): trim markdown wrap spaces (#22062)

* fix(tui): trim markdown wrap spaces

Use trim-aware wrapping for markdown prose so word-wrapped continuation lines do not keep boundary spaces.

* fix(tui): simplify markdown wrap nodes

Keep trim-aware wrapping on the rendered markdown text node while leaving nested inline segments as plain virtual text.

* fix(tui): trim definition row wrapping

Apply trim-aware wrapping to markdown definition rows so continuation lines match other prose rows.

* fix(tui): trim list and quote wrapping

Put trim-aware wrapping on the rendered list and quote rows that own markdown inline layout.

* fix(tui): preserve markdown nesting with trim wrap

Move list and quote indentation into layout padding so trim-aware wrapping does not erase nested markdown structure.

* fix(tui): trim only soft wrap spaces

Change trim-aware wrapping to remove whitespace only at soft-wrap boundaries so original leading inline spaces stay verbatim.

* fix(tui): preserve extra boundary whitespace

Trim only one soft-wrap boundary whitespace character so wrap-trim avoids leading continuations without collapsing intentional spacing.

* fix(tui): align styled wrap-trim mapping

Update styled text remapping to skip the single whitespace removed at soft-wrap boundaries without dropping preserved indentation.

* fix(tui): clean wrap trim test helpers

Clarify boundary-trim wording and strip OSC escapes from markdown render test output.

* fix(tui): strip osc before ansi in markdown tests

Remove OSC escapes from raw render output before SGR/CSI cleanup so markdown render assertions stay plain text.
This commit is contained in:
brooklyn! 2026-05-08 20:51:34 -07:00 committed by GitHub
parent 78b0008f44
commit a7e7921dbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 68 deletions

View file

@ -1,8 +1,47 @@
import { PassThrough } from 'stream'
import { Box, renderSync } from '@hermes/ink'
import React from 'react'
import { describe, expect, it } from 'vitest'
import { AUDIO_DIRECTIVE_RE, INLINE_RE, MEDIA_LINE_RE, stripInlineMarkup } from '../components/markdown.js'
import { AUDIO_DIRECTIVE_RE, INLINE_RE, Md, MEDIA_LINE_RE, stripInlineMarkup } from '../components/markdown.js'
import { stripAnsi } from '../lib/text.js'
import { DEFAULT_THEME } from '../theme.js'
const matches = (text: string) => [...text.matchAll(INLINE_RE)].map(m => m[0])
const BEL = String.fromCharCode(7)
const ESC = String.fromCharCode(27)
const CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, 'g')
const OSC_RE = new RegExp(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g')
const renderPlain = (node: React.ReactNode) => {
const stdout = new PassThrough()
const stdin = new PassThrough()
const stderr = new PassThrough()
let output = ''
Object.assign(stdout, { columns: 80, isTTY: false, rows: 24 })
Object.assign(stdin, { isTTY: false })
Object.assign(stderr, { isTTY: false })
stdout.on('data', chunk => {
output += chunk.toString()
})
const instance = renderSync(node, {
patchConsole: false,
stderr: stderr as NodeJS.WriteStream,
stdin: stdin as NodeJS.ReadStream,
stdout: stdout as NodeJS.WriteStream
})
instance.unmount()
instance.cleanup()
return output
.replace(OSC_RE, '')
.split('\n')
.map(line => stripAnsi(line).replace(CSI_RE, '').trimEnd())
}
describe('INLINE_RE emphasis', () => {
it('matches word-boundary italic/bold', () => {
@ -144,3 +183,37 @@ describe('protocol sentinels', () => {
expect(AUDIO_DIRECTIVE_RE.test('audio_as_voice')).toBe(false)
})
})
describe('Md wrapping', () => {
it('trims spaces from word-wrap continuation lines', () => {
const lines = renderPlain(
React.createElement(Box, { width: 5 }, React.createElement(Md, { t: DEFAULT_THEME, text: 'Let me' }))
)
expect(lines).toContain('Let')
expect(lines).toContain('me')
expect(lines).not.toContain(' me')
})
it('keeps nested list and quote indentation out of trim-sensitive text', () => {
const lines = renderPlain(
React.createElement(
Box,
{ flexDirection: 'column', width: 24 },
React.createElement(Md, { t: DEFAULT_THEME, text: ' - nested bullet' }),
React.createElement(Md, { t: DEFAULT_THEME, text: '>> nested quote' })
)
)
expect(lines).toContain(' • nested bullet')
expect(lines).toContain(' │ nested quote')
})
it('preserves original inline-code edge spaces', () => {
const lines = renderPlain(
React.createElement(Box, { width: 24 }, React.createElement(Md, { t: DEFAULT_THEME, text: '` hi ` ok' }))
)
expect(lines.some(line => line.startsWith(' hi ok'))).toBe(true)
})
})