LibreChat/client
Danny Avila 7c071e244b
🔢 fix: Prevent "approximately" tildes from rendering as markdown subscript (#13743)
* 🔢 fix: Prevent "approximately" tildes from rendering as markdown subscript

`remark-supersub` splits text nodes on every `~`; an even number of tildes
wraps the in-between text in `<sub>`. "Approximately" usage like
`~50% ... ~10%` pairs up and subscripts everything between the two tildes.

A backslash escape cannot fix this: micromark resolves `\~` back to a bare
`~` before supersub runs. Instead, `preprocessTilde` swaps approximation
tildes (a `~` prefixing a number, not attached to a word) for the Unicode
tilde operator `∼` (U+223C), which renders as a tilde but is not split by
supersub. Mirrors `preprocessLaTeX`: early return, single regex pass,
code-region skipping. Genuine subscripts (`H~2~O`, `a ~2~ b`), strikethrough,
escaped tildes, and home paths are preserved.

* 🔢 fix: Harden tilde preprocessing — escaped tildes, URLs, math, MarkdownLite

Addresses Codex review findings:
- Convert escaped approximation tildes too (`\~50%`): markdown decodes `\~`
  to `~` before supersub, so the escape still pairs into a subscript.
- Anchor matches to a prose boundary (start / whitespace / open bracket) and
  exclude `\(`/`\[`/`\{`, so URL path tildes (`/~50`) and math delimiters
  (`$~10$`, `$$~10$$`, `\(~10\)`) are left untouched.
- Apply preprocessTilde in MarkdownLite (user messages + search/subagent/
  code-analysis displays), which also enables remark-supersub.

* 🔧 refactor: Neutralize approximation tildes via remark plugin, not raw-text

Replaces the string-level preprocessTilde with `remarkApproxTilde`, a remark
plugin that rewrites "approximately" tildes (`~50%` → `∼`) on parsed text nodes
before remark-supersub runs.

Because it operates on the AST, code spans, fenced code blocks (backtick *and*
~~~), inline code with any backtick count, link destinations, and math spans are
structurally excluded — none are `text` nodes — resolving every raw-text edge
case Codex flagged without region-scanning heuristics. Escaped `\~` is covered
for free (markdown decodes it before the plugin runs).

- New client/src/utils/tilde.ts: `normalizeApproxTildes` (pure, per-text-node)
  + `remarkApproxTilde` plugin.
- Wired into both renderers (markdownConfig + MarkdownLite), before supersub.
- latex.ts / Markdown.tsx reverted to original; preprocessTilde removed.
- tilde.spec.ts: pure-function cases + a hand-built-tree test proving code,
  math, and link URLs are untouched while text (incl. link text) is converted.

* 🔧 fix: Cover quoted approximations and the markdown error-boundary fallback

- Broaden the boundary to any non-word, non-tilde char (`(?<![\w~])`), which
  now includes quotes — `"~50%" ... "~10%"` was still subscripting because `"`
  was not a recognized prose boundary. Safe to widen because the plugin runs on
  text nodes, so code / links / math / URLs are already excluded structurally
  (the earlier allowlist only existed to dodge URL paths in raw text).
- Add remarkApproxTilde to MarkdownErrorBoundary's fallback remark pipeline so
  the fix holds when a render error falls back to the minimal renderer.

* 🔧 fix: Preserve autolink URL labels when normalizing tildes

A GFM autolink renders the URL as its own label (a text node equal to the href),
so the broadened boundary was rewriting `~50` inside a displayed URL to `∼50`
even though the href stayed correct. Skip text nodes that are an autolink's label
(value matches the destination, allowing for the implied scheme on www/email
links), so the visible URL is preserved verbatim. Regular link labels (prose)
are still normalized.

Note: a single URL containing two `~<digit>` segments is still subscripted by
remark-supersub itself — that's pre-existing behavior (reproduces with no plugin)
and out of scope here.

* 🔧 refactor: Drop unist-util-visit runtime dep from tilde plugin

Replace the `unist-util-visit` import with a small self-contained recursive walk
over text nodes (tracking the parent for the autolink-label check). This removes
reliance on a transitively-hoisted runtime package — addressing the dependency-
hygiene concern without adding a dependency or churning the lockfile. The
type-only `unist` import remains (erased at build, no runtime resolution).

Behavior is unchanged; verified against nested emphasis and list/paragraph trees.
2026-06-14 16:59:12 -04:00
..
public 🎨 chore: Update Agent Tool with new SVG assets (#12065) 2026-03-04 09:28:19 -05:00
scripts
src 🔢 fix: Prevent "approximately" tildes from rendering as markdown subscript (#13743) 2026-06-14 16:59:12 -04:00
sw 🛟 fix: Auto-Recover from Stale Service Worker Assets After Deploys (#13686) 2026-06-11 11:57:06 -04:00
test 🧑‍🎨 refactor: Prompts/Sidebar styles for improved UI Consistency (#12426) 2026-04-09 00:02:31 -04:00
babel.config.cjs 🧑‍🎨 refactor: Prompts/Sidebar styles for improved UI Consistency (#12426) 2026-04-09 00:02:31 -04:00
check_updates.sh
index.html 🛟 fix: Auto-Recover from Stale Service Worker Assets After Deploys (#13686) 2026-06-11 11:57:06 -04:00
jest.config.cjs v0.8.6 (#13302) 2026-05-31 17:36:47 -04:00
nginx.conf 📬 docs: Add Forwarded Headers to Nginx SSL Proxy Template (#12379) 2026-03-25 13:04:19 -04:00
package.json 📦 chore: Bump jest-junit to v17.0.0 2026-06-09 20:38:30 -04:00
postcss.config.cjs
tailwind.config.cjs style(MCP): Enhance dialog accessibility and styling consistency (#11585) 2026-02-11 22:08:40 -05:00
tsconfig.json 👷 ci: Type-check the Client Workspace (#13560) 2026-06-06 18:40:31 -04:00
vite.config.ts 🛟 fix: Auto-Recover from Stale Service Worker Assets After Deploys (#13686) 2026-06-11 11:57:06 -04:00