import type { ReactNode } from 'react';
/**
* Renders the small markdown subset allowed in config strings (banner content,
* error page descriptions): links, bold, italic, and inline code. Custom
* components are deliberately not supported — these strings come from
* docs.json, not MDX.
*/
interface InlinePattern {
regex: RegExp;
render: (match: RegExpExecArray, key: number) => ReactNode;
}
const PATTERNS: InlinePattern[] = [
{
// [label](https://example.com)
regex: /\[([^\]]+)\]\(([^)\s]+)\)/,
render: (match, key) => {
const external = /^https?:\/\//.test(match[2]);
return (
{renderInlineMarkdown(match[1])}
);
},
},
{
// **bold**
regex: /\*\*([^*]+)\*\*/,
render: (match, key) => {renderInlineMarkdown(match[1])},
},
{
// *italic* (single asterisk, not part of **)
regex: /(? {renderInlineMarkdown(match[1])},
},
{
// _italic_
regex: /_([^_]+)_/,
render: (match, key) => {renderInlineMarkdown(match[1])},
},
{
// `code` — contents render verbatim
regex: /`([^`]+)`/,
render: (match, key) => {match[1]},
},
];
export function renderInlineMarkdown(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
let remaining = text;
let key = 0;
while (remaining) {
let earliest: { index: number; match: RegExpExecArray; pattern: InlinePattern } | null = null;
for (const pattern of PATTERNS) {
const match = pattern.regex.exec(remaining);
if (match && (!earliest || match.index < earliest.index)) {
earliest = { index: match.index, match, pattern };
}
}
if (!earliest) {
nodes.push(remaining);
break;
}
if (earliest.index > 0) {
nodes.push(remaining.slice(0, earliest.index));
}
nodes.push(earliest.pattern.render(earliest.match, key++));
remaining = remaining.slice(earliest.index + earliest.match[0].length);
}
return nodes;
}