fix(markdown): Ensure abbr: links render correctly in react-markdown v9+

React-markdown v9 and later versions enforce stricter default URL filtering, which inadvertently removed support for custom URL schemes like 'abbr:'.

This commit introduces a `customUrlTransform` function, now located in `markdown-utils.ts`. This function is passed to the `ReactMarkdown` component to:
- Explicitly allow the 'abbr:' protocol.
- Permit standard safe web protocols (http, https, mailto, xmpp, irc, ircs).
- Allow page-local fragments (#), protocol-relative URLs (//), and all purely relative paths.
- Disallow other potentially unsafe or unsupported URL schemes.

This restores the intended functionality for 'abbr:' links while maintaining robust URL handling and security for the Markdown component.
pull/20648/head
xuzijie1995 12 months ago
parent d22c351221
commit f78a4c8aee

@ -7,7 +7,7 @@ import RemarkGfm from 'remark-gfm'
import RehypeRaw from 'rehype-raw' import RehypeRaw from 'rehype-raw'
import { flow } from 'lodash-es' import { flow } from 'lodash-es'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils' import { preprocessLaTeX, preprocessThinkTag, customUrlTransform } from './markdown-utils'
import { import {
AudioBlock, AudioBlock,
CodeBlock, CodeBlock,
@ -65,6 +65,7 @@ export function Markdown(props: { content: string; className?: string; customDis
} }
}, },
]} ]}
urlTransform={customUrlTransform}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]} disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{ components={{
code: CodeBlock, code: CodeBlock,

@ -36,3 +36,57 @@ export const preprocessThinkTag = (content: string) => {
(str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'), (str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'),
])(content) ])(content)
} }
/**
* Transforms a URI for use in react-markdown, ensuring security and compatibility.
* This function is designed to work with react-markdown v9+ which has stricter
* default URL handling.
*
* Behavior:
* 1. Always allows the custom 'abbr:' protocol.
* 2. Always allows page-local fragments (e.g., "#some-id").
* 3. Always allows protocol-relative URLs (e.g., "//example.com/path").
* 4. Always allows purely relative paths (e.g., "path/to/file", "/abs/path").
* 5. Allows absolute URLs if their scheme is in a permitted list (case-insensitive):
* 'http:', 'https:', 'mailto:', 'xmpp:', 'irc:', 'ircs:'.
* 6. Intelligently distinguishes colons used for schemes from colons within
* paths, query parameters, or fragments of relative-like URLs.
* 7. Returns the original URI if allowed, otherwise returns `undefined` to
* signal that the URI should be removed/disallowed by react-markdown.
*/
export const customUrlTransform = (uri: string): string | undefined => {
const PERMITTED_SCHEME_REGEX = /^(https?|ircs?|mailto|xmpp|abbr):$/i;
if (uri.startsWith('#')) {
return uri;
}
if (uri.startsWith('//')) {
return uri;
}
const colonIndex = uri.indexOf(':');
if (colonIndex === -1) {
return uri;
}
const slashIndex = uri.indexOf('/');
const questionMarkIndex = uri.indexOf('?');
const hashIndex = uri.indexOf('#');
if (
(slashIndex !== -1 && colonIndex > slashIndex) ||
(questionMarkIndex !== -1 && colonIndex > questionMarkIndex) ||
(hashIndex !== -1 && colonIndex > hashIndex)
) {
return uri;
}
const scheme = uri.substring(0, colonIndex + 1).toLowerCase();
if (PERMITTED_SCHEME_REGEX.test(scheme)) {
return uri;
}
return undefined;
}
Loading…
Cancel
Save