Files
buzz/mobile/test/features/channels/message_content
Tom Brow 54ca2a3cc6 fix(mobile): render Markdown backslash escapes (#8925)
Mobile showed Markdown backslash escapes literally. A message linking a
channel as `[\#buzz-mobile](buzz://channel/…)` rendered as
`\#buzz-mobile`, and `\[text\]` rendered as LaTeX math. gpt_markdown has
no backslash escapes.

Outside code, each escaped ASCII punctuation character is now held as a
stand-in (an invisible word joiner plus the character's fullwidth form)
before parsing, so no other syntax (links, emphasis, mentions, channel
tokens, math) matches it. A new inline component and the link and image
builders restore the original character. The inbox/search preview
builder already handled escapes this way; its helpers moved to a shared
`markdown_escapes.dart`.

Two edge cases found in review are covered: shielding runs after
trailing-gallery extraction so carousel URLs and labels stay as
authored, and a bare link no longer takes a trailing backslash into its
destination. Codex review added: escaped backticks stay literal instead
of opening code, authored word joiners are not mistaken for stand-ins,
Buzz URL punctuation peeling keeps a trailing backslash, gallery
semantic labels drop escapes, the trailing-backslash peel is linear and
reruns after delimiter stripping, an escaped `\<url>` is not treated as
an autolink, `~~~` fences are skipped like backtick fences, an autolink
containing `\` keeps it in both label and destination, and the stand-ins
use fullwidth characters so the one gpt_markdown fallback that shows raw
text (multi-line strikethrough) still reads right.

Later review: fence detection looks back only over blanks and `>`
markers, so it stays linear on long lines (security review) and treats
an unclosed fence inside a quote or list as code, like the renderer
(Carl).

Behavior change: `\(x\)` and `\[x\]` now show as `(x)` and `[x]` instead
of math, per CommonMark. Desktop (buzz-app) doesn't render math either.
`$…$` math is unchanged.

### Validation

- Added tests: a `MessageContent` table covering escapes in prose, bold,
link labels (HTTP and `buzz://channel`), fenced and inline code (left
literal), a bare link ending in `\`, and escaped backticks, authored
word joiners and fullwidth characters, a `~~~` fence, a multi-line
strikethrough, `<https://example.com/a\>`, a Buzz URL ending in `.\`, an
emphasized bare URL ending in `\*`, an escaped `\<url>`, and a trailing
image gallery with escaped labels (including its semantics label), in
plain and bubble layouts. Each row also asserts no stand-in reaches
visible text or semantics. Reverting any one of these fixes fails it.
- `flutter analyze` is clean and the full mobile suite passes, except
for 11 `notification_toggle_test.dart` tests. Those fail locally on the
Studio at the CI-green a3964ab as well, so they are environmental and
unrelated to this change. A 256 KiB regression test covers the linear
fence scan, and it fails on the earlier code.
- iPhone 17 Pro simulator (iOS 27) signed in to the internal community:
loganj's original message now shows `#buzz-github-prs` and
`#buzz-mobile`.
- Full-app run on
[a3964ab](a3964ab08c):
debug build on a fresh iPhone 17 Pro simulator (iOS 27), signed in to
the internal community with live data. Covered the #buzz-github-prs
timeline (long linked lists, bare URLs, mentions, reactions, thread
summaries), a thread view, Inbox previews (the shared preview helpers),
a DM with a bare link and a channel link, and search results. Everything
rendered as before apart from the fixed escapes, with no stray stand-in
characters. Limitations: debug build, iOS only, no gallery or code-block
messages were in the visited history (the widget tests cover those), and
a 165-second recording was taken but GitHub can't take it through our
upload script. Two quirks seen are pre-existing and unchanged here: a
bare link absorbs a trailing comma, and search shows raw Markdown for
messages over 512 characters.

<details><summary>Full-app screenshots</summary>

| Channel | Channel (scrolled) | Thread |
| --- | --- | --- |
|
![channel](https://raw.githubusercontent.com/block/buzz/1b0b3e0052f65fbc7b0aca678fc1b45d1265d8c3/pr-8925--fullapp_02_channel.png)
|
![scrolled](https://raw.githubusercontent.com/block/buzz/1b0b3e0052f65fbc7b0aca678fc1b45d1265d8c3/pr-8925--fullapp_04_scroll.png)
|
![thread](https://raw.githubusercontent.com/block/buzz/1b0b3e0052f65fbc7b0aca678fc1b45d1265d8c3/pr-8925--fullapp_05_thread.png)
|
| **Inbox** | **DM** | **Search** |
|
![inbox](https://raw.githubusercontent.com/block/buzz/1b0b3e0052f65fbc7b0aca678fc1b45d1265d8c3/pr-8925--fullapp_06_inbox.png)
|
![dm](https://raw.githubusercontent.com/block/buzz/1b0b3e0052f65fbc7b0aca678fc1b45d1265d8c3/pr-8925--fullapp_07_dm.png)
|
![search](https://raw.githubusercontent.com/block/buzz/1b0b3e0052f65fbc7b0aca678fc1b45d1265d8c3/pr-8925--fullapp_08_search.png)
|

</details>

### Before / after

Before (production build, reported):


![01-before](https://raw.githubusercontent.com/block/buzz/809b2fccf98b41dc612acb9a32c9dd880ff40403/pr-8925--01-before.png)

After (this branch, same message):


![02-after](https://raw.githubusercontent.com/block/buzz/809b2fccf98b41dc612acb9a32c9dd880ff40403/pr-8925--02-after.png)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 10:35:54 -08:00
..