import React from 'react'; import { BlogSection, Callout, CheckList, type CheckListItem, CodeBlock, Mermaid, Table, } from '@san-siva/blogkit'; import type { Root, RootContent } from 'mdast'; import { extractText } from './extractText'; import type { Section } from './groupSections'; import { groupSections } from './groupSections'; import { renderPhrasingContent } from './renderPhrasingContent'; import styles from '@san-siva/stylekit/styles/index.module.scss'; type Counters = { mermaid: number }; function renderNode({ node, key, nextNode, inList = false, inCallout = false, counters, }: { node: RootContent; key: number; nextNode?: RootContent; inList?: boolean; inCallout?: boolean; counters: Counters; }): React.ReactNode { switch (node.type) { case 'paragraph': { if (inList) { return

{renderPhrasingContent(node.children)}

; } const isFollowedByParagraph = nextNode?.type === 'paragraph'; const isLastInCallout = !nextNode && inCallout; const marginClass = isFollowedByParagraph ? styles['margin-bottom--1'] : isLastInCallout ? undefined : styles['margin-bottom--2']; return (

{renderPhrasingContent(node.children)}

); } case 'code': { if (node.lang === 'mermaid') { const mermaidId = counters.mermaid++; return ( ); } return ( ); } case 'heading': { return (

{renderPhrasingContent(node.children)}

); } case 'thematicBreak': { return
; } case 'table': { const [headerRow, ...bodyRows] = node.children; const headers = headerRow?.children.map(cell => extractText(cell.children) ); const rows = bodyRows.map(row => row.children.map((cell, index) => (

{renderPhrasingContent(cell.children)}

)) ); return ( ); } case 'blockquote': { const children = node.children as RootContent[]; let calloutType: 'info' | 'warning' | 'error' = 'info'; let strippedChildren = children; const firstChild = children[0]; if (firstChild?.type === 'paragraph') { const firstInline = firstChild.children[0]; if (firstInline?.type === 'text') { const githubAlertMatch = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i.exec( firstInline.value ); if (githubAlertMatch) { const alertType = githubAlertMatch[1].toUpperCase(); calloutType = alertType === 'WARNING' ? 'warning' : alertType === 'CAUTION' ? 'error' : 'info'; strippedChildren = children.slice(1); } } } return (
{strippedChildren.map((child, index) => renderNode({ node: child, key: index, nextNode: strippedChildren[index + 1], inCallout: true, counters, }) )}
); } case 'list': { const isTaskList = node.children.every( item => item.checked !== null && item.checked !== undefined ); if (isTaskList) { const items: CheckListItem[] = node.children.map((item, index) => { const para = item.children.find(c => c.type === 'paragraph'); const children = para?.type === 'paragraph' ? (

{renderPhrasingContent(para.children)}

) : (

); return { id: String(index), isChecked: item.checked === true, children, }; }); return ; } const Tag = node.ordered ? 'ol' : 'ul'; return ( {node.children.map((item, index) => (

  • {item.children.map((child, index) => renderNode({ node: child as RootContent, key: index, inList: true, counters, }) )}
  • ))} ); } default: { return null; } } } function renderNodes( nodes: RootContent[], counters: Counters ): React.ReactNode[] { return nodes.map((node, index) => renderNode({ node, key: index, nextNode: nodes[index + 1], counters }) ); } function renderSection( section: Section, counters: Counters, key = -1 ): React.ReactNode { const title = section.titleNodes.length > 0 ? (

    {renderPhrasingContent(section.titleNodes)}

    ) : ( '' ); return ( {renderNodes(section.nodes, counters)} {section.subsections.map((subsection, index) => renderSection(subsection, counters, index) )} ); } export type RenderedMarkdown = { sections: React.ReactNode[]; }; export const renderMarkdownAst = (ast: Root): RenderedMarkdown => { const counters: Counters = { mermaid: 0 }; const grouped = groupSections(ast.children); return { sections: grouped.map((section, index) => renderSection(section, counters, index) ), }; }; export const MarkdownSections = ({ rendered, }: { rendered: RenderedMarkdown; }): React.ReactNode => <>{rendered.sections};