Files
orca/mobile/src/components/markdown-inline-token-rules.test.ts
T
Brennan Benson 9ee359550b fix(mobile): make native-chat file links and path citations tappable (STA-3331) (#12364)
* fix(mobile): make native-chat file links and path citations tappable (STA-3331)

- Linkify POSIX absolute paths in chat prose (leading-/ regex alternative;
  URL guard now keys off the char before the matched slash)
- Parse agent-style path:line(:col) citations in prose, code spans, and the
  open flow; line/column ride into the mobile file preview route
- Route non-web markdown hrefs (file: URIs, relative/absolute paths) to the
  file opener instead of silently dropping them; unknown schemes stay dead
- Resolve chat paths against the worktree root, not the terminal's live cwd
- Reuse the terminal tap-to-open flow for chat taps (haptic, preview route,
  tab activation with retries) via a shared identity-stable hook, and toast
  on misses instead of silent no-ops
- Keep snake_case paths whole (intraword underscores are literal text),
  scan bold/italic/strike spans for paths, split trailing punctuation off
  autolinks, and let taps land while the composer keyboard is up

* fix(mobile): harden chat file tap handling

* refactor(chat): share native chat href routing

* fix(mobile): detect files directly under path roots

* fix(mobile): keep inline tokens and dunder paths intact around emphasis

Review follow-ups on the chat file-link work:

- A rejected intraword `_` token left the scan index past its closing
  underscore, so every inline token between two snake_case words was
  swallowed and rendered as literal source — including markdown links,
  which became untappable. Rescan from just past the opening delimiter.
- Treat a path separator as an intraword flank so `src/__init__.py` and
  `a/__tests__/x.ts` stay whole; previously they rendered as bold plus a
  remnant that the new absolute-root pattern turned into a tap on `/x.ts`.
- Bound the `:line(:col)` tail so `src/app.ts:1e3` and `:80%` no longer
  parse a line number, while a cited range still opens its first line.
- Route chat tap failures through the composer banner (toast fallback):
  chat taps happen with the keyboard up, which covers the toast.
- Drop the tap-handler mirror's dep list; the call site rebuilds its
  accessors every render, so it could never skip on a route that
  rerenders per keystroke.

* Revert "fix(mobile): keep inline tokens and dunder paths intact around emphasis"

This reverts commit 308bfaf22b.

* fix(mobile): preserve chat file-link parsing and feedback
2026-08-04 15:46:08 -07:00

74 lines
2.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isIntrawordUnderscoreToken,
trimAutolinkTrailingPunctuation
} from './markdown-inline-token-rules'
describe('isIntrawordUnderscoreToken', () => {
it('rejects snake_case emphasis spans', () => {
const text = 'src/foo_bar.ts and src/baz_qux.ts'
const index = text.indexOf('_')
const token = text.slice(index, text.lastIndexOf('_') + 1)
expect(isIntrawordUnderscoreToken(text, index, token)).toBe(true)
})
it('keeps standalone emphasis', () => {
expect(isIntrawordUnderscoreToken('say _hello_ now', 4, '_hello_')).toBe(false)
expect(isIntrawordUnderscoreToken('_hello_.', 0, '_hello_')).toBe(false)
})
it('rejects emphasis closed against a following word', () => {
expect(isIntrawordUnderscoreToken('_foo_s bar', 0, '_foo_')).toBe(true)
})
it('rejects dunder emphasis inside a path', () => {
expect(isIntrawordUnderscoreToken('src/__init__.py', 4, '__init__')).toBe(true)
expect(isIntrawordUnderscoreToken(String.raw`src\__init__.py`, 4, '__init__')).toBe(true)
})
it('ignores non-underscore tokens', () => {
expect(isIntrawordUnderscoreToken('a*b*c', 1, '*b*')).toBe(false)
})
})
describe('trimAutolinkTrailingPunctuation', () => {
it('splits sentence punctuation off the URL', () => {
expect(trimAutolinkTrailingPunctuation('https://x.com/a.')).toEqual({
url: 'https://x.com/a',
trailing: '.'
})
expect(trimAutolinkTrailingPunctuation('https://x.com/a,')).toEqual({
url: 'https://x.com/a',
trailing: ','
})
expect(trimAutolinkTrailingPunctuation('https://x.com/a?!')).toEqual({
url: 'https://x.com/a',
trailing: '?!'
})
})
it('keeps balanced parens and strips unbalanced ones', () => {
expect(trimAutolinkTrailingPunctuation('https://x.com/a_(b)')).toEqual({
url: 'https://x.com/a_(b)',
trailing: ''
})
expect(trimAutolinkTrailingPunctuation('https://x.com/a).')).toEqual({
url: 'https://x.com/a',
trailing: ').'
})
})
it('handles long unmatched closing-parenthesis tails', () => {
const url = 'https://x.com/a_(b)'
const trailing = ')'.repeat(4096)
expect(trimAutolinkTrailingPunctuation(`${url}${trailing}`)).toEqual({ url, trailing })
})
it('leaves clean URLs untouched', () => {
expect(trimAutolinkTrailingPunctuation('https://x.com/a')).toEqual({
url: 'https://x.com/a',
trailing: ''
})
})
})