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}>;