import type { ReactNode } from 'react';
import styles from '../ReferenceDialog.module.css';
interface MarkdownSection {
id: string;
content: string;
}
interface MarkdownContentProps {
markdown: string;
idPrefix: string;
query?: string;
}
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[`*[\]().]/g, '')
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/^-+|-+$/g, '');
}
function sectionize(markdown: string, idPrefix: string): MarkdownSection[] {
const sections: MarkdownSection[] = [];
let lines: string[] = [];
let index = 0;
const commit = () => {
if (lines.length === 0) return;
const heading = lines.find((line) => /^#{1,2}\s+/.test(line));
const title = heading?.replace(/^#{1,2}\s+/, '') ?? `section-${index + 1}`;
sections.push({ id: `${idPrefix}-${slugify(title)}-${index++}`, content: lines.join('\n') });
lines = [];
};
for (const line of markdown.split('\n')) {
if (lines.length > 0 && /^##\s+/.test(line)) commit();
lines.push(line);
}
commit();
return sections;
}
function renderInline(text: string, idPrefix: string): ReactNode[] {
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*\n]+\*|\[[^\]]+\]\([^)]*\))/g;
const parts: ReactNode[] = [];
let lastIndex = 0;
let key = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(text)) !== null) {
if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
const token = match[0];
if (token[0] === '`') {
parts.push(
{token.slice(1, -1)}
,
);
} else if (token.startsWith('**')) {
parts.push({renderInline(token.slice(2, -2), idPrefix)});
} else if (token[0] === '*') {
parts.push({renderInline(token.slice(1, -1), idPrefix)});
} else {
const link = token.match(/\[([^\]]+)\]\(([^)]*)\)/);
if (!link) {
parts.push(token);
} else if (link[2].startsWith('#')) {
const targetId = `${idPrefix}-${slugify(link[2].slice(1))}`;
parts.push(
{
event.preventDefault();
document
.getElementById(targetId)
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}}
>
{renderInline(link[1], idPrefix)}
,
);
} else {
parts.push({renderInline(link[1], idPrefix)});
}
}
lastIndex = match.index + token.length;
}
if (lastIndex < text.length) parts.push(text.slice(lastIndex));
return parts;
}
function splitTableRow(line: string): string[] {
const cells: string[] = [];
let cell = '';
let inCode = false;
for (const char of line.trim().slice(1, -1)) {
if (char === '`') inCode = !inCode;
if (char === '|' && !inCode) {
cells.push(cell.trim());
cell = '';
} else {
cell += char;
}
}
cells.push(cell.trim());
return cells;
}
function parseMarkdown(markdown: string, idPrefix: string): ReactNode[] {
const lines = markdown.split('\n');
const blocks: ReactNode[] = [];
let i = 0;
let key = 0;
while (i < lines.length) {
const line = lines[i];
if (line.trim() === '---') {
blocks.push(
{codeLines.join('\n')}
,
);
continue;
}
const heading = line.match(/^(#{1,3})\s+(.*)/);
if (heading) {
const level = heading[1].length;
const className = level === 1 ? styles.tutH1 : level === 2 ? styles.tutH2 : styles.tutH3;
const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
blocks.push(
{renderInline(quoteLines.join(' '), idPrefix)}, ); continue; } if (line.startsWith('|')) { const tableLines: string[] = []; while (i < lines.length && lines[i].startsWith('|')) tableLines.push(lines[i++]); const separator = /^\|[-|: ]+\|$/; const dataStart = tableLines.length > 1 && separator.test(tableLines[1].trim()) ? 2 : 1; blocks.push(
| {renderInline(cell, idPrefix)} | ))}
|---|
| {renderInline(cell, idPrefix)} | ))}
{renderInline(paragraphLines.join(' '), idPrefix)}
, ); } return blocks; } export function MarkdownContent({ markdown, idPrefix, query = '' }: MarkdownContentProps) { const normalizedQuery = query.trim().toLowerCase(); const sections = sectionize(markdown, idPrefix).filter( (section) => !normalizedQuery || section.content.toLowerCase().includes(normalizedQuery), ); if (sections.length === 0) { return