import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'; import clsx from 'clsx'; import Prism from 'prismjs'; import { memo, useEffect, useRef, useState } from 'react'; import * as prism from './prism'; export interface TextViewerProps { src: string; className?: string; onLoad?: (event: HTMLElementEventMap['load']) => void; onError?: (event: HTMLElementEventMap['error']) => void; codeExtension?: string; isSidebarPreview?: boolean; } // TODO: ANSI support export const TextViewer = memo( ({ src, className, onLoad, onError, codeExtension, isSidebarPreview }: TextViewerProps) => { const [lines, setLines] = useState([]); const parentRef = useRef(null); const rowVirtualizer = useVirtualizer({ count: lines.length, getScrollElement: () => parentRef.current, estimateSize: () => 22 }); useEffect(() => { // Ignore empty urls if (!src || src === '#') return; const controller = new AbortController(); fetch(src, { mode: 'cors', signal: controller.signal }) .then((response) => { if (!response.ok) throw new Error(`Invalid response: ${response.statusText}`); if (!response.body) return; onLoad?.(new UIEvent('load', {})); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); return reader.read().then(function ingestLines({ done, value }): void | Promise { if (done) return; const chunks = value.split('\n'); setLines((lines) => [...lines, ...chunks]); if (isSidebarPreview) return; // Read some more, and call this function again return reader.read().then(ingestLines); }); }) .catch((error) => { if (!controller.signal.aborted) onError?.(new ErrorEvent('error', { message: `${error}` })); }); return () => controller.abort(); }, [src, onError, onLoad, codeExtension, isSidebarPreview]); return (
				
{rowVirtualizer.getVirtualItems().map((row) => ( ))}
); } ); function TextRow({ codeExtension, row, content }: { codeExtension?: string; row: VirtualItem; content: string; }) { const contentRef = useRef(null); useEffect(() => { if (contentRef.current) { const cb: IntersectionObserverCallback = (events) => { for (const event of events) { if ( !event.isIntersecting || contentRef.current?.getAttribute('data-highlighted') === 'true' ) continue; contentRef.current?.setAttribute('data-highlighted', 'true'); Prism.highlightElement(event.target, false); // Prism's async seems to be broken // With this class present TOML headers are broken Eg. `[dependencies]` will format over multiple lines const children = contentRef.current?.children; if (children) { for (const elem of children) { elem.classList.remove('table'); } } } }; new IntersectionObserver(cb).observe(contentRef.current); } }, []); return (
{codeExtension && (
{row.index + 1}
)} {content}
); }