retab
uicode-viewersource: ok · hit
Code Viewer
A fixed-line code and log viewer with line numbers, custom virtualization, source-line highlights, retry, zoom, and download.
npx shadcn add "https://ui.retab.com/r/code-viewer.json"free
Visual style
terminal-mono
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
buttondropdown-menu@retab/scroll-area@retab/skeleton@retab/utils@retab/viewer-controls@retab/use-mount-effect@retab/use-keyed-layout-effect
Files32 · source cached 10/11/2026, 3:43:02 AM
registry/new-york-v4/ui/code-viewer.tsx
"use client";
import * as React from "react";
import type { ViewerResource } from "@/lib/viewer-resource";
import { CodeViewerFallback } from "./code-viewer-chrome";
import { CodeViewerContent } from "./code-viewer-content";
import type { CodeViewerHandle, CodeViewerProps } from "./code-viewer-types";
import { PlainTextViewerFrame } from "./plain-text-viewer-frame";
export type {
CodeDocumentSource,
CodeLineRange,
CodeViewerHandle,
CodeViewerProps,
} from "./code-viewer-types";
export type CodeResourceContentProps = Omit<CodeViewerProps, "source"> & {
resource: ViewerResource;
};
export const CodeViewer = React.forwardRef<CodeViewerHandle, CodeViewerProps>(
function CodeViewer(props, ref) {
return (
<PlainTextViewerFrame
props={props}
forwardedRef={ref}
clientFallbackPolicy="always"
contentResetPolicy="inline-retry"
Fallback={CodeViewerFallback}
Content={CodeViewerContent}
/>
);
},
);
export const CodeResourceContent = React.forwardRef<
CodeViewerHandle,
CodeResourceContentProps
>(function CodeResourceContent({ resource, ...props }, ref) {
return (
<PlainTextViewerFrame
props={props}
resource={resource}
forwardedRef={ref}
clientFallbackPolicy="always"
contentResetPolicy="inline-retry"
Fallback={CodeViewerFallback}
Content={CodeViewerContent}
/>
);
});
registry/new-york-v4/ui/code-viewer-chrome.tsx
"use client";
import * as React from "react";
import { type ViewerDownloadAction } from "@/lib/viewer-download-actions";
import { Skeleton } from "./skeleton";
import { TextCodeViewerFrame } from "./text-code-viewer-chrome";
import {
ViewerControls,
ViewerControlsSkeleton,
type ViewerControlsState,
} from "./viewer-controls";
export function CodeViewerFrame({
className,
bare,
children,
}: {
className?: string;
bare?: boolean;
children: React.ReactNode;
}) {
return (
<TextCodeViewerFrame
bare={bare}
bareClassName="h-full bg-muted/20"
className={className}
dataSlot="code-viewer"
framedClassName="rounded-xl border bg-muted/30"
>
{children}
</TextCodeViewerFrame>
);
}
export function CodeViewerControls({
lineCount,
fontScale,
downloadAction,
onZoomOut,
onZoomIn,
onResetZoom,
}: {
lineCount: number;
fontScale: number;
downloadAction?: ViewerDownloadAction | null;
onZoomOut: () => void;
onZoomIn: () => void;
onResetZoom: () => void;
}) {
const controlsState = codeViewerControlsState({
lineCount,
fontScale,
downloadAction,
onZoomOut,
onZoomIn,
onResetZoom,
});
return (
<ViewerControls
title={controlsState.title}
zoom={controlsState.zoom}
downloads={controlsState.downloads}
/>
);
}
export function codeViewerControlsState({
lineCount,
fontScale,
downloadAction,
onZoomOut,
onZoomIn,
onResetZoom,
}: {
lineCount: number;
fontScale: number;
downloadAction?: ViewerDownloadAction | null;
onZoomOut: () => void;
onZoomIn: () => void;
onResetZoom: () => void;
}): ViewerControlsState {
return {
title: `${lineCount} line${lineCount === 1 ? "" : "s"}`,
zoom: {
scale: fontScale,
onZoomOut,
onZoomIn,
onFit: onResetZoom,
fitLabel: "Reset zoom",
},
downloads: downloadAction ? [downloadAction] : [],
};
}
export function CodeViewerFallback({
className,
controls = true,
download = true,
bare,
}: {
className?: string;
controls?: boolean;
download?: boolean;
bare?: boolean;
}) {
return (
<CodeViewerFrame className={className} bare={bare}>
{controls ? (
<ViewerControlsSkeleton title zoom download={download} />
) : null}
<div
className="min-h-0 flex-1 space-y-2 overflow-hidden p-4"
data-slot="code-body-skeleton"
>
{Array.from({ length: 12 }, (_, index) => (
<Skeleton
key={index}
className="h-4"
style={{ width: `${40 + ((index * 13) % 55)}%` }}
/>
))}
</div>
</CodeViewerFrame>
);
}
registry/new-york-v4/ui/text-code-viewer-chrome.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export function TextCodeViewerFrame({
bare,
bareClassName,
children,
className,
dataSlot,
framedClassName,
}: {
bare?: boolean;
bareClassName: string;
children: React.ReactNode;
className?: string;
dataSlot: string;
framedClassName: string;
}) {
return (
<div
className={cn(
"flex min-h-0 flex-col overflow-hidden",
bare ? bareClassName : framedClassName,
className,
)}
data-slot={dataSlot}
>
{children}
</div>
);
}
registry/new-york-v4/ui/plain-text-viewer-frame.tsx
"use client";
import * as React from "react";
import {
isResourceError,
isViewerFormatError,
isViewerStateError,
isViewerUnsupportedError,
ViewerFormatError,
} from "@/lib/viewer-errors";
import {
createViewerResource,
viewerContentRenderKey,
viewerResourceRenderKey,
type ViewerResource,
} from "@/lib/viewer-resource";
import type {
BlobViewerSource,
TextSource,
UrlViewerSource,
} from "@/lib/viewer-source";
import {
DEFAULT_MAX_BYTES,
DEFAULT_MAX_LINES,
type TextViewerBounds,
} from "./plain-text-resource";
import { useIsClient } from "./use-is-client";
import { ViewerErrorBoundary } from "./viewer-error";
type TextResourceSource = UrlViewerSource | BlobViewerSource | TextSource;
type ClientFallbackPolicy = "always" | "non-inline-source";
type ContentResetPolicy = "content" | "inline-retry";
export interface PlainTextViewerFrameProps<
THandle,
TProps extends PlainTextViewerFramePublicProps,
> {
props: TProps;
resource?: ViewerResource;
forwardedRef: React.ForwardedRef<THandle>;
clientFallbackPolicy: ClientFallbackPolicy;
contentResetPolicy?: ContentResetPolicy;
Fallback: React.ComponentType<PlainTextViewerFallbackProps>;
Content: React.ComponentType<
TProps & {
resource: ViewerResource;
retryVersion: number;
forwardedRef?: React.ForwardedRef<THandle>;
}
>;
}
export interface PlainTextViewerFramePublicProps extends TextViewerBounds {
source?: TextResourceSource;
className?: string;
controls?: boolean;
download?: boolean;
bare?: boolean;
}
export interface PlainTextViewerFallbackProps {
className?: string;
controls?: boolean;
download?: boolean;
bare?: boolean;
}
export function PlainTextViewerFrame<
THandle,
TProps extends PlainTextViewerFramePublicProps,
>({
props,
resource: resourceProp,
forwardedRef,
clientFallbackPolicy,
contentResetPolicy = "content",
Fallback,
Content,
}: PlainTextViewerFrameProps<THandle, TProps>) {
const [retryState, setRetryState] = React.useState({
contentKey: "",
version: 0,
});
const isClient = useIsClient();
const { source } = props;
const createdResource = React.useMemo(
() => (source ? createViewerResource(source) : null),
[source],
);
const resource = resourceProp ?? createdResource;
if (!resource) {
throw new Error("PlainTextViewerFrame requires a source or resource.");
}
const contentBaseKey = plainTextViewerContentBaseKey(resource, props);
const retryVersion =
retryState.contentKey === contentBaseKey ? retryState.version : 0;
const resetKey = plainTextViewerResetKey(resource, props, retryVersion);
const contentResetKey = plainTextViewerContentResetKey(
contentBaseKey,
retryVersion,
);
const suspenseResetKey =
contentResetPolicy === "inline-retry" && resource.sourceKind === "text"
? String(retryVersion)
: contentResetKey;
if (
!isClient &&
shouldRenderClientFallback(clientFallbackPolicy, resource.sourceKind)
) {
return (
<Fallback
className={props.className}
controls={props.controls}
download={props.download}
bare={props.bare}
/>
);
}
return (
<ViewerErrorBoundary
bare={props.bare}
className={props.className}
download={
props.controls === false || props.download === false
? null
: plainTextViewerDownloadAction(resource)
}
format="text"
mapError={plainTextViewerBoundaryError}
resetKey={resetKey}
sourceKind={resource.sourceKind}
onRetry={() =>
setRetryState((state) => ({
contentKey: contentBaseKey,
version: state.contentKey === contentBaseKey ? state.version + 1 : 1,
}))
}
>
<React.Suspense
key={suspenseResetKey}
fallback={
<Fallback
className={props.className}
controls={props.controls}
download={props.download}
bare={props.bare}
/>
}
>
<Content
{...props}
forwardedRef={forwardedRef}
retryVersion={retryVersion}
resource={resource}
/>
</React.Suspense>
</ViewerErrorBoundary>
);
}
function plainTextViewerDownloadAction(resource: ViewerResource) {
return resource.originalDownload;
}
function plainTextViewerBoundaryError(error: unknown) {
if (
isResourceError(error) ||
isViewerFormatError(error) ||
isViewerStateError(error) ||
isViewerUnsupportedError(error)
) {
return error;
}
return new ViewerFormatError({
format: "text",
kind: "render_failed",
message: "Failed to render text.",
cause: error,
});
}
function plainTextViewerResetKey(
resource: ViewerResource,
props: Pick<PlainTextViewerFramePublicProps, "maxBytes" | "maxLines">,
retryVersion: number,
): string {
const [maxBytesKey, maxLinesKey] = plainTextViewerBoundsResetKey(props);
return [
viewerResourceRenderKey(resource),
retryVersion,
maxBytesKey,
maxLinesKey,
].join("\u0000");
}
function plainTextViewerContentResetKey(
contentBaseKey: string,
retryVersion: number,
): string {
return [contentBaseKey, retryVersion].join("\u0000");
}
function plainTextViewerContentBaseKey(
resource: ViewerResource,
props: Pick<PlainTextViewerFramePublicProps, "maxBytes" | "maxLines">,
): string {
const [maxBytesKey, maxLinesKey] = plainTextViewerBoundsResetKey(props);
return [
viewerContentRenderKey(resource.content),
maxBytesKey,
maxLinesKey,
].join("\u0000");
}
function plainTextViewerBoundsResetKey(
props: Pick<PlainTextViewerFramePublicProps, "maxBytes" | "maxLines">,
) {
return [
plainTextViewerBoundResetKeyPart(props.maxBytes, DEFAULT_MAX_BYTES),
plainTextViewerBoundResetKeyPart(props.maxLines, DEFAULT_MAX_LINES),
] as const;
}
function plainTextViewerBoundResetKeyPart(
value: number | undefined,
defaultValue: number,
) {
return String(value === undefined ? defaultValue : value);
}
function shouldRenderClientFallback(
policy: ClientFallbackPolicy,
sourceKind: TextResourceSource["kind"],
) {
return policy === "always" || sourceKind !== "text";
}
registry/new-york-v4/ui/code-viewer-content.tsx
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { useMountEffect } from "@/hooks/use-mount-effect";
import type { ViewerResource } from "@/lib/viewer-resource";
import {
CodeViewerControls,
codeViewerControlsState,
CodeViewerFrame,
} from "./code-viewer-chrome";
import { scrollTopForLineRangeMetrics } from "./code-viewer-layout";
import { getCodeLongLineSelectionText } from "./code-viewer-long-lines";
import { useCodeProjectionScheduler } from "./code-viewer-projection-scheduler";
import { createCodeProjector } from "./code-viewer-projector";
import {
clampCodeViewerScale,
CODE_VIEWER_BASE_LINE_PX,
CODE_VIEWER_BLOCK_PADDING,
} from "./code-viewer-scale";
import { createCodeSyntax } from "./code-viewer-syntax";
import { useCodeViewerSyntaxStyle } from "./code-viewer-syntax-style";
import type {
CodeLineRange,
CodeViewerHandle,
CodeViewerProps,
} from "./code-viewer-types";
import { CodeViewerViewport } from "./code-viewer-viewport";
import { normalizeTextLineRange } from "./line-ranges";
import {
readTextDocument,
resolvedTextViewerBounds,
} from "./plain-text-resource";
import { useViewerControlsRegistration } from "./viewer-controls";
import { joinEffectKey } from "@/lib/effect-key";
type CodeReadingAnchor = {
lineIndex: number;
offsetPx: number;
};
type NativeFindCodeChunk = {
endLine: number;
startLine: number;
text: string;
};
type CodeIdleWindow = Window &
typeof globalThis & {
cancelIdleCallback?: Window["cancelIdleCallback"];
requestIdleCallback?: Window["requestIdleCallback"];
};
const CODE_VIEWER_DEFERRED_SYNTAX_LINE_COUNT = 500;
const CODE_VIEWER_NATIVE_FIND_CHUNK_SIZE = 128;
type CodeViewerContentProps = Omit<CodeViewerProps, "source"> & {
resource: ViewerResource;
retryVersion: number;
forwardedRef?: React.ForwardedRef<CodeViewerHandle>;
};
export function CodeViewerContent({
resource,
className,
controls = true,
download = true,
highlight,
bare = false,
maxBytes,
maxLines,
retryVersion,
forwardedRef,
}: CodeViewerContentProps) {
const bounds = resolvedTextViewerBounds({ maxBytes, maxLines });
const textDocument = readTextDocument({
content: resource.content,
retryVersion,
bounds,
});
const textLines = textDocument.lines;
const [syntaxVersion, setSyntaxVersion] = React.useState(0);
const syntax = React.useMemo(
() =>
createCodeSyntax(resource, {
deferTokens: textLines.length > CODE_VIEWER_DEFERRED_SYNTAX_LINE_COUNT,
onTokensChanged: () => setSyntaxVersion((version) => version + 1),
}),
[resource, textLines.length],
);
const syntaxIdentity =
syntaxVersion === 0
? syntax.identity
: `${syntax.identity}\u0000${syntaxVersion}`;
const highlightStart = highlight?.start;
const highlightEnd = highlight?.end;
const highlightRange = React.useMemo(
() =>
normalizeTextLineRange(
highlightStart == null || highlightEnd == null
? null
: { start: highlightStart, end: highlightEnd },
textLines.length,
),
[highlightStart, highlightEnd, textLines.length],
);
const downloadAction = download ? resource.originalDownload : null;
const [fontScale, setFontScale] = React.useState(1);
const viewportRef = React.useRef<HTMLDivElement | null>(null);
const rowHostRef = React.useRef<HTMLPreElement | null>(null);
const pendingScrollAnchorRef = React.useRef<CodeReadingAnchor | null>(null);
const projector = React.useMemo(() => createCodeProjector(), []);
const lineHeight = CODE_VIEWER_BASE_LINE_PX * fontScale;
const contentIdentity = React.useMemo(
() =>
codeContentIdentity({
contentKey: resource.content.key,
maxBytes: bounds.maxBytes,
maxLines: bounds.maxLines,
retryVersion,
}),
[bounds.maxBytes, bounds.maxLines, resource.content.key, retryVersion],
);
const gutterWidth = `calc(${String(textLines.length).length + 1}ch + 1.25rem)`;
const layoutIdentity = codeLayoutIdentity({ gutterWidth, lineHeight });
useCodeViewerSyntaxStyle();
const commitFontScale = React.useCallback(
(nextScale: number) => {
const clampedScale = clampCodeViewerScale(nextScale);
if (clampedScale === fontScale) return;
pendingScrollAnchorRef.current = captureCodeReadingAnchor({
lineCount: textLines.length,
lineHeight,
projector,
viewportElement: viewportRef.current,
});
setFontScale(clampedScale);
},
[fontScale, lineHeight, projector, textLines.length],
);
const zoom = React.useCallback(
(factor: number) => commitFontScale(fontScale * factor),
[commitFontScale, fontScale],
);
const onZoomOut = React.useCallback(() => zoom(1 / 1.2), [zoom]);
const onZoomIn = React.useCallback(() => zoom(1.2), [zoom]);
const onResetZoom = React.useCallback(
() => commitFontScale(1),
[commitFontScale],
);
useKeyedMountEffect(
joinEffectKey(["code-anchor", lineHeight, textLines.length]),
() => {
const anchor = pendingScrollAnchorRef.current;
const viewportElement = viewportRef.current;
if (!anchor || !viewportElement) return;
pendingScrollAnchorRef.current = null;
projector.scrollToLogical({
lineCount: textLines.length,
lineHeight,
logicalScrollTop: restoreCodeReadingAnchor({
anchor,
lineCount: textLines.length,
lineHeight,
}),
viewport: viewportElement,
});
},
);
const scrollLineRange = React.useCallback(
(range: CodeLineRange | null, options?: ScrollToOptions) => {
const viewportElement = viewportRef.current;
const normalizedRange = normalizeTextLineRange(range, textLines.length);
if (!viewportElement || !normalizedRange) return;
projector.scrollToLogical({
behavior: options?.behavior ?? "smooth",
lineCount: textLines.length,
lineHeight,
logicalScrollTop: scrollTopForLineRangeMetrics({
startLine: normalizedRange.start,
endLine: normalizedRange.end,
lineHeight,
paddingStart: CODE_VIEWER_BLOCK_PADDING,
viewportHeight: viewportElement.clientHeight,
}),
viewport: viewportElement,
});
},
[lineHeight, projector, textLines.length],
);
React.useImperativeHandle(
forwardedRef ?? null,
() => ({
scrollToLineRange: scrollLineRange,
getViewportElement: () => viewportRef.current,
}),
[scrollLineRange],
);
useKeyedMountEffect(
joinEffectKey(["code-highlight-scroll", highlightRange, scrollLineRange]),
() => {
if (!highlightRange) return;
scrollLineRange(highlightRange, { behavior: "smooth" });
},
);
const project = React.useCallback(() => {
const rowHost = rowHostRef.current;
const viewport = viewportRef.current;
if (!rowHost || !viewport) return false;
return projector.project({
contentIdentity,
gutterWidth,
highlightRange,
layoutIdentity,
lineHeight,
rowHost,
syntax,
syntaxIdentity,
textLines,
viewport,
});
}, [
contentIdentity,
gutterWidth,
highlightRange,
layoutIdentity,
lineHeight,
projector,
syntax,
syntaxIdentity,
textLines,
]);
const copyLongLineSelection = React.useCallback(
(event: React.ClipboardEvent<HTMLPreElement>) => {
const rowHost = rowHostRef.current;
if (!rowHost) return;
const selectedText = getCodeLongLineSelectionText({
rowHost,
selection: window.getSelection(),
textLines,
});
if (selectedText == null) return;
event.clipboardData.setData("text/plain", selectedText);
event.preventDefault();
},
[textLines],
);
useCodeProjectionScheduler({
project,
rowHostRef,
viewportRef,
});
useMountEffect(() => () => projector.destroy());
useKeyedMountEffect(joinEffectKey(["code-syntax", syntax]), () => {
setSyntaxVersion(0);
return () => syntax.destroy?.();
});
useCodeControlsRegistration({
downloadAction,
fontScale,
lineCount: textLines.length,
onResetZoom,
onZoomIn,
onZoomOut,
});
return (
<CodeViewerFrame className={className} bare={bare}>
{controls ? (
<CodeViewerControls
lineCount={textLines.length}
fontScale={fontScale}
downloadAction={downloadAction}
onZoomOut={onZoomOut}
onZoomIn={onZoomIn}
onResetZoom={onResetZoom}
/>
) : null}
<DeferredNativeFindIndex
lineCount={textLines.length}
lines={textLines}
scrollToLineRange={scrollLineRange}
/>
<CodeViewerViewport
fontScale={fontScale}
gutterWidth={gutterWidth}
lineCount={textLines.length}
lineHeight={lineHeight}
onCopy={copyLongLineSelection}
rowHostRef={rowHostRef}
viewportRef={viewportRef}
/>
</CodeViewerFrame>
);
}
function DeferredNativeFindIndex({
lines,
lineCount,
scrollToLineRange,
}: {
lines: readonly string[];
lineCount: number;
scrollToLineRange: (
range: CodeLineRange | null,
options?: ScrollToOptions,
) => void;
}) {
const [isReady, setIsReady] = React.useState(false);
useKeyedMountEffect(joinEffectKey(["code-native-find", lines]), () => {
setIsReady(false);
const show = () => setIsReady(true);
if (typeof window === "undefined") return;
const browserWindow = window as CodeIdleWindow;
if (browserWindow.requestIdleCallback && browserWindow.cancelIdleCallback) {
const idleId = browserWindow.requestIdleCallback(show, { timeout: 400 });
return () => browserWindow.cancelIdleCallback?.(idleId);
}
const timeoutId = browserWindow.setTimeout(show, 80);
return () => browserWindow.clearTimeout(timeoutId);
});
if (!isReady) return null;
return (
<NativeFindIndex
lineCount={lineCount}
lines={lines}
scrollToLineRange={scrollToLineRange}
/>
);
}
function NativeFindIndex({
lines,
lineCount,
scrollToLineRange,
}: {
lines: readonly string[];
lineCount: number;
scrollToLineRange: (
range: CodeLineRange | null,
options?: ScrollToOptions,
) => void;
}) {
const entries = React.useMemo(() => chunkNativeFindLines(lines), [lines]);
return (
<div
aria-hidden="true"
className="pointer-events-none h-0 w-0 overflow-hidden opacity-0"
data-native-find-indexed-chunks={entries.length}
data-native-find-indexed-lines={lines.length}
data-slot="code-native-find-index"
>
{entries.map((entry) => (
<NativeFindEntry
key={entry.startLine}
entry={entry}
lineCount={lineCount}
scrollToLineRange={scrollToLineRange}
/>
))}
</div>
);
}
function NativeFindEntry({
entry,
lineCount,
scrollToLineRange,
}: {
entry: NativeFindCodeChunk;
lineCount: number;
scrollToLineRange: (
range: CodeLineRange | null,
options?: ScrollToOptions,
) => void;
}) {
const ref = React.useRef<HTMLSpanElement | null>(null);
useKeyedLayoutEffect(
joinEffectKey([
"code-native-find-entry",
entry.startLine,
entry.endLine,
lineCount,
scrollToLineRange,
]),
() => {
const element = ref.current;
if (!element) return;
element.setAttribute("hidden", "until-found");
const handleBeforeMatch = () => {
scrollToLineRange(
normalizeTextLineRange(
{
end: entry.endLine,
start: entry.startLine,
},
lineCount,
),
{ behavior: "auto" },
);
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
element.setAttribute("hidden", "until-found");
});
return;
}
element.setAttribute("hidden", "until-found");
};
element.addEventListener("beforematch", handleBeforeMatch);
return () => {
element.removeEventListener("beforematch", handleBeforeMatch);
};
},
);
return (
<span
ref={ref}
className="block h-px w-px overflow-hidden whitespace-pre"
data-native-find-end-line={entry.endLine}
data-native-find-start-line={entry.startLine}
>
{entry.text || " "}
</span>
);
}
function chunkNativeFindLines(lines: readonly string[]): NativeFindCodeChunk[] {
const chunks: NativeFindCodeChunk[] = [];
for (
let startIndex = 0;
startIndex < lines.length;
startIndex += CODE_VIEWER_NATIVE_FIND_CHUNK_SIZE
) {
const endIndex = Math.min(
lines.length,
startIndex + CODE_VIEWER_NATIVE_FIND_CHUNK_SIZE,
);
chunks.push({
endLine: endIndex,
startLine: startIndex + 1,
text: lines.slice(startIndex, endIndex).join("\n"),
});
}
return chunks;
}
function useCodeControlsRegistration({
downloadAction,
fontScale,
lineCount,
onResetZoom,
onZoomIn,
onZoomOut,
}: {
downloadAction: ViewerResource["originalDownload"] | null;
fontScale: number;
lineCount: number;
onResetZoom: () => void;
onZoomIn: () => void;
onZoomOut: () => void;
}) {
const onControlsChange = useViewerControlsRegistration();
const controlsState = React.useMemo(
() =>
codeViewerControlsState({
downloadAction,
fontScale,
lineCount,
onResetZoom,
onZoomIn,
onZoomOut,
}),
[downloadAction, fontScale, lineCount, onResetZoom, onZoomIn, onZoomOut],
);
useKeyedMountEffect(
joinEffectKey(["code-controls", onControlsChange, controlsState]),
() => {
if (!onControlsChange) return;
onControlsChange(controlsState);
return () => onControlsChange(null);
},
);
}
function captureCodeReadingAnchor({
lineCount,
lineHeight,
projector,
viewportElement,
}: {
lineCount: number;
lineHeight: number;
projector: ReturnType<typeof createCodeProjector>;
viewportElement: HTMLDivElement | null;
}): CodeReadingAnchor | null {
if (!viewportElement || lineCount <= 0 || lineHeight <= 0) return null;
const scrollTop = projector.getLogicalScrollTop({
lineCount,
lineHeight,
viewport: viewportElement,
});
const contentTop = Math.max(0, scrollTop - CODE_VIEWER_BLOCK_PADDING);
const lineIndex = Math.min(
lineCount - 1,
Math.max(0, Math.floor(contentTop / lineHeight)),
);
return {
lineIndex,
offsetPx: Math.max(0, contentTop - lineIndex * lineHeight),
};
}
function restoreCodeReadingAnchor({
anchor,
lineCount,
lineHeight,
}: {
anchor: CodeReadingAnchor;
lineCount: number;
lineHeight: number;
}) {
if (lineCount <= 0 || lineHeight <= 0) return 0;
const lineIndex = Math.min(lineCount - 1, Math.max(0, anchor.lineIndex));
return (
CODE_VIEWER_BLOCK_PADDING +
lineIndex * lineHeight +
Math.min(anchor.offsetPx, Math.max(0, lineHeight - 1))
);
}
function codeContentIdentity({
contentKey,
maxBytes,
maxLines,
retryVersion,
}: {
contentKey: string;
maxBytes: number;
maxLines: number;
retryVersion: number;
}) {
return [contentKey, retryVersion, maxBytes, maxLines].join("\u0000");
}
function codeLayoutIdentity({
gutterWidth,
lineHeight,
}: {
gutterWidth: string;
lineHeight: number;
}) {
return [lineHeight, gutterWidth].join("\u0000");
}
registry/new-york-v4/ui/code-viewer-projector.ts
import {
CODE_VIEWER_BLOCK_PADDING,
CODE_VIEWER_INITIAL_VIEWPORT_HEIGHT,
CODE_VIEWER_OVERSCAN_PX,
} from "./code-viewer-scale";
import type { CodeSyntax, CodeTokenLeaf } from "./code-viewer-syntax";
import {
getCodeLogicalScrollTop,
getCodePagedLayoutTop,
getCodePhysicalScrollSize,
getCodeVirtualLineWindow,
getCodeVirtualTotalSize,
resolveCodePhysicalScrollPosition,
type CodeVirtualLine,
} from "./code-viewer-virtualization";
import { getCodeLineRenderText } from "./code-viewer-long-lines";
import { isLineInRange, type NormalizedTextLineRange } from "./line-ranges";
// Opaque fills mixed from the theme's opaque `--foreground`/`--background`
// tokens (the `--muted`/`--accent` tokens are alpha-based, so they cannot mask
// scrolled content). Shared with the viewport's full-height gutter rail.
export const CODE_GUTTER_BACKGROUND =
"color-mix(in oklab, var(--foreground) 3%, var(--background))";
const CODE_HIGHLIGHT_BACKGROUND =
"color-mix(in oklab, var(--foreground) 8%, var(--background))";
const CODE_HIGHLIGHT_ACCENT_SHADOW = "inset 2px 0 0 0 var(--primary)";
const CODE_HIGHLIGHT_NUMBER_COLOR =
"color-mix(in oklab, var(--foreground) 70%, transparent)";
export type CodeProjectionIdentity = {
contentIdentity: string;
layoutIdentity: string;
syntaxIdentity: string;
};
export type CodeProjectionInput = CodeProjectionIdentity & {
rowHost: HTMLPreElement;
viewport: HTMLDivElement;
textLines: readonly string[];
lineHeight: number;
gutterWidth: string;
highlightRange: NormalizedTextLineRange | null;
syntax: CodeSyntax;
};
export type CodeProjector = {
getLogicalScrollTop(input: CodeProjectionScrollInput): number;
project(input: CodeProjectionInput): boolean;
scrollToLogical(input: CodeProjectionScrollToInput): void;
destroy(): void;
};
export type CodeProjectionScrollInput = {
lineCount: number;
lineHeight: number;
viewport: HTMLDivElement;
};
export type CodeProjectionScrollToInput = CodeProjectionScrollInput & {
behavior?: ScrollBehavior;
logicalScrollTop: number;
};
export type CodeProjectionMetrics = {
contentPatches: number;
layoutPatches: number;
noops: number;
projections: number;
rowsCreated: number;
rowsRemoved: number;
rowsReused: number;
tokenSpanRebuilds: number;
visibleEnd: number;
visibleStart: number;
};
export type CodeProjectorOptions = {
metrics?: CodeProjectionMetrics;
};
type CodeRowCache = {
contentIdentity: string;
contentSpan: HTMLSpanElement;
gutterSpan: HTMLSpanElement;
layoutIdentity: string;
row: HTMLDivElement;
};
type VisibleRange = {
end: number;
start: number;
};
type LastProjection = CodeProjectionIdentity & {
highlightIdentity: string;
horizontalScrollLeft: number;
renderedWindowHeight: number;
renderedWindowStickyOffset: number;
renderedWindowTop: number;
logicalScrollTop: number;
scrollPageOffset: number;
totalHeight: string;
visibleEnd: number;
visibleStart: number;
};
type CodeRenderedWindow = {
height: number;
rowOffset: number;
stickyOffset: number;
top: number;
};
const MAX_RECYCLED_CODE_ROWS = 512;
export function createCodeProjectionMetrics(): CodeProjectionMetrics {
return {
contentPatches: 0,
layoutPatches: 0,
noops: 0,
projections: 0,
rowsCreated: 0,
rowsRemoved: 0,
rowsReused: 0,
tokenSpanRebuilds: 0,
visibleEnd: 0,
visibleStart: 0,
};
}
export function createCodeProjector(
options: CodeProjectorOptions = {},
): CodeProjector {
let identity: CodeProjectionIdentity | null = null;
let lastProjection: LastProjection | null = null;
let metrics = options.metrics;
let rowHost: HTMLPreElement | null = null;
let recycledRows: CodeRowCache[] = [];
const rowsByLineIndex = new Map<number, CodeRowCache>();
let scrollPageOffset = 0;
let visibleRange: VisibleRange | null = null;
return {
getLogicalScrollTop(input) {
return getCodeLogicalScrollTop({
physicalScrollTop: input.viewport.scrollTop,
scrollPageOffset,
totalSize: codeTotalSize(input),
viewportHeight: codeViewportHeight(input.viewport),
});
},
project(input) {
metrics = options.metrics;
incrementMetric(metrics, "projections");
if (rowHost !== input.rowHost) {
clearRows();
rowHost = input.rowHost;
}
const nextIdentity = codeProjectionIdentity(input);
if (
!identity ||
identity.contentIdentity !== nextIdentity.contentIdentity
) {
identity = nextIdentity;
clearRows();
} else {
identity = nextIdentity;
}
const totalSize = codeTotalSize({
lineCount: input.textLines.length,
lineHeight: input.lineHeight,
viewport: input.viewport,
});
const viewportHeight = codeViewportHeight(input.viewport);
const previousProjection = lastProjection;
const logicalScrollTop = getCodeLogicalScrollTop({
physicalScrollTop: input.viewport.scrollTop,
scrollPageOffset,
totalSize,
viewportHeight,
});
const physicalScrollPosition = resolveCodePhysicalScrollPosition({
logicalScrollTop,
scrollPageOffset,
totalSize,
viewportHeight,
});
const previousScrollPageOffset = scrollPageOffset;
scrollPageOffset = physicalScrollPosition.scrollPageOffset;
if (
physicalScrollPosition.physicalScrollTop !== input.viewport.scrollTop
) {
input.viewport.scrollTop = physicalScrollPosition.physicalScrollTop;
}
const physicalTotalSize = getCodePhysicalScrollSize({
totalSize,
viewportHeight,
});
const totalHeight = `${physicalTotalSize}px`;
const fitPerfectly = shouldFitCodePerfectly({
previousProjection,
logicalScrollTop,
viewportHeight,
});
const virtualWindow = getCodeVirtualLineWindow({
lineCount: input.textLines.length,
lineHeight: input.lineHeight,
overscanPx: fitPerfectly
? getCodeFitPerfectlyOverscanPx(input.lineHeight)
: CODE_VIEWER_OVERSCAN_PX,
paddingStart: CODE_VIEWER_BLOCK_PADDING,
scrollTop: logicalScrollTop,
viewportHeight,
});
const visibleLines = virtualWindow.lines;
const renderedWindow = codeRenderedWindow({
physicalTotalSize,
rowHost: input.rowHost,
scrollPageOffset,
totalSize,
viewportHeight,
visibleLines,
});
const nextVisibleRange = codeVisibleRange(visibleLines);
const nextProjection = codeLastProjection({
input,
logicalScrollTop,
renderedWindow,
scrollPageOffset,
totalHeight,
visibleRange: nextVisibleRange,
});
if (
lastProjection &&
isSameCodeProjection(lastProjection, nextProjection) &&
isRenderedDomValid(nextVisibleRange)
) {
incrementMetric(metrics, "noops");
return false;
}
lastProjection = nextProjection;
setMetric(metrics, "visibleStart", nextVisibleRange.start);
setMetric(metrics, "visibleEnd", nextVisibleRange.end);
syncCodeScrollLayers({
renderedWindow,
rowHost: input.rowHost,
totalHeight,
});
syncVisibleRows({
input,
nextVisibleRange,
previousScrollPageOffset,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
});
visibleRange = nextVisibleRange;
return fitPerfectly;
},
scrollToLogical(input) {
const totalSize = codeTotalSize(input);
const viewportHeight = codeViewportHeight(input.viewport);
const physicalScrollPosition = resolveCodePhysicalScrollPosition({
logicalScrollTop: input.logicalScrollTop,
scrollPageOffset,
totalSize,
viewportHeight,
});
scrollPageOffset = physicalScrollPosition.scrollPageOffset;
if (typeof input.viewport.scrollTo === "function") {
input.viewport.scrollTo({
top: physicalScrollPosition.physicalScrollTop,
behavior: input.behavior,
});
} else {
input.viewport.scrollTop = physicalScrollPosition.physicalScrollTop;
}
},
destroy() {
clearRows();
recycledRows = [];
rowHost = null;
identity = null;
scrollPageOffset = 0;
},
};
function syncVisibleRows({
input,
nextVisibleRange,
previousScrollPageOffset,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
}: {
input: CodeProjectionInput;
nextVisibleRange: VisibleRange;
previousScrollPageOffset: number;
renderedWindow: CodeRenderedWindow;
totalSize: number;
viewportHeight: number;
visibleLines: readonly CodeVirtualLine[];
}) {
if (
previousScrollPageOffset === scrollPageOffset &&
applyPartialRender({
input,
nextVisibleRange,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
})
) {
return;
}
clearMountedRows();
syncVisibleRowOrder({
input,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
});
}
function syncVisibleRowOrder({
input,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
}: {
input: CodeProjectionInput;
renderedWindow: CodeRenderedWindow;
totalSize: number;
viewportHeight: number;
visibleLines: readonly CodeVirtualLine[];
}) {
let cursor = input.rowHost.firstChild;
for (const visibleLine of visibleLines) {
const row = prepareCodeRow({
input,
renderedWindow,
totalSize,
viewportHeight,
visibleLine,
});
if (row !== cursor) {
input.rowHost.insertBefore(row, cursor);
}
cursor = row.nextSibling;
}
while (cursor) {
const nextCursor = cursor.nextSibling;
cursor.parentNode?.removeChild(cursor);
cursor = nextCursor;
}
}
function applyPartialRender({
input,
nextVisibleRange,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
}: {
input: CodeProjectionInput;
nextVisibleRange: VisibleRange;
renderedWindow: CodeRenderedWindow;
totalSize: number;
viewportHeight: number;
visibleLines: readonly CodeVirtualLine[];
}): boolean {
const previousRange = visibleRange;
if (!previousRange) return false;
if (!isRenderedDomValid(previousRange)) return false;
const overlapStart = Math.max(previousRange.start, nextVisibleRange.start);
const overlapEnd = Math.min(previousRange.end, nextVisibleRange.end);
if (overlapStart >= overlapEnd) {
return false;
}
removeCodeRowRange(previousRange.start, overlapStart);
removeCodeRowRange(overlapEnd, previousRange.end);
syncVisibleRowOrder({
input,
renderedWindow,
totalSize,
viewportHeight,
visibleLines,
});
return true;
}
function clearRows() {
for (const row of rowsByLineIndex.values()) {
recycleCodeRow(row);
}
rowHost?.replaceChildren();
rowsByLineIndex.clear();
lastProjection = null;
visibleRange = null;
}
function clearMountedRows() {
if (!visibleRange) return;
removeCodeRowRange(visibleRange.start, visibleRange.end);
visibleRange = null;
}
function removeCodeRowRange(start: number, end: number) {
for (
let index = Math.max(0, start);
index < Math.max(start, end);
index++
) {
const row = rowsByLineIndex.get(index);
if (!row) continue;
row.row.remove();
incrementMetric(metrics, "rowsRemoved");
recycleCodeRow(row);
rowsByLineIndex.delete(index);
}
}
function isRenderedDomValid(range: VisibleRange) {
if (!rowHost) return false;
const expectedLength = Math.max(0, range.end - range.start);
if (rowHost.children.length !== expectedLength) return false;
for (let offset = 0; offset < expectedLength; offset += 1) {
const index = range.start + offset;
const element = rowHost.children[offset];
const row = rowsByLineIndex.get(index);
if (!(element instanceof HTMLDivElement) || row?.row !== element) {
return false;
}
if (element.dataset.lineIndex !== String(index)) {
return false;
}
}
return true;
}
function recycleCodeRow(row: CodeRowCache) {
row.contentIdentity = "";
row.layoutIdentity = "";
if (recycledRows.length < MAX_RECYCLED_CODE_ROWS) {
recycledRows.push(row);
}
}
function prepareCodeRow({
input,
renderedWindow,
totalSize,
viewportHeight,
visibleLine,
}: {
input: CodeProjectionInput;
renderedWindow: CodeRenderedWindow;
totalSize: number;
viewportHeight: number;
visibleLine: CodeVirtualLine;
}): HTMLDivElement {
const lineNumber = visibleLine.index + 1;
const text = input.textLines[visibleLine.index] ?? "";
const isHighlighted = isLineInRange(lineNumber, input.highlightRange);
const layoutIdentity = [
lineNumber,
input.layoutIdentity,
isHighlighted ? "highlighted" : "",
].join("\u0000");
const contentIdentity = codeRowContentIdentity({
syntax: input.syntax,
text,
});
let row = rowsByLineIndex.get(visibleLine.index);
if (!row) {
row = recycledRows.pop();
if (row) {
incrementMetric(metrics, "rowsReused");
} else {
row = createCodeRow();
incrementMetric(metrics, "rowsCreated");
}
rowsByLineIndex.set(visibleLine.index, row);
}
setStyleValue(row.row.style, "height", `${visibleLine.size}px`);
setStyleValue(
row.row.style,
"transform",
`translateY(${
getCodePagedLayoutTop({
logicalTop: visibleLine.start,
scrollPageOffset,
totalSize,
viewportHeight,
}) - renderedWindow.rowOffset
}px)`,
);
if (row.layoutIdentity !== layoutIdentity) {
row.layoutIdentity = layoutIdentity;
patchCodeRowLayout({
gutterWidth: input.gutterWidth,
isHighlighted,
metrics,
lineNumber,
row,
});
}
if (row.contentIdentity !== contentIdentity) {
row.contentIdentity = contentIdentity;
patchCodeRowContent({
metrics,
row,
syntax: input.syntax,
text,
});
}
return row.row;
}
}
function codeLastProjection({
input,
logicalScrollTop,
renderedWindow,
scrollPageOffset,
totalHeight,
visibleRange,
}: {
input: CodeProjectionInput;
logicalScrollTop: number;
renderedWindow: CodeRenderedWindow;
scrollPageOffset: number;
totalHeight: string;
visibleRange: VisibleRange;
}): LastProjection {
return {
contentIdentity: input.contentIdentity,
highlightIdentity: codeHighlightIdentity(input.highlightRange),
horizontalScrollLeft: input.viewport.scrollLeft,
layoutIdentity: input.layoutIdentity,
logicalScrollTop,
renderedWindowHeight: renderedWindow.height,
renderedWindowStickyOffset: renderedWindow.stickyOffset,
renderedWindowTop: renderedWindow.top,
scrollPageOffset,
syntaxIdentity: input.syntaxIdentity,
totalHeight,
visibleEnd: visibleRange.end,
visibleStart: visibleRange.start,
};
}
function codeHighlightIdentity(range: NormalizedTextLineRange | null) {
return range ? `${range.start}:${range.end}` : "";
}
function isSameCodeProjection(previous: LastProjection, next: LastProjection) {
return (
previous.contentIdentity === next.contentIdentity &&
previous.highlightIdentity === next.highlightIdentity &&
previous.horizontalScrollLeft === next.horizontalScrollLeft &&
previous.layoutIdentity === next.layoutIdentity &&
previous.renderedWindowHeight === next.renderedWindowHeight &&
previous.renderedWindowStickyOffset === next.renderedWindowStickyOffset &&
previous.renderedWindowTop === next.renderedWindowTop &&
previous.scrollPageOffset === next.scrollPageOffset &&
previous.syntaxIdentity === next.syntaxIdentity &&
previous.totalHeight === next.totalHeight &&
previous.visibleEnd === next.visibleEnd &&
previous.visibleStart === next.visibleStart
);
}
function codeProjectionIdentity({
contentIdentity,
layoutIdentity,
syntaxIdentity,
}: CodeProjectionIdentity): CodeProjectionIdentity {
return {
contentIdentity,
layoutIdentity,
syntaxIdentity,
};
}
function codeVisibleRange(
visibleLines: readonly CodeVirtualLine[],
): VisibleRange {
const start = visibleLines[0]?.index ?? 0;
const end = visibleLines.length
? visibleLines[visibleLines.length - 1]!.index + 1
: start;
return { end, start };
}
function codeTotalSize({
lineCount,
lineHeight,
}: {
lineCount: number;
lineHeight: number;
viewport?: HTMLDivElement;
}) {
return getCodeVirtualTotalSize({
lineCount,
lineHeight,
});
}
function codeViewportHeight(viewport: HTMLDivElement) {
return viewport.clientHeight || CODE_VIEWER_INITIAL_VIEWPORT_HEIGHT;
}
function shouldFitCodePerfectly({
previousProjection,
logicalScrollTop,
viewportHeight,
}: {
previousProjection: LastProjection | null;
logicalScrollTop: number;
viewportHeight: number;
}) {
if (!previousProjection) return false;
return (
Math.abs(logicalScrollTop - previousProjection.logicalScrollTop) >
viewportHeight + CODE_VIEWER_OVERSCAN_PX * 2
);
}
function getCodeFitPerfectlyOverscanPx(lineHeight: number) {
return Math.max(
CODE_VIEWER_BLOCK_PADDING,
Number.isFinite(lineHeight) ? lineHeight : 0,
);
}
function codeRenderedWindow({
physicalTotalSize,
rowHost,
scrollPageOffset,
totalSize,
viewportHeight,
visibleLines,
}: {
physicalTotalSize: number;
rowHost: HTMLPreElement;
scrollPageOffset: number;
totalSize: number;
viewportHeight: number;
visibleLines: readonly CodeVirtualLine[];
}): CodeRenderedWindow {
const renderedWindowElement = getCodeRenderedWindowElement(rowHost);
if (!renderedWindowElement) {
return {
height: physicalTotalSize,
rowOffset: 0,
stickyOffset: 0,
top: 0,
};
}
const firstLine = visibleLines[0];
const lastLine = visibleLines[visibleLines.length - 1];
if (!firstLine || !lastLine) {
return {
height: 0,
rowOffset: 0,
stickyOffset: 0,
top: 0,
};
}
const top = getCodePagedLayoutTop({
logicalTop: firstLine.start,
scrollPageOffset,
totalSize,
viewportHeight,
});
const bottom = getCodePagedLayoutTop({
logicalTop: lastLine.start + lastLine.size,
scrollPageOffset,
totalSize,
viewportHeight,
});
const height = Math.max(0, bottom - top);
return {
height,
rowOffset: top,
stickyOffset: Math.min(0, viewportHeight - height),
top,
};
}
function syncCodeScrollLayers({
renderedWindow,
rowHost,
totalHeight,
}: {
renderedWindow: CodeRenderedWindow;
rowHost: HTMLPreElement;
totalHeight: string;
}) {
const renderedWindowElement = getCodeRenderedWindowElement(rowHost);
if (!renderedWindowElement) {
setStyleValue(rowHost.style, "height", totalHeight);
if (rowHost.parentElement instanceof HTMLElement) {
setStyleValue(rowHost.parentElement.style, "height", totalHeight);
}
return;
}
const height = `${renderedWindow.height}px`;
const top = `${renderedWindow.top}px`;
const stickyOffset = `${renderedWindow.stickyOffset}px`;
const offsetElement = getCodeRenderedWindowOffsetElement(
renderedWindowElement,
);
setStyleValue(rowHost.style, "height", height);
if (offsetElement) setStyleValue(offsetElement.style, "height", top);
setStyleValue(renderedWindowElement.style, "height", height);
setStyleValue(renderedWindowElement.style, "margin-top", "");
setStyleValue(renderedWindowElement.style, "top", stickyOffset);
setStyleValue(renderedWindowElement.style, "bottom", stickyOffset);
if (renderedWindowElement.parentElement instanceof HTMLElement) {
setStyleValue(renderedWindowElement.parentElement.style, "height", totalHeight);
}
}
function getCodeRenderedWindowElement(rowHost: HTMLPreElement) {
const element = rowHost.parentElement;
if (
element instanceof HTMLElement &&
element.dataset.codeRenderWindow != null
) {
return element;
}
return null;
}
function getCodeRenderedWindowOffsetElement(renderedWindowElement: HTMLElement) {
const element = renderedWindowElement.previousElementSibling;
if (
element instanceof HTMLElement &&
element.dataset.codeRenderOffset != null
) {
return element;
}
return null;
}
function codeRowContentIdentity({
syntax,
text,
}: {
syntax: CodeSyntax;
text: string;
}) {
return [text, syntax.identity, syntax.getLineVersion(text)].join("\u0000");
}
function createCodeRow(): CodeRowCache {
const row = document.createElement("div");
const gutterSpan = document.createElement("span");
const contentSpan = document.createElement("span");
row.className = codeRowClassName();
row.style.position = "absolute";
row.style.top = "0";
row.style.left = "0";
gutterSpan.className =
"sticky left-0 z-10 flex-shrink-0 border-r px-2 pr-3 text-right text-muted-foreground/60 select-none";
// The gutter must paint an opaque background so horizontally-scrolled code
// never shows through the sticky line-number column. The theme's `--muted`
// token is alpha-based (translucent), so the fill is mixed from the opaque
// `--foreground`/`--background` pair instead.
gutterSpan.style.backgroundColor = CODE_GUTTER_BACKGROUND;
gutterSpan.dataset.codeGutter = "";
gutterSpan.setAttribute("aria-hidden", "true");
contentSpan.className = "whitespace-pre px-2";
row.append(gutterSpan, contentSpan);
return {
contentIdentity: "",
contentSpan,
gutterSpan,
layoutIdentity: "",
row,
};
}
function patchCodeRowLayout({
gutterWidth,
isHighlighted,
metrics,
lineNumber,
row,
}: {
gutterWidth: string;
isHighlighted: boolean;
metrics: CodeProjectionMetrics | undefined;
lineNumber: number;
row: CodeRowCache;
}) {
incrementMetric(metrics, "layoutPatches");
row.row.dataset.lineIndex = String(lineNumber - 1);
row.row.dataset.lineNumber = String(lineNumber);
setStyleValue(row.gutterSpan.style, "width", gutterWidth);
setTextContent(row.gutterSpan, String(lineNumber));
// Highlighted range: a continuous opaque band (no per-row borders) with a
// single left accent stripe on the gutter that merges across adjacent lines.
setStyleValue(
row.row.style,
"background-color",
isHighlighted ? CODE_HIGHLIGHT_BACKGROUND : "",
);
setStyleValue(
row.gutterSpan.style,
"background-color",
isHighlighted ? CODE_HIGHLIGHT_BACKGROUND : CODE_GUTTER_BACKGROUND,
);
setStyleValue(
row.gutterSpan.style,
"box-shadow",
isHighlighted ? CODE_HIGHLIGHT_ACCENT_SHADOW : "",
);
setStyleValue(
row.gutterSpan.style,
"color",
isHighlighted ? CODE_HIGHLIGHT_NUMBER_COLOR : "",
);
}
function patchCodeRowContent({
metrics,
row,
syntax,
text,
}: {
metrics: CodeProjectionMetrics | undefined;
row: CodeRowCache;
syntax: CodeSyntax;
text: string;
}) {
incrementMetric(metrics, "contentPatches");
const renderText = getCodeLineRenderText(text);
if (renderText.isTruncated) {
row.row.dataset.codeLineTruncated = "";
row.contentSpan.dataset.codeLineTruncated = "";
row.contentSpan.setAttribute(
"aria-label",
`${text.length} character line preview; ${renderText.omittedCharacterCount} middle characters omitted.`,
);
row.contentSpan.title =
"Long line preview. Copying the selected row copies the complete line.";
} else {
delete row.row.dataset.codeLineTruncated;
delete row.contentSpan.dataset.codeLineTruncated;
row.contentSpan.removeAttribute("aria-label");
row.contentSpan.removeAttribute("title");
}
patchCodeContent(
row.contentSpan,
renderText.isTruncated ? null : syntax.getLineTokens(text),
renderText.text,
metrics,
);
}
function patchCodeContent(
contentSpan: HTMLSpanElement,
leaves: readonly CodeTokenLeaf[] | null,
text: string,
metrics: CodeProjectionMetrics | undefined,
) {
if (text === "") {
contentSpan.replaceChildren();
contentSpan.textContent = " ";
return;
}
if (!leaves) {
setTextContent(contentSpan, text);
return;
}
contentSpan.replaceChildren();
incrementMetric(metrics, "tokenSpanRebuilds");
const fragment = document.createDocumentFragment();
for (const leaf of leaves) {
if (!leaf.kind) {
fragment.append(document.createTextNode(leaf.text));
continue;
}
const span = document.createElement("span");
span.className = "cv-token-" + leaf.kind;
span.textContent = leaf.text;
fragment.append(span);
}
contentSpan.append(fragment);
}
function codeRowClassName() {
return "absolute top-0 left-0 flex min-w-full";
}
function setStyleValue(
style: CSSStyleDeclaration,
propertyName: string,
value: string,
) {
if (style.getPropertyValue(propertyName) !== value) {
style.setProperty(propertyName, value);
}
}
function setTextContent(element: HTMLElement, text: string) {
if (element.textContent !== text) {
element.textContent = text;
}
}
function incrementMetric(
metrics: CodeProjectionMetrics | undefined,
key: keyof CodeProjectionMetrics,
) {
if (!metrics) return;
metrics[key] += 1;
}
function setMetric(
metrics: CodeProjectionMetrics | undefined,
key: keyof CodeProjectionMetrics,
value: number,
) {
if (!metrics) return;
metrics[key] = value;
}
registry/new-york-v4/ui/code-viewer-projection-scheduler.ts
"use client";
import * as React from "react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { joinEffectKey } from "@/lib/effect-key";
import {
restoreTextViewerScrollInteractions,
suspendTextViewerScrollInteractions,
TEXT_SCROLL_INTERACTION_RESTORE_DELAY_MS,
type ScrollInteractionSnapshot,
} from "./text-viewer-scroll-interactions";
export function useCodeProjectionScheduler({
project,
rowHostRef,
viewportRef,
}: {
project: () => boolean | void;
rowHostRef?: React.RefObject<HTMLPreElement | null>;
viewportRef: React.RefObject<HTMLDivElement | null>;
}) {
const scheduledProjectionRef = React.useRef(0);
const scrollInteractionRestoreRef = React.useRef(0);
const scrollInteractionSnapshotRef =
React.useRef<ScrollInteractionSnapshot | null>(null);
const scheduleProjection = React.useCallback(() => {
if (scheduledProjectionRef.current) return;
const runProjection = () => {
scheduledProjectionRef.current = requestAnimationFrame(() => {
scheduledProjectionRef.current = 0;
if (project()) {
runProjection();
}
});
};
runProjection();
}, [project]);
useKeyedMountEffect(
joinEffectKey(["code-project", project, scheduleProjection]),
() => {
if (project()) {
scheduleProjection();
}
return () => {
cancelScheduledProjection(scheduledProjectionRef);
};
},
);
useKeyedMountEffect(
joinEffectKey([
"code-project-listeners",
scheduleProjection,
rowHostRef,
viewportRef,
]),
() => {
const viewport = viewportRef.current;
if (!viewport) return;
const handleScroll = () => {
suspendTextViewerScrollInteractions({
getInteractionTarget: () => rowHostRef?.current,
getOverflowTarget: () => rowHostRef?.current?.parentElement,
snapshotRef: scrollInteractionSnapshotRef,
});
if (scrollInteractionRestoreRef.current) {
window.clearTimeout(scrollInteractionRestoreRef.current);
}
scrollInteractionRestoreRef.current = window.setTimeout(() => {
scrollInteractionRestoreRef.current = 0;
restoreTextViewerScrollInteractions(scrollInteractionSnapshotRef);
}, TEXT_SCROLL_INTERACTION_RESTORE_DELAY_MS);
scheduleProjection();
};
viewport.addEventListener("scroll", handleScroll, {
passive: true,
});
const observer =
typeof ResizeObserver === "undefined"
? null
: new ResizeObserver(scheduleProjection);
observer?.observe(viewport);
return () => {
viewport.removeEventListener("scroll", handleScroll);
observer?.disconnect();
cancelScheduledProjection(scheduledProjectionRef);
if (scrollInteractionRestoreRef.current) {
window.clearTimeout(scrollInteractionRestoreRef.current);
scrollInteractionRestoreRef.current = 0;
}
restoreTextViewerScrollInteractions(scrollInteractionSnapshotRef);
};
},
);
}
function cancelScheduledProjection(ref: React.MutableRefObject<number>) {
if (!ref.current) return;
cancelAnimationFrame(ref.current);
ref.current = 0;
}
registry/new-york-v4/ui/code-viewer-layout.ts
import type { NormalizedTextLineRange } from "./line-ranges";
export const LINE_SCROLL_HEADROOM = 64;
export interface LineRangeMetrics {
startLine: number;
endLine: number;
lineHeight: number;
viewportHeight: number;
paddingStart?: number;
}
export function scrollTopForLineRangeMetrics({
startLine,
endLine,
lineHeight,
viewportHeight,
paddingStart = 0,
}: LineRangeMetrics) {
const rangeTop = paddingStart + (startLine - 1) * lineHeight;
const rangeBottom = paddingStart + endLine * lineHeight;
const rangeHeight = rangeBottom - rangeTop;
const targetTop =
rangeHeight <= viewportHeight
? rangeTop - (viewportHeight - rangeHeight) / 2
: rangeTop - LINE_SCROLL_HEADROOM;
return Math.max(0, targetTop);
}
export function scrollLineRangeMetricsIntoView({
viewportElement,
range,
lineHeight,
paddingStart,
options,
}: {
viewportElement: HTMLDivElement | null;
range: NormalizedTextLineRange | null;
lineHeight: number;
paddingStart?: number;
options?: ScrollToOptions;
}) {
if (!viewportElement || !range) return;
if (typeof viewportElement.scrollTo !== "function") return;
viewportElement.scrollTo({
top: scrollTopForLineRangeMetrics({
startLine: range.start,
endLine: range.end,
lineHeight,
paddingStart,
viewportHeight: viewportElement.clientHeight,
}),
behavior: "smooth",
...options,
});
}
registry/new-york-v4/ui/code-viewer-scale.ts
export const CODE_VIEWER_BASE_FONT_PX = 12;
export const CODE_VIEWER_BASE_LINE_PX = 20;
export const CODE_VIEWER_MIN_SCALE = 0.1;
export const CODE_VIEWER_MAX_SCALE = 5;
export const CODE_VIEWER_OVERSCAN_PX = 1000;
export const CODE_VIEWER_BLOCK_PADDING = 8;
export const CODE_VIEWER_INITIAL_VIEWPORT_HEIGHT = 600;
export const CODE_VIEWER_SCROLL_REBASE_CONTAINER_PX = 12_000_000;
export const CODE_VIEWER_SCROLL_REBASE_TRIGGER_PX = 1_000_000;
export const CODE_VIEWER_SCROLL_REBASE_TARGET_PX = 2_000_000;
export const CODE_VIEWER_SCROLL_REBASE_TARGET_BOTTOM_PX =
CODE_VIEWER_SCROLL_REBASE_CONTAINER_PX - CODE_VIEWER_SCROLL_REBASE_TARGET_PX;
export const CODE_VIEWER_SCROLL_REBASE_THRESHOLD_PX =
CODE_VIEWER_SCROLL_REBASE_CONTAINER_PX -
CODE_VIEWER_SCROLL_REBASE_TRIGGER_PX;
export const CODE_VIEWER_LINE_CHECKPOINT_INTERVAL = 4096;
export function clampCodeViewerScale(value: number) {
return Math.min(
CODE_VIEWER_MAX_SCALE,
Math.max(CODE_VIEWER_MIN_SCALE, value),
);
}
registry/new-york-v4/ui/code-viewer-types.ts
import type {
BlobViewerSource,
TextSource,
UrlViewerSource,
} from "@/lib/viewer-source";
import type { TextLineRange } from "./text-viewer-ranges";
import type { TextViewerBounds } from "./text-viewer-resource";
export type CodeLineRange = TextLineRange;
export interface CodeViewerHandle {
scrollToLineRange: (range: CodeLineRange, options?: ScrollToOptions) => void;
getViewportElement: () => HTMLDivElement | null;
}
export type CodeDocumentSource =
| UrlViewerSource
| BlobViewerSource
| TextSource;
export interface CodeViewerProps extends TextViewerBounds {
source: CodeDocumentSource;
className?: string;
controls?: boolean;
download?: boolean;
/** 1-based inclusive line range to highlight, or null. */
highlight?: CodeLineRange | null;
/** Drop the outer border/rounded/background so the viewer fills its container. */
bare?: boolean;
}
registry/new-york-v4/ui/code-viewer-syntax.ts
import type { ViewerResource } from "@/lib/viewer-resource";
import {
type CodeSyntaxWorkerRequest,
type CodeSyntaxWorkerResponse,
type CodeTokenLeaf,
shouldTokenizeCodeLine,
} from "./code-viewer-syntax-protocol";
import {
ensureCodePrismLanguage,
isCodePrismLanguageLoaded,
isCodePrismLanguageSupported,
tokenizeCodeLine,
} from "./code-viewer-syntax-prism";
import { createCodeSyntaxWorker } from "./code-viewer-syntax-worker";
export type { CodeTokenLeaf } from "./code-viewer-syntax-protocol";
export type CodeSyntax = {
identity: string;
destroy?: () => void;
getLineVersion(line: string): number;
getLineTokens(line: string): readonly CodeTokenLeaf[] | null;
preload?: () => Promise<void>;
};
export type CodeSyntaxMode = "auto" | "main-thread" | "worker";
export type CodeSyntaxOptions = {
deferTokens?: boolean;
onTokensChanged?: () => void;
syntaxMode?: CodeSyntaxMode;
createWorker?: () => Worker;
};
const CODE_DEFERRED_TOKENIZE_BATCH_SIZE = 12;
const CODE_DEFERRED_TOKENIZE_BUDGET_MS = 6;
export const CODE_GLOBAL_TOKEN_CACHE_LIMIT = 1024;
const CODE_WORKER_TOKENIZE_BATCH_SIZE = 64;
type CodeSyntaxIdleWindow = Window &
typeof globalThis & {
cancelIdleCallback?: Window["cancelIdleCallback"];
requestIdleCallback?: Window["requestIdleCallback"];
};
type CodeSyntaxTaskHandle =
| { kind: "idle"; id: number }
| { kind: "timeout"; id: number };
type CodeSyntaxTaskDeadline = {
timeRemaining?: () => number;
};
type CodeSyntaxNotifyHandle = { id: number };
type CodeSyntaxWorkerFactory = () => Worker;
type CodeSyntaxWorkerRelease = () => void;
type CodeSyntaxWorkerSubscription = {
isReleased: boolean;
onDone: () => void;
onError: (line: string) => void;
onTokens: (line: string, tokens: readonly CodeTokenLeaf[]) => void;
pendingCount: number;
};
type CodeSyntaxWorkerJob = {
key: string;
languageId: string;
line: string;
status: "active" | "pending";
subscribers: Set<CodeSyntaxWorkerSubscription>;
};
type CodeSyntaxWorkerBatch = {
generation: number;
jobs: CodeSyntaxWorkerJob[];
languageId: string;
requestId: number;
};
type CodeSyntaxWorkerSlot = {
activeBatch: CodeSyntaxWorkerBatch | null;
worker: Worker;
};
type CodeSyntaxWorkerPool = {
createWorker: CodeSyntaxWorkerFactory;
dispatchHandle: CodeSyntaxTaskHandle | null;
jobsByKey: Map<string, CodeSyntaxWorkerJob>;
maxWorkers: number;
pendingJobs: Map<string, CodeSyntaxWorkerJob>;
requestId: number;
slots: CodeSyntaxWorkerSlot[];
};
const globalTokenCache = new Map<string, readonly CodeTokenLeaf[]>();
const globalWorkerPoolsByFactory = new WeakMap<
CodeSyntaxWorkerFactory,
CodeSyntaxWorkerPool
>();
const globalWorkerPools = new Set<CodeSyntaxWorkerPool>();
const CODE_GLOBAL_WORKER_POOL_SIZE = 2;
// File extension -> Prism language id. Prism does not map extensions to
// languages, so the viewer keeps the small explicit map.
const LANGUAGE_BY_EXTENSION: Record<string, string> = {
json: "json",
json5: "json",
js: "javascript",
mjs: "javascript",
cjs: "javascript",
jsx: "jsx",
ts: "typescript",
mts: "typescript",
cts: "typescript",
tsx: "tsx",
py: "python",
yaml: "yaml",
yml: "yaml",
sh: "bash",
bash: "bash",
zsh: "bash",
sql: "sql",
go: "go",
rs: "rust",
java: "java",
md: "markdown",
markdown: "markdown",
css: "css",
html: "markup",
htm: "markup",
xml: "markup",
svg: "markup",
};
// MIME -> Prism language id, used only for inline sources with no extension.
const LANGUAGE_BY_MIME: Record<string, string> = {
"application/json": "json",
"text/javascript": "javascript",
"application/javascript": "javascript",
"text/typescript": "typescript",
"application/typescript": "typescript",
"text/x-python": "python",
"application/x-python": "python",
"text/yaml": "yaml",
"application/yaml": "yaml",
"application/x-yaml": "yaml",
"text/x-sh": "bash",
"application/x-sh": "bash",
"application/sql": "sql",
"text/markdown": "markdown",
"text/css": "css",
"text/html": "markup",
"application/xml": "markup",
"text/xml": "markup",
};
export const CODE_VIEWER_SYNTAX_STYLE = `
.cv-token-comment { color: var(--cv-token-comment, #6e7781); font-style: italic; }
.cv-token-property,
.cv-token-tag,
.cv-token-attr-name,
.cv-token-symbol { color: var(--cv-token-property, #0550ae); }
.cv-token-string,
.cv-token-char,
.cv-token-attr-value,
.cv-token-url,
.cv-token-regex { color: var(--cv-token-string, #0a7d33); }
.cv-token-number { color: var(--cv-token-number, #b5690c); }
.cv-token-keyword,
.cv-token-boolean,
.cv-token-null,
.cv-token-constant,
.cv-token-atrule,
.cv-token-important { color: var(--cv-token-keyword, #8250df); }
.cv-token-function,
.cv-token-class-name,
.cv-token-builtin { color: var(--cv-token-function, #8250df); }
.cv-token-variable { color: var(--cv-token-variable, #953800); }
.cv-token-punctuation,
.cv-token-operator { color: var(--cv-token-punctuation, color-mix(in oklab, var(--foreground) 55%, transparent)); }
.dark .cv-token-comment { color: var(--cv-token-comment, #8b949e); }
.dark .cv-token-property,
.dark .cv-token-tag,
.dark .cv-token-attr-name,
.dark .cv-token-symbol { color: var(--cv-token-property, #6cb6ff); }
.dark .cv-token-string,
.dark .cv-token-char,
.dark .cv-token-attr-value,
.dark .cv-token-url,
.dark .cv-token-regex { color: var(--cv-token-string, #8ddb8c); }
.dark .cv-token-number { color: var(--cv-token-number, #e3b341); }
.dark .cv-token-keyword,
.dark .cv-token-boolean,
.dark .cv-token-null,
.dark .cv-token-constant,
.dark .cv-token-atrule,
.dark .cv-token-important { color: var(--cv-token-keyword, #dcbdfb); }
.dark .cv-token-function,
.dark .cv-token-class-name,
.dark .cv-token-builtin { color: var(--cv-token-function, #d2a8ff); }
.dark .cv-token-variable { color: var(--cv-token-variable, #ffa657); }
`;
export function createCodeSyntax(
resource: ViewerResource,
options: CodeSyntaxOptions = {},
): CodeSyntax {
const detectedLanguageId = codeLanguageId(resource);
if (!detectedLanguageId || !isCodePrismLanguageSupported(detectedLanguageId)) {
return {
identity: "plain",
getLineVersion: () => 0,
getLineTokens: () => null,
};
}
const languageId = detectedLanguageId;
const tokenVersions = new Map<string, number>();
const asyncTokenSnapshots = new Map<string, readonly CodeTokenLeaf[]>();
const pendingLines = new Set<string>();
const workerReleases = new Set<CodeSyntaxWorkerRelease>();
const workerFactory = options.createWorker ?? createCodeSyntaxWorker;
const useWorker = shouldUseWorker(options);
let flushHandle: CodeSyntaxTaskHandle | null = null;
let notifyHandle: CodeSyntaxNotifyHandle | null = null;
let grammarPromise: Promise<void> | null = null;
let isGrammarReady = isCodePrismLanguageLoaded(languageId);
let isGrammarFailed = false;
let isWorkerFailed = false;
let isDestroyed = false;
let hasPendingTokenChanges = false;
if (!useWorker) {
void preloadMainThreadGrammar();
}
return {
destroy,
getLineVersion,
getLineTokens,
identity: languageId,
preload: preloadMainThreadGrammar,
};
function destroy() {
isDestroyed = true;
pendingLines.clear();
cancelFlush();
cancelNotification();
for (const releaseWorker of workerReleases) {
releaseWorker();
}
workerReleases.clear();
}
function getLineVersion(line: string) {
return tokenVersions.get(line) ?? 0;
}
function getLineTokens(line: string) {
if (isDestroyed) return null;
if (!shouldTokenizeCodeLine(line) || isGrammarFailed) return null;
const cachedTokens = getGlobalLineTokens(languageId, line);
if (cachedTokens) return cachedTokens;
if (!shouldTokenizeInWorker() && isGrammarReady && !options.deferTokens) {
const tokens = tokenizeCodeLine(languageId, line);
if (tokens) {
return setGlobalLineTokens(languageId, line, tokens);
}
}
pendingLines.add(line);
scheduleTokenization();
return null;
}
function scheduleTokenization() {
if (isDestroyed || pendingLines.size === 0) return;
if (shouldTokenizeInWorker()) {
scheduleWorkerTokenization();
return;
}
scheduleMainThreadTokenization();
}
function scheduleWorkerTokenization() {
if (!shouldTokenizeInWorker() || flushHandle) return;
flushHandle = scheduleCodeSyntaxTask(() => {
flushHandle = null;
flushWorkerTokenBatch();
});
}
function flushWorkerTokenBatch() {
if (!shouldTokenizeInWorker() || isDestroyed) return;
const lines = takePendingLines(CODE_WORKER_TOKENIZE_BATCH_SIZE);
if (lines.length === 0) return;
const uncachedLines: string[] = [];
let didCacheTokens = false;
for (const line of lines) {
const cachedTokens = getGlobalLineTokens(languageId, line);
if (cachedTokens) {
didCacheTokens =
cacheAsyncLineTokens(line, cachedTokens) || didCacheTokens;
} else {
uncachedLines.push(line);
}
}
if (didCacheTokens) queueTokenChangeNotification();
if (uncachedLines.length === 0) {
scheduleTokenization();
return;
}
let didFinishWorkerRequest = false;
let releaseWorker: CodeSyntaxWorkerRelease | null = null;
releaseWorker = requestCodeSyntaxWorkerTokens({
createWorker: workerFactory,
languageId,
lines: uncachedLines,
onDone: () => {
didFinishWorkerRequest = true;
if (releaseWorker) workerReleases.delete(releaseWorker);
scheduleTokenization();
},
onError: (line) => {
if (isDestroyed) return;
isWorkerFailed = true;
pendingLines.add(line);
scheduleMainThreadTokenization();
},
onTokens: (line, tokens) => {
if (isDestroyed) return;
if (cacheAsyncLineTokens(line, tokens)) queueTokenChangeNotification();
},
});
if (!didFinishWorkerRequest) workerReleases.add(releaseWorker);
scheduleTokenization();
}
function scheduleMainThreadTokenization() {
if (isDestroyed || flushHandle || isGrammarFailed) return;
if (!isGrammarReady) {
void preloadMainThreadGrammar();
return;
}
flushHandle = scheduleCodeSyntaxTask(flushMainThreadTokenBatch);
}
async function preloadMainThreadGrammar() {
if (shouldTokenizeInWorker() || isGrammarReady || isGrammarFailed) return;
if (!grammarPromise) {
grammarPromise = ensureCodePrismLanguage(languageId)
.then(() => {
if (isDestroyed) return;
isGrammarReady = true;
scheduleMainThreadTokenization();
})
.catch(() => {
if (isDestroyed) return;
isGrammarFailed = true;
pendingLines.clear();
});
}
await grammarPromise;
}
function flushMainThreadTokenBatch(deadline?: CodeSyntaxTaskDeadline) {
flushHandle = null;
if (isDestroyed || isGrammarFailed || !isGrammarReady) return;
const startedAt = codeSyntaxNow();
let processedLineCount = 0;
while (pendingLines.size > 0) {
const pendingLine = pendingLines.values().next().value;
if (pendingLine == null) break;
pendingLines.delete(pendingLine);
const cachedTokens = getGlobalLineTokens(languageId, pendingLine);
if (cachedTokens) {
hasPendingTokenChanges =
cacheAsyncLineTokens(pendingLine, cachedTokens) ||
hasPendingTokenChanges;
} else {
const tokens = tokenizeCodeLine(languageId, pendingLine);
if (tokens) {
hasPendingTokenChanges =
cacheAsyncLineTokens(pendingLine, tokens) || hasPendingTokenChanges;
}
}
processedLineCount += 1;
if (
shouldYieldDeferredTokenization({
deadline,
processedLineCount,
startedAt,
})
) {
break;
}
}
if (pendingLines.size > 0) {
scheduleMainThreadTokenization();
return;
}
if (hasPendingTokenChanges) {
hasPendingTokenChanges = false;
queueTokenChangeNotification();
}
}
function takePendingLines(limit: number) {
const lines: string[] = [];
for (const line of pendingLines) {
pendingLines.delete(line);
lines.push(line);
if (lines.length >= limit) break;
}
return lines;
}
function cacheAsyncLineTokens(
line: string,
tokens: readonly CodeTokenLeaf[],
) {
const cachedTokens = setGlobalLineTokens(languageId, line, tokens);
const previousTokens = asyncTokenSnapshots.get(line);
if (
previousTokens &&
areCodeTokenLeavesEqual(previousTokens, cachedTokens)
) {
return false;
}
asyncTokenSnapshots.set(line, cachedTokens);
tokenVersions.set(line, (tokenVersions.get(line) ?? 0) + 1);
return true;
}
function shouldTokenizeInWorker() {
return useWorker && !isWorkerFailed;
}
function queueTokenChangeNotification() {
if (isDestroyed || notifyHandle) return;
notifyHandle = scheduleCodeSyntaxNotification(() => {
notifyHandle = null;
if (!isDestroyed) options.onTokensChanged?.();
});
}
function cancelFlush() {
if (!flushHandle) return;
cancelCodeSyntaxTask(flushHandle);
flushHandle = null;
}
function cancelNotification() {
if (!notifyHandle) return;
cancelCodeSyntaxNotification(notifyHandle);
notifyHandle = null;
}
}
export function clearCodeSyntaxGlobalTokenCacheForTests() {
globalTokenCache.clear();
for (const pool of globalWorkerPools) {
cancelCodeSyntaxWorkerPoolDispatch(pool);
for (const slot of pool.slots) {
slot.worker.terminate();
}
pool.slots = [];
pool.pendingJobs.clear();
pool.jobsByKey.clear();
}
}
function shouldUseWorker(options: CodeSyntaxOptions) {
if (options.syntaxMode === "main-thread") return false;
if (options.syntaxMode === "worker") return true;
return typeof Worker !== "undefined";
}
function getGlobalLineTokens(languageId: string, line: string) {
const key = codeGlobalTokenCacheKey(languageId, line);
const tokens = globalTokenCache.get(key);
if (!tokens) return null;
globalTokenCache.delete(key);
globalTokenCache.set(key, tokens);
return tokens;
}
function setGlobalLineTokens(
languageId: string,
line: string,
tokens: readonly CodeTokenLeaf[],
) {
const key = codeGlobalTokenCacheKey(languageId, line);
const cachedTokens = getGlobalLineTokens(languageId, line);
if (cachedTokens && areCodeTokenLeavesEqual(cachedTokens, tokens)) {
return cachedTokens;
}
globalTokenCache.delete(key);
globalTokenCache.set(key, tokens);
while (globalTokenCache.size > CODE_GLOBAL_TOKEN_CACHE_LIMIT) {
const firstKey = globalTokenCache.keys().next().value;
if (firstKey === undefined) return tokens;
globalTokenCache.delete(firstKey);
}
return tokens;
}
function codeGlobalTokenCacheKey(languageId: string, line: string) {
return `${languageId}\0${line}`;
}
function requestCodeSyntaxWorkerTokens({
createWorker,
languageId,
lines,
onDone,
onError,
onTokens,
}: {
createWorker: CodeSyntaxWorkerFactory;
languageId: string;
lines: readonly string[];
onDone: () => void;
onError: (line: string) => void;
onTokens: (line: string, tokens: readonly CodeTokenLeaf[]) => void;
}): CodeSyntaxWorkerRelease {
const pool = getCodeSyntaxWorkerPool(createWorker);
const subscription: CodeSyntaxWorkerSubscription = {
isReleased: false,
onDone,
onError,
onTokens,
pendingCount: 0,
};
const jobs: CodeSyntaxWorkerJob[] = [];
for (const line of lines) {
const cachedTokens = getGlobalLineTokens(languageId, line);
if (cachedTokens) {
onTokens(line, cachedTokens);
continue;
}
const key = codeGlobalTokenCacheKey(languageId, line);
let job = pool.jobsByKey.get(key);
if (!job) {
job = {
key,
languageId,
line,
status: "pending",
subscribers: new Set(),
};
pool.jobsByKey.set(key, job);
pool.pendingJobs.set(key, job);
}
job.subscribers.add(subscription);
subscription.pendingCount += 1;
jobs.push(job);
}
if (subscription.pendingCount === 0) {
onDone();
return () => undefined;
}
dispatchCodeSyntaxWorkerPool(pool);
return () => {
if (subscription.isReleased) return;
subscription.isReleased = true;
for (const job of jobs) {
releaseCodeSyntaxWorkerSubscription(pool, job, subscription);
}
};
}
function getCodeSyntaxWorkerPool(createWorker: CodeSyntaxWorkerFactory) {
let pool = globalWorkerPoolsByFactory.get(createWorker);
if (!pool) {
pool = {
createWorker,
dispatchHandle: null,
jobsByKey: new Map(),
maxWorkers:
createWorker === createCodeSyntaxWorker
? CODE_GLOBAL_WORKER_POOL_SIZE
: 1,
pendingJobs: new Map(),
requestId: 0,
slots: [],
};
globalWorkerPoolsByFactory.set(createWorker, pool);
globalWorkerPools.add(pool);
}
return pool;
}
function scheduleCodeSyntaxWorkerPoolDispatch(pool: CodeSyntaxWorkerPool) {
if (pool.pendingJobs.size === 0 || pool.dispatchHandle) return;
pool.dispatchHandle = scheduleCodeSyntaxTask(() => {
pool.dispatchHandle = null;
dispatchCodeSyntaxWorkerPool(pool);
});
}
function cancelCodeSyntaxWorkerPoolDispatch(pool: CodeSyntaxWorkerPool) {
if (!pool.dispatchHandle) return;
cancelCodeSyntaxTask(pool.dispatchHandle);
pool.dispatchHandle = null;
}
function dispatchCodeSyntaxWorkerPool(pool: CodeSyntaxWorkerPool) {
while (pool.pendingJobs.size > 0) {
const slot = getIdleCodeSyntaxWorkerSlot(pool);
if (!slot) return;
const jobs = takeCodeSyntaxWorkerJobs(pool, CODE_WORKER_TOKENIZE_BATCH_SIZE);
if (jobs.length === 0) return;
pool.requestId += 1;
const requestId = pool.requestId;
const languageId = jobs[0]?.languageId ?? "";
const batch: CodeSyntaxWorkerBatch = {
generation: requestId,
jobs,
languageId,
requestId,
};
slot.activeBatch = batch;
const request: CodeSyntaxWorkerRequest = {
type: "tokenize",
generation: batch.generation,
languageId,
lines: jobs.map((job) => job.line),
requestId,
};
slot.worker.postMessage(request);
}
}
function getIdleCodeSyntaxWorkerSlot(pool: CodeSyntaxWorkerPool) {
const idleSlot = pool.slots.find((slot) => !slot.activeBatch);
if (idleSlot) return idleSlot;
if (pool.slots.length >= pool.maxWorkers) return null;
try {
const slot: CodeSyntaxWorkerSlot = {
activeBatch: null,
worker: pool.createWorker(),
};
slot.worker.onmessage = (event: MessageEvent<CodeSyntaxWorkerResponse>) => {
handleCodeSyntaxWorkerPoolMessage(pool, slot, event.data);
};
slot.worker.onerror = () => {
handleCodeSyntaxWorkerPoolFailure(pool, slot);
};
slot.worker.onmessageerror = () => {
handleCodeSyntaxWorkerPoolFailure(pool, slot);
};
pool.slots.push(slot);
return slot;
} catch {
failPendingCodeSyntaxWorkerJobs(pool);
return null;
}
}
function takeCodeSyntaxWorkerJobs(
pool: CodeSyntaxWorkerPool,
limit: number,
) {
const firstJob = pool.pendingJobs.values().next().value;
if (!firstJob) return [];
const jobs: CodeSyntaxWorkerJob[] = [];
for (const job of pool.pendingJobs.values()) {
if (job.languageId !== firstJob.languageId) continue;
pool.pendingJobs.delete(job.key);
job.status = "active";
jobs.push(job);
if (jobs.length >= limit) break;
}
return jobs;
}
function handleCodeSyntaxWorkerPoolMessage(
pool: CodeSyntaxWorkerPool,
slot: CodeSyntaxWorkerSlot,
message: CodeSyntaxWorkerResponse,
) {
const batch = slot.activeBatch;
if (
!batch ||
message.generation !== batch.generation ||
message.languageId !== batch.languageId ||
message.requestId !== batch.requestId
) {
return;
}
slot.activeBatch = null;
if (message.type === "error") {
terminateCodeSyntaxWorkerSlot(pool, slot);
finishCodeSyntaxWorkerJobsWithError(pool, batch.jobs);
scheduleCodeSyntaxWorkerPoolDispatch(pool);
return;
}
const resultsByLine = new Map(
message.results.map((result) => [result.line, result.tokens] as const),
);
for (const job of batch.jobs) {
pool.jobsByKey.delete(job.key);
const tokens = resultsByLine.get(job.line) ?? null;
if (tokens) setGlobalLineTokens(job.languageId, job.line, tokens);
for (const subscriber of job.subscribers) {
if (!subscriber.isReleased && tokens) {
subscriber.onTokens(job.line, tokens);
}
completeCodeSyntaxWorkerSubscription(subscriber);
}
job.subscribers.clear();
}
dispatchCodeSyntaxWorkerPool(pool);
}
function handleCodeSyntaxWorkerPoolFailure(
pool: CodeSyntaxWorkerPool,
slot: CodeSyntaxWorkerSlot,
) {
const batch = slot.activeBatch;
slot.activeBatch = null;
terminateCodeSyntaxWorkerSlot(pool, slot);
if (batch) finishCodeSyntaxWorkerJobsWithError(pool, batch.jobs);
scheduleCodeSyntaxWorkerPoolDispatch(pool);
}
function terminateCodeSyntaxWorkerSlot(
pool: CodeSyntaxWorkerPool,
slot: CodeSyntaxWorkerSlot,
) {
slot.worker.terminate();
pool.slots = pool.slots.filter((candidate) => candidate !== slot);
}
function failPendingCodeSyntaxWorkerJobs(pool: CodeSyntaxWorkerPool) {
const jobs = Array.from(pool.pendingJobs.values());
pool.pendingJobs.clear();
finishCodeSyntaxWorkerJobsWithError(pool, jobs);
}
function finishCodeSyntaxWorkerJobsWithError(
pool: CodeSyntaxWorkerPool,
jobs: readonly CodeSyntaxWorkerJob[],
) {
for (const job of jobs) {
pool.jobsByKey.delete(job.key);
pool.pendingJobs.delete(job.key);
for (const subscriber of job.subscribers) {
if (!subscriber.isReleased) subscriber.onError(job.line);
completeCodeSyntaxWorkerSubscription(subscriber);
}
job.subscribers.clear();
}
}
function releaseCodeSyntaxWorkerSubscription(
pool: CodeSyntaxWorkerPool,
job: CodeSyntaxWorkerJob,
subscriber: CodeSyntaxWorkerSubscription,
) {
job.subscribers.delete(subscriber);
if (job.subscribers.size > 0 || job.status === "active") return;
pool.pendingJobs.delete(job.key);
pool.jobsByKey.delete(job.key);
}
function completeCodeSyntaxWorkerSubscription(
subscriber: CodeSyntaxWorkerSubscription,
) {
if (subscriber.isReleased) return;
subscriber.pendingCount -= 1;
if (subscriber.pendingCount <= 0) {
subscriber.isReleased = true;
subscriber.onDone();
}
}
function scheduleCodeSyntaxTask(
callback: (deadline?: CodeSyntaxTaskDeadline) => void,
): CodeSyntaxTaskHandle {
const browserWindow = window as CodeSyntaxIdleWindow;
if (browserWindow.requestIdleCallback) {
return {
kind: "idle",
id: browserWindow.requestIdleCallback(callback, { timeout: 80 }),
};
}
return {
kind: "timeout",
id: browserWindow.setTimeout(() => callback(), 0),
};
}
function cancelCodeSyntaxTask(handle: CodeSyntaxTaskHandle) {
const browserWindow = window as CodeSyntaxIdleWindow;
if (handle.kind === "idle") {
browserWindow.cancelIdleCallback?.(handle.id);
return;
}
browserWindow.clearTimeout(handle.id);
}
function scheduleCodeSyntaxNotification(
callback: () => void,
): CodeSyntaxNotifyHandle {
return { id: window.setTimeout(callback, 0) };
}
function cancelCodeSyntaxNotification(handle: CodeSyntaxNotifyHandle) {
window.clearTimeout(handle.id);
}
function areCodeTokenLeavesEqual(
first: readonly CodeTokenLeaf[],
second: readonly CodeTokenLeaf[],
) {
if (first === second) return true;
if (first.length !== second.length) return false;
for (let index = 0; index < first.length; index += 1) {
const firstLeaf = first[index];
const secondLeaf = second[index];
if (
firstLeaf?.kind !== secondLeaf?.kind ||
firstLeaf?.text !== secondLeaf?.text
) {
return false;
}
}
return true;
}
function shouldYieldDeferredTokenization({
deadline,
processedLineCount,
startedAt,
}: {
deadline?: CodeSyntaxTaskDeadline;
processedLineCount: number;
startedAt: number;
}) {
if (processedLineCount <= 0) return false;
if (deadline?.timeRemaining && deadline.timeRemaining() <= 1) return true;
if (processedLineCount >= CODE_DEFERRED_TOKENIZE_BATCH_SIZE) return true;
return codeSyntaxNow() - startedAt >= CODE_DEFERRED_TOKENIZE_BUDGET_MS;
}
function codeSyntaxNow() {
return typeof performance === "undefined" ? Date.now() : performance.now();
}
function codeLanguageId(resource: ViewerResource): string | null {
const extension = resource.fileName.toLowerCase().split(".").pop();
const byExtension = extension ? LANGUAGE_BY_EXTENSION[extension] : undefined;
if (byExtension) return byExtension;
const mimeType = resource.content.mimeType
?.toLowerCase()
.split(";")[0]
.trim();
return (mimeType && LANGUAGE_BY_MIME[mimeType]) ?? null;
}
registry/new-york-v4/ui/code-viewer-syntax-protocol.ts
export const CODE_LINE_TOKENIZE_MAX = 2000;
export type CodeTokenLeaf = {
kind: string;
text: string;
};
export type CodeSyntaxWorkerRequest = {
type: "tokenize";
requestId: number;
generation: number;
languageId: string;
lines: string[];
};
export type CodeSyntaxWorkerResponse =
| {
type: "tokens";
requestId: number;
generation: number;
languageId: string;
results: CodeSyntaxWorkerTokenResult[];
}
| {
type: "error";
requestId: number;
generation: number;
languageId: string;
message: string;
};
export type CodeSyntaxWorkerTokenResult = {
line: string;
tokens: CodeTokenLeaf[] | null;
};
export function shouldTokenizeCodeLine(line: string) {
return line.length > 0 && line.length <= CODE_LINE_TOKENIZE_MAX;
}
registry/new-york-v4/ui/code-viewer-syntax-prism.ts
import Prism from "prismjs";
import type { CodeTokenLeaf } from "./code-viewer-syntax-protocol";
Prism.manual = true;
const coreLanguages = new Set(["css", "javascript", "markup"]);
const languageLoaders: Record<string, () => Promise<unknown>> = {
bash: () => import("prismjs/components/prism-bash"),
diff: () => import("prismjs/components/prism-diff"),
dockerfile: () => import("prismjs/components/prism-docker"),
go: () => import("prismjs/components/prism-go"),
java: () => import("prismjs/components/prism-java"),
json: () => import("prismjs/components/prism-json"),
jsx: () => import("prismjs/components/prism-jsx"),
markdown: () => import("prismjs/components/prism-markdown"),
python: () => import("prismjs/components/prism-python"),
rust: () => import("prismjs/components/prism-rust"),
ruby: () => import("prismjs/components/prism-ruby"),
sql: () => import("prismjs/components/prism-sql"),
tsx: async () => {
await Promise.all([
import("prismjs/components/prism-typescript"),
import("prismjs/components/prism-jsx"),
]);
return import("prismjs/components/prism-tsx");
},
typescript: () => import("prismjs/components/prism-typescript"),
yaml: () => import("prismjs/components/prism-yaml"),
};
const loadingLanguages = new Map<string, Promise<void>>();
export function isCodePrismLanguageLoaded(languageId: string) {
return Boolean(Prism.languages[languageId]);
}
export function isCodePrismLanguageSupported(languageId: string) {
return coreLanguages.has(languageId) || languageId in languageLoaders;
}
export async function ensureCodePrismLanguage(languageId: string) {
if (isCodePrismLanguageLoaded(languageId)) return;
if (coreLanguages.has(languageId)) return;
let loading = loadingLanguages.get(languageId);
if (!loading) {
const loadLanguage = languageLoaders[languageId];
if (!loadLanguage) {
loading = Promise.reject(
new Error(`Unsupported code syntax language: ${languageId}`),
);
} else {
loading = loadLanguage().then(() => undefined);
}
loadingLanguages.set(languageId, loading);
}
await loading;
}
export function tokenizeCodeLine(languageId: string, line: string) {
const grammar = Prism.languages[languageId] ?? null;
if (!grammar) return null;
return flattenCodeTokens(Prism.tokenize(line, grammar));
}
function flattenCodeTokens(
tokens: Array<string | Prism.Token>,
parentKind = "",
leaves: CodeTokenLeaf[] = [],
): CodeTokenLeaf[] {
for (const token of tokens) {
if (typeof token === "string") {
leaves.push({ kind: parentKind, text: token });
} else if (Array.isArray(token.content)) {
flattenCodeTokens(
token.content as Array<string | Prism.Token>,
token.type,
leaves,
);
} else if (typeof token.content === "string") {
leaves.push({ kind: token.type, text: token.content });
} else {
flattenCodeTokens([token.content as Prism.Token], token.type, leaves);
}
}
return leaves;
}
registry/new-york-v4/ui/code-viewer-syntax-worker.ts
export function createCodeSyntaxWorker(): Worker {
return new Worker(new URL("./code-viewer-syntax.worker.ts", import.meta.url), {
type: "module",
});
}
registry/new-york-v4/ui/code-viewer-syntax.worker.ts
import {
type CodeSyntaxWorkerRequest,
type CodeSyntaxWorkerResponse,
shouldTokenizeCodeLine,
} from "./code-viewer-syntax-protocol";
import {
ensureCodePrismLanguage,
tokenizeCodeLine,
} from "./code-viewer-syntax-prism";
const workerSelf = self as unknown as {
onmessage: ((event: MessageEvent<CodeSyntaxWorkerRequest>) => void) | null;
postMessage(message: CodeSyntaxWorkerResponse): void;
};
function post(message: CodeSyntaxWorkerResponse) {
workerSelf.postMessage(message);
}
async function tokenizeInWorker(request: CodeSyntaxWorkerRequest) {
await ensureCodePrismLanguage(request.languageId);
post({
type: "tokens",
requestId: request.requestId,
generation: request.generation,
languageId: request.languageId,
results: request.lines.map((line) => ({
line,
tokens: shouldTokenizeCodeLine(line)
? tokenizeCodeLine(request.languageId, line)
: null,
})),
});
}
workerSelf.onmessage = (event: MessageEvent<CodeSyntaxWorkerRequest>) => {
const request = event.data;
if (request.type !== "tokenize") return;
void tokenizeInWorker(request).catch((error) => {
post({
type: "error",
requestId: request.requestId,
generation: request.generation,
languageId: request.languageId,
message: error instanceof Error ? error.message : String(error),
});
});
};
registry/new-york-v4/ui/code-viewer-prism-components.d.ts
declare module "prismjs/components/prism-bash";
declare module "prismjs/components/prism-diff";
declare module "prismjs/components/prism-docker";
declare module "prismjs/components/prism-go";
declare module "prismjs/components/prism-java";
declare module "prismjs/components/prism-json";
declare module "prismjs/components/prism-jsx";
declare module "prismjs/components/prism-markdown";
declare module "prismjs/components/prism-python";
declare module "prismjs/components/prism-rust";
declare module "prismjs/components/prism-ruby";
declare module "prismjs/components/prism-sql";
declare module "prismjs/components/prism-tsx";
declare module "prismjs/components/prism-typescript";
declare module "prismjs/components/prism-yaml";
registry/new-york-v4/ui/code-viewer-syntax-style.tsx
"use client";
import * as React from "react";
import { CODE_VIEWER_SYNTAX_STYLE } from "./code-viewer-syntax";
const CODE_VIEWER_SYNTAX_STYLE_ID = "retab-code-viewer-syntax-style";
export function useCodeViewerSyntaxStyle() {
React.useInsertionEffect(() => {
let style = document.getElementById(CODE_VIEWER_SYNTAX_STYLE_ID);
if (style) return;
style = document.createElement("style");
style.id = CODE_VIEWER_SYNTAX_STYLE_ID;
style.textContent = CODE_VIEWER_SYNTAX_STYLE;
document.head.append(style);
}, []);
}
registry/new-york-v4/ui/code-viewer-virtualization.ts
import {
CODE_VIEWER_BLOCK_PADDING,
CODE_VIEWER_LINE_CHECKPOINT_INTERVAL,
CODE_VIEWER_SCROLL_REBASE_CONTAINER_PX,
CODE_VIEWER_SCROLL_REBASE_TARGET_BOTTOM_PX,
CODE_VIEWER_SCROLL_REBASE_TARGET_PX,
CODE_VIEWER_SCROLL_REBASE_THRESHOLD_PX,
CODE_VIEWER_SCROLL_REBASE_TRIGGER_PX,
} from "./code-viewer-scale";
export interface CodeVirtualLine {
index: number;
key: number;
size: number;
start: number;
}
export interface CodeVirtualLineWindow {
lineCount: number;
lineHeight: number;
overscanPx: number;
paddingStart: number;
scrollTop: number;
viewportHeight: number;
}
export interface CodeVirtualPixelWindow {
bottom: number;
top: number;
}
export interface CodeVirtualLineWindowResult {
lines: CodeVirtualLine[];
pixelWindow: CodeVirtualPixelWindow;
}
export interface CodeLineCheckpoint {
index: number;
start: number;
}
export interface CodeScrollRebaseState {
scrollPageOffset: number;
}
export interface CodeScrollRebasePosition extends CodeScrollRebaseState {
physicalScrollTop: number;
}
export function getCodeVirtualLines({
lineCount,
lineHeight,
overscanPx,
paddingStart,
scrollTop,
viewportHeight,
}: CodeVirtualLineWindow): CodeVirtualLine[] {
return getCodeVirtualLineWindow({
lineCount,
lineHeight,
overscanPx,
paddingStart,
scrollTop,
viewportHeight,
}).lines;
}
export function getCodeVirtualLineWindow({
lineCount,
lineHeight,
overscanPx,
paddingStart,
scrollTop,
viewportHeight,
}: CodeVirtualLineWindow): CodeVirtualLineWindowResult {
const safeLineCount = safeCount(lineCount);
const safeLineHeight = safeSize(lineHeight);
if (safeLineCount === 0) {
return {
lines: [],
pixelWindow: { bottom: 0, top: 0 },
};
}
const safeScrollTop = Math.max(0, finiteNumber(scrollTop));
const safeViewportHeight = Math.max(0, finiteNumber(viewportHeight));
const safePaddingStart = safePadding(paddingStart);
const totalSize = getCodeVirtualTotalSize({
lineCount: safeLineCount,
lineHeight: safeLineHeight,
paddingStart: safePaddingStart,
});
const window = getCodeVirtualPixelWindow({
overscanPx,
scrollHeight: totalSize,
scrollTop: safeScrollTop,
viewportHeight: safeViewportHeight,
});
const start = getCodeLineIndexAtOffset({
lineCount: safeLineCount,
lineHeight: safeLineHeight,
offset: window.top,
paddingStart: safePaddingStart,
});
const end = Math.min(
safeLineCount,
getCodeLineIndexAfterOffset({
lineCount: safeLineCount,
lineHeight: safeLineHeight,
offset: window.bottom,
paddingStart: safePaddingStart,
}),
);
return {
lines: Array.from({ length: end - start }, (_, offset) => {
const index = start + offset;
return {
index,
key: index,
size: safeLineHeight,
start: safePaddingStart + index * safeLineHeight,
};
}),
pixelWindow: window,
};
}
export function getCodeLineCheckpoint({
lineCount,
lineHeight,
offset,
paddingStart = CODE_VIEWER_BLOCK_PADDING,
}: {
lineCount: number;
lineHeight: number;
offset: number;
paddingStart?: number;
}): CodeLineCheckpoint {
const safeLineCount = safeCount(lineCount);
if (safeLineCount === 0) {
return {
index: 0,
start: safePadding(paddingStart),
};
}
const safeLineHeight = safeSize(lineHeight);
const safePaddingStart = safePadding(paddingStart);
const lineIndex = clamp(
Math.floor(Math.max(0, finiteNumber(offset) - safePaddingStart) / safeLineHeight),
0,
safeLineCount - 1,
);
const checkpointIndex =
Math.floor(lineIndex / CODE_VIEWER_LINE_CHECKPOINT_INTERVAL) *
CODE_VIEWER_LINE_CHECKPOINT_INTERVAL;
return {
index: checkpointIndex,
start: safePaddingStart + checkpointIndex * safeLineHeight,
};
}
export function getCodeLineIndexAtOffset({
lineCount,
lineHeight,
offset,
paddingStart = CODE_VIEWER_BLOCK_PADDING,
}: {
lineCount: number;
lineHeight: number;
offset: number;
paddingStart?: number;
}) {
const safeLineCount = safeCount(lineCount);
if (safeLineCount === 0) return 0;
const safeLineHeight = safeSize(lineHeight);
const checkpoint = getCodeLineCheckpoint({
lineCount: safeLineCount,
lineHeight: safeLineHeight,
offset,
paddingStart,
});
const indexFromCheckpoint = Math.floor(
Math.max(0, finiteNumber(offset) - checkpoint.start) / safeLineHeight,
);
return clamp(checkpoint.index + indexFromCheckpoint, 0, safeLineCount - 1);
}
export function getCodeLineIndexAfterOffset({
lineCount,
lineHeight,
offset,
paddingStart = CODE_VIEWER_BLOCK_PADDING,
}: {
lineCount: number;
lineHeight: number;
offset: number;
paddingStart?: number;
}) {
const safeLineCount = safeCount(lineCount);
if (safeLineCount === 0) return 0;
const safeLineHeight = safeSize(lineHeight);
const checkpoint = getCodeLineCheckpoint({
lineCount: safeLineCount,
lineHeight: safeLineHeight,
offset,
paddingStart,
});
const indexFromCheckpoint = Math.ceil(
Math.max(0, finiteNumber(offset) - checkpoint.start) / safeLineHeight,
);
return clamp(checkpoint.index + indexFromCheckpoint, 0, safeLineCount);
}
export function getCodeVirtualPixelWindow({
overscanPx,
scrollHeight,
scrollTop,
viewportHeight,
}: {
overscanPx: number;
scrollHeight: number;
scrollTop: number;
viewportHeight: number;
}): CodeVirtualPixelWindow {
const safeOverscanPx = safePadding(overscanPx);
const safeViewportHeight = Math.max(0, finiteNumber(viewportHeight));
const windowHeight = safeViewportHeight + safeOverscanPx * 2;
const safeScrollHeight = Math.max(0, finiteNumber(scrollHeight));
if (windowHeight >= safeScrollHeight) {
return { bottom: safeScrollHeight, top: 0 };
}
const scrollCenter = Math.max(0, finiteNumber(scrollTop)) + safeViewportHeight / 2;
let top = scrollCenter - windowHeight / 2;
let bottom = top + windowHeight;
if (top < 0) {
top = 0;
bottom = windowHeight;
}
if (bottom > safeScrollHeight) {
bottom = safeScrollHeight;
top = safeScrollHeight - windowHeight;
}
return {
bottom: Math.ceil(Math.max(bottom, top)),
top: Math.floor(Math.max(0, top)),
};
}
export function getCodeVirtualTotalSize({
lineCount,
lineHeight,
paddingEnd = CODE_VIEWER_BLOCK_PADDING,
paddingStart = CODE_VIEWER_BLOCK_PADDING,
}: {
lineCount: number;
lineHeight: number;
paddingEnd?: number;
paddingStart?: number;
}) {
return (
safePadding(paddingStart) +
safeCount(lineCount) * safeSize(lineHeight) +
safePadding(paddingEnd)
);
}
export function getCodePhysicalScrollSize({
totalSize,
viewportHeight,
}: {
totalSize: number;
viewportHeight: number;
}) {
const safeTotalSize = safeSize(totalSize);
return shouldRebaseCodeScroll({ totalSize: safeTotalSize, viewportHeight })
? Math.min(safeTotalSize, CODE_VIEWER_SCROLL_REBASE_CONTAINER_PX)
: safeTotalSize;
}
export function getCodeLogicalScrollTop({
physicalScrollTop,
scrollPageOffset,
totalSize,
viewportHeight,
}: {
physicalScrollTop: number;
scrollPageOffset: number;
totalSize: number;
viewportHeight: number;
}) {
return clamp(
finiteNumber(physicalScrollTop) + safePadding(scrollPageOffset),
0,
getCodeMaxLogicalScrollTop({ totalSize, viewportHeight }),
);
}
export function resolveCodePhysicalScrollPosition({
logicalScrollTop,
scrollPageOffset,
totalSize,
viewportHeight,
}: {
logicalScrollTop: number;
scrollPageOffset: number;
totalSize: number;
viewportHeight: number;
}): CodeScrollRebasePosition {
const safeLogicalScrollTop = clamp(
finiteNumber(logicalScrollTop),
0,
getCodeMaxLogicalScrollTop({ totalSize, viewportHeight }),
);
if (!shouldRebaseCodeScroll({ totalSize, viewportHeight })) {
return {
physicalScrollTop: clamp(
safeLogicalScrollTop,
0,
getCodeMaxPhysicalScrollTop({ totalSize, viewportHeight }),
),
scrollPageOffset: 0,
};
}
const currentPageOffset = clampCodeScrollPageOffset({
scrollPageOffset,
totalSize,
viewportHeight,
});
const physicalScrollTop = safeLogicalScrollTop - currentPageOffset;
const maxPhysicalScrollTop = getCodeMaxPhysicalScrollTop({
totalSize,
viewportHeight,
});
const maxPageOffset = getCodeMaxScrollPageOffset({ totalSize, viewportHeight });
const shouldMoveDown =
physicalScrollTop > CODE_VIEWER_SCROLL_REBASE_THRESHOLD_PX &&
currentPageOffset < maxPageOffset;
const shouldMoveUp =
physicalScrollTop < CODE_VIEWER_SCROLL_REBASE_TRIGGER_PX &&
currentPageOffset > 0;
if (
physicalScrollTop < 0 ||
physicalScrollTop > maxPhysicalScrollTop ||
shouldMoveDown ||
shouldMoveUp
) {
return resolveCodeScrollPageWindow({
logicalScrollTop: safeLogicalScrollTop,
preferredPhysicalScrollTop: shouldMoveUp
? Math.min(
CODE_VIEWER_SCROLL_REBASE_TARGET_BOTTOM_PX,
maxPhysicalScrollTop,
)
: CODE_VIEWER_SCROLL_REBASE_TARGET_PX,
totalSize,
viewportHeight,
});
}
return {
physicalScrollTop: roundCodeScrollPixel(
clamp(physicalScrollTop, 0, maxPhysicalScrollTop),
),
scrollPageOffset: currentPageOffset,
};
}
export function getCodePagedLayoutTop({
logicalTop,
scrollPageOffset,
totalSize,
viewportHeight,
}: {
logicalTop: number;
scrollPageOffset: number;
totalSize: number;
viewportHeight: number;
}) {
if (!shouldRebaseCodeScroll({ totalSize, viewportHeight })) {
return finiteNumber(logicalTop);
}
return Math.max(0, finiteNumber(logicalTop) - safePadding(scrollPageOffset));
}
function shouldRebaseCodeScroll({
totalSize,
viewportHeight,
}: {
totalSize: number;
viewportHeight: number;
}) {
return (
getCodeMaxLogicalScrollTop({ totalSize, viewportHeight }) >
CODE_VIEWER_SCROLL_REBASE_THRESHOLD_PX
);
}
function getCodeMaxLogicalScrollTop({
totalSize,
viewportHeight,
}: {
totalSize: number;
viewportHeight: number;
}) {
return Math.max(safeSize(totalSize) - Math.max(0, finiteNumber(viewportHeight)), 0);
}
function getCodeMaxPhysicalScrollTop({
totalSize,
viewportHeight,
}: {
totalSize: number;
viewportHeight: number;
}) {
return Math.max(
getCodePhysicalScrollSize({ totalSize, viewportHeight }) -
Math.max(0, finiteNumber(viewportHeight)),
0,
);
}
function getCodeMaxScrollPageOffset({
totalSize,
viewportHeight,
}: {
totalSize: number;
viewportHeight: number;
}) {
return Math.max(
getCodeMaxLogicalScrollTop({ totalSize, viewportHeight }) -
getCodeMaxPhysicalScrollTop({ totalSize, viewportHeight }),
0,
);
}
function clampCodeScrollPageOffset({
scrollPageOffset,
totalSize,
viewportHeight,
}: {
scrollPageOffset: number;
totalSize: number;
viewportHeight: number;
}) {
return clamp(
safePadding(scrollPageOffset),
0,
getCodeMaxScrollPageOffset({ totalSize, viewportHeight }),
);
}
function resolveCodeScrollPageWindow({
logicalScrollTop,
preferredPhysicalScrollTop,
totalSize,
viewportHeight,
}: {
logicalScrollTop: number;
preferredPhysicalScrollTop: number;
totalSize: number;
viewportHeight: number;
}): CodeScrollRebasePosition {
let physicalScrollTop = roundCodeScrollPixel(
clamp(
finiteNumber(preferredPhysicalScrollTop),
0,
getCodeMaxPhysicalScrollTop({ totalSize, viewportHeight }),
),
);
let scrollPageOffset = clampCodeScrollPageOffset({
scrollPageOffset: logicalScrollTop - physicalScrollTop,
totalSize,
viewportHeight,
});
physicalScrollTop = roundCodeScrollPixel(
clamp(
logicalScrollTop - scrollPageOffset,
0,
getCodeMaxPhysicalScrollTop({ totalSize, viewportHeight }),
),
);
scrollPageOffset = clampCodeScrollPageOffset({
scrollPageOffset: logicalScrollTop - physicalScrollTop,
totalSize,
viewportHeight,
});
return { physicalScrollTop, scrollPageOffset };
}
function finiteNumber(value: number) {
return Number.isFinite(value) ? value : 0;
}
function roundCodeScrollPixel(value: number) {
if (typeof window === "undefined") return Math.round(value);
const ratio = window.devicePixelRatio || 1;
return Math.round(value * ratio) / ratio;
}
function safeCount(value: number) {
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
}
function safeSize(value: number) {
return Number.isFinite(value) && value > 0 ? value : 1;
}
function safePadding(value: number) {
return Number.isFinite(value) && value > 0 ? value : 0;
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
registry/new-york-v4/ui/code-viewer-viewport.tsx
"use client";
import * as React from "react";
import { CODE_GUTTER_BACKGROUND } from "./code-viewer-projector";
import {
CODE_VIEWER_BASE_FONT_PX,
CODE_VIEWER_INITIAL_VIEWPORT_HEIGHT,
} from "./code-viewer-scale";
import {
getCodePhysicalScrollSize,
getCodeVirtualTotalSize,
} from "./code-viewer-virtualization";
import { ScrollArea } from "./scroll-area";
const CODE_VIEWER_DEFAULT_VIEWPORT_WIDTH = 800;
export function CodeViewerViewport({
fontScale,
gutterWidth,
lineCount,
lineHeight,
onCopy,
rowHostRef,
viewportRef,
}: {
fontScale: number;
gutterWidth: string;
lineCount: number;
lineHeight: number;
onCopy?: React.ClipboardEventHandler<HTMLPreElement>;
rowHostRef: React.RefObject<HTMLPreElement | null>;
viewportRef: React.RefObject<HTMLDivElement | null>;
}) {
const fontSize = `${CODE_VIEWER_BASE_FONT_PX * fontScale}px`;
const totalSize = getCodeVirtualTotalSize({
lineCount,
lineHeight,
});
const physicalTotalSize = getCodePhysicalScrollSize({
totalSize,
viewportHeight: CODE_VIEWER_INITIAL_VIEWPORT_HEIGHT,
});
return (
<div className="bg-background relative min-h-0 flex-1">
{/* Fixed full-height gutter rail, behind the scrolling content: the
line-number column and its divider always reach the bottom of the
viewport and never move while scrolling, because it lives outside the
scroll container. The per-row gutters paint the numbers — and mask
horizontally-scrolled code — on top of it. The viewport itself is
transparent so this shows through below the last line. */}
<div
aria-hidden
data-code-gutter-rail=""
className="pointer-events-none absolute inset-y-0 left-0 z-0 border-r font-mono"
style={{
width: gutterWidth,
backgroundColor: CODE_GUTTER_BACKGROUND,
fontSize,
}}
/>
<ScrollArea
className="absolute inset-0 z-10"
viewportProps={{ style: { overflowAnchor: "none" } }}
viewportRef={viewportRef}
>
<div
data-code-scroll-spacer=""
className="relative w-max min-w-full font-mono"
style={{
contain: "layout style",
fontSize,
height: physicalTotalSize,
lineHeight: `${lineHeight}px`,
minWidth: CODE_VIEWER_DEFAULT_VIEWPORT_WIDTH,
}}
>
<div
aria-hidden
data-code-render-offset=""
style={{
contain: "layout size",
height: 0,
}}
/>
<div
data-code-render-window=""
className="w-full"
style={{
bottom: 0,
contain: "layout style inline-size",
display: "flex",
flexDirection: "column",
height: physicalTotalSize,
isolation: "isolate",
position: "sticky",
top: 0,
}}
>
<pre
onCopy={onCopy}
ref={rowHostRef}
className="relative w-full"
suppressHydrationWarning
style={{
fontSize,
height: physicalTotalSize,
lineHeight: `${lineHeight}px`,
}}
/>
</div>
</div>
</ScrollArea>
</div>
);
}
registry/new-york-v4/ui/viewer-download.tsx
"use client";
import * as React from "react";
import { Download } from "lucide-react";
import { cn } from "@/lib/utils";
import {
ViewerDownloadError,
type ViewerDownloadAction,
type ViewerDownloadPayload,
} from "@/lib/viewer-download-actions";
import { Spinner } from "@/components/ui/spinner";
import { Button, buttonVariants } from "./button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./dropdown-menu";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { joinEffectKey } from "@/lib/effect-key";
export interface TriggerViewerDownloadOptions {
signal?: AbortSignal;
}
export async function triggerViewerDownload(
action: ViewerDownloadAction,
options?: TriggerViewerDownloadOptions,
): Promise<void> {
if (action.isDisabled) {
throw new ViewerDownloadError({
actionId: action.id,
kind: "disabled",
message: "This download is disabled.",
});
}
let payload: ViewerDownloadPayload;
try {
payload = await action.getPayload(options);
} catch (error) {
if (isAbortError(error)) {
throw new ViewerDownloadError({
actionId: action.id,
kind: "aborted",
message: "Download was cancelled.",
cause: error,
});
}
throw new ViewerDownloadError({
actionId: action.id,
kind: "payload_failed",
message: "Could not prepare this download.",
cause: error,
});
}
if (payload.kind === "none") return;
if (payload.kind === "href") {
try {
clickDownload(payload.href, action.fileName);
} catch (error) {
throw new ViewerDownloadError({
actionId: action.id,
kind: "unsupported",
message: "Could not start this download.",
cause: error,
});
}
return;
}
try {
const blob =
payload.kind === "blob"
? payload.blob
: new Blob([payload.text], {
type: payload.mimeType ?? "text/plain;charset=utf-8",
});
const url = URL.createObjectURL(blob);
try {
clickDownload(url, action.fileName);
} finally {
URL.revokeObjectURL(url);
}
} catch (error) {
throw new ViewerDownloadError({
actionId: action.id,
kind: "unsupported",
message: "Could not start this download.",
cause: error,
});
}
}
export type ViewerDownloadErrorHandler = (
error: ViewerDownloadError,
action: ViewerDownloadAction,
) => void;
export interface ViewerDownloadTrigger {
pendingActionId: string | null;
triggerDownload: (action: ViewerDownloadAction) => void;
}
export interface ViewerDownloadTriggerOptions {
/** Reports non-aborted action failures; visible failure UI belongs to consumers. */
onError?: ViewerDownloadErrorHandler;
resetKey?: unknown;
}
export interface ViewerDownloadControlProps {
actions: Array<ViewerDownloadAction | null | undefined>;
variant?: React.ComponentProps<typeof Button>["variant"];
size?: React.ComponentProps<typeof Button>["size"];
className?: string;
showLabel?: boolean;
/** Reports non-aborted action failures; visible failure UI belongs to consumers. */
onError?: ViewerDownloadErrorHandler;
}
export interface ViewerDownloadButtonProps
extends Omit<ViewerDownloadControlProps, "actions"> {
action: ViewerDownloadAction | null;
}
export interface ViewerDownloadMenuProps
extends Omit<ViewerDownloadControlProps, "actions"> {
actions: ViewerDownloadAction[];
}
export function useViewerDownloadHref(
action: ViewerDownloadAction | null,
): string | null {
const shouldCreateHref = action?.origin !== "derived";
const payload = shouldCreateHref ? getSynchronousPayload(action) : null;
return shouldCreateHref && payload?.kind === "href" ? payload.href : null;
}
export function useViewerDownloadTrigger({
onError,
resetKey = "",
}: ViewerDownloadTriggerOptions = {}): ViewerDownloadTrigger {
const [pendingActionId, setPendingActionId] = React.useState<string | null>(
null,
);
const abortControllerRef = React.useRef<AbortController | null>(null);
useKeyedMountEffect(joinEffectKey([resetKey]), () => {
return () => {
abortControllerRef.current?.abort();
abortControllerRef.current = null;
};
});
const triggerDownload = React.useCallback(
(action: ViewerDownloadAction) => {
abortControllerRef.current?.abort();
const abortController = new AbortController();
abortControllerRef.current = abortController;
setPendingActionId(action.id);
void triggerViewerDownload(action, { signal: abortController.signal })
.catch((error) => {
reportDownloadError(error, action, onError);
})
.finally(() => {
if (abortControllerRef.current === abortController) {
abortControllerRef.current = null;
setPendingActionId(null);
}
});
},
[onError],
);
return { pendingActionId, triggerDownload };
}
export function ViewerDownloadControl({
actions,
variant = "ghost",
size = "iconSm",
className = "size-7",
showLabel = false,
onError,
}: ViewerDownloadControlProps) {
const enabledActions = actions.filter(
(action): action is ViewerDownloadAction => Boolean(action),
);
if (enabledActions.length <= 1) {
return (
<ViewerDownloadButton
action={enabledActions[0] ?? null}
variant={variant}
size={size}
className={className}
showLabel={showLabel}
onError={onError}
/>
);
}
return (
<ViewerDownloadMenu
actions={enabledActions}
variant={variant}
size={size}
className={className}
showLabel={showLabel}
onError={onError}
/>
);
}
export function ViewerDownloadButton({
action,
variant = "ghost",
size = "iconSm",
className = "size-7",
showLabel = false,
onError,
}: ViewerDownloadButtonProps) {
const href = useViewerDownloadHref(action);
const label = action?.label ?? "Download";
const disabled = !action || action.isDisabled;
const hasDownloadHref = Boolean(href);
const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({
onError,
resetKey: action,
});
const isPending = Boolean(action && pendingActionId === action.id);
const handleClick = React.useCallback(() => {
if (!action || hasDownloadHref) return;
triggerDownload(action);
}, [action, hasDownloadHref, triggerDownload]);
if (href) {
return (
<a
href={href}
download={action?.fileName}
className={cn(buttonVariants({ variant, size }), className)}
aria-label={label}
title={label}
data-slot="button"
>
<Download className={showLabel ? "mr-1.5 size-4" : undefined} />
{showLabel ? label : null}
</a>
);
}
return (
<Button
variant={variant}
size={size}
className={className}
aria-label={label}
title={label}
disabled={disabled || isPending}
onClick={handleClick}
>
{isPending ? (
<Spinner className="size-4 animate-spin" />
) : (
<Download className={showLabel ? "mr-1.5 size-4" : undefined} />
)}
{showLabel ? label : null}
</Button>
);
}
export function ViewerDownloadMenu({
actions,
variant = "ghost",
size = "iconSm",
className = "size-7",
showLabel = false,
onError,
}: ViewerDownloadMenuProps) {
const actionSetKey = actions.map((action) => action.id).join("\u0000");
const label = "Download";
const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({
onError,
resetKey: actionSetKey,
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant={variant}
size={size}
className={className}
aria-label={label}
title={label}
disabled={pendingActionId != null}
>
{pendingActionId != null ? (
<Spinner className="size-4 animate-spin" />
) : (
<Download className={showLabel ? "mr-1.5 size-4" : undefined} />
)}
{showLabel ? label : null}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{actions.map((action) => (
<DropdownMenuItem
key={action.id}
disabled={action.isDisabled || pendingActionId != null}
onClick={() => triggerDownload(action)}
>
<Download />
<span>{action.label}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
function clickDownload(href: string, fileName: string) {
const anchor = document.createElement("a");
anchor.href = href;
anchor.download = fileName;
anchor.rel = "noreferrer";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
}
function getSynchronousPayload(
action: ViewerDownloadAction | null,
): ViewerDownloadPayload | null {
if (!action || action.isDisabled) return null;
const payload = action.getPayload();
return payload instanceof Promise ? null : payload;
}
function reportDownloadError(
error: unknown,
action: ViewerDownloadAction,
onError: ViewerDownloadErrorHandler | undefined,
) {
if (!(error instanceof ViewerDownloadError)) return;
if (error.kind === "aborted") return;
onError?.(error, action);
}
function isAbortError(error: unknown) {
return error instanceof DOMException && error.name === "AbortError";
}
registry/new-york-v4/ui/viewer-error.tsx
"use client";
import * as React from "react";
import { RotateCcw } from "lucide-react";
import { cn } from "@/lib/utils";
import type { ViewerDownloadAction } from "@/lib/viewer-download-actions";
import {
toViewerErrorInfo,
type ViewerErrorContext,
type ViewerFormat,
} from "@/lib/viewer-errors";
import { Button } from "./button";
import { ViewerDownloadButton } from "./viewer-download";
export interface ViewerErrorStateProps extends ViewerErrorContext {
error: unknown;
download?: ViewerDownloadAction | null;
className?: string;
bare?: boolean;
variant?: "card" | "document" | "inline";
onRetry?: () => void;
}
export function ViewerErrorState({
error,
format,
sourceKind,
canDownload,
retry,
download,
className,
bare = false,
variant = "card",
onRetry,
}: ViewerErrorStateProps) {
const info = toViewerErrorInfo(error, {
format,
sourceKind,
canDownload: canDownload ?? Boolean(download && !download.isDisabled),
retry,
});
const showRetry = info.isRetryable && onRetry;
const showDownload =
info.isDownloadUseful && download != null && !download.isDisabled;
return (
<div
className={cn(errorStateClassName({ bare, variant }), className)}
data-error-domain={info.domain}
data-error-format={info.format}
data-error-kind={info.kind}
data-error-message={info.message}
data-slot="viewer-error"
role="alert"
>
<p>{info.userMessage}</p>
{showRetry || showDownload ? (
<div className="flex items-center gap-2">
{showRetry ? (
<Button variant="outline" size="sm" onClick={onRetry}>
<RotateCcw className="mr-1.5 size-4" />
Retry
</Button>
) : null}
{showDownload ? (
<ViewerDownloadButton
action={download}
variant={showRetry ? "ghost" : "outline"}
size="sm"
className=""
showLabel
/>
) : null}
</div>
) : null}
</div>
);
}
export interface ViewerErrorBoundaryProps extends ViewerErrorContext {
children: React.ReactNode;
resetKey?: unknown;
download?: ViewerDownloadAction | null;
className?: string;
bare?: boolean;
variant?: "card" | "document" | "inline";
mapError?: (error: unknown) => unknown;
onRetry?: (error: unknown) => void;
onCaughtError?: (error: unknown, errorInfo: React.ErrorInfo) => void;
}
export class ViewerErrorBoundary extends React.Component<
ViewerErrorBoundaryProps,
{ error: unknown | null; retryKey: number }
> {
state: Readonly<{ error: unknown | null; retryKey: number }> = {
error: null,
retryKey: 0,
};
componentDidUpdate(previousProps: ViewerErrorBoundaryProps) {
if (
previousProps.resetKey !== this.props.resetKey &&
this.state.error != null
) {
this.setState({ error: null });
}
}
static getDerivedStateFromError(error: unknown) {
return { error };
}
componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
this.props.onCaughtError?.(error, errorInfo);
}
render() {
if (this.state.error != null) {
const error = this.props.mapError
? this.props.mapError(this.state.error)
: this.state.error;
return (
<ViewerErrorState
error={error}
format={this.props.format}
sourceKind={this.props.sourceKind}
canDownload={this.props.canDownload}
retry={this.props.retry}
download={this.props.download}
className={this.props.className}
bare={this.props.bare}
variant={this.props.variant}
onRetry={() => {
this.setState((state) => ({
error: null,
retryKey: state.retryKey + 1,
}));
this.props.onRetry?.(this.state.error);
}}
/>
);
}
return (
<React.Fragment key={this.state.retryKey}>
{this.props.children}
</React.Fragment>
);
}
}
function errorStateClassName({
bare,
variant,
}: {
bare: boolean;
variant: "card" | "document" | "inline";
}) {
if (variant === "inline") {
return "flex h-24 flex-col items-center justify-center gap-3 px-3 text-center text-xs text-muted-foreground";
}
return cn(
"flex min-h-64 flex-col items-center justify-center gap-3 p-6 text-center text-sm text-muted-foreground",
bare ? "bg-muted/20" : "rounded-xl border bg-muted/30",
variant === "document" && "min-h-full",
);
}
export type { ViewerFormat };
registry/new-york-v4/ui/text-viewer-ranges.ts
export interface TextLineRange {
start: number;
end: number;
}
export interface NormalizedTextLineRange extends TextLineRange {
readonly normalized: true;
}
const clamp = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value));
export function normalizeTextLineRange(
range: TextLineRange | null | undefined,
lineCount: number,
): NormalizedTextLineRange | null {
if (
!range ||
!Number.isFinite(range.start) ||
!Number.isFinite(range.end) ||
!Number.isFinite(lineCount) ||
lineCount <= 0
) {
return null;
}
const maxLine = Math.floor(lineCount);
const rawStart = Math.trunc(range.start);
const rawEnd = Math.trunc(range.end);
const start = Math.min(rawStart, rawEnd);
const end = Math.max(rawStart, rawEnd);
if (end < 1 || start > maxLine) return null;
return {
start: clamp(start, 1, maxLine),
end: clamp(end, 1, maxLine),
normalized: true,
};
}
export function isLineInRange(
lineNumber: number,
range: NormalizedTextLineRange | null,
) {
return range != null && lineNumber >= range.start && lineNumber <= range.end;
}
registry/new-york-v4/ui/line-ranges.ts
export * from "./text-viewer-ranges";
registry/new-york-v4/ui/text-viewer-resource.ts
import {
isResourceError,
isViewerFormatError,
ViewerFormatError,
type ViewerFormatErrorMapperOptions,
} from "@/lib/viewer-errors";
import type {
ViewerContentIdentity,
ViewerContentPayload,
ViewerContentText,
} from "@/lib/viewer-resource";
export const DEFAULT_MAX_BYTES = 1_000_000;
export const DEFAULT_MAX_LINES = 10_000;
export const MAX_TEXT_RESOURCE_CACHE_ENTRIES = 64;
export const TEXT_LINE_DETACHMENT_SOURCE_MIN_LENGTH = 64 * 1024;
export const TEXT_LINE_DETACHMENT_MAX_LINE_LENGTH = 16 * 1024;
export interface TextViewerBounds {
maxBytes?: number;
maxLines?: number;
}
export type TextViewerTooLargeReason = "bytes" | "lines";
export type TextViewerBoundName = "maxBytes" | "maxLines";
export class TextViewerTooLargeError extends ViewerFormatError {
readonly reason: TextViewerTooLargeReason;
constructor(reason: TextViewerTooLargeReason) {
super({
format: "text",
kind: "bounds",
message: `Text file exceeds ${reason} limit`,
});
this.name = "TextViewerTooLargeError";
this.reason = reason;
}
}
export class TextViewerInvalidBoundsError extends ViewerFormatError {
readonly boundName: TextViewerBoundName;
constructor(boundName: TextViewerBoundName) {
super({
format: "text",
kind: "bounds",
message: `${boundName} must be a positive integer`,
});
this.name = "TextViewerInvalidBoundsError";
this.boundName = boundName;
}
}
export function toTextFormatError(
error: unknown,
options: ViewerFormatErrorMapperOptions = {
kind: "load_failed",
message: "Failed to load text.",
},
): ViewerFormatError {
if (isViewerFormatError(error)) return error;
return new ViewerFormatError({
format: "text",
kind: options.kind,
message: options.message,
cause: error,
});
}
export interface PreparedTextDocument {
text: string;
lines: readonly string[];
lineCount: number;
}
interface TextResource {
promise: Promise<PreparedTextDocument>;
status: "pending" | "resolved" | "rejected";
value?: PreparedTextDocument;
error?: unknown;
}
const textResourceCache = new Map<string, TextResource>();
export type TextViewerContent = ViewerContentIdentity &
ViewerContentPayload &
ViewerContentText;
function textViewerResourceKey({
content,
retryVersion,
bounds,
}: {
content: ViewerContentIdentity;
retryVersion: number;
bounds: Required<TextViewerBounds>;
}) {
return `${content.key}\0${retryVersion}\0${bounds.maxBytes}\0${bounds.maxLines}`;
}
export function clearTextViewerResourceCacheForTests() {
textResourceCache.clear();
}
export function resolvedTextViewerBounds({
maxBytes = DEFAULT_MAX_BYTES,
maxLines = DEFAULT_MAX_LINES,
}: TextViewerBounds = {}): Required<TextViewerBounds> {
return {
maxBytes: resolveTextViewerBound(maxBytes, "maxBytes"),
maxLines: resolveTextViewerBound(maxLines, "maxLines"),
};
}
export function assertTextWithinBounds(
text: string,
bounds: Required<TextViewerBounds>,
) {
prepareTextDocument(text, bounds);
}
// Split into stable source lines. The prose viewer may wrap each source line
// into several visual lines, but highlighting and scroll APIs stay source-line
// based.
export function splitTextLines(text: string) {
return splitTextLinesForDocument(text).lines;
}
export function shouldDetachTextLine({
lineLength,
sourceLength,
}: {
lineLength: number;
sourceLength: number;
}) {
return (
sourceLength >= TEXT_LINE_DETACHMENT_SOURCE_MIN_LENGTH &&
lineLength > 0 &&
lineLength <= TEXT_LINE_DETACHMENT_MAX_LINE_LENGTH
);
}
export function detachTextLine(line: string) {
return line.length === 0 ? line : ` ${line}`.slice(1);
}
export function readTextResource({
content,
retryVersion,
bounds,
}: {
content: TextViewerContent;
retryVersion: number;
bounds: Required<TextViewerBounds>;
}) {
return readTextDocument({ content, retryVersion, bounds }).text;
}
export function readTextDocument({
content,
retryVersion,
bounds,
}: {
content: TextViewerContent;
retryVersion: number;
bounds: Required<TextViewerBounds>;
}): PreparedTextDocument {
const resourceKey = textViewerResourceKey({ content, retryVersion, bounds });
const inlineText = inlineTextResource(content);
if (inlineText != null) {
return getInlineTextDocument({
bounds,
resourceKey,
text: inlineText,
});
}
const textResource = getTextResource({ content, resourceKey, bounds });
if (textResource.status === "resolved") {
return textResource.value ?? emptyPreparedTextDocument(bounds);
}
if (textResource.status === "rejected") throw textResource.error;
throw textResource.promise;
}
export function prepareTextDocument(
text: string,
bounds: Required<TextViewerBounds>,
options: { isByteLengthChecked?: boolean } = {},
): PreparedTextDocument {
if (
!options.isByteLengthChecked &&
new TextEncoder().encode(text).byteLength > bounds.maxBytes
) {
throw new TextViewerTooLargeError("bytes");
}
const lines = splitTextLinesForDocument(text, {
maxLines: bounds.maxLines,
}).lines;
return {
lineCount: lines.length,
lines,
text,
};
}
function inlineTextResource(content: ViewerContentPayload) {
return content.payload.kind === "text" ? content.payload.text : null;
}
function splitTextLinesForDocument(
text: string,
options: { maxLines?: number } = {},
) {
const maxLines = options.maxLines ?? Number.POSITIVE_INFINITY;
const lines: string[] = [];
let lineStart = 0;
for (let index = 0; index < text.length; index += 1) {
const breakLength = textLineBreakLength(text, index);
if (breakLength === 0) continue;
appendTextLine(lines, text, lineStart, index, maxLines);
index += breakLength - 1;
lineStart = index + 1;
}
appendTextLine(lines, text, lineStart, text.length, maxLines);
return { lines };
}
function appendTextLine(
lines: string[],
text: string,
start: number,
end: number,
maxLines: number,
) {
if (lines.length >= maxLines) {
throw new TextViewerTooLargeError("lines");
}
const line = text.slice(start, end);
lines.push(
shouldDetachTextLine({
lineLength: end - start,
sourceLength: text.length,
})
? detachTextLine(line)
: line,
);
}
function textLineBreakLength(text: string, index: number) {
const code = text.charCodeAt(index);
if (code === 0x0d) {
return text.charCodeAt(index + 1) === 0x0a ? 2 : 1;
}
return code === 0x0a || code === 0x2028 || code === 0x2029 ? 1 : 0;
}
function getInlineTextDocument({
bounds,
resourceKey,
text,
}: {
bounds: Required<TextViewerBounds>;
resourceKey: string;
text: string;
}) {
const cached = textResourceCache.get(resourceKey);
if (cached?.status === "resolved" && cached.value) return cached.value;
const document = prepareTextDocument(text, bounds);
textResourceCache.set(resourceKey, {
promise: Promise.resolve(document),
status: "resolved",
value: document,
});
trimTextResourceCache();
return document;
}
function getTextResource({
content,
resourceKey,
bounds,
}: {
content: TextViewerContent;
resourceKey: string;
bounds: Required<TextViewerBounds>;
}) {
let textResource = textResourceCache.get(resourceKey);
if (!textResource) {
const nextResource: TextResource = {
status: "pending",
promise: readBoundedTextResource(content, bounds).then((text) =>
prepareTextDocument(text, bounds, { isByteLengthChecked: true }),
),
};
nextResource.promise.then(
(value) => {
nextResource.status = "resolved";
nextResource.value = value;
},
(error) => {
nextResource.status = "rejected";
nextResource.error = error;
},
);
textResource = nextResource;
textResourceCache.set(resourceKey, textResource);
trimTextResourceCache();
}
return textResource;
}
async function readBoundedTextResource(
content: ViewerContentText,
bounds: Required<TextViewerBounds>,
) {
try {
return await content.readText(bounds);
} catch (error) {
if (isResourceError(error)) throw error;
throw toTextFormatError(error);
}
}
function emptyPreparedTextDocument(
bounds: Required<TextViewerBounds>,
): PreparedTextDocument {
return prepareTextDocument("", bounds);
}
function resolveTextViewerBound(value: number, boundName: TextViewerBoundName) {
if (!Number.isSafeInteger(value) || value < 1) {
throw new TextViewerInvalidBoundsError(boundName);
}
return value;
}
function trimTextResourceCache() {
while (textResourceCache.size > MAX_TEXT_RESOURCE_CACHE_ENTRIES) {
const firstKey = textResourceCache.keys().next().value;
if (firstKey === undefined) return;
textResourceCache.delete(firstKey);
}
}
registry/new-york-v4/ui/plain-text-resource.ts
export * from "./text-viewer-resource";
registry/new-york-v4/lib/viewer-source.ts
export type FileCategory =
| "pdf"
| "docx"
| "xlsx"
| "pptx"
| "csv"
| "image"
| "markdown"
| "html"
| "email"
| "text"
| "unsupported";
export type ViewerSource = UrlViewerSource | TextSource | BlobViewerSource;
export interface UrlViewerSource {
kind: "url";
url: string;
fileName?: string;
mimeType?: string;
downloadUrl?: string;
identityKey?: string;
}
export interface TextSource {
kind: "text";
text: string;
fileName?: string;
mimeType?: string;
identityKey?: string;
}
export interface BlobViewerSource {
kind: "blob";
blob: Blob;
identityKey: string;
fileName?: string;
mimeType?: string;
downloadUrl?: string;
}
export interface ViewerDescriptor {
source: ViewerSource;
category: FileCategory;
identityKey: string;
displayName: string;
fileName: string;
mimeType?: string;
}
const EXTENSION_CATEGORY: Record<string, FileCategory> = {
pdf: "pdf",
docx: "docx",
xlsx: "xlsx",
xls: "xlsx",
xlsm: "xlsx",
pptx: "pptx",
csv: "csv",
tsv: "csv",
png: "image",
jpg: "image",
jpeg: "image",
gif: "image",
webp: "image",
avif: "image",
bmp: "image",
svg: "image",
ico: "image",
tif: "image",
tiff: "image",
md: "markdown",
markdown: "markdown",
mdx: "text",
html: "html",
htm: "html",
eml: "email",
txt: "text",
text: "text",
log: "text",
json: "text",
jsonl: "text",
json5: "text",
ndjson: "text",
xml: "text",
yaml: "text",
yml: "text",
toml: "text",
ini: "text",
env: "text",
js: "text",
mjs: "text",
cjs: "text",
jsx: "text",
ts: "text",
tsx: "text",
css: "text",
scss: "text",
less: "text",
py: "text",
rb: "text",
go: "text",
rs: "text",
java: "text",
kt: "text",
c: "text",
h: "text",
cpp: "text",
cc: "text",
cs: "text",
php: "text",
sh: "text",
bash: "text",
zsh: "text",
sql: "text",
graphql: "text",
proto: "text",
lua: "text",
r: "text",
swift: "text",
scala: "text",
pl: "text",
vue: "text",
svelte: "text",
};
export function extensionOf(name: string): string | null {
const clean = name.split(/[?#]/)[0];
const base = clean.split("/").pop() ?? clean;
const dot = base.lastIndexOf(".");
return dot > 0 ? base.slice(dot + 1).toLowerCase() : null;
}
export function extractName(url: string): string {
const clean = url.split(/[?#]/)[0];
return clean.split("/").pop() || "file";
}
export function detectCategory(
fileName: string,
mimeType?: string,
): FileCategory {
const ext = extensionOf(fileName);
if (ext && EXTENSION_CATEGORY[ext]) return EXTENSION_CATEGORY[ext];
if (mimeType) {
const fromMime = categoryFromMime(mimeType);
if (fromMime) return fromMime;
}
return "unsupported";
}
export function resolveViewerDescriptor({
source,
category,
}: {
source: ViewerSource;
category?: FileCategory;
}): ViewerDescriptor {
const resolvedMimeType =
source.mimeType ??
(source.kind === "blob" && source.blob.type ? source.blob.type : undefined);
const displayName = source.fileName ?? defaultDisplayName(source);
const fileName = source.fileName ?? defaultFileName(source);
const resolvedCategory =
category ?? detectCategory(displayName, resolvedMimeType);
return {
source,
category: resolvedCategory,
identityKey: source.identityKey ?? defaultIdentityKey(source),
displayName,
fileName,
mimeType: resolvedMimeType,
};
}
function categoryFromMime(mimeType: string): FileCategory | null {
const mime = mimeType.toLowerCase().split(";")[0].trim();
if (mime === "application/pdf") return "pdf";
if (mime.includes("wordprocessingml")) return "docx";
if (mime.includes("spreadsheet") || mime.includes("ms-excel")) return "xlsx";
if (mime.includes("presentation") || mime.includes("ms-powerpoint")) {
return "pptx";
}
if (mime === "text/csv" || mime === "text/tab-separated-values") return "csv";
if (mime === "text/markdown") return "markdown";
if (mime === "text/html") return "html";
if (mime === "message/rfc822" || mime === "message/global") {
return "email";
}
if (mime.startsWith("image/")) return "image";
if (mime === "application/json" || mime === "application/xml") return "text";
if (mime.startsWith("text/")) return "text";
return null;
}
function defaultDisplayName(source: ViewerSource) {
if (source.kind === "url") return source.url;
if (source.kind === "text") return "text.txt";
return "file";
}
function defaultFileName(source: ViewerSource) {
if (source.kind === "url") return extractName(source.url);
if (source.kind === "text") return "text.txt";
return "file";
}
function defaultIdentityKey(source: ViewerSource) {
if (source.kind === "url") return `url:${source.url}`;
if (source.kind === "text") return textPayloadIdentityKey(source.text);
return source.identityKey;
}
export function textPayloadIdentityKey(text: string) {
return textPayloadKey(text);
}
export function textPayloadKey(text: string) {
return `text:${text.length}:${hashString(text)}`;
}
function hashString(text: string) {
let hash = 0x811c9dc5;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0).toString(36);
}
registry/new-york-v4/lib/viewer-resource.ts
import {
createBlobDownloadAction,
createHrefDownloadAction,
createTextDownloadAction,
type ViewerDownloadAction,
} from "@/lib/viewer-download-actions";
import {
isAbortError,
ResourceError,
type ResourceTooLargeReason,
} from "@/lib/viewer-errors";
import {
resolveViewerDescriptor,
textPayloadKey,
type BlobViewerSource,
type FileCategory,
type TextSource,
type UrlViewerSource,
type ViewerDescriptor,
type ViewerSource,
} from "@/lib/viewer-source";
export interface ResourceReadOptions {
cache?: RequestCache;
signal?: AbortSignal;
}
export interface TextReadOptions extends ResourceReadOptions {
maxBytes?: number;
maxLines?: number;
}
export interface ByteRange {
start: number;
end: number;
}
export interface ByteRangeResult {
buffer: ArrayBuffer;
contentRange?: {
start: number;
end: number;
total: number | null;
};
isComplete: boolean;
}
export interface ViewerResourceKeys {
readonly load: string;
readonly presentation: string;
readonly resource: string;
}
export type ViewerResourcePayload =
| { kind: "url"; url: string }
| { kind: "blob"; blob: Blob }
| { kind: "text"; text: string };
export interface ViewerResourceContent {
readonly key: string;
readonly sourceKind: ViewerSource["kind"];
readonly directUrl: string | null;
readonly mimeType?: string;
readonly payload: ViewerResourcePayload;
readBlob(options?: ResourceReadOptions): Promise<Blob>;
readBytes(options?: ResourceReadOptions): Promise<ArrayBuffer>;
readText(options?: TextReadOptions): Promise<string>;
readStream(
options?: ResourceReadOptions,
): Promise<ReadableStream<Uint8Array>>;
readRange(
range: ByteRange,
options?: ResourceReadOptions,
): Promise<ByteRangeResult>;
}
export type ViewerContentIdentity = Pick<
ViewerResourceContent,
"key" | "sourceKind"
>;
export type ViewerContentDirectUrl = ViewerContentIdentity &
Pick<ViewerResourceContent, "directUrl">;
export type ViewerContentPayload = ViewerContentIdentity &
Pick<ViewerResourceContent, "payload">;
export type ViewerContentMime = ViewerContentIdentity &
Pick<ViewerResourceContent, "mimeType">;
export type ViewerContentBlob = ViewerContentIdentity &
Pick<ViewerResourceContent, "readBlob">;
export type ViewerContentBytes = ViewerContentIdentity &
Pick<ViewerResourceContent, "readBytes">;
export type ViewerContentText = ViewerContentIdentity &
Pick<ViewerResourceContent, "readText">;
export type ViewerContentStream = ViewerContentIdentity &
Pick<ViewerResourceContent, "readStream">;
export type ViewerContentRange = ViewerContentIdentity &
Pick<ViewerResourceContent, "readRange">;
export interface ViewerResource {
readonly descriptor: ViewerDescriptor;
readonly sourceKind: ViewerSource["kind"];
readonly keys: ViewerResourceKeys;
readonly identityKey: string;
readonly fileName: string;
readonly mimeType?: string;
readonly content: ViewerResourceContent;
readonly originalDownload: ViewerDownloadAction;
}
const URL_RESOURCE_REGISTRY_MAX = 128;
const TEXT_RESOURCE_REGISTRY_MAX = 64;
// LF, CR, CRLF, LINE SEPARATOR (U+2028), and PARAGRAPH SEPARATOR (U+2029) — the
// ECMAScript LineTerminator set, matching what a browser breaks on in a
// `white-space: pre` block. Kept in sync with text-viewer-resource's splitter.
const TEXT_LINE_BREAK_PATTERN = /\r\n|[\n\r\u2028\u2029]/g;
const urlViewerResourceRegistry = new Map<string, ViewerResource>();
const urlViewerResourceContentRegistry = new Map<
string,
ViewerResourceContent
>();
const textViewerResourceRegistry = new Map<string, ViewerResource>();
const textViewerResourceContentRegistry = new Map<
string,
ViewerResourceContent
>();
let blobViewerResourceRegistry = new WeakMap<
Blob,
Map<string, ViewerResource>
>();
let blobViewerResourceContentRegistry = new WeakMap<
Blob,
Map<string, ViewerResourceContent>
>();
const blobObjectKeys = new WeakMap<Blob, string>();
let nextBlobObjectKey = 0;
export function createViewerResource(
source: ViewerSource,
category?: FileCategory,
): ViewerResource {
const descriptor = resolveViewerDescriptor({ source, category });
const keys = viewerResourceKeys(source, descriptor);
if (source.kind === "url") {
return internUrlResource(source, descriptor, keys);
}
if (source.kind === "blob") {
return internBlobResource(source, descriptor, keys);
}
return internTextResource(source, descriptor, keys);
}
export function clearViewerResourceRegistryForTests() {
urlViewerResourceRegistry.clear();
urlViewerResourceContentRegistry.clear();
textViewerResourceRegistry.clear();
textViewerResourceContentRegistry.clear();
blobViewerResourceRegistry = new WeakMap();
blobViewerResourceContentRegistry = new WeakMap();
}
function internUrlResource(
source: UrlViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const cached = urlViewerResourceRegistry.get(keys.resource);
if (cached) return cached;
const resource = createUrlResource(source, descriptor, keys);
urlViewerResourceRegistry.set(keys.resource, resource);
pruneUrlResourceRegistry();
return resource;
}
function internBlobResource(
source: BlobViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
let resources = blobViewerResourceRegistry.get(source.blob);
if (!resources) {
resources = new Map();
blobViewerResourceRegistry.set(source.blob, resources);
}
const cached = resources.get(keys.resource);
if (cached) return cached;
const resource = createBlobResource(source, descriptor, keys);
resources.set(keys.resource, resource);
return resource;
}
function internTextResource(
source: TextSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const cached = textViewerResourceRegistry.get(keys.resource);
if (cached) return cached;
const resource = createTextResource(source, descriptor, keys);
textViewerResourceRegistry.set(keys.resource, resource);
pruneTextResourceRegistry();
return resource;
}
function pruneUrlResourceRegistry() {
while (urlViewerResourceRegistry.size > URL_RESOURCE_REGISTRY_MAX) {
const firstKey = urlViewerResourceRegistry.keys().next().value;
if (!firstKey) return;
urlViewerResourceRegistry.delete(firstKey);
}
}
function pruneUrlResourceContentRegistry() {
while (urlViewerResourceContentRegistry.size > URL_RESOURCE_REGISTRY_MAX) {
const firstKey = urlViewerResourceContentRegistry.keys().next().value;
if (!firstKey) return;
urlViewerResourceContentRegistry.delete(firstKey);
}
}
function pruneTextResourceRegistry() {
while (textViewerResourceRegistry.size > TEXT_RESOURCE_REGISTRY_MAX) {
const firstKey = textViewerResourceRegistry.keys().next().value;
if (!firstKey) return;
textViewerResourceRegistry.delete(firstKey);
}
}
function pruneTextResourceContentRegistry() {
while (textViewerResourceContentRegistry.size > TEXT_RESOURCE_REGISTRY_MAX) {
const firstKey = textViewerResourceContentRegistry.keys().next().value;
if (!firstKey) return;
textViewerResourceContentRegistry.delete(firstKey);
}
}
function viewerResourceKeys(
source: ViewerSource,
descriptor: ViewerDescriptor,
): ViewerResourceKeys {
const load = viewerResourceLoadKey(source, descriptor);
const presentation = viewerResourcePresentationKey(source, descriptor);
return {
load,
presentation,
resource: [load, presentation].join("\u0000"),
};
}
function viewerResourceLoadKey(
source: ViewerSource,
descriptor: ViewerDescriptor,
) {
return [
source.kind,
source.identityKey ?? "",
sourceMimeType(source) ?? "",
directLoadCacheKey(source),
payloadCacheKey(source, descriptor),
].join("\u0000");
}
function viewerResourcePresentationKey(
source: ViewerSource,
descriptor: ViewerDescriptor,
) {
return [
descriptor.category,
descriptor.displayName,
descriptor.fileName,
descriptor.mimeType ?? "",
downloadCacheKey(source),
].join("\u0000");
}
function directLoadCacheKey(source: ViewerSource) {
return source.kind === "url" ? source.url : "";
}
function downloadCacheKey(source: ViewerSource) {
if (source.kind === "text") return "";
return source.downloadUrl ?? "";
}
function payloadCacheKey(source: ViewerSource, descriptor: ViewerDescriptor) {
if (source.kind === "url") return "";
if (source.kind === "blob") return blobObjectKey(source.blob);
return source.identityKey ? "" : descriptor.identityKey;
}
export function viewerResourceRenderKey(resource: ViewerResource): string {
const load = [
resource.sourceKind,
resource.identityKey,
resource.mimeType ?? resource.content.mimeType ?? "",
resource.content.directUrl ?? "",
viewerContentRenderKey(resource.content),
].join("\u0000");
return [load, resource.keys.presentation].join("\u0000");
}
export function viewerContentRenderKey(content: ViewerResourceContent): string {
if (content.payload.kind === "text")
return textPayloadKey(content.payload.text);
return content.key;
}
function sourceMimeType(source: ViewerSource) {
if (source.kind === "blob") return source.mimeType ?? source.blob.type;
return source.mimeType;
}
function blobObjectKey(blob: Blob) {
let key = blobObjectKeys.get(blob);
if (!key) {
nextBlobObjectKey += 1;
key = `blob-object:${nextBlobObjectKey}`;
blobObjectKeys.set(blob, key);
}
return key;
}
export function blobSource(
bytes: Blob | ArrayBuffer | Uint8Array,
metadata: {
identityKey: string;
fileName?: string;
mimeType?: string;
downloadUrl?: string;
},
): BlobViewerSource {
const blob =
bytes instanceof Blob
? bytes
: new Blob(
[bytes instanceof ArrayBuffer ? bytes : new Uint8Array(bytes)],
{
type: metadata.mimeType ?? "",
},
);
return {
kind: "blob",
blob,
fileName: metadata.fileName,
mimeType: metadata.mimeType ?? blob.type,
downloadUrl: metadata.downloadUrl,
identityKey: metadata.identityKey,
};
}
function createUrlResource(
source: UrlViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const content = internUrlResourceContent(source, keys);
const originalDownload = createHrefDownloadAction({
id: "download-original",
label: "Download",
href: source.downloadUrl ?? source.url,
fileName: descriptor.fileName,
});
return resourceBase(source, descriptor, keys, { content, originalDownload });
}
function internUrlResourceContent(
source: UrlViewerSource,
keys: ViewerResourceKeys,
): ViewerResourceContent {
const cached = urlViewerResourceContentRegistry.get(keys.load);
if (cached) return cached;
const content = resourceContentBase(source, keys, {
directUrl: source.url,
payload: { kind: "url", url: source.url },
readBlob: async ({ cache, signal } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
validateFullContentResponse(response);
return readResponseBlob(response);
},
readBytes: async ({ cache, signal } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
validateFullContentResponse(response);
return readResponseArrayBuffer(response);
},
readText: async ({ cache, signal, maxBytes, maxLines } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
return readBoundedResponseText(response, { maxBytes, maxLines });
},
readStream: async ({ cache, signal } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
validateFullContentResponse(response);
if (!response.body) {
if (response.status === 204 || response.status === 205) {
return emptyByteStream();
}
throw new ResourceError({
kind: "unsupported_capability",
message: "This response cannot be streamed.",
});
}
return response.body;
},
readRange: async (range, { cache, signal } = {}) => {
validateByteRange(range);
const { start, end } = range;
const init = {
signal,
headers: { Range: `bytes=${start}-${end}` },
};
const response = await fetchResource(
source.url,
cache ? { ...init, cache } : init,
);
const buffer = await readResponseArrayBuffer(response);
const contentRange = parseContentRange(
response.headers.get("content-range"),
);
validateUrlRangeResponse({
bufferLength: buffer.byteLength,
contentRange,
range,
status: response.status,
});
return {
buffer,
contentRange,
isComplete: isByteRangeComplete({
bufferLength: buffer.byteLength,
contentRange,
requestedLength: end - start + 1,
status: response.status,
}),
};
},
});
urlViewerResourceContentRegistry.set(keys.load, content);
pruneUrlResourceContentRegistry();
return content;
}
function emptyByteStream() {
return new ReadableStream<Uint8Array>({
start(controller) {
controller.close();
},
});
}
function createBlobResource(
source: BlobViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const blob = source.blob;
const content = internBlobResourceContent(source, keys);
const originalDownload = source.downloadUrl
? createHrefDownloadAction({
id: "download-original",
label: "Download",
href: source.downloadUrl,
fileName: descriptor.fileName,
})
: createBlobDownloadAction({
id: "download-original",
label: "Download",
blob,
fileName: descriptor.fileName,
});
return resourceBase(source, descriptor, keys, { content, originalDownload });
}
function internBlobResourceContent(
source: BlobViewerSource,
keys: ViewerResourceKeys,
): ViewerResourceContent {
const blob = source.blob;
let contents = blobViewerResourceContentRegistry.get(blob);
if (!contents) {
contents = new Map();
blobViewerResourceContentRegistry.set(blob, contents);
}
const cached = contents.get(keys.load);
if (cached) return cached;
const content = resourceContentBase(source, keys, {
directUrl: null,
payload: { kind: "blob", blob },
readBlob: async () => blob,
readBytes: async () => blob.arrayBuffer(),
readText: async ({ maxBytes, maxLines } = {}) =>
readBoundedBlobText(blob, { maxBytes, maxLines }),
readStream: async () => blob.stream(),
readRange: async (range) => {
validateByteRange(range);
const { start, end } = range;
validateKnownByteRangeStart(start, blob.size);
const rangeBlob = blob.slice(start, end + 1);
return {
buffer: await rangeBlob.arrayBuffer(),
contentRange: {
start,
end: Math.min(end, blob.size - 1),
total: blob.size,
},
isComplete: end >= blob.size - 1,
};
},
});
contents.set(keys.load, content);
return content;
}
function createTextResource(
source: TextSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const content = internTextResourceContent(source, keys);
const originalDownload = createTextDownloadAction({
id: "download-original",
label: "Download",
text: source.text,
fileName: descriptor.fileName,
mimeType: descriptor.mimeType,
});
return resourceBase(source, descriptor, keys, { content, originalDownload });
}
function internTextResourceContent(
source: TextSource,
keys: ViewerResourceKeys,
): ViewerResourceContent {
const cached = textViewerResourceContentRegistry.get(keys.load);
if (cached) return cached;
const content = resourceContentBase(source, keys, {
directUrl: null,
payload: { kind: "text", text: source.text },
readBlob: async () =>
new Blob([source.text], {
type: "text/plain;charset=utf-8",
}),
readBytes: async () =>
typedArrayBuffer(new TextEncoder().encode(source.text)),
readText: async ({ maxBytes, maxLines } = {}) =>
readBoundedInlineText(source.text, { maxBytes, maxLines }),
readStream: async () => new Blob([source.text]).stream(),
readRange: async (range) => {
validateByteRange(range);
const { start, end } = range;
const buffer = new TextEncoder().encode(source.text);
validateKnownByteRangeStart(start, buffer.byteLength);
const slice = buffer.slice(start, end + 1);
return {
buffer: typedArrayBuffer(slice),
contentRange: {
start,
end: Math.min(end, buffer.byteLength - 1),
total: buffer.byteLength,
},
isComplete: end >= buffer.byteLength - 1,
};
},
});
textViewerResourceContentRegistry.set(keys.load, content);
pruneTextResourceContentRegistry();
return content;
}
function resourceBase(
source: ViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
options: {
content: ViewerResourceContent;
originalDownload: ViewerDownloadAction;
},
): ViewerResource {
const { content, originalDownload } = options;
return Object.freeze({
descriptor,
sourceKind: source.kind,
keys,
identityKey: descriptor.identityKey,
fileName: descriptor.fileName,
mimeType: descriptor.mimeType,
content,
originalDownload,
});
}
function resourceContentBase(
source: ViewerSource,
keys: ViewerResourceKeys,
methods: Omit<ViewerResourceContent, "key" | "sourceKind" | "mimeType">,
): ViewerResourceContent {
return Object.freeze({
key: keys.load,
sourceKind: source.kind,
mimeType: sourceMimeType(source),
...methods,
});
}
function typedArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength,
) as ArrayBuffer;
}
async function fetchResource(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
let response: Response;
try {
response = await fetch(input, init);
} catch (error) {
if (isAbortError(error)) {
throw new ResourceError({
kind: "aborted",
message: "Loading was cancelled.",
cause: error,
});
}
throw new ResourceError({
kind: "fetch_failed",
message: "Could not fetch this resource.",
cause: error,
});
}
if (!response.ok && response.status !== 206) {
throw new ResourceError({
kind: "http_error",
message: `Failed to load resource: ${response.status}`,
status: response.status,
});
}
return response;
}
async function readBoundedResponseText(
response: Response,
bounds: { maxBytes?: number; maxLines?: number },
) {
validateFullContentResponse(response);
const maxBytes = bounds.maxBytes;
if (
isContentLengthOverLimit(response.headers.get("content-length"), maxBytes)
) {
throw tooLarge("bytes");
}
const body = response.body;
if (!body) {
const buffer = await readResponseArrayBuffer(response);
if (maxBytes != null && buffer.byteLength > maxBytes) {
throw tooLarge("bytes");
}
const text = new TextDecoder().decode(buffer);
assertLineLimit(text, bounds.maxLines);
return text;
}
const reader = body.getReader();
const decoder = new TextDecoder();
const lineLimitTracker = createLineLimitTracker(bounds.maxLines);
let receivedBytes = 0;
let text = "";
while (true) {
const { done, value } = await readResponseStreamChunk(reader);
if (done) break;
receivedBytes += value.byteLength;
if (maxBytes != null && receivedBytes > maxBytes) {
await cancelReaderSilently(reader);
throw tooLarge("bytes");
}
const chunkText = decoder.decode(value, { stream: true });
try {
lineLimitTracker.push(chunkText);
} catch (error) {
await cancelReaderSilently(reader);
throw error;
}
text += chunkText;
}
const finalText = decoder.decode();
lineLimitTracker.push(finalText);
text += finalText;
return text;
}
function isContentLengthOverLimit(
contentLength: string | null,
maxBytes: number | undefined,
) {
if (maxBytes == null || contentLength == null) return false;
const normalizedLength = contentLength.trim().replace(/^0+(?=\d)/, "");
if (!/^\d+$/.test(normalizedLength)) return false;
const maxLength = String(maxBytes);
return (
normalizedLength.length > maxLength.length ||
(normalizedLength.length === maxLength.length &&
normalizedLength > maxLength)
);
}
function validateFullContentResponse(response: Response) {
if (response.status !== 206) return;
const contentRange = parseContentRange(response.headers.get("content-range"));
if (
contentRange?.total != null &&
contentRange.start === 0 &&
contentRange.end === contentRange.total - 1
) {
return;
}
throw new ResourceError({
kind: "partial_content",
message: "Full response returned partial content.",
status: response.status,
});
}
async function readResponseStreamChunk(
reader: ReadableStreamDefaultReader<Uint8Array>,
) {
try {
return await reader.read();
} catch (error) {
throw resourceReadError(error);
}
}
async function readResponseArrayBuffer(response: Response) {
try {
return await response.arrayBuffer();
} catch (error) {
throw resourceReadError(error);
}
}
async function readResponseBlob(response: Response) {
try {
return await response.blob();
} catch (error) {
throw resourceReadError(error);
}
}
function resourceReadError(error: unknown) {
if (isAbortError(error)) {
return new ResourceError({
kind: "aborted",
message: "Loading was cancelled.",
cause: error,
});
}
return new ResourceError({
kind: "fetch_failed",
message: "Could not read this resource.",
cause: error,
});
}
async function readBoundedBlobText(
blob: Blob,
bounds: { maxBytes?: number; maxLines?: number },
) {
if (bounds.maxBytes != null && blob.size > bounds.maxBytes) {
throw tooLarge("bytes");
}
const text = await blob.text();
assertLineLimit(text, bounds.maxLines);
return text;
}
function readBoundedInlineText(
text: string,
{ maxBytes, maxLines }: { maxBytes?: number; maxLines?: number },
) {
// For inline sources the string *is* the resource, so its UTF-8 byte length
// is the authoritative size to measure against maxBytes.
if (
maxBytes != null &&
new TextEncoder().encode(text).byteLength > maxBytes
) {
throw tooLarge("bytes");
}
assertLineLimit(text, maxLines);
return text;
}
// Used after a transferred-byte check has already enforced maxBytes (URL/blob).
// Re-encoding the decoded text here would double-count: invalid UTF-8 decodes to
// U+FFFD (3 bytes each), inflating the measured size past the real wire bytes
// and falsely rejecting small resources as "too large".
function assertLineLimit(text: string, maxLines: number | undefined) {
if (
maxLines != null &&
text.split(TEXT_LINE_BREAK_PATTERN).length > maxLines
) {
throw tooLarge("lines");
}
}
function tooLarge(reason: ResourceTooLargeReason) {
return new ResourceError({
kind: "too_large",
tooLargeReason: reason,
message: `Resource exceeds ${reason} limit.`,
});
}
async function cancelReaderSilently(
reader: ReadableStreamDefaultReader<Uint8Array>,
) {
try {
await reader.cancel();
} catch {
// Preserve the user-facing load failure; cancellation is best-effort cleanup.
}
}
function validateByteRange({ start, end }: ByteRange) {
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end < start
) {
throw new ResourceError({
kind: "invalid_range",
message: "Byte range must use non-negative integer bounds.",
});
}
}
function validateKnownByteRangeStart(start: number, total: number) {
if (start > 0 && start >= total) {
throw new ResourceError({
kind: "invalid_range",
message: "Byte range starts past the available resource.",
});
}
}
function validateUrlRangeResponse({
bufferLength,
contentRange,
range,
status,
}: {
bufferLength: number;
contentRange: ByteRangeResult["contentRange"];
range: ByteRange;
status: number;
}) {
if (status === 200) {
if (range.start !== 0 || bufferLength > range.end - range.start + 1) {
throw new ResourceError({
kind: "invalid_range",
message: "Full response does not match the requested byte range.",
});
}
return;
}
if (status !== 206) {
throw new ResourceError({
kind: "invalid_range",
message: "Range response must return full or partial content.",
});
}
if (!contentRange) {
throw new ResourceError({
kind: "invalid_range",
message: "Partial content response is missing a valid byte range.",
});
}
const declaredLength = contentRange.end - contentRange.start + 1;
if (
contentRange.start !== range.start ||
contentRange.end < contentRange.start ||
contentRange.end > range.end ||
(contentRange.total != null && contentRange.end >= contentRange.total) ||
declaredLength !== bufferLength
) {
throw new ResourceError({
kind: "invalid_range",
message: "Response byte range does not match the requested range.",
});
}
}
function isByteRangeComplete({
bufferLength,
contentRange,
requestedLength,
status,
}: {
bufferLength: number;
contentRange: ByteRangeResult["contentRange"];
requestedLength: number;
status: number;
}) {
if (status === 200) return true;
if (contentRange?.total != null) {
if (contentRange.total <= 0) return true;
return contentRange.end >= contentRange.total - 1;
}
if (contentRange) return false;
return bufferLength < requestedLength;
}
function isStandaloneLineBreak(character: string) {
const code = character.charCodeAt(0);
return code === 0x0a || code === 0x2028 || code === 0x2029;
}
function createLineLimitTracker(maxLines: number | undefined) {
let lineCount = 1;
let previousWasCR = false;
return {
push(text: string) {
if (maxLines == null || text.length === 0) return;
for (const character of text) {
if (previousWasCR) {
previousWasCR = false;
if (character === "\n") continue;
}
if (character === "\r") {
lineCount += 1;
previousWasCR = true;
} else if (isStandaloneLineBreak(character)) {
// LF, plus LINE/PARAGRAPH SEPARATOR (U+2028/U+2029); none pair with CR.
lineCount += 1;
}
if (lineCount > maxLines) {
throw tooLarge("lines");
}
}
},
};
}
function parseContentRange(value: string | null) {
if (!value) return undefined;
const match = value.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)\s*$/i);
if (!match) return undefined;
const start = parseContentRangeNumber(match[1]);
const end = parseContentRangeNumber(match[2]);
const total =
match[3] === "*" ? null : parseContentRangeNumber(match[3] ?? "");
if (start == null || end == null || total === undefined) return undefined;
return {
start,
end,
total,
};
}
function parseContentRangeNumber(value: string) {
const number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
registry/new-york-v4/lib/viewer-download-actions.ts
export type ViewerDownloadOrigin = "original" | "derived";
export type ViewerDownloadPayload =
| { kind: "href"; href: string }
| { kind: "blob"; blob: Blob }
| { kind: "text"; text: string; mimeType?: string }
| { kind: "none" };
export interface ViewerDownloadAction {
id: string;
label: string;
fileName: string;
origin: ViewerDownloadOrigin;
isDisabled?: boolean;
getPayload: (options?: {
signal?: AbortSignal;
}) => ViewerDownloadPayload | Promise<ViewerDownloadPayload>;
}
export type ViewerDownloadErrorKind =
| "disabled"
| "aborted"
| "payload_failed"
| "unsupported";
export class ViewerDownloadError extends Error {
readonly kind: ViewerDownloadErrorKind;
readonly actionId: string;
override readonly cause?: unknown;
constructor({
actionId,
kind,
message,
cause,
}: {
actionId: string;
kind: ViewerDownloadErrorKind;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerDownloadError";
this.actionId = actionId;
this.kind = kind;
this.cause = cause;
}
}
export function createHrefDownloadAction({
id,
label = "Download",
href,
fileName,
origin = "original",
}: {
id: string;
label?: string;
href: string;
fileName: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
getPayload: () => ({ kind: "href", href }),
};
}
export function createBlobDownloadAction({
id,
label = "Download",
blob,
fileName,
origin = "original",
}: {
id: string;
label?: string;
blob: Blob;
fileName: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
getPayload: () => ({ kind: "blob", blob }),
};
}
export function createTextDownloadAction({
id,
label = "Download",
text,
fileName,
mimeType,
origin = "original",
}: {
id: string;
label?: string;
text: string;
fileName: string;
mimeType?: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
getPayload: () => ({ kind: "text", text, mimeType }),
};
}
export function createDisabledDownloadAction({
id,
label = "Download",
fileName,
origin = "original",
}: {
id: string;
label?: string;
fileName: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
isDisabled: true,
getPayload: () => ({ kind: "none" }),
};
}
registry/new-york-v4/lib/viewer-errors.ts
export type ViewerFormat =
| "pdf"
| "image"
| "text"
| "csv"
| "docx"
| "xlsx"
| "pptx"
| "file";
export type ViewerErrorDomain =
| "resource"
| "format"
| "state"
| "unsupported"
| "unknown";
export type ResourceErrorKind =
| "fetch_failed"
| "http_error"
| "aborted"
| "invalid_range"
| "partial_content"
| "too_large"
| "unsupported_capability"
| "unknown";
export type ResourceTooLargeReason = "bytes" | "lines";
export class ResourceError extends Error {
readonly domain = "resource";
readonly kind: ResourceErrorKind;
readonly status?: number;
readonly tooLargeReason?: ResourceTooLargeReason;
override readonly cause?: unknown;
constructor({
kind,
message,
status,
tooLargeReason,
cause,
}: {
kind: ResourceErrorKind;
message: string;
status?: number;
tooLargeReason?: ResourceTooLargeReason;
cause?: unknown;
}) {
super(message);
this.name = "ResourceError";
this.kind = kind;
this.status = status;
this.tooLargeReason = tooLargeReason;
this.cause = cause;
}
}
export type ViewerFormatErrorKind =
| "bounds"
| "decode_failed"
| "disposed"
| "index_out_of_range"
| "load_failed"
| "parse_failed"
| "render_failed"
| "worker_failed"
| "unknown";
export interface ViewerFormatErrorMapperOptions {
kind: ViewerFormatErrorKind;
message: string;
}
export class ViewerFormatError extends Error {
readonly domain = "format";
readonly format: ViewerFormat;
readonly kind: ViewerFormatErrorKind;
override readonly cause?: unknown;
constructor({
format,
kind,
message,
cause,
}: {
format: ViewerFormat;
kind: ViewerFormatErrorKind;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerFormatError";
this.format = format;
this.kind = kind;
this.cause = cause;
}
}
export type ViewerStateErrorKind =
| "invalid_bounds"
| "invalid_target"
| "out_of_range"
| "stale_resource"
| "unknown";
export class ViewerStateError extends Error {
readonly domain = "state";
readonly format?: ViewerFormat;
readonly kind: ViewerStateErrorKind;
override readonly cause?: unknown;
constructor({
format,
kind,
message,
cause,
}: {
format?: ViewerFormat;
kind: ViewerStateErrorKind;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerStateError";
this.format = format;
this.kind = kind;
this.cause = cause;
}
}
export class ViewerUnsupportedError extends Error {
readonly domain = "unsupported";
readonly format?: ViewerFormat;
readonly sourceKind?: string;
override readonly cause?: unknown;
constructor({
format,
sourceKind,
message,
cause,
}: {
format?: ViewerFormat;
sourceKind?: string;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerUnsupportedError";
this.format = format;
this.sourceKind = sourceKind;
this.cause = cause;
}
}
export interface ViewerErrorInfo {
domain: ViewerErrorDomain;
format?: ViewerFormat;
kind: string;
message: string;
status?: number;
isRetryable: boolean;
isDownloadUseful: boolean;
userMessage: string;
cause?: unknown;
}
export interface ViewerErrorContext {
format?: ViewerFormat;
sourceKind?: "url" | "blob" | "text";
canDownload?: boolean;
retry?: "auto" | "always" | "never";
}
export function isAbortError(error: unknown): boolean {
return (
(error instanceof DOMException && error.name === "AbortError") ||
(error instanceof Error && error.name === "AbortError")
);
}
export function isResourceError(error: unknown): error is ResourceError {
return (
error instanceof ResourceError ||
isErrorLike(error, "ResourceError", "resource")
);
}
export function isViewerFormatError(
error: unknown,
): error is ViewerFormatError {
return (
error instanceof ViewerFormatError ||
isErrorLike(error, "ViewerFormatError", "format")
);
}
export function isViewerStateError(error: unknown): error is ViewerStateError {
return (
error instanceof ViewerStateError ||
isErrorLike(error, "ViewerStateError", "state")
);
}
export function isViewerUnsupportedError(
error: unknown,
): error is ViewerUnsupportedError {
return (
error instanceof ViewerUnsupportedError ||
isErrorLike(error, "ViewerUnsupportedError", "unsupported")
);
}
export function toViewerErrorInfo(
error: unknown,
context: ViewerErrorContext = {},
): ViewerErrorInfo {
const canDownload = context.canDownload ?? true;
if (isResourceError(error)) {
return {
domain: "resource",
format: context.format,
kind: error.kind,
message: error.message,
status: error.status,
isRetryable: retryable(
context,
resourceErrorDefaultRetry(error, context),
),
isDownloadUseful: canDownload && error.kind !== "aborted",
userMessage: resourceErrorUserMessage(error),
cause: error.cause,
};
}
if (isViewerFormatError(error)) {
const format = error.format ?? context.format;
return {
domain: "format",
format,
kind: error.kind,
message: error.message,
isRetryable: retryable(
context,
formatErrorDefaultRetry(error, context, format),
),
isDownloadUseful: canDownload,
userMessage: formatErrorUserMessage(format, error.kind, error),
cause: error.cause,
};
}
if (isViewerStateError(error)) {
return {
domain: "state",
format: error.format ?? context.format,
kind: error.kind,
message: error.message,
isRetryable: retryable(context, false),
isDownloadUseful: canDownload,
userMessage: stateErrorUserMessage(error.kind),
cause: error.cause,
};
}
if (isViewerUnsupportedError(error)) {
return {
domain: "unsupported",
format: error.format ?? context.format,
kind: "unsupported",
message: error.message,
isRetryable: retryable(context, false),
isDownloadUseful: canDownload,
userMessage: "This file cannot be previewed here.",
cause: error.cause,
};
}
if (isAbortError(error)) {
return {
domain: "resource",
format: context.format,
kind: "aborted",
message: "Loading was cancelled.",
isRetryable: retryable(context, false),
isDownloadUseful: false,
userMessage: "Loading was cancelled.",
cause: error,
};
}
const message = error instanceof Error ? error.message : String(error);
return {
domain: "unknown",
format: context.format,
kind: "unknown",
message,
isRetryable: retryable(context, unknownErrorDefaultRetry(context)),
isDownloadUseful: canDownload,
userMessage: fallbackUserMessage(context.format),
cause: error,
};
}
function isErrorLike(error: unknown, name: string, domain: ViewerErrorDomain) {
if (!error || typeof error !== "object") return false;
const candidate = error as {
name?: unknown;
domain?: unknown;
kind?: unknown;
};
return (
(candidate.name === name || candidate.domain === domain) &&
typeof candidate.kind === "string"
);
}
function retryable(context: ViewerErrorContext, fallback: boolean) {
if (context.retry === "always") return true;
if (context.retry === "never") return false;
return fallback;
}
function resourceErrorDefaultRetry(
error: ResourceError,
context: ViewerErrorContext,
) {
if (error.kind === "aborted") return false;
if (error.kind === "invalid_range") return false;
if (error.kind === "too_large") return false;
if (error.kind === "unsupported_capability") return false;
return context.sourceKind === "url";
}
function formatErrorDefaultRetry(
error: ViewerFormatError,
context: ViewerErrorContext,
format: ViewerFormat | undefined,
) {
if (format === "text" && error.kind === "bounds") return false;
if (error.kind === "disposed") return false;
if (error.kind === "index_out_of_range") return false;
if (format === "docx") return true;
return context.sourceKind === "url";
}
function unknownErrorDefaultRetry(context: ViewerErrorContext) {
if (context.format === "docx") return true;
return context.sourceKind === "url";
}
function resourceErrorUserMessage(error: ResourceError) {
if (error.kind === "http_error") {
return error.status
? `Failed to load file: ${error.status}.`
: "Couldn't load this file.";
}
if (error.kind === "fetch_failed") return "Couldn't load this file.";
if (error.kind === "aborted") return "Loading was cancelled.";
if (error.kind === "invalid_range") return "This source range is invalid.";
if (error.kind === "too_large") {
return error.tooLargeReason === "lines"
? "This file has too many lines to preview."
: "This file is too large to preview.";
}
if (error.kind === "partial_content") {
return "This source returned partial content and cannot be previewed here.";
}
if (error.kind === "unsupported_capability") {
return "This source cannot be previewed here.";
}
return "Couldn't load this file.";
}
function formatErrorUserMessage(
format: ViewerFormat | undefined,
kind: string,
error?: unknown,
) {
if (format === "pdf") return "Couldn't load this PDF.";
if (format === "image") {
if (kind === "index_out_of_range")
return "This image page is out of range.";
if (kind === "decode_failed") return "Couldn't decode this image.";
return "Couldn't load this image.";
}
if (format === "text") {
if (kind === "render_failed") return "Couldn't render this text file.";
if (kind === "bounds") {
const boundsError = error as {
reason?: unknown;
boundName?: unknown;
};
if (boundsError.reason === "lines") {
return "This text file has too many lines to preview.";
}
if (boundsError.reason === "bytes") {
return "This text file is too large to preview.";
}
if (typeof boundsError.boundName === "string") {
return "Text viewer bounds are invalid.";
}
}
return "Couldn't load this text file.";
}
if (format === "csv") return "Couldn't parse this table.";
if (format === "docx") return "Couldn't render this document.";
if (format === "xlsx") return "Couldn't parse this spreadsheet.";
if (format === "pptx") {
if (kind === "render_failed") return "Couldn't render this slide.";
return "Couldn't load this presentation.";
}
return "Couldn't load this file.";
}
function stateErrorUserMessage(kind: ViewerStateErrorKind) {
if (kind === "invalid_bounds") return "Viewer bounds are invalid.";
if (kind === "invalid_target") return "The requested target is invalid.";
if (kind === "out_of_range") return "The requested item is out of range.";
if (kind === "stale_resource") return "This viewer state is no longer valid.";
return "Couldn't load this file.";
}
function fallbackUserMessage(format: ViewerFormat | undefined) {
if (format === "pdf") return "Couldn't load this PDF.";
if (format === "image") return "Couldn't load this image.";
if (format === "text") return "Couldn't load this text file.";
if (format === "csv") return "Couldn't parse this table.";
if (format === "docx") return "Couldn't load this document.";
if (format === "xlsx") return "Couldn't load this spreadsheet.";
if (format === "pptx") return "Couldn't load this presentation.";
return "Couldn't load this file.";
}
registry/new-york-v4/ui/use-is-client.ts
"use client";
import * as React from "react";
const emptySubscribe = () => () => {};
const getClientSnapshot = () => true;
const getServerSnapshot = () => false;
/**
* SSR gate: `false` on the server (and during hydration's first pass),
* `true` on the client.
*
* This must stay a synchronous external-store read, NOT the
* `useState(false)` + mount-effect flip. The flip pattern makes every
* viewer mount its Suspense boundary in a later update; when two such
* boundaries suspend on pending resources in the same flush as other
* commit-phase updates (viewer sidebar/geometry registration), React 19's
* retry lanes desynchronize and re-attempt each other's boundary on every
* commit — an unbounded synchronous suspend/retry loop that starves the
* event loop (jsdom tests OOM; browsers busy-spin until the resource
* resolves). With the store read, client renders suspend on mount, which
* never enters that loop. Regression-guarded in
* tests/pdf-viewer-thumbnails.test.tsx ("shares one document resource…").
*/
export function useIsClient(): boolean {
return React.useSyncExternalStore(
emptySubscribe,
getClientSnapshot,
getServerSnapshot,
);
}
registry/new-york-v4/ui/code-viewer-long-lines.ts
export const CODE_VIEWER_LONG_LINE_RENDER_MAX = 4096;
const CODE_VIEWER_LONG_LINE_HEAD_CHARS = 3072;
const CODE_VIEWER_LONG_LINE_TAIL_CHARS = 768;
export type CodeLineRenderText = {
isTruncated: boolean;
omittedCharacterCount: number;
text: string;
};
export function getCodeLineRenderText(text: string): CodeLineRenderText {
if (text.length <= CODE_VIEWER_LONG_LINE_RENDER_MAX) {
return {
isTruncated: false,
omittedCharacterCount: 0,
text,
};
}
const omittedCharacterCount =
text.length -
CODE_VIEWER_LONG_LINE_HEAD_CHARS -
CODE_VIEWER_LONG_LINE_TAIL_CHARS;
return {
isTruncated: true,
omittedCharacterCount,
text:
text.slice(0, CODE_VIEWER_LONG_LINE_HEAD_CHARS) +
` ... ${omittedCharacterCount} chars omitted ... ` +
text.slice(-CODE_VIEWER_LONG_LINE_TAIL_CHARS),
};
}
export function getCodeLongLineSelectionText({
rowHost,
selection,
textLines,
}: {
rowHost: HTMLPreElement;
selection: Selection | null;
textLines: readonly string[];
}) {
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
return null;
}
const selectedLineIndexes = new Set<number>();
let includesTruncatedLine = false;
for (let rangeIndex = 0; rangeIndex < selection.rangeCount; rangeIndex += 1) {
const range = selection.getRangeAt(rangeIndex);
if (!rangeIntersectsNode(range, rowHost)) continue;
for (const row of rowHost.querySelectorAll<HTMLElement>(
"[data-line-index]",
)) {
if (!rangeIntersectsNode(range, row)) continue;
const lineIndex = Number(row.dataset.lineIndex);
if (
Number.isInteger(lineIndex) &&
lineIndex >= 0 &&
lineIndex < textLines.length
) {
selectedLineIndexes.add(lineIndex);
}
if (row.dataset.codeLineTruncated != null) {
includesTruncatedLine = true;
}
}
}
if (!includesTruncatedLine || selectedLineIndexes.size === 0) return null;
return Array.from(selectedLineIndexes)
.sort((first, second) => first - second)
.map((lineIndex) => textLines[lineIndex] ?? "")
.join("\n");
}
function rangeIntersectsNode(range: Range, node: Node) {
try {
if (range.intersectsNode(node)) return true;
} catch {
// Detached test DOM can make intersectsNode throw even when the selected
// content is plainly inside the row.
}
return (
node === range.commonAncestorContainer ||
node.contains(range.commonAncestorContainer)
);
}
registry/new-york-v4/ui/text-viewer-scroll-interactions.ts
"use client";
import * as React from "react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { joinEffectKey } from "@/lib/effect-key";
export const TEXT_SCROLL_INTERACTION_RESTORE_DELAY_MS = 120;
export type ScrollInteractionSnapshot = {
overflowTarget: HTMLElement | null;
overflowX: InlineStyleSnapshot | null;
pointerEvents: InlineStyleSnapshot;
target: HTMLElement;
};
type InlineStyleSnapshot = {
priority: string;
value: string;
};
export function useTextViewerScrollInteractions<Viewport extends HTMLElement>({
getInteractionTarget,
getOverflowTarget,
onScroll,
viewportRef,
}: {
getInteractionTarget: () => HTMLElement | null | undefined;
getOverflowTarget?: () => HTMLElement | null | undefined;
onScroll: (viewport: Viewport) => void;
viewportRef: React.RefObject<Viewport | null>;
}) {
const restoreTimerRef = React.useRef(0);
const snapshotRef = React.useRef<ScrollInteractionSnapshot | null>(null);
useKeyedMountEffect(
joinEffectKey([
getInteractionTarget,
getOverflowTarget,
onScroll,
viewportRef,
]),
() => {
const viewport = viewportRef.current;
if (!viewport) return;
const handleScroll = () => {
suspendTextViewerScrollInteractions({
getInteractionTarget,
getOverflowTarget,
snapshotRef,
});
if (restoreTimerRef.current) {
window.clearTimeout(restoreTimerRef.current);
}
restoreTimerRef.current = window.setTimeout(() => {
restoreTimerRef.current = 0;
restoreTextViewerScrollInteractions(snapshotRef);
}, TEXT_SCROLL_INTERACTION_RESTORE_DELAY_MS);
onScroll(viewport);
};
viewport.addEventListener("scroll", handleScroll, { passive: true });
return () => {
viewport.removeEventListener("scroll", handleScroll);
if (restoreTimerRef.current) {
window.clearTimeout(restoreTimerRef.current);
restoreTimerRef.current = 0;
}
restoreTextViewerScrollInteractions(snapshotRef);
};
},
);
}
export function suspendTextViewerScrollInteractions({
getInteractionTarget,
getOverflowTarget,
snapshotRef,
}: {
getInteractionTarget: () => HTMLElement | null | undefined;
getOverflowTarget?: () => HTMLElement | null | undefined;
snapshotRef: React.MutableRefObject<ScrollInteractionSnapshot | null>;
}) {
const target = getInteractionTarget();
if (!target) {
restoreTextViewerScrollInteractions(snapshotRef);
return;
}
const overflowTarget = isMobileSafari()
? (getOverflowTarget?.() ?? target.parentElement ?? target)
: null;
const current = snapshotRef.current;
if (
current &&
current.target === target &&
current.overflowTarget === overflowTarget
) {
target.style.pointerEvents = "none";
overflowTarget?.style.setProperty("overflow-x", "hidden");
return;
}
restoreTextViewerScrollInteractions(snapshotRef);
snapshotRef.current = {
overflowTarget,
overflowX: overflowTarget
? readInlineStyle(overflowTarget, "overflow-x")
: null,
pointerEvents: readInlineStyle(target, "pointer-events"),
target,
};
target.style.pointerEvents = "none";
overflowTarget?.style.setProperty("overflow-x", "hidden");
}
export function restoreTextViewerScrollInteractions(
snapshotRef: React.MutableRefObject<ScrollInteractionSnapshot | null>,
) {
const snapshot = snapshotRef.current;
if (!snapshot) return;
restoreInlineStyle(snapshot.target, "pointer-events", snapshot.pointerEvents);
if (snapshot.overflowTarget && snapshot.overflowX) {
restoreInlineStyle(
snapshot.overflowTarget,
"overflow-x",
snapshot.overflowX,
);
}
snapshotRef.current = null;
}
function isMobileSafari() {
if (typeof navigator === "undefined") return false;
const userAgent = navigator.userAgent;
return (
/Safari/i.test(userAgent) &&
/Mobile/i.test(userAgent) &&
!/CriOS|FxiOS|EdgiOS/i.test(userAgent)
);
}
function readInlineStyle(
element: HTMLElement,
propertyName: string,
): InlineStyleSnapshot {
return {
priority: element.style.getPropertyPriority(propertyName),
value: element.style.getPropertyValue(propertyName),
};
}
function restoreInlineStyle(
element: HTMLElement,
propertyName: string,
snapshot: InlineStyleSnapshot,
) {
if (!snapshot.value) {
element.style.removeProperty(propertyName);
return;
}
element.style.setProperty(propertyName, snapshot.value, snapshot.priority);
}