import { useEffect, useState, type CSSProperties, type ReactNode } from 'react'; import type { OneNotePageDto, OneNoteResourcePayloadDto, } from '../onenote/model/dto.js'; import type { OneNoteColor, OneNoteContentBlock, OneNoteImageBlock, OneNoteInkBlock, OneNoteInkStroke, OneNoteLayout, OneNoteTextRun, OneNoteTextStyle, } from '../onenote/one/content-model.js'; interface PageReaderProps { page?: OneNotePageDto; loading: boolean; error?: string; loadResource?(resourceId: string): Promise; downloadResource?(resourceId: string, filename?: string): Promise; } function formatDate(value: string | undefined): string | null { if (!value) return null; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', }).format(date); } export function PageReader({ page, loading, error, loadResource, downloadResource, }: PageReaderProps) { if (loading) { return (

Loading structured page content…

); } if (error) { return (

Page unavailable

Could not read this page

{error}

); } if (!page) { return (

Select a readable page to inspect its content.

); } const createdAt = formatDate(page.createdAt); const updatedAt = formatDate(page.updatedAt); return (

Selected page

{page.title || 'Untitled page'}

{createdAt || updatedAt ? (
{createdAt ? ( <>
Created
{createdAt}
) : null} {updatedAt ? ( <>
Modified
{updatedAt}
) : null}
) : null}
{page.blocks.length === 0 && page.text.length === 0 ? (

No readable content was extracted.

) : page.blocks.length === 0 ? (

{page.text}

) : ( )}
); } function ContentBlocks({ blocks, loadResource, downloadResource, }: { blocks: readonly OneNoteContentBlock[]; loadResource?: PageReaderProps['loadResource']; downloadResource?: PageReaderProps['downloadResource']; }) { return blocks.map((block) => ( )); } function ContentBlockView({ block, loadResource, downloadResource, }: { block: OneNoteContentBlock; loadResource?: PageReaderProps['loadResource']; downloadResource?: PageReaderProps['downloadResource']; }) { switch (block.kind) { case 'text': return (

{block.runs.map((run, index) => ( ))}

); case 'outline': return (
); case 'outline-group': return (
); case 'outline-element': return (
); case 'table': return (
{block.columnWidths.length > 0 ? ( {block.columnWidths.map((width, index) => ( ))} ) : null} {block.rows.map((row) => ( {row.cells.map((cell) => ( ))} ))}
); case 'image': return ( ); case 'attachment': return ( ); case 'ink': return ; case 'unsupported': return ( ); } } function TextRunView({ run }: { run: OneNoteTextRun }) { if (run.style.hidden) return null; const content = textWithBreaks(run.text); const style = textStyle(run.style); return run.href ? ( {content} ) : ( {content} ); } function textWithBreaks(text: string): ReactNode[] { return text .replaceAll('\u000b', '\n') .split('\n') .flatMap((part, index, values) => index + 1 < values.length ? [ {part},
, ] : [{part}] ); } function ImageView({ image, loadResource, downloadResource, }: { image: OneNoteImageBlock; loadResource?: PageReaderProps['loadResource']; downloadResource?: PageReaderProps['downloadResource']; }) { const [state, setState] = useState< | { phase: 'loading' } | { phase: 'ready'; resourceId: string; url: string } | { phase: 'unavailable'; resourceId: string; message: string } >({ phase: 'loading' }); useEffect(() => { let disposed = false; let objectUrl: string | undefined; if (!image.resourceId || !loadResource) { return; } const resourceId = image.resourceId; void loadResource(image.resourceId) .then((resource) => { if ( disposed || !resource.browserRenderable || (resource.mediaType !== 'image/png' && resource.mediaType !== 'image/jpeg') ) { if (!disposed) { setState({ phase: 'unavailable', resourceId, message: 'This image format can be downloaded but not displayed safely.', }); } return; } objectUrl = URL.createObjectURL( new Blob([resource.bytes], { type: resource.mediaType }) ); setState({ phase: 'ready', resourceId, url: objectUrl }); }) .catch((error: unknown) => { if (!disposed) { setState({ phase: 'unavailable', resourceId, message: error instanceof Error ? error.message : 'The image could not be loaded.', }); } }); return () => { disposed = true; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [image.resourceId, loadResource]); const visibleState = !image.resourceId || !loadResource ? { phase: 'unavailable' as const, resourceId: image.resourceId ?? '', message: 'The image payload is not available.', } : state.phase !== 'loading' && state.resourceId === image.resourceId ? state : { phase: 'loading' as const }; const fallback = visibleState.phase === 'unavailable' ? visibleState.message : 'Loading embedded image…'; const visual = visibleState.phase === 'ready' ? ( {image.altText ) : (
{fallback}
); return (
{image.href && visibleState.phase === 'ready' ? ( {visual} ) : ( visual )} {image.altText || image.filename ? (
{image.altText ?? image.filename}
) : null} {visibleState.phase === 'unavailable' && image.resourceId && downloadResource ? ( ) : null}
); } function InkView({ ink }: { ink: OneNoteInkBlock }) { const box = ink.boundingBox ?? boundingBoxFromInk(ink.strokes); const padding = Math.max( 140, ...ink.strokes.map((stroke) => stroke.width ?? 0) ); const viewBox = box ? `${box.x - padding / 2} ${box.y - padding / 2} ${Math.max(box.width + padding, 1)} ${Math.max(box.height + padding, 1)}` : '0 0 1 1'; const width = box ? Math.max((box.width + padding) / (2540 / 96), 1) : 1; const height = box ? Math.max((box.height + padding) / (2540 / 96), 1) : 1; return (
{ink.strokes.length > 0 ? ( {ink.strokes.map((stroke, index) => ( ))} ) : null} {ink.children.map((child) => ( ))}
); } function inkPath(stroke: OneNoteInkStroke): string { const [first, ...points] = stroke.points; if (!first) return ''; if (points.length === 0) return `M ${first.x} ${first.y} l 0 0`; return `M ${first.x} ${first.y} L ${points .map((point) => `${point.x} ${point.y}`) .join(' ')}`; } function boundingBoxFromInk( strokes: readonly OneNoteInkStroke[] ): { x: number; y: number; width: number; height: number } | undefined { let xMin = Infinity; let yMin = Infinity; let xMax = -Infinity; let yMax = -Infinity; for (const stroke of strokes) { for (const point of stroke.points) { xMin = Math.min(xMin, point.x); yMin = Math.min(yMin, point.y); xMax = Math.max(xMax, point.x); yMax = Math.max(yMax, point.y); } } return Number.isFinite(xMin) ? { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin } : undefined; } function inkColor(value: number | undefined): string { if (value === undefined) return 'currentColor'; return `rgb(${value & 0xff}, ${(value >>> 8) & 0xff}, ${(value >>> 16) & 0xff})`; } function AttachmentView({ resourceId, filename, size, downloadResource, }: { resourceId?: string; filename: string; size?: number; downloadResource?: PageReaderProps['downloadResource']; }) { const [status, setStatus] = useState(); const download = async () => { if (!resourceId || !downloadResource) return; setStatus('Preparing local download…'); try { await downloadResource(resourceId, filename); setStatus(undefined); } catch (error) { setStatus( error instanceof Error ? error.message : 'The attachment could not be downloaded.' ); } }; return ( ); } function paragraphStyle( block: Extract ): CSSProperties { return { textAlign: block.paragraphAlignment === 'unknown' ? undefined : block.paragraphAlignment, marginTop: halfInches(block.paragraphSpaceBefore), marginBottom: halfInches(block.paragraphSpaceAfter), lineHeight: block.paragraphLineSpacingExact === undefined ? undefined : halfInches(block.paragraphLineSpacingExact), }; } function textStyle(style: OneNoteTextStyle): CSSProperties { const decorations = [ style.underline ? 'underline' : undefined, style.strikethrough ? 'line-through' : undefined, ].filter(Boolean); return { fontFamily: style.font, fontSize: style.fontSize === undefined ? undefined : `${Math.min(Math.max(style.fontSize / 2, 6), 144)}pt`, fontWeight: style.bold ? 700 : undefined, fontStyle: style.italic ? 'italic' : undefined, textDecoration: decorations.length > 0 ? decorations.join(' ') : undefined, verticalAlign: style.superscript ? 'super' : style.subscript ? 'sub' : undefined, color: colorCss(style.fontColor), backgroundColor: colorCss(style.highlight), }; } function colorCss(color: OneNoteColor | undefined): string | undefined { if (!color || color.kind === 'auto') return undefined; const red = color.red ?? 0; const green = color.green ?? 0; const blue = color.blue ?? 0; return color.kind === 'rgba' ? `rgba(${red}, ${green}, ${blue}, ${(color.alpha ?? 255) / 255})` : `rgb(${red}, ${green}, ${blue})`; } function layoutStyle(layout: OneNoteLayout): CSSProperties { return { maxWidth: halfInches(layout.maxWidth), maxHeight: halfInches(layout.maxHeight), marginInlineStart: halfInches(layout.offsetHorizontal, -1_000, 10_000), marginTop: halfInches(layout.offsetVertical, -1_000, 10_000), }; } function halfInches( value: number | undefined, minimum = 0, maximum = 10_000 ): string | undefined { if (value === undefined || !Number.isFinite(value)) return undefined; return `${Math.min(Math.max(value * 48, minimum), maximum)}px`; } function formatBytes(size: number): string { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KiB`; return `${(size / (1024 * 1024)).toFixed(1)} MiB`; }