Reproduction link
Minimal snippet below (no CodeSandbox — the behavior is reproducible in any React app).
Steps to reproduce
Render XMarkdown in streaming mode (streaming.hasNextChunk = true) with an incompleteMarkdownComponentMap that maps link to a placeholder component, then feed it a still-incomplete link (closing ) not yet arrived).
import XMarkdown, {type ComponentProps} from '@ant-design/x-markdown';
const streaming = {
hasNextChunk: true,
incompleteMarkdownComponentMap: {link: 'incomplete-link'},
};
const components = {
'incomplete-link': (props: ComponentProps) => {
const raw = decodeURIComponent(String(props['data-raw'] || ''));
const label = raw.match(/^\[([^\]]*)\]/)?.[1] ?? raw.slice(1);
return <a href="#">{label}</a>;
},
};
// (A) Incomplete link in a paragraph — works as documented.
<XMarkdown components={components} streaming={streaming}>
{'See [some label](https://exa'}
</XMarkdown>
// (B) The same incomplete link as a list item — broken.
<XMarkdown components={components} streaming={streaming}>
{'- [some label](https://exa'}
</XMarkdown>
Current behavior
- (A) renders the
incomplete-link placeholder: <a>some label</a>. ✅
- (B) renders the raw markdown as text —
- [some label](https://exa — with the URL fragment turned into an autolink, and the placeholder is never used. ❌
The placeholder only starts working for (B) once the closing ) streams in and the link becomes complete. Because the incomplete state is shown as raw markdown and the complete state as a rendered link, the visible text noticeably changes width (it shrinks) at the moment the link completes — a distracting layout shift in a streaming UI.
The trigger is specifically an incomplete link whose [ immediately follows an unordered list marker (- , * , + ). An incomplete link elsewhere — in a paragraph, or later within the same list item (e.g. - see [some label](https://exa) — is recognized correctly and gets the placeholder. Ordered-list markers (1. ) are also fine. The same raw-rendering happens for incomplete images (![...]() directly after a list marker.
Expected behavior
An incomplete link directly after a list marker should be recognized as an incomplete link and rendered via incompleteMarkdownComponentMap / incomplete-link, the same as in a paragraph — so it doesn't flash as raw markdown and shift layout when it completes.
Version
@ant-design/x-markdown: 2.3.0 (the responsible streaming-recognizer code path appears unchanged through the current 2.8.0)
- React: 19
- Reproduced in both a browser and jsdom.
Reproduction link
Minimal snippet below (no CodeSandbox — the behavior is reproducible in any React app).
Steps to reproduce
Render
XMarkdownin streaming mode (streaming.hasNextChunk = true) with anincompleteMarkdownComponentMapthat mapslinkto a placeholder component, then feed it a still-incomplete link (closing)not yet arrived).Current behavior
incomplete-linkplaceholder:<a>some label</a>. ✅- [some label](https://exa— with the URL fragment turned into an autolink, and the placeholder is never used. ❌The placeholder only starts working for (B) once the closing
)streams in and the link becomes complete. Because the incomplete state is shown as raw markdown and the complete state as a rendered link, the visible text noticeably changes width (it shrinks) at the moment the link completes — a distracting layout shift in a streaming UI.The trigger is specifically an incomplete link whose
[immediately follows an unordered list marker (-,*,+). An incomplete link elsewhere — in a paragraph, or later within the same list item (e.g.- see [some label](https://exa) — is recognized correctly and gets the placeholder. Ordered-list markers (1.) are also fine. The same raw-rendering happens for incomplete images (![...]() directly after a list marker.Expected behavior
An incomplete link directly after a list marker should be recognized as an incomplete link and rendered via
incompleteMarkdownComponentMap/incomplete-link, the same as in a paragraph — so it doesn't flash as raw markdown and shift layout when it completes.Version
@ant-design/x-markdown: 2.3.0 (the responsible streaming-recognizer code path appears unchanged through the current 2.8.0)