import { useCallback, useEffect, useRef, useState } from 'react'; import { AppShell } from '@add-ideas/toolbox-shell-react'; import '@add-ideas/toolbox-shell-react/styles.css'; import { DiagnosticsPanel } from './components/DiagnosticsPanel.js'; import { ExportControls, type OneNoteExportFormat, } from './components/ExportControls.js'; import { FileDropZone } from './components/FileDropZone.js'; import { HelpDialog } from './components/HelpDialog.js'; import { PackageInspector } from './components/PackageInspector.js'; import { PageReader } from './components/PageReader.js'; import { PackageNavigation, SingleSectionNavigation, } from './components/SectionNavigation.js'; import type { OneNoteNotebookNodeDto, OneNotePackageDto, OneNotePackagedSectionDto, OneNotePageDto, OneNoteResourcePayloadDto, OneNoteSectionDto, } from './onenote/model/dto.js'; import type { ParserDiagnostic } from './onenote/model/diagnostics.js'; import { selectSourceFiles } from './source-file.js'; import { toolboxApp } from './toolbox/manifest.js'; import { OneNoteWorkerClient, WorkerClientError, } from './worker/onenote.client.js'; type LoadedSource = | { kind: 'one'; sessionId: string; section: OneNoteSectionDto; } | { kind: 'onepkg'; sessionId: string; notebook: OneNotePackageDto; }; type ApplicationState = | { phase: 'idle' } | { phase: 'reading'; fileName: string } | { phase: 'loaded'; fileName: string; source: LoadedSource } | { phase: 'error'; title: string; message: string; detail?: string; }; type PageState = | { phase: 'empty' } | { phase: 'loading'; pageId: string } | { phase: 'loaded'; page: OneNotePageDto } | { phase: 'error'; message: string }; function uniqueDiagnostics( diagnostics: ParserDiagnostic[] ): ParserDiagnostic[] { const seen = new Set(); return diagnostics.filter((diagnostic) => { const key = JSON.stringify([ diagnostic.severity, diagnostic.code, diagnostic.message, diagnostic.offset, diagnostic.structure, diagnostic.propertyId, ]); if (seen.has(key)) return false; seen.add(key); return true; }); } function sectionsInNotebookOrder( notebook: OneNotePackageDto ): OneNotePackagedSectionDto[] { const sectionById = new Map( notebook.sections.map((section) => [section.id, section] as const) ); const ordered: OneNotePackagedSectionDto[] = []; const seen = new Set(); const visit = (nodes: OneNoteNotebookNodeDto[]) => { for (const node of nodes) { if (node.kind === 'section-group') { visit(node.children); continue; } const section = sectionById.get(node.sectionId); if (section && !seen.has(section.id)) { ordered.push(section); seen.add(section.id); } } }; visit(notebook.tree.nodes); ordered.push(...notebook.sections.filter((section) => !seen.has(section.id))); return ordered; } export interface OneNoteApplicationProps { createWorkerClient?: () => OneNoteWorkerClient; } export function OneNoteApplication({ createWorkerClient = () => new OneNoteWorkerClient(), }: OneNoteApplicationProps) { const [state, setState] = useState({ phase: 'idle' }); const [pageState, setPageState] = useState({ phase: 'empty' }); const [selectedSectionId, setSelectedSectionId] = useState< string | undefined >(); const activeClient = useRef(null); const generation = useRef(0); const pageGeneration = useRef(0); const clearFile = useCallback(() => { generation.current += 1; pageGeneration.current += 1; activeClient.current?.terminate(); activeClient.current = null; setSelectedSectionId(undefined); setPageState({ phase: 'empty' }); setState({ phase: 'idle' }); }, []); useEffect(() => { return () => { generation.current += 1; pageGeneration.current += 1; activeClient.current?.terminate(); }; }, []); const loadPage = async ( sessionId: string, pageId: string, sectionId?: string ) => { const client = activeClient.current; const requestGeneration = ++pageGeneration.current; if (!client) { setPageState({ phase: 'error', message: 'The local parser session is no longer available.', }); return; } setPageState({ phase: 'loading', pageId }); try { const response = await client.getPage(sessionId, pageId, sectionId); if (requestGeneration !== pageGeneration.current) return; if (response.type === 'failure') { setPageState({ phase: 'error', message: response.error.message }); return; } setPageState({ phase: 'loaded', page: response.page }); } catch (error) { if (requestGeneration !== pageGeneration.current) return; setPageState({ phase: 'error', message: error instanceof WorkerClientError ? error.message : 'The selected page could not be read from the parser session.', }); } }; const openFiles = async (files: File[]) => { const selection = selectSourceFiles(files); if (!selection.ok) { setState({ phase: 'error', title: selection.title, message: selection.message, }); return; } generation.current += 1; pageGeneration.current += 1; const requestGeneration = generation.current; activeClient.current?.terminate(); const client = createWorkerClient(); activeClient.current = client; setSelectedSectionId(undefined); setPageState({ phase: 'empty' }); setState({ phase: 'reading', fileName: selection.source.name }); try { const buffers = await Promise.all( [selection.source, ...selection.cabinetParts].map((file) => file.arrayBuffer() ) ); if (generation.current !== requestGeneration) return; const response = await client.parseSource( selection.kind, selection.source.name, buffers[0]!, selection.cabinetParts.map((part, index) => ({ fileName: part.name, bytes: buffers[index + 1]!, })) ); if (generation.current !== requestGeneration) return; if (response.type === 'failure') { client.terminate(); if (activeClient.current === client) activeClient.current = null; setState({ phase: 'error', title: 'Could not open this OneNote source', message: response.error.message, detail: response.error.structure ? `${response.error.code} ยท ${response.error.structure}` : response.error.code, }); return; } if (response.type === 'section') { setState({ phase: 'loaded', fileName: selection.source.name, source: { kind: 'one', sessionId: response.sessionId, section: response.section, }, }); const firstPage = response.section.pages[0]; if (firstPage) void loadPage(response.sessionId, firstPage.id); return; } const orderedSections = sectionsInNotebookOrder(response.notebook); const preferredSection = orderedSections.find( (section) => section.parseStatus === 'parsed' && section.section !== undefined && section.section.pages.length > 0 ) ?? orderedSections.find( (section) => section.parseStatus === 'parsed' && section.section ) ?? orderedSections[0]; setSelectedSectionId(preferredSection?.id); setState({ phase: 'loaded', fileName: selection.source.name, source: { kind: 'onepkg', sessionId: response.sessionId, notebook: response.notebook, }, }); const firstPage = preferredSection?.section?.pages[0]; if (firstPage) { void loadPage(response.sessionId, firstPage.id, preferredSection?.id); } } catch (error) { if (generation.current !== requestGeneration) return; client.terminate(); if (activeClient.current === client) activeClient.current = null; setState({ phase: 'error', title: 'Local parser stopped', message: error instanceof WorkerClientError ? error.message : 'The local file could not be handed to the parser worker.', }); } }; const selectedPageId = pageState.phase === 'loading' ? pageState.pageId : pageState.phase === 'loaded' ? pageState.page.id : undefined; const selectedPage = pageState.phase === 'loaded' ? pageState.page : undefined; const selectPackageSection = (sectionId: string) => { if (state.phase !== 'loaded' || state.source.kind !== 'onepkg') return; setSelectedSectionId(sectionId); const section = state.source.notebook.sections.find( (candidate) => candidate.id === sectionId ); const firstPage = section?.section?.pages[0]; if (firstPage) { void loadPage(state.source.sessionId, firstPage.id, sectionId); } else { pageGeneration.current += 1; setPageState({ phase: 'empty' }); } }; const previewSelectedResource = useCallback( async (resourceId: string): Promise => { const client = activeClient.current; if (!client || state.phase !== 'loaded') { throw new WorkerClientError( 'session-not-available', 'The local parser session is no longer available.' ); } const response = await client.getResourcePreview( state.source.sessionId, resourceId, state.source.kind === 'onepkg' ? selectedSectionId : undefined ); if (response.type === 'failure') { throw new WorkerClientError( response.error.code, response.error.message ); } return response.resource; }, [selectedSectionId, state] ); const downloadSelectedResource = useCallback( async (resourceId: string, suggestedName?: string): Promise => { const client = activeClient.current; if (!client || state.phase !== 'loaded') { throw new WorkerClientError( 'session-not-available', 'The local parser session is no longer available.' ); } const response = await client.getResourceDownload( state.source.sessionId, resourceId, state.source.kind === 'onepkg' ? selectedSectionId : undefined ); if (response.type === 'failure') { throw new WorkerClientError( response.error.code, response.error.message ); } const resource = response.resource; const filename = safeDownloadFilename( resource.filename ?? suggestedName ?? `onenote-resource.${resource.extension ?? 'bin'}` ); downloadBrowserFile(resource.bytes, resource.mediaType, filename); }, [selectedSectionId, state] ); const exportLoadedSource = useCallback( async (format: OneNoteExportFormat): Promise => { const client = activeClient.current; if (!client || state.phase !== 'loaded') { throw new WorkerClientError( 'session-not-available', 'The local parser session is no longer available.' ); } const response = await client.exportSession( state.source.sessionId, format ); if (response.type === 'failure') { throw new WorkerClientError( response.error.code, response.error.message ); } downloadBrowserFile( response.artifact.bytes, response.artifact.mediaType, safeDownloadFilename(response.artifact.filename) ); }, [state] ); const visibleDiagnostics = uniqueDiagnostics( (() => { if (state.phase !== 'loaded') return []; const pageDiagnostics = selectedPage?.diagnostics ?? []; if (state.source.kind === 'one') { return [...state.source.section.diagnostics, ...pageDiagnostics]; } const selectedSection = state.source.notebook.sections.find( (section) => section.id === selectedSectionId ); return [ ...state.source.notebook.diagnostics, ...(selectedSection?.diagnostics ?? []), ...(selectedSection?.section?.diagnostics ?? []), ...pageDiagnostics, ]; })() ); return (
{state.phase === 'idle' ? ( void openFiles(files)} /> ) : null} {state.phase === 'reading' ? (

Local parser worker

Opening {state.fileName}

Validating the source and extracting supported content in this browser.

) : null} {state.phase === 'loaded' ? ( <>

{state.source.kind === 'one' ? 'OneNote section' : 'ONEPKG'}

{state.fileName}

{state.source.kind === 'onepkg' ? ( ) : null}
{state.source.kind === 'one' ? ( void loadPage(state.source.sessionId, pageId) } /> ) : ( void loadPage( state.source.sessionId, pageId, selectedSectionId ) } /> )}
) : null} {state.phase === 'error' ? (

Could not open file

{state.title}

{state.message}

{state.detail ? {state.detail} : null}
) : null}
); } function safeDownloadFilename(value: string): string { const basename = value.split(/[\\/]/u).at(-1) ?? value; const withoutUnsafeCharacters = [...basename.normalize('NFC')] .map((character) => { const code = character.codePointAt(0) ?? 0; return code <= 0x1f || code === 0x7f || '<>:"|?*'.includes(character) ? '_' : character; }) .join(''); const cleaned = withoutUnsafeCharacters.replace(/[. ]+$/gu, '').trim(); return (cleaned || 'onenote-resource.bin').slice(0, 180); } function downloadBrowserFile( bytes: ArrayBuffer, mediaType: string, filename: string ): void { const url = URL.createObjectURL(new Blob([bytes], { type: mediaType })); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.rel = 'noopener'; document.body.append(anchor); anchor.click(); anchor.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 1_000); } export function App() { const [helpOpen, setHelpOpen] = useState(false); return ( setHelpOpen(true) }} > {helpOpen ? setHelpOpen(false)} /> : null} ); }