Files
tolaria/src/components/editorRawModeSync.ts
2026-05-13 02:18:19 +02:00

197 lines
5.5 KiB
TypeScript

import type { useCreateBlockNote } from '@blocknote/react'
import type { MutableRefObject, SetStateAction } from 'react'
import type { VaultEntry } from '../types'
import { hasDurableEditorBlocks } from '../utils/editorDurableMarkdown'
import {
serializeRichEditorDocumentToMarkdown,
} from '../utils/richEditorMarkdown'
interface Tab {
entry: VaultEntry
content: string
}
export interface PendingRawExitContent {
path: string
content: string
}
export interface RawModeContentTransition {
pendingExitContent: PendingRawExitContent | null
rawModeContentOverride: PendingRawExitContent | null
}
export function createRawModeContentTransition(): RawModeContentTransition {
return {
pendingExitContent: null,
rawModeContentOverride: null,
}
}
function resolvePendingRawExitContentAction(
action: SetStateAction<PendingRawExitContent | null>,
current: PendingRawExitContent | null,
): PendingRawExitContent | null {
return typeof action === 'function' ? action(current) : action
}
export function withPendingRawExitContent(
transition: RawModeContentTransition,
action: SetStateAction<PendingRawExitContent | null>,
): RawModeContentTransition {
return {
...transition,
pendingExitContent: resolvePendingRawExitContentAction(action, transition.pendingExitContent),
}
}
export function withRawModeContentOverride(
transition: RawModeContentTransition,
action: SetStateAction<PendingRawExitContent | null>,
): RawModeContentTransition {
return {
...transition,
rawModeContentOverride: resolvePendingRawExitContentAction(action, transition.rawModeContentOverride),
}
}
export function buildPendingRawExitContent(
path: string | null,
content: string | null,
): PendingRawExitContent | null {
if (!path || content === null) return null
return { path, content }
}
export function serializeEditorDocumentToMarkdown(
editor: ReturnType<typeof useCreateBlockNote>,
tabContent: string,
vaultPath?: string,
notePath?: string,
): string {
return serializeRichEditorDocumentToMarkdown(editor, tabContent, vaultPath, notePath)
}
export function applyPendingRawExitContent(
tabs: Tab[],
pendingRawExitContent: PendingRawExitContent | null,
): Tab[] {
if (!pendingRawExitContent) return tabs
let changed = false
const nextTabs = tabs.map((tab) => {
if (tab.entry.path !== pendingRawExitContent.path || tab.content === pendingRawExitContent.content) {
return tab
}
changed = true
return {
...tab,
content: pendingRawExitContent.content,
}
})
return changed ? nextTabs : tabs
}
function syncContentIfChanged({
currentContent,
nextContent,
path,
onContentChange,
}: {
currentContent: string | null
nextContent: string
path: string
onContentChange?: (path: string, content: string) => void
}) {
if (currentContent === nextContent) return
onContentChange?.(path, nextContent)
}
export function syncActiveTabIntoRawBuffer(options: {
editor: ReturnType<typeof useCreateBlockNote>
activeTabPath: string | null
activeTabContent: string | null
rawLatestContentRef: MutableRefObject<string | null>
serializeRichEditorContent?: boolean
vaultPath?: string
}) {
const {
editor,
activeTabPath,
activeTabContent,
rawLatestContentRef,
serializeRichEditorContent = true,
vaultPath,
} = options
if (!activeTabPath || activeTabContent === null) return null
const shouldSerializeRichEditorContent = serializeRichEditorContent || hasDurableEditorBlocks(editor.document)
const syncedContent = shouldSerializeRichEditorContent
? serializeEditorDocumentToMarkdown(editor, activeTabContent, vaultPath, activeTabPath)
: activeTabContent
rawLatestContentRef.current = syncedContent
return syncedContent
}
export function rememberPendingRawExitContent(options: {
activeTabPath: string | null
activeTabContent: string | null
rawInitialContent: string | null
rawLatestContentRef: MutableRefObject<string | null>
onContentChange?: (path: string, content: string) => void
}) {
const {
activeTabPath,
activeTabContent,
rawInitialContent,
rawLatestContentRef,
onContentChange,
} = options
const pendingRawExitContent = buildPendingRawExitContent(activeTabPath, rawLatestContentRef.current)
if (!pendingRawExitContent) return null
if (pendingRawExitContent.content === rawInitialContent) return null
if (pendingRawExitContent.content === activeTabContent) return null
syncContentIfChanged({
currentContent: activeTabContent,
nextContent: pendingRawExitContent.content,
path: pendingRawExitContent.path,
onContentChange,
})
return pendingRawExitContent
}
export function resolvePendingRawExitContent(options: {
activeTabPath: string | null
tabs: Tab[]
pendingRawExitContent: PendingRawExitContent | null
}) {
const { activeTabPath, tabs, pendingRawExitContent } = options
if (!pendingRawExitContent) return null
if (activeTabPath !== pendingRawExitContent.path) {
return null
}
const syncedTab = tabs.find((tab) => tab.entry.path === pendingRawExitContent.path)
if (syncedTab?.content === pendingRawExitContent.content) {
return null
}
return pendingRawExitContent
}
export function resolveRawModeContent(options: {
activeTab: Tab | null
rawModeContentOverride: PendingRawExitContent | null
}) {
const { activeTab, rawModeContentOverride } = options
if (!activeTab) return null
if (rawModeContentOverride?.path === activeTab.entry.path) {
return rawModeContentOverride.content
}
return activeTab.content
}