fix(native-chat): cover footnote definitions and correct the doc comment

GFM adds a second definition construct. `[^note]: I meant this literally` was
still rendering an empty bubble on a user row — the same total content loss,
one construct over — so disable `gfmFootnoteDefinition` alongside `definition`.

The comment claimed reference links survive. Measured, they do not: they resolve
*against* the definitions this removes, so `[d]`, `[d][]` and `![alt][d]` now
render literally. That is the right reading for prose, but the comment said the
opposite. Also pin that the untouched-content cases render something, since
comparing the two prop values to each other is trivially true if the plugin
ever stops attaching.
This commit is contained in:
Brennan Benson
2026-08-16 21:05:43 -07:00
parent 2b0a4aaa5a
commit 4cd957976e
2 changed files with 24 additions and 9 deletions
@@ -22,6 +22,12 @@ describe('disableLinkDefinitions', () => {
['inside a bullet', '- [Image #1]: /tmp/a.png', '[Image #1]: /tmp/a.png'],
['inside an ordered item', '1. [Image #1]: /tmp/a.png', '[Image #1]: /tmp/a.png'],
['inside a blockquote', '> [Image #1]: /tmp/a.png', '[Image #1]: /tmp/a.png'],
// GFM adds a second definition construct that swallows prose the same way.
[
'as a footnote definition',
'[^note]: I meant this literally',
'[^note]: I meant this literally'
],
// A link label may span lines, so the swallow is not a single-line shape.
[
'with a wrapped label',
@@ -49,6 +55,10 @@ describe('disableLinkDefinitions', () => {
'~~~\ncode\n```\n[Image #1]: /tmp/a.png\n~~~',
'| a | b |\n| - | - |\n| 1 | 2 |'
])('leaves content the pipeline already rendered untouched', (typed) => {
// Why the second assertion: comparing the two prop values to each other is
// trivially true if the plugin ever stops attaching, so pin that these
// render something before trusting that they render the same thing.
expect(rendered(typed, true)).not.toBe('')
expect(rendered(typed, true)).toBe(rendered(typed, false))
})
@@ -1,19 +1,24 @@
import type { Processor } from 'unified'
/**
* Stop CommonMark from reading `[label]: /some/target` as a link reference
* definition.
* Stop the parser from reading `[label]: /some/target` (and its GFM footnote
* sibling `[^label]: text`) as a definition.
*
* Definitions render to nothing at all, so on content that is prose rather than
* authored Markdown — a chat turn the user typed — such a line silently deletes
* itself. Escaping the bracket in the source text only reaches lines at the top
* level; a definition inside a list item or blockquote, or one whose label wraps
* across lines, is still swallowed. Turning the construct off covers every
* container, and leaves inline links, reference *links*, and GFM intact.
* A definition renders to nothing at all, so on content that is prose rather
* than authored Markdown — a chat turn the user typed — such a line silently
* deletes itself. Escaping the bracket in the source text only reaches lines at
* the top level; a definition inside a list item or blockquote, or one whose
* label wraps across lines, is still swallowed. Turning the constructs off
* covers every container without rewriting a byte of what the user wrote.
*
* Inline links and the rest of GFM are untouched. Reference links necessarily
* render literally, because they resolve *against* the definitions this removes
* — which is the right reading for prose, where `[d]` is far more likely to be
* a bracket the user typed than a link they meant to define.
*/
export function remarkDisableDefinitions(this: Processor): undefined {
const data = this.data()
const extensions = (data.micromarkExtensions ??= [])
extensions.push({ disable: { null: ['definition'] } })
extensions.push({ disable: { null: ['definition', 'gfmFootnoteDefinition'] } })
return undefined
}