import { type BlockLike, type DurableBlockCodec, type DurableFencePayloadInput, injectDurableMarkdownBlocks, preProcessDurableMarkdownBlocks, readCodeBlockLanguage, readInlineText, } from './durableMarkdownBlocks' export const TLDRAW_BLOCK_TYPE = 'tldrawBlock' export const TLDRAW_DEFAULT_HEIGHT = '520' const TOKEN_PREFIX = '@@TOLARIA_TLDRAW_BLOCK:' const TOKEN_SUFFIX = '@@' interface TldrawPayload { boardId: string height: string snapshot: string width: string } interface SnapshotSource { snapshot: string } interface FenceAttribute { value: string } interface FenceAttributeRequest { info: string name: 'height' | 'id' | 'width' } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function decodeTldrawPayload(payload: unknown): TldrawPayload | null { if (!isRecord(payload)) return null if (typeof payload.boardId !== 'string') return null if (typeof payload.snapshot !== 'string') return null return { boardId: payload.boardId, height: typeof payload.height === 'string' ? payload.height : TLDRAW_DEFAULT_HEIGHT, snapshot: payload.snapshot, width: typeof payload.width === 'string' ? payload.width : '', } } function readFenceAttribute({ info, name }: FenceAttributeRequest): string { for (const match of info.matchAll(/\b([A-Za-z][\w-]*)=(?:"([^"]+)"|'([^']+)'|([^\s]+))/gu)) { if (match.at(1) === name) return match.at(2) ?? match.at(3) ?? match.at(4) ?? '' } return '' } function readFenceMetadata(info: string): Pick { return { boardId: readFenceAttribute({ info, name: 'id' }), height: readFenceAttribute({ info, name: 'height' }) || TLDRAW_DEFAULT_HEIGHT, width: readFenceAttribute({ info, name: 'width' }), } } function readTldrawFenceMetadata(info: string): Pick | null { const [language = '', ...infoParts] = info.trim().split(/\s+/u) if (language.toLowerCase() !== 'tldraw') return null return readFenceMetadata(infoParts.join(' ')) } function buildTldrawPayload({ lines, start, end, metadata }: DurableFencePayloadInput): TldrawPayload { const fenceMetadata = metadata as Pick return { ...fenceMetadata, snapshot: lines.slice(start + 1, end).join('').trim(), } } function buildTldrawBlock(block: BlockLike, payload: TldrawPayload): BlockLike { return { ...block, type: TLDRAW_BLOCK_TYPE, props: { ...(block.props ?? {}), boardId: payload.boardId, height: payload.height, snapshot: payload.snapshot, width: payload.width, }, content: undefined, children: [], } } function readTldrawCodeBlock(block: BlockLike): TldrawPayload | null { if (block.type !== 'codeBlock') return null if (readCodeBlockLanguage({ block }) !== 'tldraw') return null const snapshot = readInlineText(block.content) if (snapshot === null) return null return { boardId: '', height: TLDRAW_DEFAULT_HEIGHT, snapshot: snapshot.trim(), width: '', } } function fenceLengthForSnapshot({ snapshot }: SnapshotSource): number { const longestRun = Math.max(0, ...Array.from(snapshot.matchAll(/`+/gu), match => match[0].length)) return Math.max(3, longestRun + 1) } function escapeFenceAttribute({ value }: FenceAttribute): string { return value.replace(/"/gu, '"') } export function tldrawFenceSource({ boardId, height, snapshot, width }: TldrawPayload): string { const fence = '`'.repeat(fenceLengthForSnapshot({ snapshot })) const metadata = tldrawFenceMetadata({ boardId, height, width }) const body = snapshot.endsWith('\n') ? snapshot : `${snapshot}\n` return `${fence}tldraw${metadata}\n${body}${fence}` } function tldrawFenceMetadata({ boardId, height, width }: Omit): string { const attributes: string[] = [] if (boardId) attributes.push(`id="${escapeFenceAttribute({ value: boardId })}"`) if (height) attributes.push(`height="${escapeFenceAttribute({ value: height })}"`) if (width) attributes.push(`width="${escapeFenceAttribute({ value: width })}"`) return attributes.length > 0 ? ` ${attributes.join(' ')}` : '' } export function isTldrawBlock(block: BlockLike): boolean { return block.type === TLDRAW_BLOCK_TYPE && typeof block.props?.snapshot === 'string' && typeof block.props?.boardId === 'string' } export function tldrawMarkdown(block: BlockLike): string { const props = block.props ?? {} return tldrawFenceSource({ boardId: props.boardId ?? '', height: props.height ?? TLDRAW_DEFAULT_HEIGHT, snapshot: props.snapshot ?? '{}', width: props.width ?? '', }) } export const tldrawMarkdownCodec: DurableBlockCodec = { tokenPrefix: TOKEN_PREFIX, tokenSuffix: TOKEN_SUFFIX, readFenceMetadata: readTldrawFenceMetadata, buildPayload: buildTldrawPayload, decodePayload: decodeTldrawPayload, buildBlock: (block, payload) => buildTldrawBlock(block, payload as TldrawPayload), readCodeBlock: readTldrawCodeBlock, isBlock: isTldrawBlock, serializeBlock: tldrawMarkdown, } export function preProcessTldrawMarkdown({ markdown }: { markdown: string }): string { return preProcessDurableMarkdownBlocks({ markdown, codecs: [tldrawMarkdownCodec] }) } export function injectTldrawInBlocks(blocks: unknown[]): unknown[] { return injectDurableMarkdownBlocks({ blocks, codecs: [tldrawMarkdownCodec] }) }