/* eslint-disable react-refresh/only-export-components -- module-level schema, not a component file */ import { audioParse, createCodeBlockSpec, BlockNoteSchema, createAudioBlockConfig, createVideoBlockConfig, defaultInlineContentSpecs, videoParse, } from '@blocknote/core' import { AudioBlock, AudioToExternalHTML, createReactBlockSpec, createReactInlineContentSpec, VideoBlock, VideoToExternalHTML, } from '@blocknote/react' import { lazy, Suspense, type ComponentProps } from 'react' import { resolveWikilinkColor as resolveColor } from '../utils/wikilinkColors' import { resolveEntry } from '../utils/wikilink' import { MATH_BLOCK_TYPE, MATH_INLINE_TYPE, renderMathToHtml } from '../utils/mathMarkdown' import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown' import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown' import type { VaultEntry } from '../types' import { createTolariaCodeBlockOptions } from './codeBlockOptions' import { NoteTitleIcon } from './NoteTitleIcon' import { MermaidDiagram } from './MermaidDiagram' import { SafeHtmlSpan } from './SafeMarkup' import { updateTldrawBlockPropsSafely } from './tldrawBlockProps' import { useExternalMediaPreview } from '../utils/mediaPreviewRuntime' const TldrawWhiteboard = lazy(() => import('./TldrawWhiteboard').then(module => ({ default: module.TldrawWhiteboard, }))) type AudioBlockProps = ComponentProps type VideoBlockProps = ComponentProps type MediaBlockPreviewProps = { block: { props: { showPreview: boolean } } } // Module-level cache so the WikiLink renderer (defined outside React) can access entries export const _wikilinkEntriesRef: { current: VaultEntry[] } = { current: [] } function resolveWikilinkColor(target: string) { return resolveColor(_wikilinkEntriesRef.current, target) } /** Resolve the display text and optional note icon for a wikilink target. * Priority: pipe display text → entry title → humanised path stem */ function resolveDisplayInfo(target: string): { text: string; icon: string | null } { const pipeIdx = target.indexOf('|') if (pipeIdx !== -1) { const entry = resolveEntry(_wikilinkEntriesRef.current, target.slice(0, pipeIdx)) return { text: target.slice(pipeIdx + 1), icon: entry?.icon ?? null } } const entry = resolveEntry(_wikilinkEntriesRef.current, target) if (entry) { return { text: entry.title, icon: entry.icon ?? null } } const last = target.split('/').pop() ?? target return { text: last.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), icon: null } } export const WikiLink = createReactInlineContentSpec( { type: "wikilink" as const, propSchema: { target: { default: "" }, }, content: "none", }, { render: (props) => { const target = props.inlineContent.props.target const { color, isBroken } = resolveWikilinkColor(target) const { text, icon } = resolveDisplayInfo(target) return ( {text} ) }, } ) function MathRender({ latex, displayMode }: { latex: string; displayMode: boolean }) { const source = displayMode ? `$$\n${latex}\n$$` : `$${latex}$` return ( ) } export const MathInline = createReactInlineContentSpec( { type: MATH_INLINE_TYPE, propSchema: { latex: { default: '' }, }, content: 'none', }, { render: (props) => ( ), }, ) const MathBlock = createReactBlockSpec( { type: MATH_BLOCK_TYPE, propSchema: { latex: { default: '' }, }, content: 'none', }, { render: (props) => (
), }, ) function readCodeElementLanguage(code: Element): string | null { const language = code.getAttribute('data-language') ?? Array.from(code.classList) .find(className => className.startsWith('language-')) ?.replace(/^language-/u, '') if (!language) return null return language.trim().split(/\s+/u)[0]?.toLowerCase() ?? null } function readMermaidPreElement(element: HTMLElement): { source: string; diagram: string } | undefined { if (element.tagName !== 'PRE') return undefined if (element.childElementCount !== 1 || element.firstElementChild?.tagName !== 'CODE') return undefined const code = element.firstElementChild if (readCodeElementLanguage(code) !== 'mermaid') return undefined const diagram = code.textContent?.endsWith('\n') ? code.textContent : `${code.textContent ?? ''}\n` return { diagram, source: mermaidFenceSource({ diagram }), } } const MermaidBlock = createReactBlockSpec( { type: MERMAID_BLOCK_TYPE, propSchema: { source: { default: '' }, diagram: { default: '' }, }, content: 'none', }, { runsBefore: ['codeBlock'], parse: readMermaidPreElement, render: (props) => ( ), }, ) export function mediaBlockPropsForPreviewRuntime( props: T, externalMediaPreview: boolean, ): T { if (!externalMediaPreview) return props return { ...props, block: { ...props.block, props: { ...props.block.props, showPreview: false, }, }, } } export function TolariaAudioBlock(props: AudioBlockProps) { const externalMediaPreview = useExternalMediaPreview() return } export function TolariaVideoBlock(props: VideoBlockProps) { const externalMediaPreview = useExternalMediaPreview() return } const AudioBlockSpec = createReactBlockSpec( createAudioBlockConfig, (config) => ({ render: TolariaAudioBlock, parse: audioParse(config), toExternalHTML: AudioToExternalHTML, runsBefore: ['file'], }), ) const VideoBlockSpec = createReactBlockSpec( createVideoBlockConfig, (config) => ({ render: TolariaVideoBlock, parse: videoParse(config), toExternalHTML: VideoToExternalHTML, runsBefore: ['file'], }), ) const TldrawBlock = createReactBlockSpec( { type: TLDRAW_BLOCK_TYPE, propSchema: { boardId: { default: '' }, height: { default: TLDRAW_DEFAULT_HEIGHT }, snapshot: { default: '{}' }, width: { default: '' }, }, content: 'none', }, { runsBefore: ['codeBlock'], meta: { selectable: false }, render: (props) => ( }> { updateTldrawBlockPropsSafely({ blockId: props.block.id, editor: props.editor, nextProps: (currentProps) => ({ ...currentProps, snapshot, }), }) }} onSizeChange={(size) => { updateTldrawBlockPropsSafely({ blockId: props.block.id, editor: props.editor, nextProps: (currentProps) => ({ ...currentProps, height: size.height, width: size.width, }), }) }} /> ), }, ) const codeBlock = createCodeBlockSpec(createTolariaCodeBlockOptions()) const audioBlock = AudioBlockSpec() const mathBlock = MathBlock() const mermaidBlock = MermaidBlock() const tldrawBlock = TldrawBlock() const videoBlock = VideoBlockSpec() export const schema = BlockNoteSchema.create({ inlineContentSpecs: { ...defaultInlineContentSpecs, wikilink: WikiLink, mathInline: MathInline, }, }).extend({ blockSpecs: { audio: audioBlock, mathBlock, mermaidBlock, tldrawBlock, codeBlock, video: videoBlock, }, })