import { Fragment, type ReactNode } from "react"; import { normalizeCalloutKind } from "@/shared/editor/schema/callout-model"; import { resolveLanguage } from "@/shared/editor/schema/code-block-model"; import { CalloutPresentation } from "@/shared/editor/presentation/callout"; import { highlightCodeToSegments } from "@/shared/editor/highlight/code-highlight-runtime"; import { CodeBlockPresentation, getCodeLanguageLabel } from "@/shared/editor/presentation/code-block"; import { PageMentionPresentation } from "@/shared/editor/presentation/page-mention"; import { readOutlineLevel, type OutlineLevel } from "@/shared/editor/model/outline"; import type { SitesImageProps } from "@/shared/sites/island-schemas"; import { readOptionalSitesReactRenderContext } from "@/sites/react-render-context"; import { CopyCodeButtonIslandHost } from "@/sites/island-hosts/copy-code-button"; import { SitesImageIslandHost } from "@/sites/island-hosts/sites-image"; import { readOptionalString, readPositiveNumber } from "./attrs"; import { normalizeImageBrightness } from "@/shared/editor/schema/image-model"; import type { StaticNodeMappingProps } from "./types"; type HeadingTag = `h${OutlineLevel}`; type ParagraphTextAlign = "left" | "center" | "right" | "justify"; const HEADING_TAGS: Record = { 1: "h1", 2: "h2", 3: "h3", 4: "h4", 5: "h5", 6: "h6", }; export function createBlandSitesStaticNodeMappings() { const renderContext = readOptionalSitesReactRenderContext(); const pageContext = renderContext?.kind === "page" ? renderContext : null; let headingIndex = 0; return { heading: ({ node, children }: StaticNodeMappingProps) => { const level = readOutlineLevel(node.attrs.level); const Tag = readHeadingTag(level); const bid = readOptionalString(node.attrs.bid); const anchorId = pageContext ? readHeadingAnchorId(pageContext.headingAnchorIds, headingIndex++) : undefined; const textAlign = readParagraphTextAlign(node.attrs.textAlign); const style = textAlign ? { textAlign } : undefined; return ( {children} ); }, paragraph: ({ node, children }: StaticNodeMappingProps) => { const bid = readOptionalString(node.attrs.bid); const textAlign = readParagraphTextAlign(node.attrs.textAlign); const style = textAlign ? { textAlign } : undefined; return (

{node.childCount === 0 ?
: children}

); }, taskItem: ({ node, children }: StaticNodeMappingProps) => { const checked = readBooleanTaskChecked(node.attrs.checked); return (
  • {children}
  • ); }, details: ({ node, children }: StaticNodeMappingProps) => { const open = node.attrs.open === true || node.attrs.open === "true"; const bid = readOptionalString(node.attrs.bid); return (
    {children}
    ); }, detailsSummary: ({ node, children }: StaticNodeMappingProps) => { const placeholder = readOptionalString(node.attrs.placeholder) ?? "Summary"; return ( {node.childCount === 0 ?
    : children}
    ); }, detailsContent: ({ children }: StaticNodeMappingProps) => (
    {children}
    ), callout: ({ node, children }: StaticNodeMappingProps) => ( {children} ), codeBlock: ({ node }: StaticNodeMappingProps) => { const language = resolveLanguage(node.attrs.language); return ( } > {renderHighlightedCode(language, node.textContent)} ); }, image: ({ node }: StaticNodeMappingProps) => { const imageProps = readImageProps(node); if (!imageProps) return null; return ; }, pageMention: ({ node }: StaticNodeMappingProps) => { const pageId = readOptionalString(node.attrs.pageId); const mention = pageId && pageContext ? pageContext.resolvePageMention(pageId) : null; return ( ); }, tableRow: ({ node, children }: StaticNodeMappingProps) => { const height = readPositiveNumber(node.attrs.height); const style = height ? { height: `${height}px` } : undefined; return {children}; }, tableCell: ({ node, children }: StaticNodeMappingProps) => { const colSpan = readPositiveNumber(node.attrs.colspan) ?? 1; const rowSpan = readPositiveNumber(node.attrs.rowspan) ?? 1; const colwidth = Array.isArray(node.attrs.colwidth) ? (node.attrs.colwidth as number[]) : null; return ( {children} ); }, tableHeader: ({ node, children }: StaticNodeMappingProps) => { const colSpan = readPositiveNumber(node.attrs.colspan) ?? 1; const rowSpan = readPositiveNumber(node.attrs.rowspan) ?? 1; const colwidth = Array.isArray(node.attrs.colwidth) ? (node.attrs.colwidth as number[]) : null; return ( {children} ); }, }; } function readHeadingTag(level: OutlineLevel): HeadingTag { return HEADING_TAGS[level]; } function readHeadingAnchorId(anchorIds: readonly (string | null)[], index: number): string | undefined { const anchorId = anchorIds[index]; return typeof anchorId === "string" ? anchorId : undefined; } function readParagraphTextAlign(value: unknown): ParagraphTextAlign | null { switch (value) { case "left": case "center": case "right": case "justify": return value; default: return null; } } function readImageProps(node: StaticNodeMappingProps["node"]): SitesImageProps | null { const src = readOptionalString(node.attrs.src); if (!src) return null; const align = node.attrs.align === "center" || node.attrs.align === "right" ? node.attrs.align : "left"; return { bid: readOptionalString(node.attrs.bid), src, alt: readOptionalString(node.attrs.alt), title: readOptionalString(node.attrs.title), align, width: readPositiveNumber(node.attrs.width), naturalWidth: readPositiveNumber(node.attrs.naturalWidth), naturalHeight: readPositiveNumber(node.attrs.naturalHeight), brightness: normalizeImageBrightness(node.attrs.brightness), }; } function readBooleanTaskChecked(value: unknown): boolean { return value === true || value === "true" || value === ""; } function getTaskItemCheckboxLabel(textContent: string, checked: boolean): string { const label = textContent.trim() || "empty task item"; return `Task item checkbox for ${label}${checked ? ", checked" : ", unchecked"}`; } function StaticCodeBlockControls({ language }: { language: string }) { const displayName = getCodeLanguageLabel(language); return (
    ); } function renderHighlightedCode(language: string, code: string): ReactNode[] { return highlightCodeToSegments(language, code).map((segment, index) => { if (segment.classes.length === 0) { return {segment.text}; } return ( {segment.text} ); }); }