/** * Converts a Markdown+JSX string into a Lexical AST node array. * Specifically adapted for klz-cables.com custom Component Blocks. */ function propValue(chunk: string, prop: string): string { // Match prop="value" or prop='value' or prop={value} // and also multiline props like prop={\n [\n {...}\n ]\n} // For arrays or complex objects passed as props, basic regex might fail, // but the MDX in klz-cables usually uses simpler props or children. const match = chunk.match(new RegExp(`${prop}=["']([^"']+)["']`)) || chunk.match(new RegExp(`${prop}=\\{([^}]+)\\}`)); return match ? match[1] : ''; } function extractItemsProp(chunk: string, startTag: string): any[] { // Match items={ [ ... ] } robustly without stopping at inner object braces const itemsMatch = chunk.match(/items=\{\s*(\[[\s\S]*?\])\s*\}/); if (itemsMatch) { try { const arrayString = itemsMatch[1].trim(); // Since klz-cables MDX passes pure JS object arrays like `items={[{title: 'A', content: 'B'}]}`, // parsing it via Regex to JSON is extremely brittle due to unquoted keys and trailing commas. // Using `new Function` safely evaluates the array AST directly in this Node script environment. const fn = new Function(`return ${arrayString};`); return fn(); } catch (_e: any) { console.warn(`Could not parse items array for block ${startTag}:`, _e.message); return []; } } return []; } function blockNode(blockType: string, fields: Record) { return { type: 'block', format: '', version: 2, fields: { blockType, ...fields } }; } function ensureChildren(parsedNodes: any[]): any[] { // Lexical root nodes require at least one child node, or validation fails if (parsedNodes.length === 0) { return [ { type: 'paragraph', format: '', indent: 0, version: 1, children: [{ mode: 'normal', type: 'text', text: ' ', version: 1 }], }, ]; } return parsedNodes; } function parseInlineMarkdown(text: string): any[] { // Simple regex-based inline parser for bold and italic // Matches **bold**, __bold__, *italic*, _italic_ const regex = /(\*\*|__|TextNode)(.*?)\1|(\*|_)(.*?)\3/g; const nodes: any[] = []; let lastIndex = 0; let match; const createTextNode = (content: string, format = 0) => ({ detail: 0, format, mode: 'normal', style: '', text: content, type: 'text', version: 1, }); const rawMatch = text.matchAll( /(\*\*(.*?)\*\*|__(.*?)__|(? lastIndex) { nodes.push(createTextNode(text.slice(lastIndex, offset))); } const boldContent = m[2] || m[3]; const italicContent = m[4] || m[5]; if (boldContent) { nodes.push(createTextNode(boldContent, 1)); // 1 = Bold } else if (italicContent) { nodes.push(createTextNode(italicContent, 2)); // 2 = Italic } lastIndex = offset + m[0].length; } // Trailing plain text if (lastIndex < text.length) { nodes.push(createTextNode(text.slice(lastIndex))); } return nodes.length > 0 ? nodes : [createTextNode(text)]; } export function parseMarkdownToLexical(markdown: string): any[] { const paragraphNode = (text: string) => ({ type: 'paragraph', format: '', indent: 0, version: 1, direction: 'ltr', children: parseInlineMarkdown(text), }); const nodes: any[] = []; let content = markdown; // Strip frontmatter const fm = content.match(/^---\s*\n[\s\S]*?\n---/); if (fm) content = content.replace(fm[0], '').trim(); // 1. EXTRACT MULTILINE WRAPPERS BEFORE CHUNKING const extractBlocks = [ { tag: 'HighlightBox', regex: /]*)>([\s\S]*?)<\/HighlightBox>/g, build: (props: string, inner: string) => blockNode('highlightBox', { title: propValue(``, 'title'), color: propValue(``, 'color') || 'primary', content: { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr', children: ensureChildren(parseMarkdownToLexical(inner.trim())), }, }, }), }, { tag: 'ChatBubble', regex: /]*)>([\s\S]*?)<\/ChatBubble>/g, build: (props: string, inner: string) => blockNode('chatBubble', { author: propValue(``, 'author') || 'KLZ Team', avatar: propValue(``, 'avatar'), role: propValue(``, 'role') || 'Assistant', align: propValue(``, 'align') || 'left', content: { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr', children: ensureChildren(parseMarkdownToLexical(inner.trim())), }, }, }), }, { tag: 'Callout', regex: /]*)>([\s\S]*?)<\/Callout>/g, build: (props: string, inner: string) => blockNode('callout', { type: propValue(``, 'type') || 'info', title: propValue(``, 'title'), content: { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr', children: ensureChildren(parseMarkdownToLexical(inner.trim())), }, }, }), }, { tag: 'ProductTabs', regex: /]*)>([\s\S]*?)<\/ProductTabs>/g, build: (props: string, inner: string) => { const fullTag = ``; const dataMatch = fullTag.match(/data=\{({[\s\S]*?})\}\s*\/>/); let technicalItems = []; let voltageTables = []; if (dataMatch) { try { const parsedData = JSON.parse(dataMatch[1]); technicalItems = parsedData.technicalItems || []; voltageTables = parsedData.voltageTables || []; voltageTables.forEach((vt: any) => { vt.rows?.forEach((row: any) => { if (row.cells) { row.cells = row.cells.map((c: any) => typeof c !== 'object' ? { value: String(c) } : c, ); } }); }); } catch (e) { console.warn('Failed to parse ProductTabs JSON data:', e); } } return blockNode('productTabs', { technicalItems, voltageTables, content: { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr', children: ensureChildren(parseMarkdownToLexical(inner.trim())), }, }, }); }, }, ]; function cleanMdxContent(text: string): string { return text .replace(/]*>/g, '') .replace(/<\/section>/g, '') .replace(/]*>(.*?)<\/h3>/g, '### $1\n\n') .replace(/]*>(.*?)<\/p>/g, '$1\n\n') .replace(/]*>(.*?)<\/strong>/g, '**$1**') .replace(/ /g, ' ') .trim(); } content = cleanMdxContent(content); const placeholders = new Map(); let placeholderIdx = 0; for (const block of extractBlocks) { content = content.replace(block.regex, (match, propsMatch, innerMatch) => { const id = `__BLOCK_PLACEHOLDER_${placeholderIdx++}__`; placeholders.set(id, block.build(propsMatch, innerMatch)); return `\n\n${id}\n\n`; }); } // 2. CHUNK THE REST const rawChunks = content.split(/\n\s*\n/); for (let chunk of rawChunks) { chunk = chunk.trim(); if (!chunk) continue; if (chunk.startsWith('__BLOCK_PLACEHOLDER_')) { nodes.push(placeholders.get(chunk)); continue; } if (chunk.includes('