retab
uimarkdown-viewersource: ok · refreshed
Markdown Viewer
A continuous Markdown viewer forked from the Chenglou Pretext text viewer, with block virtualization and no visible page model.
npx shadcn add "https://ui.retab.com/r/markdown-viewer.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Files50
registry/new-york-v4/ui/markdown-viewer.tsx
"use client";
import * as React from "react";
import {
MarkdownGreenfieldContent,
type MarkdownViewerProps,
} from "./markdown-greenfield-content";
import type { ViewerResource } from "@/lib/viewer-resource";
import { PlainTextViewerFrame } from "./plain-text-viewer-frame";
import { TextViewerFallback } from "./text-viewer-chrome";
import type { TextViewerHandle } from "./text-viewer-types";
export type { MarkdownViewerProps } from "./markdown-greenfield-content";
export type {
TextDocumentSource,
TextLineRange,
TextViewerHandle,
TextViewerProps,
} from "./text-viewer-types";
export type MarkdownResourceContentProps = Omit<
MarkdownViewerProps,
"source"
> & {
resource: ViewerResource;
};
export const MarkdownViewer = React.forwardRef<
TextViewerHandle,
MarkdownViewerProps
>(function MarkdownViewer(props, ref) {
return (
<PlainTextViewerFrame
props={props}
forwardedRef={ref}
clientFallbackPolicy="non-inline-source"
Fallback={TextViewerFallback}
Content={MarkdownGreenfieldContent}
/>
);
});
export const MarkdownResourceContent = React.forwardRef<
TextViewerHandle,
MarkdownResourceContentProps
>(function MarkdownResourceContent({ resource, ...props }, ref) {
return (
<PlainTextViewerFrame
props={props}
resource={resource}
forwardedRef={ref}
clientFallbackPolicy="non-inline-source"
Fallback={TextViewerFallback}
Content={MarkdownGreenfieldContent}
/>
);
});
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/text-viewer-chrome.tsx
"use client";
import * as React from "react";
import { AlertCircle, Check, Copy } from "lucide-react";
import { useMountEffect } from "@/hooks/use-mount-effect";
import { type ViewerDownloadAction } from "@/lib/viewer-download-actions";
import { Skeleton } from "./skeleton";
import { TextCodeViewerFrame } from "./text-code-viewer-chrome";
import type { ViewerDownloadErrorHandler } from "./viewer-download";
import {
ViewerControls,
ViewerControlButton,
ViewerControlsSkeleton,
} from "./viewer-controls";
export type ViewerClipboardCopyStatus = "copied" | "failed" | "idle";
export function TextViewerFrame({
className,
bare,
children,
}: {
className?: string;
bare?: boolean;
children: React.ReactNode;
}) {
return (
<TextCodeViewerFrame
bare={bare}
bareClassName="h-full bg-background"
className={className}
dataSlot="text-viewer"
framedClassName="rounded-xl border bg-background"
>
{children}
</TextCodeViewerFrame>
);
}
export function TextViewerFallback({
className,
controls = true,
download = true,
bare,
}: {
className?: string;
controls?: boolean;
download?: boolean;
bare?: boolean;
}) {
return (
<TextViewerFrame className={className} bare={bare}>
{controls ? (
<ViewerControlsSkeleton title zoom download={download} />
) : null}
<div
className="min-h-0 flex-1 space-y-3 overflow-hidden p-5"
data-slot="text-body-skeleton"
>
{Array.from({ length: 10 }, (_, index) => (
<Skeleton
key={index}
className="h-4"
style={{ width: `${48 + ((index * 17) % 44)}%` }}
/>
))}
</div>
</TextViewerFrame>
);
}
export function TextViewerControls({
wordCount,
fontScale,
copyText,
copyLabel = "Copy text",
downloadAction,
extra,
leading,
onDownloadError,
onZoomOut,
onZoomIn,
onResetZoom,
}: {
wordCount: number;
fontScale: number;
copyText?: string;
copyLabel?: string;
downloadAction?: ViewerDownloadAction | null;
extra?: React.ReactNode;
leading?: React.ReactNode;
onDownloadError?: ViewerDownloadErrorHandler;
onZoomOut: () => void;
onZoomIn: () => void;
onResetZoom: () => void;
}) {
const copyControl =
copyText == null ? null : (
<TextViewerCopyControl label={copyLabel} text={copyText} />
);
return (
<ViewerControls
title={leading ?? `${wordCount} word${wordCount === 1 ? "" : "s"}`}
zoom={{
scale: fontScale,
onZoomOut,
onZoomIn,
onFit: onResetZoom,
fitLabel: "Reset zoom",
}}
downloads={downloadAction ? [downloadAction] : undefined}
onDownloadError={onDownloadError}
extra={
extra == null && copyControl == null ? null : (
<span className="flex min-w-0 items-center gap-1">
{extra}
{copyControl}
</span>
)
}
/>
);
}
function TextViewerCopyControl({
label,
text,
}: {
label: string;
text: string;
}) {
const { copy, status } = useViewerClipboardCopy();
const copyText = () => {
copy(text);
};
const buttonLabel =
status === "copied"
? "Copied"
: status === "failed"
? "Copy failed"
: label;
return (
<ViewerControlButton label={buttonLabel} onClick={copyText} type="button">
{status === "copied" ? (
<Check />
) : status === "failed" ? (
<AlertCircle />
) : (
<Copy />
)}
</ViewerControlButton>
);
}
export function useViewerClipboardCopy({
resetDelay = 1200,
}: {
resetDelay?: number;
} = {}) {
const [status, setStatus] = React.useState<ViewerClipboardCopyStatus>("idle");
const timeoutRef = React.useRef<number | null>(null);
const isMountedRef = React.useRef(true);
const copyAttemptRef = React.useRef(0);
useMountEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
clearViewerClipboardCopyReset(timeoutRef);
};
});
const scheduleReset = React.useCallback(() => {
clearViewerClipboardCopyReset(timeoutRef);
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = null;
if (isMountedRef.current) setStatus("idle");
}, resetDelay);
}, [resetDelay]);
const copy = React.useCallback(
(text: string) => {
clearViewerClipboardCopyReset(timeoutRef);
const copyAttempt = copyAttemptRef.current + 1;
copyAttemptRef.current = copyAttempt;
const isCurrentAttempt = () =>
isMountedRef.current && copyAttemptRef.current === copyAttempt;
try {
const clipboard = navigator.clipboard;
const writeText = clipboard?.writeText;
if (typeof writeText !== "function") {
setStatus("failed");
scheduleReset();
return;
}
void Promise.resolve(writeText.call(clipboard, text)).then(
() => {
if (!isCurrentAttempt()) return;
setStatus("copied");
scheduleReset();
},
() => {
if (!isCurrentAttempt()) return;
setStatus("failed");
scheduleReset();
},
);
} catch {
setStatus("failed");
scheduleReset();
}
},
[scheduleReset],
);
return { copy, status };
}
function clearViewerClipboardCopyReset(
timeoutRef: React.MutableRefObject<number | null>,
) {
if (timeoutRef.current === null) return;
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
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/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-layout.ts
"use client";
import {
layoutNextLineRange,
materializeLineRange,
measureLineStats,
measureNaturalWidth,
prepareWithSegments,
type LayoutCursor,
type LayoutLine,
type PreparedTextWithSegments,
} from "@chenglou/pretext";
import {
layoutNextRichInlineLineRange,
materializeRichInlineLineRange,
measureRichInlineStats,
prepareRichInline,
type PreparedRichInline,
type RichInlineCursor,
type RichInlineLine,
} from "@chenglou/pretext/rich-inline";
import { marked, type Token, type Tokens } from "marked";
import { splitTextLines } from "./text-viewer-resource";
export type TextViewerMode = "markdown" | "text";
export interface TextStyleConfig {
fontEpoch?: number;
fontScale: number;
}
export interface PreparedTextDocument {
blocks: PreparedTextBlock[];
mode: TextViewerMode;
sourceLineCount: number;
wordCount: number;
}
export type PreparedTextBlock =
| PreparedInlineTextBlock
| PreparedCodeTextBlock
| PreparedImageTextBlock
| PreparedRuleTextBlock
| PreparedTableTextBlock;
export interface PreparedTextBlockBase {
contentLeft: number;
listDepth: number;
marginTop: number;
markerClassName: string | null;
markerLeft: number | null;
markerText: string | null;
quoteDepth: number;
quoteRailLefts: number[];
sourceEndLine: number;
sourceStartLine: number;
}
export interface PreparedInlineTextBlock extends PreparedTextBlockBase {
classNames: string[];
fallbackText: string;
flow: PreparedRichInline | null;
fonts: string[];
headingId: string | null;
hrefs: Array<string | null>;
kind: "inline";
lineHeight: number;
texts: string[];
titles: Array<string | null>;
variant: InlineVariant;
}
export interface PreparedCodeTextBlock extends PreparedTextBlockBase {
fallbackText: string;
font: string;
kind: "code";
language: string | null;
lineHeight: number;
prepared: PreparedTextWithSegments | null;
}
export interface PreparedImageTextBlock extends PreparedTextBlockBase {
alt: string;
href: string | null;
kind: "image";
src: string | null;
title: string | null;
}
export interface PreparedRuleTextBlock extends PreparedTextBlockBase {
height: number;
kind: "rule";
}
export interface PreparedTableTextBlock extends PreparedTextBlockBase {
alignments: TableColumnAlignment[];
columnWidths: number[];
header: PreparedTableCell[];
kind: "table";
rowSourceStartLines: number[];
rows: PreparedTableCell[][];
}
export interface PreparedTableCell {
className: string;
href: string | null;
text: string;
title: string | null;
}
export interface TextDocumentFrame {
frames: TextBlockFrame[];
totalHeight: number;
width: number;
}
export type TextBlockFrame =
| InlineTextBlockFrame
| CodeTextBlockFrame
| ImageTextBlockFrame
| RuleTextBlockFrame
| TableTextBlockFrame;
interface TextBlockFrameBase {
blockIndex: number;
bottom: number;
contentLeft: number;
height: number;
listDepth: number;
markerClassName: string | null;
markerLeft: number | null;
markerText: string | null;
quoteDepth: number;
quoteRailLefts: number[];
sourceEndLine: number;
sourceStartLine: number;
scale: number;
top: number;
}
export interface InlineTextBlockFrame extends TextBlockFrameBase {
kind: "inline";
lineCount: number;
lineHeight: number;
usedWidth: number;
}
export interface CodeTextBlockFrame extends TextBlockFrameBase {
kind: "code";
language: string | null;
lineCount: number;
lineHeight: number;
width: number;
}
export interface ImageTextBlockFrame extends TextBlockFrameBase {
alt: string;
imageHeight: number;
imageWidth: number;
kind: "image";
}
export interface RuleTextBlockFrame extends TextBlockFrameBase {
kind: "rule";
width: number;
}
export interface TableTextBlockFrame extends TextBlockFrameBase {
columnWidths: number[];
headerHeight: number;
kind: "table";
rowHeights: number[];
rowOffsets: number[];
rowCount: number;
rowSourceStartLines: number[];
tableWidth: number;
}
export interface InlineFragmentLayout {
className: string;
font: string;
href: string | null;
leadingGap: number;
text: string;
title: string | null;
}
export interface InlineLineLayout {
fragments: InlineFragmentLayout[];
lineIndex: number;
top: number;
width: number;
}
export interface CodeLineLayout {
lineIndex: number;
line: LayoutLine;
top: number;
}
export interface TextLineWindow {
firstLine: number;
lastLine: number;
}
export type TableColumnAlignment = "center" | "left" | "right";
export interface TableRowWindow {
afterHeight: number;
beforeHeight: number;
endIndex: number;
startIndex: number;
}
type InlineVariant = "body" | "heading-1" | "heading-2";
type MarkState = {
bold: boolean;
italic: boolean;
strike: boolean;
href: string | null;
title: string | null;
};
type ParseContext = {
listDepth: number;
quoteDepth: number;
};
type HeadingIdRegistry = Map<string, number>;
type InlinePiece = {
breakMode: "never" | "normal";
className: string;
extraWidth: number;
font: string;
href: string | null;
text: string;
title: string | null;
};
const BODY_FONT_PX = 15;
const BODY_LINE_PX = 24;
const HEADING_ONE_FONT_PX = 22;
const HEADING_ONE_LINE_PX = 32;
const HEADING_TWO_FONT_PX = 18;
const HEADING_TWO_LINE_PX = 28;
const CODE_FONT_PX = 13;
const CODE_LINE_PX = 20;
const MARKER_FONT_PX = 12;
const CHIP_FONT_PX = 12;
const INLINE_CODE_EXTRA_WIDTH = 12;
const IMAGE_EXTRA_WIDTH = 16;
const CODE_BLOCK_PADDING_X = 12;
const CODE_BLOCK_PADDING_Y = 10;
const DOCUMENT_PADDING_Y = 16;
const BLOCK_GAP = 12;
const RICH_BLOCK_GAP = 6;
const HARD_BREAK_GAP = 4;
const LIST_ITEM_GAP = 4;
const LIST_MARKER_GAP = 10;
const LIST_NESTING_INDENT = 20;
const BLOCKQUOTE_INDENT = 18;
const RAIL_OFFSET = 5;
const RULE_HEIGHT = 20;
const IMAGE_BLOCK_HEIGHT = 220;
const IMAGE_BLOCK_MAX_WIDTH = 720;
const IMAGE_BLOCK_MIN_WIDTH = 220;
const IMAGE_PLACEHOLDER_HEIGHT = 72;
const TABLE_CELL_FONT_PX = 13;
const TABLE_CELL_PADDING_X = 14;
const TABLE_COLUMN_MIN_WIDTH = 72;
const TABLE_COLUMN_MAX_WIDTH = 320;
const TABLE_HEADER_HEIGHT = 38;
const TABLE_ROW_MIN_HEIGHT = 34;
const TABLE_ROW_LINE_HEIGHT = 20;
const TABLE_ROW_OVERSCAN = 4;
const HARD_WRAPPED_LINE_MIN_LENGTH = 52;
const HARD_WRAPPED_RUN_AVERAGE_LENGTH = 64;
const SANS_FAMILY = "Arial, Helvetica, sans-serif";
const SERIF_FAMILY = "Georgia, Times New Roman, serif";
const MONO_FAMILY = '"SF Mono", Menlo, Monaco, Consolas, monospace';
const EMPTY_MARK_STATE: MarkState = {
bold: false,
href: null,
italic: false,
strike: false,
title: null,
};
const PREPARED_TEXT_DOCUMENT_CACHE_LIMIT = 32;
const PREPARED_TEXT_DOCUMENT_CACHE_VERSION = "prepared-text-v1";
const TEXT_DOCUMENT_FRAME_CACHE_LIMIT = 12;
const LINE_RANGE_WIDTH_CACHE_LIMIT = 8;
const LINE_RANGE_MATERIALIZED_LINE_CACHE_LIMIT = 1024;
const LINE_RANGE_CHECKPOINT_CACHE_LIMIT = 128;
const LINE_RANGE_CHECKPOINT_INTERVAL = 64;
const markerWidthCache = new Map<string, number>();
const preparedTextDocumentCache = new Map<string, PreparedTextDocument>();
const preparedTextLineSourceIds = new WeakMap<readonly string[], number>();
const textDocumentFrameCache = new WeakMap<
PreparedTextDocument,
Map<string, TextDocumentFrame>
>();
const inlineLineRangeCaches = new WeakMap<
PreparedInlineTextBlock,
Map<string, RichInlineLineRangeCache>
>();
const codeLineRangeCaches = new WeakMap<
PreparedCodeTextBlock,
Map<string, CodeLineRangeCache>
>();
let nextPreparedTextLineSourceId = 1;
type RichInlineLineRangeCache = {
checkpoints: Array<LineRangeCheckpoint<RichInlineCursor>>;
lines: Map<number, RichInlineLine>;
};
type CodeLineRangeCache = {
checkpoints: Array<LineRangeCheckpoint<LayoutCursor>>;
lines: Map<number, LayoutLine>;
};
type LineRangeCheckpoint<Cursor> = {
cursor: Cursor;
lineIndex: number;
};
export function resolveTextViewerMode({
fileName,
mimeType,
}: {
fileName: string;
mimeType?: string;
}): TextViewerMode {
const lowerName = fileName.toLowerCase();
const lowerMime = mimeType?.toLowerCase().split(";")[0].trim();
return lowerName.endsWith(".md") ||
lowerName.endsWith(".markdown") ||
lowerMime === "text/markdown"
? "markdown"
: "text";
}
export function createPreparedTextDocument({
lines,
mode,
text,
style,
}: {
lines?: readonly string[];
mode: TextViewerMode;
text: string;
style: TextStyleConfig;
}): PreparedTextDocument {
const sourceLines = mode === "text" ? lines : undefined;
const cacheKey = preparedTextDocumentCacheKey({
lines: sourceLines,
mode,
text,
style,
});
const cached = preparedTextDocumentCache.get(cacheKey);
if (cached) {
preparedTextDocumentCache.delete(cacheKey);
preparedTextDocumentCache.set(cacheKey, cached);
return cached;
}
const document = createUncachedPreparedTextDocument({
lines: sourceLines,
mode,
text,
style,
});
preparedTextDocumentCache.set(cacheKey, document);
trimPreparedTextDocumentCache();
return document;
}
export function clearPreparedTextDocumentCacheForTests() {
preparedTextDocumentCache.clear();
}
function createUncachedPreparedTextDocument({
lines,
mode,
text,
style,
}: {
lines?: readonly string[];
mode: TextViewerMode;
text: string;
style: TextStyleConfig;
}): PreparedTextDocument {
const sourceLines = mode === "text" ? lines : undefined;
const sourceLineCount = sourceLines?.length ?? splitTextLines(text).length;
const blocks =
mode === "markdown"
? parseMarkdownBlocks(text, style)
: buildPlainTextBlocks(sourceLines ?? splitTextLines(text), style);
return {
blocks,
mode,
sourceLineCount,
wordCount: sourceLines
? countTextLineWords(sourceLines)
: countTextWords(text),
};
}
function preparedTextDocumentCacheKey({
lines,
mode,
text,
style,
}: {
lines?: readonly string[];
mode: TextViewerMode;
text: string;
style: TextStyleConfig;
}) {
const sourceKey = lines
? ["lines", preparedTextLineSourceId(lines), lines.length]
: ["text", text.length, hashTextForPreparedDocument(text)];
return [
PREPARED_TEXT_DOCUMENT_CACHE_VERSION,
mode,
style.fontEpoch ?? 0,
safeScale(style.fontScale),
...sourceKey,
].join("\u0000");
}
function preparedTextLineSourceId(lines: readonly string[]) {
const cached = preparedTextLineSourceIds.get(lines);
if (cached !== undefined) return cached;
const id = nextPreparedTextLineSourceId;
nextPreparedTextLineSourceId += 1;
preparedTextLineSourceIds.set(lines, id);
return id;
}
function trimPreparedTextDocumentCache() {
while (preparedTextDocumentCache.size > PREPARED_TEXT_DOCUMENT_CACHE_LIMIT) {
const firstKey = preparedTextDocumentCache.keys().next().value;
if (firstKey === undefined) return;
preparedTextDocumentCache.delete(firstKey);
}
}
function getTextDocumentFrameCache(document: PreparedTextDocument) {
let cache = textDocumentFrameCache.get(document);
if (!cache) {
cache = new Map();
textDocumentFrameCache.set(document, cache);
}
return cache;
}
function textDocumentFrameCacheKey({
contentWidth,
fontScale,
}: {
contentWidth: number;
fontScale: number;
}) {
return [contentWidth, fontScale].join("\u0000");
}
function getRichInlineLineRangeCache(
block: PreparedInlineTextBlock,
lineWidth: number,
) {
let blockCache = inlineLineRangeCaches.get(block);
if (!blockCache) {
blockCache = new Map();
inlineLineRangeCaches.set(block, blockCache);
}
const cacheKey = textLineWidthCacheKey(lineWidth);
let cache = blockCache.get(cacheKey);
if (cache) {
setBoundedCacheEntry(
blockCache,
cacheKey,
cache,
LINE_RANGE_WIDTH_CACHE_LIMIT,
);
return cache;
}
cache = {
checkpoints: [
{
cursor: { graphemeIndex: 0, itemIndex: 0, segmentIndex: 0 },
lineIndex: 0,
},
],
lines: new Map(),
};
setBoundedCacheEntry(
blockCache,
cacheKey,
cache,
LINE_RANGE_WIDTH_CACHE_LIMIT,
);
return cache;
}
function getCodeLineRangeCache(
block: PreparedCodeTextBlock,
innerWidth: number,
) {
let blockCache = codeLineRangeCaches.get(block);
if (!blockCache) {
blockCache = new Map();
codeLineRangeCaches.set(block, blockCache);
}
const cacheKey = textLineWidthCacheKey(innerWidth);
let cache = blockCache.get(cacheKey);
if (cache) {
setBoundedCacheEntry(
blockCache,
cacheKey,
cache,
LINE_RANGE_WIDTH_CACHE_LIMIT,
);
return cache;
}
cache = {
checkpoints: [
{
cursor: { graphemeIndex: 0, segmentIndex: 0 },
lineIndex: 0,
},
],
lines: new Map(),
};
setBoundedCacheEntry(
blockCache,
cacheKey,
cache,
LINE_RANGE_WIDTH_CACHE_LIMIT,
);
return cache;
}
function textLineWidthCacheKey(width: number) {
return String(width);
}
function setBoundedCacheEntry<K, V>(
cache: Map<K, V>,
key: K,
value: V,
limit: number,
) {
cache.delete(key);
cache.set(key, value);
while (cache.size > limit) {
const firstKey = cache.keys().next().value;
if (firstKey === undefined) return;
cache.delete(firstKey);
}
}
function touchCacheEntry<K, V>(cache: Map<K, V>, key: K, value: V) {
cache.delete(key);
cache.set(key, value);
}
function trimMaterializedLineCache<Line>(cache: Map<number, Line>) {
while (cache.size > LINE_RANGE_MATERIALIZED_LINE_CACHE_LIMIT) {
const firstKey = cache.keys().next().value;
if (firstKey === undefined) return;
cache.delete(firstKey);
}
}
function nearestLineRangeCheckpoint<Cursor>(
checkpoints: readonly LineRangeCheckpoint<Cursor>[],
lineIndex: number,
) {
let low = 0;
let high = checkpoints.length - 1;
let match = checkpoints[0]!;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
const checkpoint = checkpoints[middle]!;
if (checkpoint.lineIndex <= lineIndex) {
match = checkpoint;
low = middle + 1;
} else {
high = middle - 1;
}
}
return match;
}
function maybeStoreLineRangeCheckpoint<Cursor>(
checkpoints: Array<LineRangeCheckpoint<Cursor>>,
lineIndex: number,
cursor: Cursor,
force = false,
) {
if (
!force &&
(lineIndex === 0 || lineIndex % LINE_RANGE_CHECKPOINT_INTERVAL !== 0)
) {
return;
}
let low = 0;
let high = checkpoints.length - 1;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
const checkpoint = checkpoints[middle]!;
if (checkpoint.lineIndex === lineIndex) {
checkpoints[middle] = { cursor, lineIndex };
return;
}
if (checkpoint.lineIndex < lineIndex) {
low = middle + 1;
} else {
high = middle - 1;
}
}
checkpoints.splice(low, 0, { cursor, lineIndex });
while (checkpoints.length > LINE_RANGE_CHECKPOINT_CACHE_LIMIT) {
checkpoints.splice(checkpoints.length > 1 ? 1 : 0, 1);
}
}
function cloneLayoutCursor(cursor: LayoutCursor): LayoutCursor {
return {
graphemeIndex: cursor.graphemeIndex,
segmentIndex: cursor.segmentIndex,
};
}
function cloneRichInlineCursor(cursor: RichInlineCursor): RichInlineCursor {
return {
graphemeIndex: cursor.graphemeIndex,
itemIndex: cursor.itemIndex,
segmentIndex: cursor.segmentIndex,
};
}
export function layoutTextDocument({
contentWidth,
document,
fontScale = 1,
}: {
contentWidth: number;
document: PreparedTextDocument;
fontScale?: number;
}): TextDocumentFrame {
const safeContentWidth = safeWidth(contentWidth);
const safeFontScale = safeScale(fontScale);
const cacheKey = textDocumentFrameCacheKey({
contentWidth: safeContentWidth,
fontScale: safeFontScale,
});
const cachedFrame = getTextDocumentFrameCache(document).get(cacheKey);
if (cachedFrame) return cachedFrame;
const frames: TextBlockFrame[] = [];
let y = DOCUMENT_PADDING_Y;
for (let index = 0; index < document.blocks.length; index++) {
const block = document.blocks[index]!;
y += block.marginTop;
const frame = layoutTextBlock({
block,
blockIndex: index,
contentWidth: safeContentWidth,
scale: safeFontScale,
top: y,
});
frames.push(frame);
y = frame.bottom;
}
const frame = {
frames,
totalHeight: y + DOCUMENT_PADDING_Y,
width: safeContentWidth,
};
setBoundedCacheEntry(
getTextDocumentFrameCache(document),
cacheKey,
frame,
TEXT_DOCUMENT_FRAME_CACHE_LIMIT,
);
return frame;
}
export function materializeInlineVisibleLines({
block,
frame,
lineWindow,
maxWidth,
viewportBottom,
viewportTop,
}: {
block: PreparedInlineTextBlock;
frame: InlineTextBlockFrame;
lineWindow?: TextLineWindow | null;
maxWidth: number;
viewportBottom: number;
viewportTop: number;
}): InlineLineLayout[] {
const window =
lineWindow ??
getInlineVisibleLineWindow({ frame, viewportBottom, viewportTop });
if (!window) return [];
if (!block.flow) {
return [
{
fragments: fallbackInlineFragments(block),
lineIndex: 0,
top: 0,
width: frame.usedWidth,
},
];
}
const lineWidth = safeWidth((maxWidth - frame.contentLeft) / frame.scale);
return getRichInlineMaterializedLineWindow({
block,
lineWidth,
window,
}).map(({ line, lineIndex }) => ({
fragments: richInlineFragments(block, line),
lineIndex,
top: lineIndex * frame.lineHeight,
width: line.width * frame.scale,
}));
}
export function materializeCodeVisibleLines({
block,
contentWidth,
frame,
lineWindow,
viewportBottom,
viewportTop,
}: {
block: PreparedCodeTextBlock;
contentWidth: number;
frame: CodeTextBlockFrame;
lineWindow?: TextLineWindow | null;
viewportBottom: number;
viewportTop: number;
}): CodeLineLayout[] {
const window =
lineWindow ??
getCodeVisibleLineWindow({ frame, viewportBottom, viewportTop });
if (!window) return [];
if (!block.prepared) {
return [
{
lineIndex: 0,
line: {
end: { graphemeIndex: 0, segmentIndex: 0 },
start: { graphemeIndex: 0, segmentIndex: 0 },
text: block.fallbackText,
width: frame.width,
},
top: CODE_BLOCK_PADDING_Y,
},
];
}
const boxWidth = safeWidth(contentWidth - frame.contentLeft);
const innerWidth = safeWidth(
(boxWidth - CODE_BLOCK_PADDING_X * 2) / frame.scale,
);
return getCodeMaterializedLineWindow({
block,
innerWidth,
window,
}).map(({ line, lineIndex }) => ({
lineIndex,
line,
top: CODE_BLOCK_PADDING_Y + lineIndex * frame.lineHeight,
}));
}
function getRichInlineMaterializedLineWindow({
block,
lineWidth,
window,
}: {
block: PreparedInlineTextBlock;
lineWidth: number;
window: TextLineWindow;
}) {
if (!block.flow) return [];
const cache = getRichInlineLineRangeCache(block, lineWidth);
const materializedLines = new Map<number, RichInlineLine>();
let firstMissing = Number.POSITIVE_INFINITY;
let lastMissing = -1;
for (
let lineIndex = window.firstLine;
lineIndex <= window.lastLine;
lineIndex++
) {
const cachedLine = cache.lines.get(lineIndex);
if (cachedLine) {
touchCacheEntry(cache.lines, lineIndex, cachedLine);
materializedLines.set(lineIndex, cachedLine);
continue;
}
firstMissing = Math.min(firstMissing, lineIndex);
lastMissing = lineIndex;
}
if (lastMissing >= firstMissing) {
fillRichInlineMaterializedLineCache({
cache,
flow: block.flow,
lineWidth,
materializedLines,
window: {
firstLine: firstMissing,
lastLine: lastMissing,
},
});
}
const lines: Array<{ line: RichInlineLine; lineIndex: number }> = [];
for (
let lineIndex = window.firstLine;
lineIndex <= window.lastLine;
lineIndex++
) {
const line = materializedLines.get(lineIndex);
if (line) lines.push({ line, lineIndex });
}
trimMaterializedLineCache(cache.lines);
return lines;
}
function fillRichInlineMaterializedLineCache({
cache,
flow,
lineWidth,
materializedLines,
window,
}: {
cache: RichInlineLineRangeCache;
flow: PreparedRichInline;
lineWidth: number;
materializedLines: Map<number, RichInlineLine>;
window: TextLineWindow;
}) {
const checkpoint = nearestLineRangeCheckpoint(
cache.checkpoints,
window.firstLine,
);
let cursor = cloneRichInlineCursor(checkpoint.cursor);
for (
let lineIndex = checkpoint.lineIndex;
lineIndex <= window.lastLine;
lineIndex++
) {
const cachedLine = cache.lines.get(lineIndex);
if (cachedLine) {
cursor = cloneRichInlineCursor(cachedLine.end);
if (lineIndex >= window.firstLine) {
touchCacheEntry(cache.lines, lineIndex, cachedLine);
materializedLines.set(lineIndex, cachedLine);
}
maybeStoreLineRangeCheckpoint(
cache.checkpoints,
lineIndex + 1,
cloneRichInlineCursor(cachedLine.end),
true,
);
continue;
}
const range = layoutNextRichInlineLineRange(flow, lineWidth, cursor);
if (!range) return;
const nextCursor = cloneRichInlineCursor(range.end);
const nextLineIndex = lineIndex + 1;
if (lineIndex >= window.firstLine) {
const line = materializeRichInlineLineRange(flow, range);
cache.lines.set(lineIndex, line);
materializedLines.set(lineIndex, line);
maybeStoreLineRangeCheckpoint(
cache.checkpoints,
lineIndex,
cloneRichInlineCursor(cursor),
true,
);
}
maybeStoreLineRangeCheckpoint(
cache.checkpoints,
nextLineIndex,
nextCursor,
lineIndex >= window.firstLine,
);
cursor = nextCursor;
}
}
function getCodeMaterializedLineWindow({
block,
innerWidth,
window,
}: {
block: PreparedCodeTextBlock;
innerWidth: number;
window: TextLineWindow;
}) {
if (!block.prepared) return [];
const cache = getCodeLineRangeCache(block, innerWidth);
const materializedLines = new Map<number, LayoutLine>();
let firstMissing = Number.POSITIVE_INFINITY;
let lastMissing = -1;
for (
let lineIndex = window.firstLine;
lineIndex <= window.lastLine;
lineIndex++
) {
const cachedLine = cache.lines.get(lineIndex);
if (cachedLine) {
touchCacheEntry(cache.lines, lineIndex, cachedLine);
materializedLines.set(lineIndex, cachedLine);
continue;
}
firstMissing = Math.min(firstMissing, lineIndex);
lastMissing = lineIndex;
}
if (lastMissing >= firstMissing) {
fillCodeMaterializedLineCache({
cache,
innerWidth,
materializedLines,
prepared: block.prepared,
window: {
firstLine: firstMissing,
lastLine: lastMissing,
},
});
}
const lines: Array<{ line: LayoutLine; lineIndex: number }> = [];
for (
let lineIndex = window.firstLine;
lineIndex <= window.lastLine;
lineIndex++
) {
const line = materializedLines.get(lineIndex);
if (line) lines.push({ line, lineIndex });
}
trimMaterializedLineCache(cache.lines);
return lines;
}
function fillCodeMaterializedLineCache({
cache,
innerWidth,
materializedLines,
prepared,
window,
}: {
cache: CodeLineRangeCache;
innerWidth: number;
materializedLines: Map<number, LayoutLine>;
prepared: PreparedTextWithSegments;
window: TextLineWindow;
}) {
const checkpoint = nearestLineRangeCheckpoint(
cache.checkpoints,
window.firstLine,
);
let cursor = cloneLayoutCursor(checkpoint.cursor);
for (
let lineIndex = checkpoint.lineIndex;
lineIndex <= window.lastLine;
lineIndex++
) {
const cachedLine = cache.lines.get(lineIndex);
if (cachedLine) {
cursor = cloneLayoutCursor(cachedLine.end);
if (lineIndex >= window.firstLine) {
touchCacheEntry(cache.lines, lineIndex, cachedLine);
materializedLines.set(lineIndex, cachedLine);
}
maybeStoreLineRangeCheckpoint(
cache.checkpoints,
lineIndex + 1,
cloneLayoutCursor(cachedLine.end),
true,
);
continue;
}
const range = layoutNextLineRange(prepared, cursor, innerWidth);
if (!range) return;
const nextCursor = cloneLayoutCursor(range.end);
const nextLineIndex = lineIndex + 1;
if (lineIndex >= window.firstLine) {
const line = materializeLineRange(prepared, range);
cache.lines.set(lineIndex, line);
materializedLines.set(lineIndex, line);
maybeStoreLineRangeCheckpoint(
cache.checkpoints,
lineIndex,
cloneLayoutCursor(cursor),
true,
);
}
maybeStoreLineRangeCheckpoint(
cache.checkpoints,
nextLineIndex,
nextCursor,
lineIndex >= window.firstLine,
);
cursor = nextCursor;
}
}
export function getInlineVisibleLineWindow({
frame,
viewportBottom,
viewportTop,
}: {
frame: InlineTextBlockFrame;
viewportBottom: number;
viewportTop: number;
}): TextLineWindow | null {
return getVisibleLineWindow({
lineCount: frame.lineCount,
lineHeight: frame.lineHeight,
originTop: frame.top,
viewportBottom,
viewportTop,
});
}
export function getCodeVisibleLineWindow({
frame,
viewportBottom,
viewportTop,
}: {
frame: CodeTextBlockFrame;
viewportBottom: number;
viewportTop: number;
}): TextLineWindow | null {
return getVisibleLineWindow({
lineCount: frame.lineCount,
lineHeight: frame.lineHeight,
originTop: frame.top + CODE_BLOCK_PADDING_Y,
viewportBottom,
viewportTop,
});
}
function getVisibleLineWindow({
lineCount,
lineHeight,
originTop,
viewportBottom,
viewportTop,
}: {
lineCount: number;
lineHeight: number;
originTop: number;
viewportBottom: number;
viewportTop: number;
}): TextLineWindow | null {
const firstLine = Math.max(
0,
Math.floor((viewportTop - originTop) / lineHeight) - 1,
);
const lastLine = Math.min(
lineCount - 1,
Math.ceil((viewportBottom - originTop) / lineHeight) + 1,
);
return lastLine < firstLine ? null : { firstLine, lastLine };
}
export function getTableVisibleRowWindow({
frame,
viewportBottom,
viewportTop,
}: {
frame: TableTextBlockFrame;
viewportBottom: number;
viewportTop: number;
}): TableRowWindow {
const relativeTop = viewportTop - frame.top - frame.headerHeight;
const relativeBottom = viewportBottom - frame.top - frame.headerHeight;
const bodyHeight = frame.rowOffsets[frame.rowOffsets.length - 1] ?? 0;
const startIndex = Math.max(
0,
findTableRowAtOffset(frame.rowOffsets, relativeTop) - TABLE_ROW_OVERSCAN,
);
const endIndex = Math.min(
frame.rowCount,
findTableRowEndAtOffset(frame.rowOffsets, relativeBottom) +
TABLE_ROW_OVERSCAN,
);
return {
afterHeight: Math.max(0, bodyHeight - (frame.rowOffsets[endIndex] ?? 0)),
beforeHeight: frame.rowOffsets[startIndex] ?? 0,
endIndex,
startIndex,
};
}
export function textFrameIntersectsLineRange({
frame,
range,
}: {
frame: TextBlockFrame;
range: { end: number; start: number } | null;
}) {
return (
range != null &&
frame.sourceStartLine <= range.end &&
frame.sourceEndLine >= range.start
);
}
export function serializeMarkdownTableForClipboard(
block: PreparedTableTextBlock,
) {
const rows = [block.header, ...block.rows];
return rows
.map((row) => {
return block.header
.map((_headerCell, index) => tableClipboardCell(row[index]?.text ?? ""))
.join("\t");
})
.join("\n");
}
function parseMarkdownBlocks(
markdown: string,
style: TextStyleConfig,
): PreparedTextBlock[] {
const sourceEndLine = splitTextLines(markdown).length;
try {
const headingIds: HeadingIdRegistry = new Map();
const frontmatter = extractMarkdownFrontmatter(markdown);
if (frontmatter) {
const blocks: PreparedTextBlock[] = [];
appendBlockGroup(
blocks,
[
buildCodeBlock({
ctx: { listDepth: 0, quoteDepth: 0 },
language: "yaml",
sourceEndLine: frontmatter.endLine,
sourceStartLine: 1,
style,
text: frontmatter.text,
}),
],
0,
);
appendBlockGroup(
blocks,
parseBlockTokens(marked.lexer(frontmatter.body, { gfm: true }), {
ctx: { listDepth: 0, quoteDepth: 0 },
headingIds,
sourceEndLine,
sourceStartLine: frontmatter.endLine + 1,
style,
}),
BLOCK_GAP,
);
return blocks;
}
return parseBlockTokens(marked.lexer(markdown, { gfm: true }), {
ctx: { listDepth: 0, quoteDepth: 0 },
headingIds,
sourceEndLine,
sourceStartLine: 1,
style,
});
} catch {
return buildPlainTextBlocks(splitTextLines(markdown), style);
}
}
function parseBlockTokens(
tokens: readonly Token[],
{
ctx,
headingIds,
sourceEndLine,
sourceStartLine,
style,
}: {
ctx: ParseContext;
headingIds: HeadingIdRegistry;
sourceEndLine: number;
sourceStartLine: number;
style: TextStyleConfig;
},
): PreparedTextBlock[] {
const blocks: PreparedTextBlock[] = [];
let cursorLine = sourceStartLine;
for (const token of tokens) {
// Advance by the number of line breaks the token's raw spans. `marked`
// concatenates token raws to reconstruct the source, so this keeps the
// cursor aligned even when a block's trailing newline is absorbed into the
// following `space` token (a "\n\n" gap is two breaks, i.e. one blank line).
const breaks = countSourceLineBreaks(token.raw);
const tokenStartLine = Math.min(cursorLine, sourceEndLine);
const tokenEndLine = Math.min(
sourceEndLine,
Math.max(
tokenStartLine,
cursorLine + breaks - (endsWithSourceLineBreak(token.raw) ? 1 : 0),
),
);
cursorLine += breaks;
switch (token.type) {
case "space":
case "def":
continue;
case "paragraph":
if (isStandaloneImageParagraph(token)) {
appendBlockGroup(
blocks,
[
buildImageBlock({
ctx,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
token: token.tokens[0] as Tokens.Image,
}),
],
BLOCK_GAP,
);
continue;
}
appendBlockGroup(
blocks,
buildInlineBlocks({
ctx,
lines: collectInlinePieceLines(token.tokens ?? [], "body", style),
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
variant: "body",
}),
BLOCK_GAP,
);
continue;
case "heading": {
const variant = headingVariant(token.depth);
const lines = collectInlinePieceLines(
token.tokens ?? [],
variant,
style,
);
appendBlockGroup(
blocks,
buildInlineBlocks({
ctx,
headingId: createMarkdownHeadingId(lines, headingIds),
lines,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
variant,
}),
BLOCK_GAP + 4,
);
continue;
}
case "code":
appendBlockGroup(
blocks,
[
buildCodeBlock({
ctx,
language: sanitizeMarkdownLanguage(token.lang),
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
text: token.text,
}),
],
RICH_BLOCK_GAP,
);
continue;
case "list":
appendBlockGroup(
blocks,
buildListBlocks({
ctx,
headingIds,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
token: token as Tokens.List,
}),
BLOCK_GAP,
);
continue;
case "blockquote":
appendBlockGroup(
blocks,
parseBlockTokens(token.tokens ?? [], {
ctx: {
listDepth: ctx.listDepth,
quoteDepth: ctx.quoteDepth + 1,
},
headingIds,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
}),
RICH_BLOCK_GAP,
);
continue;
case "hr":
appendBlockGroup(
blocks,
[
buildRuleBlock({
ctx,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
}),
],
BLOCK_GAP + 2,
);
continue;
case "table":
appendBlockGroup(
blocks,
[
buildTableBlock({
ctx,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
token: token as Tokens.Table,
}),
],
RICH_BLOCK_GAP,
);
continue;
case "html": {
const htmlText = token.text.trim().length > 0 ? token.text : token.raw;
if (token.block || ("pre" in token && token.pre === true)) {
appendBlockGroup(
blocks,
[
buildCodeBlock({
ctx,
language: null,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
text: htmlText,
}),
],
RICH_BLOCK_GAP,
);
} else {
appendPlainTextFallback({
blocks,
ctx,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
text: htmlText,
});
}
continue;
}
case "text":
if (Array.isArray(token.tokens) && token.tokens.length > 0) {
appendBlockGroup(
blocks,
buildInlineBlocks({
ctx,
lines: collectInlinePieceLines(token.tokens, "body", style),
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
variant: "body",
}),
BLOCK_GAP,
);
} else {
appendPlainTextFallback({
blocks,
ctx,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
text: token.text,
});
}
continue;
default: {
const fallbackText = fallbackTextForToken(token);
if (fallbackText) {
appendPlainTextFallback({
blocks,
ctx,
sourceEndLine: tokenEndLine,
sourceStartLine: tokenStartLine,
style,
text: fallbackText,
});
}
}
}
}
return blocks;
}
function buildPlainTextBlocks(
lines: readonly string[],
style: TextStyleConfig,
): PreparedTextBlock[] {
const blocks: PreparedTextBlock[] = [];
let run: string[] = [];
let runStartLine = 1;
function appendLine(line: string, sourceLine: number) {
blocks.push(buildPlainTextLineBlock(line, sourceLine, style));
}
function flushRun(endLine: number) {
if (run.length === 0) return;
if (shouldJoinPlainTextRun(run)) {
blocks.push(
buildInlineBlock({
ctx: { listDepth: 0, quoteDepth: 0 },
pieces: [
createTextPiece(
joinPlainTextRun(run),
EMPTY_MARK_STATE,
"body",
style,
)!,
],
sourceEndLine: endLine,
sourceStartLine: runStartLine,
style,
variant: "body",
}),
);
} else {
for (let index = 0; index < run.length; index++) {
appendLine(run[index]!, runStartLine + index);
}
}
run = [];
}
for (let index = 0; index < lines.length; index++) {
const line = lines[index]!;
const sourceLine = index + 1;
if (line.trim() === "") {
flushRun(sourceLine - 1);
appendLine(" ", sourceLine);
runStartLine = sourceLine + 1;
continue;
}
if (run.length === 0) runStartLine = sourceLine;
run.push(line);
}
flushRun(lines.length);
return blocks;
}
function buildPlainTextLineBlock(
line: string,
sourceLine: number,
style: TextStyleConfig,
): PreparedInlineTextBlock {
return buildInlineBlock({
ctx: { listDepth: 0, quoteDepth: 0 },
pieces: [createTextPiece(line || " ", EMPTY_MARK_STATE, "body", style)!],
sourceEndLine: sourceLine,
sourceStartLine: sourceLine,
style,
variant: "body",
});
}
function shouldJoinPlainTextRun(lines: readonly string[]) {
if (lines.length < 2) return false;
const trimmed = lines.map((line) => line.trim()).filter(Boolean);
if (
trimmed.length < 2 ||
countRecordLikeLines(trimmed) > trimmed.length / 3
) {
return false;
}
const lengths = trimmed.map((line) => line.length);
const average =
lengths.reduce((total, length) => total + length, 0) / lengths.length;
if (average < HARD_WRAPPED_RUN_AVERAGE_LENGTH) return false;
const internal = lengths.slice(0, -1);
const shortInternalCount = internal.filter(
(length) => length < HARD_WRAPPED_LINE_MIN_LENGTH,
).length;
return shortInternalCount <= Math.floor(internal.length * 0.15);
}
function countRecordLikeLines(lines: readonly string[]) {
return lines.reduce((count, line) => {
return count + (isRecordLikePlainTextLine(line) ? 1 : 0);
}, 0);
}
function isRecordLikePlainTextLine(line: string) {
return (
/^\d{4}-\d{2}-\d{2}(?:[T\s]|\b)/.test(line) ||
/^\d{1,6}[:.)\]]\s/.test(line) ||
/^[-*+]\s/.test(line) ||
/^(?:TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\b/.test(line) ||
/\b(?:TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\b/.test(line.slice(0, 80)) ||
/^[{[]/.test(line)
);
}
function joinPlainTextRun(lines: readonly string[]) {
return lines.map((line) => line.trim()).join(" ");
}
function appendPlainTextFallback({
blocks,
ctx,
sourceEndLine,
sourceStartLine,
style,
text,
}: {
blocks: PreparedTextBlock[];
ctx: ParseContext;
sourceEndLine: number;
sourceStartLine: number;
style: TextStyleConfig;
text: string;
}) {
appendBlockGroup(
blocks,
buildInlineBlocks({
ctx,
lines: [
[createTextPiece(text, EMPTY_MARK_STATE, "body", style)].filter(
Boolean,
) as InlinePiece[],
],
sourceEndLine,
sourceStartLine,
style,
variant: "body",
}),
BLOCK_GAP,
);
}
function buildListBlocks({
ctx,
headingIds,
sourceEndLine,
sourceStartLine,
style,
token,
}: {
ctx: ParseContext;
headingIds: HeadingIdRegistry;
sourceEndLine: number;
sourceStartLine: number;
style: TextStyleConfig;
token: Tokens.List;
}): PreparedTextBlock[] {
const blocks: PreparedTextBlock[] = [];
const itemCtx = {
listDepth: ctx.listDepth + 1,
quoteDepth: ctx.quoteDepth,
};
let itemCursorLine = sourceStartLine;
for (let index = 0; index < token.items.length; index++) {
const item = token.items[index]!;
const breaks = countSourceLineBreaks(item.raw);
const itemStartLine = Math.min(sourceEndLine, itemCursorLine);
const itemEndLine = Math.min(
sourceEndLine,
Math.max(
itemStartLine,
itemCursorLine + breaks - (endsWithSourceLineBreak(item.raw) ? 1 : 0),
),
);
itemCursorLine += breaks;
let itemBlocks = parseBlockTokens(item.tokens, {
ctx: itemCtx,
headingIds,
sourceEndLine: itemEndLine,
sourceStartLine: itemStartLine,
style,
});
if (itemBlocks.length === 0) {
itemBlocks = buildInlineBlocks({
ctx: itemCtx,
lines: [
[createTextPiece(item.text, EMPTY_MARK_STATE, "body", style)].filter(
Boolean,
) as InlinePiece[],
],
sourceEndLine: itemEndLine,
sourceStartLine: itemStartLine,
style,
variant: "body",
});
}
decorateListItemBlocks(
itemBlocks,
resolveListMarkerText(token, item, index),
resolveListMarkerClassName(token, item),
style,
);
appendBlockGroup(blocks, itemBlocks, LIST_ITEM_GAP);
}
return blocks;
}
function decorateListItemBlocks(
blocks: PreparedTextBlock[],
markerText: string,
markerClassName: string,
style: TextStyleConfig,
) {
if (blocks.length === 0) return;
const markerArea = measureMarkerWidth(markerText, style) + LIST_MARKER_GAP;
for (let index = 0; index < blocks.length; index++) {
blocks[index] = shiftBlock(blocks[index]!, markerArea);
}
const firstBlock = blocks[0]!;
blocks[0] = {
...firstBlock,
markerClassName,
markerLeft: firstBlock.contentLeft - markerArea,
markerText,
} satisfies PreparedTextBlock;
}
function buildInlineBlocks({
ctx,
headingId = null,
lines,
sourceEndLine,
sourceStartLine,
style,
variant,
}: {
ctx: ParseContext;
headingId?: string | null;
lines: InlinePiece[][];
sourceEndLine: number;
sourceStartLine: number;
style: TextStyleConfig;
variant: InlineVariant;
}) {
const blocks: PreparedTextBlock[] = [];
for (const pieces of lines) {
if (pieces.length === 0) continue;
const isFirstBlock = blocks.length === 0;
blocks.push({
...buildInlineBlock({
ctx,
pieces,
sourceEndLine,
sourceStartLine,
style,
variant,
}),
headingId: isFirstBlock ? headingId : null,
marginTop: isFirstBlock ? 0 : HARD_BREAK_GAP,
});
}
return blocks;
}
function buildInlineBlock({
ctx,
pieces,
sourceEndLine,
sourceStartLine,
style,
variant,
}: {
ctx: ParseContext;
pieces: InlinePiece[];
sourceEndLine: number;
sourceStartLine: number;
style: TextStyleConfig;
variant: InlineVariant;
}): PreparedInlineTextBlock {
return {
...createBlockBase(ctx, sourceStartLine, sourceEndLine),
classNames: pieces.map((piece) => piece.className),
fallbackText: pieces.map((piece) => piece.text).join(""),
flow: prepareRichInlineSafe(pieces),
fonts: pieces.map((piece) => piece.font),
headingId: null,
hrefs: pieces.map((piece) => piece.href),
kind: "inline",
lineHeight: lineHeightForVariant(variant, style),
texts: pieces.map((piece) => piece.text),
titles: pieces.map((piece) => piece.title),
variant,
};
}
function buildCodeBlock({
ctx,
language,
sourceEndLine,
sourceStartLine,
style,
text,
}: {
ctx: ParseContext;
language: string | null;
sourceEndLine: number;
sourceStartLine: number;
style: TextStyleConfig;
text: string;
}): PreparedCodeTextBlock {
const code = stripSingleTrailingNewline(text);
const font = codeFont(style);
return {
...createBlockBase(ctx, sourceStartLine, sourceEndLine),
fallbackText: code,
font,
kind: "code",
language,
lineHeight: CODE_LINE_PX * style.fontScale,
prepared: prepareWithSegmentsSafe(code || " ", font, {
whiteSpace: "pre-wrap",
}),
};
}
function buildImageBlock({
ctx,
sourceEndLine,
sourceStartLine,
token,
}: {
ctx: ParseContext;
sourceEndLine: number;
sourceStartLine: number;
token: Tokens.Image;
}): PreparedImageTextBlock {
const alt = token.text?.trim() || token.href || "Markdown image";
return {
...createBlockBase(ctx, sourceStartLine, sourceEndLine),
alt,
href: parseMarkdownHref(token.href),
kind: "image",
src: parseMarkdownImageSrc(token.href),
title: sanitizeMarkdownTitle(token.title),
};
}
function buildRuleBlock({
ctx,
sourceEndLine,
sourceStartLine,
}: {
ctx: ParseContext;
sourceEndLine: number;
sourceStartLine: number;
}): PreparedRuleTextBlock {
return {
...createBlockBase(ctx, sourceStartLine, sourceEndLine),
height: RULE_HEIGHT,
kind: "rule",
};
}
function buildTableBlock({
ctx,
sourceEndLine,
sourceStartLine,
token,
}: {
ctx: ParseContext;
sourceEndLine: number;
sourceStartLine: number;
token: Tokens.Table;
}): PreparedTableTextBlock {
const header = token.header.map((cell) => tableCellFromTokens(cell.tokens));
const rows = token.rows.map((row) =>
row.map((cell) => tableCellFromTokens(cell.tokens)),
);
const rowSourceStartLines = token.rows.map(
(_row, index) => sourceStartLine + 2 + index,
);
const alignments = token.align.map((alignment) =>
tableColumnAlignment(alignment),
);
return {
...createBlockBase(ctx, sourceStartLine, sourceEndLine),
alignments,
columnWidths: measureTableColumnWidths(header, rows),
header,
kind: "table",
rowSourceStartLines,
rows,
};
}
function createBlockBase(
ctx: ParseContext,
sourceStartLine: number,
sourceEndLine: number,
): PreparedTextBlockBase {
const listIndent = Math.max(0, ctx.listDepth - 1) * LIST_NESTING_INDENT;
const contentLeft = listIndent + ctx.quoteDepth * BLOCKQUOTE_INDENT;
const quoteRailLefts = Array.from({ length: ctx.quoteDepth }, (_, depth) => {
return listIndent + depth * BLOCKQUOTE_INDENT + RAIL_OFFSET;
});
return {
contentLeft,
listDepth: ctx.listDepth,
marginTop: 0,
markerClassName: null,
markerLeft: null,
markerText: null,
quoteDepth: ctx.quoteDepth,
quoteRailLefts,
sourceEndLine,
sourceStartLine,
};
}
function collectInlinePieceLines(
tokens: readonly Token[],
variant: InlineVariant,
style: TextStyleConfig,
): InlinePiece[][] {
const lines: InlinePiece[][] = [[]];
function currentLine() {
return lines[lines.length - 1]!;
}
function pushLineBreak() {
lines.push([]);
}
function pushPiece(piece: InlinePiece | null) {
if (!piece) return;
const line = currentLine();
const previous = line[line.length - 1];
if (previous && canMergeInlinePieces(previous, piece)) {
previous.text += piece.text;
return;
}
line.push(piece);
}
function walk(tokenList: readonly Token[], marks: MarkState) {
for (const token of tokenList) {
switch (token.type) {
case "text":
if (Array.isArray(token.tokens) && token.tokens.length > 0) {
walk(token.tokens, marks);
} else {
pushPiece(createTextPiece(token.text, marks, variant, style));
}
continue;
case "escape":
pushPiece(createTextPiece(token.text, marks, variant, style));
continue;
case "strong":
walk(token.tokens ?? [], { ...marks, bold: true });
continue;
case "em":
walk(token.tokens ?? [], { ...marks, italic: true });
continue;
case "del":
walk(token.tokens ?? [], { ...marks, strike: true });
continue;
case "codespan":
pushPiece(createCodePiece(token.text, style));
continue;
case "link":
walk(token.tokens ?? [], {
...marks,
href: parseMarkdownHref(token.href),
title: sanitizeMarkdownTitle(token.title),
});
continue;
case "image":
pushPiece(createImagePiece(token.text || token.href, style));
continue;
case "br":
pushLineBreak();
continue;
case "checkbox":
pushPiece(
createTextPiece(
token.checked ? "[x] " : "[ ] ",
marks,
variant,
style,
),
);
continue;
case "html":
pushPiece(createTextPiece(token.text, marks, variant, style));
continue;
default: {
const fallback = fallbackTextForToken(token);
if (fallback) {
pushPiece(createTextPiece(fallback, marks, variant, style));
}
}
}
}
}
walk(tokens, EMPTY_MARK_STATE);
while (lines.length > 0 && lines[lines.length - 1]!.length === 0) {
lines.pop();
}
return lines;
}
function createTextPiece(
text: string,
marks: MarkState,
variant: InlineVariant,
style: TextStyleConfig,
): InlinePiece | null {
if (!text) return null;
return {
breakMode: "normal",
className: inlineClassName(variant, marks),
extraWidth: 0,
font: inlineFont(variant, marks, style),
href: marks.href,
text,
title: marks.title,
};
}
function createCodePiece(
text: string,
style: TextStyleConfig,
): InlinePiece | null {
if (!text) return null;
return {
breakMode: "normal",
className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[0.92em]",
extraWidth: INLINE_CODE_EXTRA_WIDTH,
font: codeInlineFont(style),
href: null,
text,
title: null,
};
}
function codeFont(style: TextStyleConfig) {
return `500 ${CODE_FONT_PX * style.fontScale}px ${MONO_FAMILY}`;
}
function codeInlineFont(style: TextStyleConfig) {
return `600 ${CODE_FONT_PX * style.fontScale}px ${MONO_FAMILY}`;
}
function createImagePiece(text: string, style: TextStyleConfig): InlinePiece {
return {
breakMode: "never",
className:
"inline-flex min-h-5 items-center rounded-full bg-muted px-2 text-xs font-medium text-muted-foreground",
extraWidth: IMAGE_EXTRA_WIDTH,
font: `600 ${CHIP_FONT_PX * style.fontScale}px ${SANS_FAMILY}`,
href: null,
text: text || "image",
title: null,
};
}
function layoutTextBlock({
block,
blockIndex,
contentWidth,
scale,
top,
}: {
block: PreparedTextBlock;
blockIndex: number;
contentWidth: number;
scale: number;
top: number;
}): TextBlockFrame {
switch (block.kind) {
case "inline": {
const lineWidth = safeWidth((contentWidth - block.contentLeft) / scale);
const stats = block.flow
? measureRichInlineStatsSafe(block.flow, lineWidth, block.fallbackText)
: estimateInlineStats(block.fallbackText, lineWidth);
const lineCount = Math.max(1, stats.lineCount);
const lineHeight = block.lineHeight * scale;
const height = lineCount * lineHeight;
return {
...frameBase(block, blockIndex, top, height, scale),
kind: "inline",
lineCount,
lineHeight,
usedWidth: stats.maxLineWidth * scale,
};
}
case "code": {
const boxWidth = safeWidth(contentWidth - block.contentLeft);
const innerWidth = safeWidth(
(boxWidth - CODE_BLOCK_PADDING_X * 2) / scale,
);
const stats = block.prepared
? measureLineStatsSafe(block.prepared, innerWidth, block.fallbackText)
: estimateInlineStats(block.fallbackText, innerWidth);
const lineCount = Math.max(1, stats.lineCount);
const width = Math.min(
boxWidth,
Math.max(1, stats.maxLineWidth * scale + CODE_BLOCK_PADDING_X * 2),
);
const lineHeight = block.lineHeight * scale;
const height = lineCount * lineHeight + CODE_BLOCK_PADDING_Y * 2;
return {
...frameBase(block, blockIndex, top, height, scale),
kind: "code",
language: block.language,
lineCount,
lineHeight,
width,
};
}
case "image": {
const availableWidth = safeWidth(contentWidth - block.contentLeft);
const imageWidth = Math.min(
IMAGE_BLOCK_MAX_WIDTH,
Math.max(IMAGE_BLOCK_MIN_WIDTH, availableWidth),
);
const imageHeight = block.src
? IMAGE_BLOCK_HEIGHT
: IMAGE_PLACEHOLDER_HEIGHT;
return {
...frameBase(block, blockIndex, top, imageHeight, scale),
alt: block.alt,
imageHeight,
imageWidth,
kind: "image",
};
}
case "rule": {
return {
...frameBase(block, blockIndex, top, block.height, scale),
kind: "rule",
width: safeWidth(contentWidth - block.contentLeft),
};
}
case "table": {
const availableWidth = safeWidth(contentWidth - block.contentLeft);
const intrinsicWidth = block.columnWidths.reduce(
(total, width) => total + width,
0,
);
const tableWidth = Math.max(availableWidth, intrinsicWidth);
const rowHeights = measureTableRowHeights(block.rows, block.columnWidths);
const rowOffsets = buildRowOffsets(rowHeights);
const bodyHeight = rowOffsets[rowOffsets.length - 1] ?? 0;
const height = TABLE_HEADER_HEIGHT + bodyHeight + 2;
return {
...frameBase(block, blockIndex, top, height, scale),
columnWidths: block.columnWidths,
headerHeight: TABLE_HEADER_HEIGHT,
kind: "table",
rowHeights,
rowOffsets,
rowCount: block.rows.length,
rowSourceStartLines: block.rowSourceStartLines,
tableWidth,
};
}
}
}
function frameBase(
block: PreparedTextBlock,
blockIndex: number,
top: number,
height: number,
scale: number,
): TextBlockFrameBase {
return {
blockIndex,
bottom: top + height,
contentLeft: block.contentLeft,
height,
listDepth: block.listDepth,
markerClassName: block.markerClassName,
markerLeft: block.markerLeft,
markerText: block.markerText,
quoteDepth: block.quoteDepth,
quoteRailLefts: block.quoteRailLefts,
scale,
sourceEndLine: block.sourceEndLine,
sourceStartLine: block.sourceStartLine,
top,
};
}
function richInlineFragments(
block: PreparedInlineTextBlock,
line: RichInlineLine,
): InlineFragmentLayout[] {
return line.fragments.map((fragment) => ({
className: block.classNames[fragment.itemIndex] ?? "",
font:
block.fonts[fragment.itemIndex] ??
inlineFont("body", EMPTY_MARK_STATE, { fontScale: 1 }),
href: block.hrefs[fragment.itemIndex] ?? null,
leadingGap: fragment.gapBefore,
text: fragment.text,
title: block.titles[fragment.itemIndex] ?? null,
}));
}
function fallbackInlineFragments(
block: PreparedInlineTextBlock,
): InlineFragmentLayout[] {
const fragments = block.texts.map((text, index) => ({
className: block.classNames[index] ?? "",
font:
block.fonts[index] ??
inlineFont(block.variant, EMPTY_MARK_STATE, { fontScale: 1 }),
href: block.hrefs[index] ?? null,
leadingGap: 0,
text,
title: block.titles[index] ?? null,
}));
return fragments.length > 0
? fragments
: [
{
className: inlineClassName(block.variant, EMPTY_MARK_STATE),
font: inlineFont(block.variant, EMPTY_MARK_STATE, { fontScale: 1 }),
href: null,
leadingGap: 0,
text: block.fallbackText || " ",
title: null,
},
];
}
function prepareRichInlineSafe(pieces: InlinePiece[]) {
try {
return prepareRichInline(
pieces.map((piece) => ({
break: piece.breakMode,
extraWidth: piece.extraWidth,
font: piece.font,
text: piece.text,
})),
);
} catch {
return null;
}
}
function prepareWithSegmentsSafe(
text: string,
font: string,
options?: Parameters<typeof prepareWithSegments>[2],
) {
try {
return prepareWithSegments(text, font, options);
} catch {
return null;
}
}
function measureRichInlineStatsSafe(
flow: PreparedRichInline,
width: number,
fallbackText: string,
) {
try {
return measureRichInlineStats(flow, width);
} catch {
return estimateInlineStats(fallbackText, width);
}
}
function measureLineStatsSafe(
prepared: PreparedTextWithSegments,
width: number,
fallbackText: string,
) {
try {
return measureLineStats(prepared, width);
} catch {
return estimateInlineStats(fallbackText, width);
}
}
function estimateInlineStats(text: string, width: number) {
const columns = Math.max(1, Math.floor(width / 8));
const lines = splitTextLines(text || " ");
const lineCount = lines.reduce(
(sum, line) => sum + Math.max(1, Math.ceil((line || " ").length / columns)),
0,
);
const maxLineWidth = Math.min(
width,
Math.max(...lines.map((line) => (line || " ").length * 8), 1),
);
return { lineCount, maxLineWidth };
}
function measureMarkerWidth(text: string, style: TextStyleConfig) {
const font = `600 ${MARKER_FONT_PX * style.fontScale}px ${MONO_FAMILY}`;
const cacheKey = `${font}\u0000${text}`;
const cached = markerWidthCache.get(cacheKey);
if (cached != null) return cached;
let width = Math.max(8, text.length * MARKER_FONT_PX * style.fontScale);
const prepared = prepareWithSegmentsSafe(text, font);
if (prepared) {
try {
width = measureNaturalWidth(prepared);
} catch {
// keep estimate
}
}
markerWidthCache.set(cacheKey, width);
return width;
}
function appendBlockGroup(
target: PreparedTextBlock[],
group: PreparedTextBlock[],
firstMargin: number,
) {
if (group.length === 0) return;
for (let index = 0; index < group.length; index++) {
const block = group[index]!;
target.push({
...block,
marginTop:
index === 0 ? (target.length === 0 ? 0 : firstMargin) : block.marginTop,
} satisfies PreparedTextBlock);
}
}
function extractMarkdownFrontmatter(markdown: string) {
const lines = splitTextLines(markdown);
if (lines[0]?.trim() !== "---") return null;
for (let index = 1; index < lines.length; index++) {
if (lines[index]!.trim() !== "---") continue;
if (index === 1) return null;
return {
body: lines.slice(index + 1).join("\n"),
endLine: index + 1,
text: lines.slice(1, index).join("\n"),
};
}
return null;
}
function createMarkdownHeadingId(
lines: readonly InlinePiece[][],
headingIds: HeadingIdRegistry,
) {
const text = lines
.flatMap((line) => line.map((piece) => piece.text))
.join(" ");
const base = slugifyMarkdownHeading(text) || "section";
const count = headingIds.get(base) ?? 0;
headingIds.set(base, count + 1);
return count === 0 ? base : `${base}-${count}`;
}
function slugifyMarkdownHeading(text: string) {
return text
.trim()
.toLowerCase()
.normalize("NFKD")
.replace(/[^\p{Letter}\p{Number}\s-]/gu, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
function shiftBlock(
block: PreparedTextBlock,
delta: number,
): PreparedTextBlock {
return {
...block,
contentLeft: block.contentLeft + delta,
} satisfies PreparedTextBlock;
}
function resolveListMarkerText(
list: Tokens.List,
item: Tokens.ListItem,
index: number,
) {
if (item.task) return item.checked ? "☑" : "☐";
if (list.ordered) {
const start = typeof list.start === "number" ? list.start : 1;
return `${start + index}.`;
}
return "•";
}
function resolveListMarkerClassName(list: Tokens.List, item: Tokens.ListItem) {
if (item.task) return "text-muted-foreground";
return list.ordered ? "text-muted-foreground" : "text-muted-foreground";
}
function isStandaloneImageParagraph(
token: Token,
): token is Tokens.Paragraph & { tokens: [Tokens.Image] } {
return (
token.type === "paragraph" &&
Array.isArray(token.tokens) &&
token.tokens.length === 1 &&
token.tokens[0]?.type === "image"
);
}
function parseMarkdownHref(href: string | null | undefined) {
return sanitizeMarkdownUrl(href, {
allowedAbsoluteProtocols: new Set(["http:", "https:", "mailto:"]),
allowRelative: true,
});
}
function parseMarkdownImageSrc(src: string | null | undefined) {
return sanitizeMarkdownUrl(src, {
allowedAbsoluteProtocols: new Set(["http:", "https:", "blob:"]),
allowRelative: true,
});
}
function sanitizeMarkdownTitle(title: string | null | undefined) {
if (!title) return null;
return title.replace(/[\u0000-\u001f\u007f]/g, "").trim() || null;
}
function sanitizeMarkdownLanguage(language: string | null | undefined) {
if (!language) return null;
return (
language
.replace(/[\u0000-\u001f\u007f]/g, "")
.trim()
.slice(0, 40) || null
);
}
function sanitizeMarkdownUrl(
value: string | null | undefined,
{
allowedAbsoluteProtocols,
allowRelative,
}: {
allowedAbsoluteProtocols: ReadonlySet<string>;
allowRelative: boolean;
},
) {
if (!value) return null;
const trimmed = value.trim();
if (!trimmed || /[\u0000-\u001f\u007f]/.test(trimmed)) return null;
if (trimmed.startsWith("#")) return trimmed;
if (allowRelative && /^\.{0,2}\//.test(trimmed)) return trimmed;
if (allowRelative && /^\//.test(trimmed)) return trimmed;
try {
const url = new URL(trimmed);
return allowedAbsoluteProtocols.has(url.protocol) ? url.href : null;
} catch {
return null;
}
}
function fallbackTextForToken(token: Token) {
if ("text" in token && typeof token.text === "string") return token.text;
return token.raw ?? "";
}
function countSourceLineBreaks(raw: string | null | undefined) {
if (!raw) return 0;
const matches = raw.match(/\r\n|[\n\r\u2028\u2029]/g);
return matches ? matches.length : 0;
}
function endsWithSourceLineBreak(raw: string | null | undefined) {
return raw ? /(?:\r\n|[\n\r\u2028\u2029])$/.test(raw) : false;
}
function tableCellFromTokens(tokens: readonly Token[]): PreparedTableCell {
const { className, href, text, title } = inlineTokensToTableCell(tokens);
return {
className,
href,
text: text || " ",
title,
};
}
function inlineTokensToTableCell(tokens: readonly Token[]): PreparedTableCell {
let className = "";
let href: string | null = null;
let text = "";
let title: string | null = null;
for (const token of tokens) {
switch (token.type) {
case "strong":
text += inlineTokensToTableCell(token.tokens ?? []).text;
className = cnClassNames(className, "font-semibold");
break;
case "em":
text += inlineTokensToTableCell(token.tokens ?? []).text;
className = cnClassNames(className, "italic");
break;
case "del":
text += inlineTokensToTableCell(token.tokens ?? []).text;
className = cnClassNames(className, "line-through");
break;
case "link": {
const child = inlineTokensToTableCell(token.tokens ?? []);
text += child.text;
href = href ?? parseMarkdownHref(token.href);
title = title ?? sanitizeMarkdownTitle(token.title);
className = cnClassNames(className, "text-primary underline");
break;
}
case "codespan":
text += token.text;
className = cnClassNames(className, "font-mono");
break;
case "escape":
case "text":
case "html":
text += token.text;
break;
case "br":
text += " ";
break;
case "image":
text += token.text || token.href;
break;
default:
text += fallbackTextForToken(token);
}
}
return { className, href, text, title };
}
function measureTableColumnWidths(
header: readonly PreparedTableCell[],
rows: readonly PreparedTableCell[][],
) {
return header.map((cell, columnIndex) => {
const columnTexts = [
cell.text,
...rows.map((row) => row[columnIndex]?.text ?? ""),
];
const maxLength = Math.max(
1,
...columnTexts.map((text) => Math.min(48, text.length)),
);
return Math.min(
TABLE_COLUMN_MAX_WIDTH,
Math.max(
TABLE_COLUMN_MIN_WIDTH,
maxLength * TABLE_CELL_FONT_PX * 0.58 + TABLE_CELL_PADDING_X * 2,
),
);
});
}
function measureTableRowHeights(
rows: readonly PreparedTableCell[][],
columnWidths: readonly number[],
) {
return rows.map((row) => {
const lineCount = Math.max(
1,
...row.map((cell, index) =>
measureTableCellLineCount(cell.text, columnWidths[index] ?? 0),
),
);
return Math.max(
TABLE_ROW_MIN_HEIGHT,
lineCount * TABLE_ROW_LINE_HEIGHT + 12,
);
});
}
function measureTableCellLineCount(text: string, columnWidth: number) {
const innerWidth = Math.max(1, columnWidth - TABLE_CELL_PADDING_X * 2);
const charsPerLine = Math.max(
1,
Math.floor(innerWidth / (TABLE_CELL_FONT_PX * 0.58)),
);
return splitTextLines(text || " ").reduce((count, line) => {
return count + Math.max(1, Math.ceil((line || " ").length / charsPerLine));
}, 0);
}
function buildRowOffsets(rowHeights: readonly number[]) {
const offsets = [0];
for (const height of rowHeights) {
offsets.push(offsets[offsets.length - 1]! + height);
}
return offsets;
}
function findTableRowAtOffset(rowOffsets: readonly number[], offset: number) {
if (offset <= 0) return 0;
const rowCount = Math.max(0, rowOffsets.length - 1);
let low = 0;
let high = rowCount;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if ((rowOffsets[mid + 1] ?? 0) <= offset) low = mid + 1;
else high = mid;
}
return low;
}
function findTableRowEndAtOffset(
rowOffsets: readonly number[],
offset: number,
) {
if (offset <= 0) return 0;
const rowCount = Math.max(0, rowOffsets.length - 1);
let low = 0;
let high = rowCount;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if ((rowOffsets[mid] ?? 0) < offset) low = mid + 1;
else high = mid;
}
return low;
}
function tableColumnAlignment(
alignment: "center" | "left" | "right" | null,
): TableColumnAlignment {
if (alignment === "center" || alignment === "right") return alignment;
return "left";
}
function tableClipboardCell(text: string) {
return text.replace(/[\t\r\n]+/g, " ").trim();
}
function cnClassNames(...classes: Array<string | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
function headingVariant(depth: number): InlineVariant {
if (depth <= 1) return "heading-1";
if (depth === 2) return "heading-2";
return "body";
}
function lineHeightForVariant(variant: InlineVariant, style: TextStyleConfig) {
switch (variant) {
case "heading-1":
return HEADING_ONE_LINE_PX * style.fontScale;
case "heading-2":
return HEADING_TWO_LINE_PX * style.fontScale;
case "body":
return BODY_LINE_PX * style.fontScale;
}
}
function inlineFont(
variant: InlineVariant,
marks: MarkState,
style: TextStyleConfig,
) {
const italic = marks.italic ? "italic " : "";
switch (variant) {
case "heading-1":
return `${italic}${marks.bold ? 800 : 700} ${HEADING_ONE_FONT_PX * style.fontScale}px ${SERIF_FAMILY}`;
case "heading-2":
return `${italic}${marks.bold ? 800 : 700} ${HEADING_TWO_FONT_PX * style.fontScale}px ${SERIF_FAMILY}`;
case "body":
return `${italic}${marks.bold ? 700 : marks.href ? 500 : 400} ${BODY_FONT_PX * style.fontScale}px ${SANS_FAMILY}`;
}
}
function inlineClassName(variant: InlineVariant, marks: MarkState) {
const classes = [
"inline-block wrap-break-word whitespace-pre-wrap whitespace-pre align-baseline leading-none",
];
if (variant === "heading-1")
classes.push("font-serif text-[1.45em] font-semibold");
if (variant === "heading-2")
classes.push("font-serif text-[1.18em] font-semibold");
if (marks.bold) classes.push("font-semibold");
if (marks.italic) classes.push("italic");
if (marks.strike) classes.push("line-through");
if (marks.href) classes.push("text-primary underline underline-offset-2");
return classes.join(" ");
}
function canMergeInlinePieces(a: InlinePiece, b: InlinePiece) {
return (
a.breakMode === b.breakMode &&
a.className === b.className &&
a.extraWidth === b.extraWidth &&
a.font === b.font &&
a.href === b.href &&
a.title === b.title
);
}
function stripSingleTrailingNewline(text: string) {
return text.endsWith("\n") ? text.slice(0, -1) : text;
}
function countTextWords(text: string) {
const matches = text.trim().match(/\S+/g);
return matches?.length ?? 0;
}
function countTextLineWords(lines: readonly string[]) {
return lines.reduce((count, line) => count + countTextWords(line), 0);
}
function hashTextForPreparedDocument(text: string) {
let hash = 2166136261;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(36);
}
function safeWidth(width: number) {
return Number.isFinite(width) && width > 0 ? width : 1;
}
function safeScale(scale: number) {
return Number.isFinite(scale) && scale > 0 ? scale : 1;
}
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/ui/text-viewer-scale.ts
export const TEXT_VIEWER_MIN_SCALE = 0.1;
export const TEXT_VIEWER_MAX_SCALE = 5;
export const TEXT_VIEWER_BLOCK_PADDING = 8;
export function clampTextViewerScale(value: number) {
return Math.min(
TEXT_VIEWER_MAX_SCALE,
Math.max(TEXT_VIEWER_MIN_SCALE, value),
);
}
registry/new-york-v4/ui/text-viewer-types.ts
import type {
BlobViewerSource,
TextSource,
UrlViewerSource,
} from "@/lib/viewer-source";
import type { TextViewerMode } from "./text-viewer-layout";
import type { TextLineRange } from "./text-viewer-ranges";
import type { TextViewerBounds } from "./text-viewer-resource";
export type { TextLineRange };
export interface TextViewerHandle {
scrollToLineRange: (range: TextLineRange, options?: ScrollToOptions) => void;
getViewportElement: () => HTMLDivElement | null;
}
export type TextDocumentSource =
| UrlViewerSource
| BlobViewerSource
| TextSource;
export interface TextViewerProps extends TextViewerBounds {
source: TextDocumentSource;
className?: string;
controls?: boolean;
download?: boolean;
/** 1-based inclusive line range to highlight, or null. */
highlight?: TextLineRange | null;
/** Drop the outer border/rounded/background so the viewer fills its container. */
bare?: boolean;
/** Explicitly select text or markdown when the caller has already classified the source. */
mode?: TextViewerMode;
}
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/markdown-greenfield-content.tsx
"use client";
import * as React from "react";
import { useMountEffect } from "@/hooks/use-mount-effect";
import type { ViewerResource } from "@/lib/viewer-resource";
import {
createMarkdownGreenfieldDocument,
findMarkdownGreenfieldChunkByBlockId,
findMarkdownGreenfieldChunkBySourceLine,
findMarkdownGreenfieldFragmentTargetById,
type MarkdownGreenfieldChunk,
} from "./markdown-greenfield-document";
import { useMarkdownGreenfieldDocument } from "./markdown-greenfield-document-store";
import {
layoutMarkdownGreenfieldDocument,
MARKDOWN_GREENFIELD_CHUNK_MAX_INLINE_SIZE,
MARKDOWN_GREENFIELD_LAYOUT_POLICY_VERSION,
type MarkdownGreenfieldChunkFrame,
type MarkdownGreenfieldMeasurementContext,
} from "./markdown-greenfield-layout";
import { useMarkdownGreenfieldRendererFrame } from "./markdown-greenfield-renderer-frame";
import { MarkdownGreenfieldChunkRenderer } from "./markdown-greenfield-renderer";
import {
createMarkdownGreenfieldVisibleProjection,
getMarkdownGreenfieldProjectedVisibleFrames,
getMarkdownGreenfieldScrollAnchor,
getMarkdownGreenfieldScrollTopForLineRange,
getMarkdownGreenfieldVisibleProjection,
getMarkdownGreenfieldVisibleRange,
isMarkdownGreenfieldVisibleProjectionSameWindow,
resolveMarkdownGreenfieldScrollAnchor,
type MarkdownGreenfieldScrollAnchor,
type MarkdownGreenfieldVisibleProjection,
} from "./markdown-greenfield-virtualizer";
import { ScrollArea } from "./scroll-area";
import { TextViewerControls, TextViewerFrame } from "./text-viewer-chrome";
import { normalizeTextLineRange } from "./text-viewer-ranges";
import {
readTextResource,
resolvedTextViewerBounds,
} from "./text-viewer-resource";
import { clampTextViewerScale } from "./text-viewer-scale";
import { useTextViewerScrollInteractions } from "./text-viewer-scroll-interactions";
import type { TextViewerHandle, TextViewerProps } from "./text-viewer-types";
import { getTextInverseStickyWindow } from "./text-viewer-virtualization";
import type { ViewerDownloadErrorHandler } from "./viewer-download";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
const DEFAULT_VIEWPORT_HEIGHT = 640;
const DEFAULT_VIEWPORT_WIDTH = 900;
const INITIAL_CONTENT_WIDTH = 820;
const VIEWER_HORIZONTAL_PADDING = 32;
const OVERSCAN_PX = 800;
const NATIVE_FIND_TEXT_BATCH_CHUNKS = 16;
const NATIVE_FIND_TEXT_IDLE_MIN_REMAINING_MS = 4;
const MARKDOWN_GREENFIELD_HIGHLIGHT_STYLE = {
backgroundColor:
"color-mix(in oklab, var(--foreground) 8%, var(--background))",
boxShadow: "inset 2px 0 0 0 var(--primary)",
} satisfies React.CSSProperties;
type MarkdownIdleWindow = Window &
typeof globalThis & {
cancelIdleCallback?: Window["cancelIdleCallback"];
requestIdleCallback?: Window["requestIdleCallback"];
};
type ViewportSize = {
height: number;
width: number;
};
type MarkdownScrollToLineOptions = ScrollToOptions & {
preferredChunkId?: string | null;
};
type NativeFindTextRecord = {
chunk: MarkdownGreenfieldChunk;
text: string;
};
type NativeFindTextState = {
entries: readonly NativeFindTextRecord[];
key: string | null;
};
export type MarkdownViewerProps = TextViewerProps & {
/** Bind document fragment IDs to window.location.hash. Disable for embedded previews. */
urlFragmentNavigation?: boolean;
};
export type MarkdownGreenfieldContentProps = Omit<
MarkdownViewerProps,
"source"
> & {
resource: ViewerResource;
retryVersion: number;
forwardedRef?: React.ForwardedRef<TextViewerHandle>;
};
export function MarkdownGreenfieldContent({
resource,
className,
controls = true,
download = true,
highlight,
bare = false,
maxBytes,
maxLines,
retryVersion,
forwardedRef,
urlFragmentNavigation = true,
}: MarkdownGreenfieldContentProps) {
const bounds = React.useMemo(
() => resolvedTextViewerBounds({ maxBytes, maxLines }),
[maxBytes, maxLines],
);
const text = React.useMemo(
() =>
readTextResource({
bounds,
content: resource.content,
retryVersion,
}),
[bounds, resource.content, retryVersion],
);
const parsedDocument = useMarkdownGreenfieldDocument(text);
const pendingDocument = React.useMemo(
() => createMarkdownGreenfieldDocument(" "),
[],
);
const document = parsedDocument ?? pendingDocument;
const isDocumentPending = parsedDocument == null;
const documentMeasurementId = React.useMemo(
() => measurementDocumentIdForText(text),
[text],
);
const downloadAction = download ? resource.originalDownload : null;
const [downloadError, setDownloadError] = React.useState("");
const [fontScale, setFontScale] = React.useState(1);
const [measuredHeightsRevision, setMeasuredHeightsRevision] =
React.useState(0);
const [viewportSize, setViewportSize] = React.useState<ViewportSize>({
height: 0,
width: 0,
});
const [contentWidth, setContentWidth] = React.useState(INITIAL_CONTENT_WIDTH);
const documentRef = React.useRef(document);
const measuredHeightsRef = React.useRef(new Map<string, number>());
const viewportRef = React.useRef<HTMLDivElement | null>(null);
const stickyWindowRef = React.useRef<HTMLDivElement | null>(null);
const scrollTopRef = React.useRef(0);
const scrollFrameRef = React.useRef<number | null>(null);
const frameChunksRef = React.useRef<readonly MarkdownGreenfieldChunkFrame[]>(
[],
);
const viewportHeightRef = React.useRef(DEFAULT_VIEWPORT_HEIGHT);
const pendingMeasuredHeightsRef = React.useRef(new Map<string, number>());
const measuredHeightFrameRef = React.useRef<number | null>(null);
const pendingAnchorRef = React.useRef<MarkdownGreenfieldScrollAnchor | null>(
null,
);
const prevFrameChunksRef = React.useRef<
readonly MarkdownGreenfieldChunkFrame[] | null
>(null);
const viewportHeight = viewportSize.height || DEFAULT_VIEWPORT_HEIGHT;
const measuredViewportWidth = viewportSize.width || DEFAULT_VIEWPORT_WIDTH;
const captureAnchor = React.useCallback(() => {
// Read the live scroll position from the DOM rather than the `scrollTop`
// state, which lags behind during fast scrolling. Capturing a stale value
// here makes the anchor-restore effect yank the viewport back to an old
// (often near-top) position when a freshly revealed chunk is measured.
const liveScrollTop =
viewportRef.current?.scrollTop ?? scrollTopRef.current;
pendingAnchorRef.current = getMarkdownGreenfieldScrollAnchor({
frames: frameChunksRef.current,
scrollTop: liveScrollTop,
});
}, []);
const markdownRendererFrame = useMarkdownGreenfieldRendererFrame({
fallbackViewportInlineSize: measuredViewportWidth,
onBeforeLayoutMotion: captureAnchor,
});
const viewportWidth = markdownRendererFrame.viewportInlineSize;
const measuredHeightLookup = React.useMemo(
() => ({
get: (
chunk: MarkdownGreenfieldChunk,
context: MarkdownGreenfieldMeasurementContext,
) =>
measuredHeightsRef.current.get(
measuredHeightKey({
chunk,
context,
documentMeasurementId,
}),
),
cacheKey: `${documentMeasurementId}:${measuredHeightsRevision}`,
}),
[documentMeasurementId, measuredHeightsRevision],
);
const frame = React.useMemo(
() =>
layoutMarkdownGreenfieldDocument({
contentWidth,
document,
fontScale,
measuredHeights: measuredHeightLookup,
}),
[contentWidth, document, fontScale, measuredHeightLookup],
);
documentRef.current = document;
frameChunksRef.current = frame.chunks;
viewportHeightRef.current = viewportHeight;
const highlightRange = React.useMemo(
() => normalizeTextLineRange(highlight, document.lineCount),
[document.lineCount, highlight],
);
const visibleHighlightRange = highlightRange;
const [visibleProjection, setVisibleProjection] =
React.useState<MarkdownGreenfieldVisibleProjection>(() =>
getMarkdownGreenfieldVisibleProjection({
frames: frame.chunks,
overscanPx: OVERSCAN_PX,
scrollTop: scrollTopRef.current,
viewportHeight,
}),
);
const visibleProjectionRef = React.useRef(visibleProjection);
const visibleFrames = React.useMemo(
() =>
getMarkdownGreenfieldProjectedVisibleFrames({
frames: frame.chunks,
projection: visibleProjection,
}),
[frame.chunks, visibleProjection],
);
const stickyWindow = React.useMemo(
() =>
getTextInverseStickyWindow({
renderedBottom: visibleFrames.at(-1)?.bottom ?? 0,
renderedTop:
visibleFrames[0]?.index === 0 ? 0 : (visibleFrames[0]?.top ?? 0),
totalHeight: frame.totalHeight,
viewportHeight,
}),
[frame.totalHeight, viewportHeight, visibleFrames],
);
const projectVisibleProjection = React.useCallback(() => {
const frames = frameChunksRef.current;
const nextRange = getMarkdownGreenfieldVisibleRange({
frames,
overscanPx: OVERSCAN_PX,
scrollTop: scrollTopRef.current,
viewportHeight: viewportHeightRef.current,
});
const currentProjection = visibleProjectionRef.current;
if (
isMarkdownGreenfieldVisibleProjectionSameWindow({
frames,
projection: currentProjection,
range: nextRange,
})
) {
if (
currentProjection.range.start !== nextRange.start ||
currentProjection.range.end !== nextRange.end
) {
visibleProjectionRef.current = {
frameIds: currentProjection.frameIds,
range: nextRange,
};
}
return;
}
const nextProjection = createMarkdownGreenfieldVisibleProjection({
frames,
range: nextRange,
});
visibleProjectionRef.current = nextProjection;
setVisibleProjection(nextProjection);
}, []);
const commitScrollTop = React.useCallback(
(nextScrollTop: number) => {
scrollTopRef.current = nextScrollTop;
projectVisibleProjection();
},
[projectVisibleProjection],
);
const scheduleScrollTop = React.useCallback(
(nextScrollTop: number) => {
scrollTopRef.current = nextScrollTop;
if (scrollFrameRef.current !== null) return;
if (typeof requestAnimationFrame !== "function") {
commitScrollTop(scrollTopRef.current);
return;
}
scrollFrameRef.current = requestAnimationFrame(() => {
scrollFrameRef.current = null;
commitScrollTop(scrollTopRef.current);
});
},
[commitScrollTop],
);
const handleViewportScroll = React.useCallback(
(viewport: HTMLElement) => scheduleScrollTop(viewport.scrollTop),
[scheduleScrollTop],
);
const getScrollInteractionTarget = React.useCallback(
() => stickyWindowRef.current,
[],
);
const getScrollOverflowTarget = React.useCallback(
() => stickyWindowRef.current,
[],
);
const flushMeasuredHeights = React.useCallback(() => {
measuredHeightFrameRef.current = null;
const pending = pendingMeasuredHeightsRef.current;
if (!pending.size) return;
pendingMeasuredHeightsRef.current = new Map();
let didChange = false;
const measuredHeights = measuredHeightsRef.current;
for (const [key, height] of pending) {
if (Math.abs((measuredHeights.get(key) ?? 0) - height) < 1) continue;
measuredHeights.set(key, height);
didChange = true;
}
if (didChange) {
setMeasuredHeightsRevision((revision) => revision + 1);
}
}, []);
const scheduleMeasuredHeightsFlush = React.useCallback(() => {
if (measuredHeightFrameRef.current !== null) return;
if (typeof requestAnimationFrame !== "function") {
flushMeasuredHeights();
return;
}
measuredHeightFrameRef.current =
requestAnimationFrame(flushMeasuredHeights);
}, [flushMeasuredHeights]);
useKeyedLayoutEffect(joinEffectKey([commitScrollTop, document]), () => {
measuredHeightsRef.current = new Map();
pendingMeasuredHeightsRef.current = new Map();
setMeasuredHeightsRevision((revision) => revision + 1);
commitScrollTop(0);
viewportRef.current?.scrollTo({ left: 0, top: 0 });
});
useMountEffect(() => () => {
if (
scrollFrameRef.current !== null &&
typeof cancelAnimationFrame === "function"
) {
cancelAnimationFrame(scrollFrameRef.current);
}
if (
measuredHeightFrameRef.current !== null &&
typeof cancelAnimationFrame === "function"
) {
cancelAnimationFrame(measuredHeightFrameRef.current);
}
});
useTextViewerScrollInteractions({
getInteractionTarget: getScrollInteractionTarget,
getOverflowTarget: getScrollOverflowTarget,
onScroll: handleViewportScroll,
viewportRef,
});
useKeyedLayoutEffect(
joinEffectKey([
"markdown-viewport-resize",
markdownRendererFrame.usesShellGeometry,
]),
() => {
const viewport = viewportRef.current;
if (!viewport) return;
const readSize = () => {
setViewportSize((current) => {
const nextHeight = viewport.clientHeight;
const nextWidth =
markdownRendererFrame.usesShellGeometry && current.width > 0
? current.width
: viewport.clientWidth;
const next = {
height: nextHeight,
width: nextWidth,
};
return current.height === next.height && current.width === next.width
? current
: next;
});
};
readSize();
const observer =
typeof ResizeObserver === "undefined"
? null
: new ResizeObserver(readSize);
observer?.observe(viewport);
return () => observer?.disconnect();
},
);
useKeyedLayoutEffect(joinEffectKey([captureAnchor, viewportWidth]), () => {
const nextWidth = Math.max(
1,
viewportWidth - VIEWER_HORIZONTAL_PADDING * 2,
);
setContentWidth((current) => {
if (current === nextWidth) return current;
captureAnchor();
return nextWidth;
});
});
useKeyedLayoutEffect(joinEffectKey([commitScrollTop, frame.chunks]), () => {
const viewport = viewportRef.current;
const previousChunks = prevFrameChunksRef.current;
prevFrameChunksRef.current = frame.chunks;
if (!viewport) return;
// Intentional full reflows (width, zoom, font readiness, mode switch) set an
// explicit anchor and want to restore the reader's relative position.
const anchor = pendingAnchorRef.current;
if (anchor) {
pendingAnchorRef.current = null;
const nextScrollTop = resolveMarkdownGreenfieldScrollAnchor({
anchor,
frames: frame.chunks,
});
if (nextScrollTop == null) return;
viewport.scrollTop = nextScrollTop;
commitScrollTop(nextScrollTop);
return;
}
// Measurement-driven changes keep whatever the reader is looking at stable by
// compensating scrollTop for the height delta of chunks ENTIRELY above the
// viewport top. Only already-measured chunks (real height in both layouts)
// count: a chunk's first estimate->measured correction happens as it is
// revealed near/below the viewport, never while the reader sits above it, so
// excluding it avoids the estimate-collapse plunge while still pinning the
// viewport when async rich blocks above re-settle.
if (!previousChunks) return;
const liveScrollTop = viewport.scrollTop;
const previousById = new Map(
previousChunks.map((chunkFrame) => [chunkFrame.id, chunkFrame]),
);
let delta = 0;
for (const chunkFrame of frame.chunks) {
const previous = previousById.get(chunkFrame.id);
if (
previous &&
previous.measuredHeight != null &&
chunkFrame.measuredHeight != null &&
previous.bottom <= liveScrollTop
) {
delta += chunkFrame.measuredHeight - previous.measuredHeight;
}
}
if (delta === 0) return;
const nextScrollTop = Math.max(0, liveScrollTop + delta);
viewport.scrollTop = nextScrollTop;
commitScrollTop(nextScrollTop);
});
useKeyedLayoutEffect(
joinEffectKey(["markdown-visible-range", frame.chunks, viewportHeight]),
() => {
projectVisibleProjection();
},
);
const scrollToLineRange = React.useCallback(
(
range: ReturnType<typeof normalizeTextLineRange>,
options?: MarkdownScrollToLineOptions,
) => {
const viewport = viewportRef.current;
if (!viewport || !range) return;
const currentDocument = documentRef.current;
const preferredChunkId =
options?.preferredChunkId ??
findMarkdownGreenfieldChunkBySourceLine(currentDocument, range.start)
?.id;
const top = getMarkdownGreenfieldScrollTopForLineRange({
chunks: currentDocument.chunks,
frames: frameChunksRef.current,
preferredChunkId,
range,
viewportHeight: viewport.clientHeight || viewportHeightRef.current,
});
if (top == null) return;
viewport.scrollTo({
behavior: resolveScrollBehavior(options?.behavior),
left: options?.left,
top,
});
commitScrollTop(top);
},
[commitScrollTop],
);
// A stable handle to the latest scrollToLineRange. Target-driven effects use
// this ref so layout measurement changes cannot yank the viewport back to a
// highlight/search/hash target while the reader is scrolling.
const scrollToLineRangeRef = React.useRef(scrollToLineRange);
useKeyedLayoutEffect(joinEffectKey([scrollToLineRange]), () => {
scrollToLineRangeRef.current = scrollToLineRange;
});
React.useImperativeHandle(
forwardedRef ?? null,
() => ({
getViewportElement: () => viewportRef.current,
scrollToLineRange: (range, options) => {
scrollToLineRangeRef.current(
normalizeTextLineRange(range, document.lineCount),
options,
);
},
}),
[document.lineCount],
);
// Scrolling is an imperative DOM mutation that must run before paint to avoid
// a visible jump, so these reactions to a changed target line/range live in
// layout-timed reactions.
useKeyedLayoutEffect(joinEffectKey([highlightRange]), () => {
if (!highlightRange) return;
scrollToLineRangeRef.current(highlightRange);
});
// Subscribes to the browser's hash/history (a non-React external source) and
// scrolls the matching fragment into view before paint.
useKeyedLayoutEffect(joinEffectKey([document, urlFragmentNavigation]), () => {
if (!urlFragmentNavigation) return;
const scrollToCurrentHash = () => {
const hash = window.location.hash;
if (!hash) return;
const target = findMarkdownGreenfieldFragmentTargetById(document, hash);
if (!target) return;
const chunk = findMarkdownGreenfieldChunkByBlockId(
document,
target.blockId,
);
scrollToLineRangeRef.current(
normalizeTextLineRange(
{ end: target.sourceLine, start: target.sourceLine },
document.lineCount,
),
{ behavior: "auto", preferredChunkId: chunk?.id },
);
};
scrollToCurrentHash();
window.addEventListener("hashchange", scrollToCurrentHash);
window.addEventListener("popstate", scrollToCurrentHash);
return () => {
window.removeEventListener("hashchange", scrollToCurrentHash);
window.removeEventListener("popstate", scrollToCurrentHash);
};
});
const recordMeasuredHeight = React.useCallback(
(chunk: MarkdownGreenfieldChunk, height: number) => {
if (!Number.isFinite(height) || height <= 0) return;
const key = measuredHeightKey({
chunk,
context: {
fontScale,
policyVersion: MARKDOWN_GREENFIELD_LAYOUT_POLICY_VERSION,
width: Math.max(1, contentWidth),
},
documentMeasurementId,
});
pendingMeasuredHeightsRef.current.set(key, height);
scheduleMeasuredHeightsFlush();
},
[
contentWidth,
documentMeasurementId,
fontScale,
scheduleMeasuredHeightsFlush,
],
);
const handleDownloadError = React.useCallback<ViewerDownloadErrorHandler>(
(error) => {
if (error.kind === "aborted") return;
setDownloadError(error.message || "Could not download Markdown.");
},
[],
);
const zoom = (factor: number) => {
captureAnchor();
setFontScale((scale) => clampTextViewerScale(scale * factor));
};
const resetZoom = () => {
captureAnchor();
setFontScale(1);
};
return (
<TextViewerFrame className={className} bare={bare}>
{controls ? (
<TextViewerControls
downloadAction={downloadAction}
extra={<DownloadError message={downloadError} />}
fontScale={fontScale}
wordCount={
isDocumentPending
? estimateMarkdownWordCount(text)
: document.wordCount
}
onDownloadError={handleDownloadError}
onResetZoom={resetZoom}
onZoomIn={() => zoom(1.2)}
onZoomOut={() => zoom(1 / 1.2)}
/>
) : null}
{/* overflow-anchor:none disables the browser's native scroll anchoring,
which otherwise fights this component's virtualization: as chunks
mount/unmount and the canvas height corrects, the browser shifts
scrollTop on its own, jerking the viewport while the reader scrolls. */}
<ScrollArea
className="bg-background min-h-0 flex-1"
orientation="vertical"
viewportClassName="bg-background [overflow-anchor:none]"
viewportRef={viewportRef}
viewportProps={{
onClickCapture: (event) =>
handleRenderedClick({ document, event, scrollToLineRange }),
}}
>
<div
ref={markdownRendererFrame.setDocumentSurfaceElement}
className="relative min-w-0"
data-projection="unified-hast-markdown"
data-slot="markdown-virtual-canvas"
style={{
height: Math.max(frame.totalHeight, viewportHeight),
minWidth: viewportWidth,
transformOrigin: markdownRendererFrame.transformOrigin,
}}
>
{isDocumentPending ? null : (
<DeferredNativeFindIndex
chunks={document.chunks}
lineCount={document.lineCount}
scrollToLineRange={scrollToLineRange}
/>
)}
{isDocumentPending ? (
<div
aria-label="Preparing Markdown document"
className="text-muted-foreground absolute inset-x-4 top-0 flex min-h-40 items-center justify-center text-sm"
data-slot="markdown-loading-state"
role="status"
>
Preparing Markdown...
</div>
) : document.text.trim() ? (
<>
<div
aria-hidden="true"
data-slot="markdown-sticky-before-buffer"
style={{
contain: "layout size",
height: stickyWindow.beforeHeight,
}}
/>
<div
ref={stickyWindowRef}
data-slot="markdown-sticky-window"
style={{
bottom: stickyWindow.stickyOffset,
contain: "layout style inline-size",
display: "flex",
flexDirection: "column",
height: stickyWindow.renderedHeight,
isolation: "isolate",
left: 0,
overflow: "visible",
position: "sticky",
top: stickyWindow.stickyOffset,
width: "100%",
}}
>
<div
data-slot="markdown-sticky-content"
style={{
height: stickyWindow.renderedHeight,
position: "relative",
width: "100%",
}}
>
{visibleFrames.map((chunkFrame) => {
const chunk = document.chunks[chunkFrame.index];
if (!chunk) return null;
return (
<ChunkFrame
key={chunk.id}
chunk={chunk}
frame={chunkFrame}
highlightRange={visibleHighlightRange}
highlighted={chunkIntersectsLineRange({
chunkFrame,
range: visibleHighlightRange,
})}
measurementKey={measuredHeightKey({
chunk,
context: {
fontScale,
policyVersion:
MARKDOWN_GREENFIELD_LAYOUT_POLICY_VERSION,
width: Math.max(1, contentWidth),
},
documentMeasurementId,
})}
renderedTop={stickyWindow.renderedTop}
onMeasuredHeight={recordMeasuredHeight}
>
<MarkdownGreenfieldChunkRenderer
chunk={chunk}
fontScale={fontScale}
urlFragmentNavigation={urlFragmentNavigation}
/>
</ChunkFrame>
);
})}
</div>
</div>
<div
aria-hidden="true"
data-slot="markdown-sticky-after-buffer"
style={{
contain: "layout size",
height: stickyWindow.afterHeight,
}}
/>
</>
) : (
<div
aria-label="Empty Markdown document"
className="text-muted-foreground absolute inset-x-4 top-0 flex min-h-40 items-center justify-center text-sm"
data-slot="markdown-empty-state"
role="status"
>
Empty Markdown document
</div>
)}
</div>
</ScrollArea>
</TextViewerFrame>
);
}
function DeferredNativeFindIndex({
chunks,
lineCount,
scrollToLineRange,
}: {
chunks: readonly MarkdownGreenfieldChunk[];
lineCount: number;
scrollToLineRange: (
range: ReturnType<typeof normalizeTextLineRange>,
options?: MarkdownScrollToLineOptions,
) => void;
}) {
const [isReady, setIsReady] = React.useState(false);
useKeyedMountEffect(joinEffectKey([chunks]), () => {
setIsReady(false);
const show = () => setIsReady(true);
if (typeof window === "undefined") return;
const browserWindow = window as MarkdownIdleWindow;
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
chunks={chunks}
lineCount={lineCount}
scrollToLineRange={scrollToLineRange}
/>
);
}
function NativeFindIndex({
chunks,
lineCount,
scrollToLineRange,
}: {
chunks: readonly MarkdownGreenfieldChunk[];
lineCount: number;
scrollToLineRange: (
range: ReturnType<typeof normalizeTextLineRange>,
options?: MarkdownScrollToLineOptions,
) => void;
}) {
const entries = useBatchedNativeFindTextEntries(chunks);
return (
<div
aria-hidden="true"
className="pointer-events-none absolute top-0 left-0 h-px w-px overflow-hidden opacity-0"
data-slot="markdown-native-find-index"
data-native-find-indexed-chunks={entries.length}
data-native-find-total-chunks={chunks.length}
>
{entries.map(({ chunk, text }) => (
<NativeFindEntry
key={chunk.id}
chunk={chunk}
lineCount={lineCount}
scrollToLineRange={scrollToLineRange}
text={text}
/>
))}
</div>
);
}
function NativeFindEntry({
chunk,
lineCount,
scrollToLineRange,
text,
}: {
chunk: MarkdownGreenfieldChunk;
lineCount: number;
scrollToLineRange: (
range: ReturnType<typeof normalizeTextLineRange>,
options?: MarkdownScrollToLineOptions,
) => void;
text: string;
}) {
const ref = React.useRef<HTMLSpanElement | null>(null);
useKeyedLayoutEffect(
joinEffectKey([
chunk.id,
chunk.sourceEndLine,
chunk.sourceStartLine,
lineCount,
scrollToLineRange,
]),
() => {
const element = ref.current;
if (!element) return;
element.setAttribute("hidden", "until-found");
const handleBeforeMatch = () => {
scrollToLineRange(
normalizeTextLineRange(
{
end: chunk.sourceEndLine,
start: chunk.sourceStartLine,
},
lineCount,
),
{ behavior: "auto", preferredChunkId: chunk.id },
);
requestAnimationFrame(() => {
element.setAttribute("hidden", "until-found");
});
};
element.addEventListener("beforematch", handleBeforeMatch);
return () => {
element.removeEventListener("beforematch", handleBeforeMatch);
};
},
);
return (
<span
ref={ref}
className="absolute top-0 left-0 block h-px w-px overflow-hidden whitespace-pre"
data-native-find-chunk-id={chunk.id}
data-native-find-end-line={chunk.sourceEndLine}
data-native-find-start-line={chunk.sourceStartLine}
>
{text || " "}
</span>
);
}
function useBatchedNativeFindTextEntries(
chunks: readonly MarkdownGreenfieldChunk[],
) {
const buildKey = React.useMemo(
() => joinEffectKey(["native-find-text", chunks]),
[chunks],
);
const [state, setState] = React.useState<NativeFindTextState>({
entries: [],
key: null,
});
useKeyedMountEffect(buildKey, () => {
let isCancelled = false;
let cancelScheduledBatch: (() => void) | null = null;
let index = 0;
const entries: NativeFindTextRecord[] = [];
setState({ entries: [], key: buildKey });
const runBatch = (deadline?: IdleDeadline) => {
cancelScheduledBatch = null;
if (isCancelled) return;
let processed = 0;
while (
index < chunks.length &&
shouldBuildNativeFindTextInCurrentBatch(deadline, processed)
) {
const chunk = chunks[index];
index += 1;
if (!chunk) continue;
entries.push({
chunk,
text: chunk.nativeFindText,
});
processed += 1;
}
if (processed > 0) {
setState({ entries: entries.slice(), key: buildKey });
}
if (index < chunks.length) {
cancelScheduledBatch = scheduleNativeFindTextBatch(runBatch);
}
};
cancelScheduledBatch = scheduleNativeFindTextBatch(runBatch);
return () => {
isCancelled = true;
cancelScheduledBatch?.();
};
});
return state.key === buildKey ? state.entries : [];
}
function shouldBuildNativeFindTextInCurrentBatch(
deadline: IdleDeadline | undefined,
processed: number,
) {
if (processed === 0) return true;
if (processed >= NATIVE_FIND_TEXT_BATCH_CHUNKS) return false;
if (!deadline) return true;
return deadline.timeRemaining() > NATIVE_FIND_TEXT_IDLE_MIN_REMAINING_MS;
}
function scheduleNativeFindTextBatch(
callback: (deadline?: IdleDeadline) => void,
) {
if (typeof window === "undefined") return null;
const browserWindow = window as MarkdownIdleWindow;
if (browserWindow.requestIdleCallback) {
const idleId = browserWindow.requestIdleCallback(callback, {
timeout: 250,
});
return () => browserWindow.cancelIdleCallback?.(idleId);
}
const timeoutId = browserWindow.setTimeout(() => callback(), 0);
return () => browserWindow.clearTimeout(timeoutId);
}
function DownloadError({ message }: { message: string }) {
if (!message) return null;
return (
<span
className="text-destructive max-w-48 truncate text-xs"
data-slot="markdown-download-error"
role="status"
>
{message}
</span>
);
}
function ChunkFrame({
children,
chunk,
frame,
highlightRange,
highlighted,
measurementKey,
renderedTop,
onMeasuredHeight,
}: {
children: React.ReactNode;
chunk: MarkdownGreenfieldChunk;
frame: MarkdownGreenfieldChunkFrame;
highlightRange: { end: number; start: number } | null;
highlighted: boolean;
measurementKey: string;
renderedTop: number;
onMeasuredHeight: (chunk: MarkdownGreenfieldChunk, height: number) => void;
}) {
const ref = React.useRef<HTMLDivElement | null>(null);
useKeyedLayoutEffect(joinEffectKey([chunk, onMeasuredHeight]), () => {
measure();
const element = ref.current;
if (!element || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(measure);
observer.observe(element);
return () => observer.disconnect();
function measure() {
const element = ref.current;
if (!element) return;
onMeasuredHeight(chunk, element.getBoundingClientRect().height);
}
});
return (
<section
ref={ref}
aria-label={
highlighted
? `Highlighted source lines ${highlightRange?.start}-${highlightRange?.end}`
: `Markdown lines ${frame.sourceStartLine} to ${frame.sourceEndLine}`
}
className={[
"absolute left-1/2 w-full -translate-x-1/2 px-8 py-1",
].join(" ")}
data-markdown-chunk=""
data-markdown-highlighted={highlighted ? "" : undefined}
data-pretext-measured-height-key={measurementKey}
data-source-highlight-end={highlighted ? highlightRange?.end : undefined}
data-source-highlight-start={
highlighted ? highlightRange?.start : undefined
}
data-source-end-line={frame.sourceEndLine}
data-source-start-line={frame.sourceStartLine}
role={highlighted ? "region" : undefined}
style={{
maxWidth: MARKDOWN_GREENFIELD_CHUNK_MAX_INLINE_SIZE,
top: frame.top - renderedTop,
...(highlighted ? MARKDOWN_GREENFIELD_HIGHLIGHT_STYLE : null),
}}
>
{children}
</section>
);
}
function resolveScrollBehavior(behavior: ScrollBehavior | undefined) {
if (behavior) return behavior;
if (
typeof window !== "undefined" &&
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
) {
return "auto";
}
return "smooth";
}
function handleRenderedClick({
document,
event,
scrollToLineRange,
}: {
document: ReturnType<typeof createMarkdownGreenfieldDocument>;
event: React.MouseEvent;
scrollToLineRange: (
range: ReturnType<typeof normalizeTextLineRange>,
options?: MarkdownScrollToLineOptions,
) => void;
}) {
const link = (event.target as HTMLElement | null)?.closest("a[href]");
const href = link?.getAttribute("href");
if (!href?.startsWith("#")) return;
const target = findMarkdownGreenfieldFragmentTargetById(document, href);
if (!target) return;
const chunk = findMarkdownGreenfieldChunkByBlockId(document, target.blockId);
event.preventDefault();
window.history.pushState(null, "", href);
scrollToLineRange(
normalizeTextLineRange(
{ end: target.sourceLine, start: target.sourceLine },
document.lineCount,
),
{ behavior: "smooth", preferredChunkId: chunk?.id },
);
}
function chunkIntersectsLineRange({
chunkFrame,
range,
}: {
chunkFrame: MarkdownGreenfieldChunkFrame;
range: { end: number; start: number } | null;
}) {
if (!range) return false;
return (
chunkFrame.sourceStartLine <= range.end &&
chunkFrame.sourceEndLine >= range.start
);
}
function measuredHeightKey({
chunk,
context,
documentMeasurementId,
}: {
chunk: MarkdownGreenfieldChunk;
context: MarkdownGreenfieldMeasurementContext;
documentMeasurementId: string;
}) {
return [
documentMeasurementId,
chunk.id,
Math.round(context.width),
context.fontScale.toFixed(4),
context.policyVersion,
].join(":");
}
function measurementDocumentIdForText(text: string) {
let hash = 2166136261;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return `md-${text.length}-${(hash >>> 0).toString(36)}`;
}
function estimateMarkdownWordCount(text: string) {
return text.trim().split(/\s+/).filter(Boolean).length;
}
registry/new-york-v4/ui/markdown-greenfield-code-highlight.tsx
"use client";
import * as React from "react";
import {
ensureCodePrismLanguage,
tokenizeCodeLine as tokenizePrismCodeLine,
} from "./code-viewer-syntax-prism";
import { shouldTokenizeCodeLine } from "./code-viewer-syntax-protocol";
import { markdownCodeTokensToHtml } from "./markdown-greenfield-code-highlight-html";
import {
MARKDOWN_CODE_HIGHLIGHT_BATCH_SIZE,
MARKDOWN_CODE_HIGHLIGHT_CACHE_LIMIT,
MARKDOWN_CODE_HIGHLIGHT_RENDERER_VERSION,
type MarkdownCodeHighlightLineRequest,
type MarkdownCodeHighlightLineResult,
type MarkdownCodeHighlightWorkerRequest,
type MarkdownCodeHighlightWorkerResponse,
} from "./markdown-greenfield-code-highlight-protocol";
export const MARKDOWN_CODE_HIGHLIGHT_STYLES = `
.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); }
`;
type MarkdownCodeLineHtmlRequest = MarkdownCodeHighlightLineRequest & {
highlightPattern: string;
key: string;
languageId: string;
};
type MarkdownCodeHighlightTaskHandle =
| { id: number; kind: "idle" }
| { id: number; kind: "timeout" };
type MarkdownCodeHighlightIdleWindow = Window &
typeof globalThis & {
cancelIdleCallback?: Window["cancelIdleCallback"];
requestIdleCallback?: Window["requestIdleCallback"];
};
const resolvedMarkdownCodeLineHtml = new Map<string, string | null>();
const pendingMarkdownCodeLineHtml = new Map<
string,
MarkdownCodeLineHtmlRequest
>();
const markdownCodeLineHtmlSubscribers = new Map<string, Set<() => void>>();
const activeWorkerRequests = new Map<
number,
readonly MarkdownCodeLineHtmlRequest[]
>();
let markdownCodeHighlightFlushHandle: MarkdownCodeHighlightTaskHandle | null =
null;
let markdownCodeHighlightWorker: Worker | null = null;
let isMarkdownCodeHighlightWorkerFailed = false;
let markdownCodeHighlightRequestId = 0;
export function isSafeHighlightedCodeLine(line: number) {
return Number.isInteger(line) && line > 0 && line <= 100_000;
}
export function normalizeCodeLanguage(language: string | null) {
const value = (language ?? "text").toLowerCase();
const aliases: Record<string, string> = {
bash: "shell",
docker: "dockerfile",
javascript: "js",
jsonc: "json",
md: "markdown",
patch: "diff",
rb: "ruby",
"shell-session": "shell",
terminal: "shell",
typescript: "ts",
yml: "yaml",
};
return aliases[value] ?? value;
}
export function diffLineKind(line: string) {
if (line.startsWith("+") && !line.startsWith("+++")) return "add";
if (line.startsWith("-") && !line.startsWith("---")) return "remove";
return null;
}
export function renderCodeLine({
fallbackLanguage,
line,
lineHtml,
pattern,
}: {
fallbackLanguage: string;
line: string;
lineHtml: string | undefined;
pattern: string;
}) {
if (lineHtml !== undefined) {
return (
<span
data-pretext-code-line-html=""
dangerouslySetInnerHTML={{ __html: lineHtml }}
/>
);
}
if (!pattern) return renderFallbackCodeTokens(line || " ", fallbackLanguage);
const index = line.indexOf(pattern);
if (index < 0) return renderFallbackCodeTokens(line || " ", fallbackLanguage);
return (
<>
{renderFallbackCodeTokens(line.slice(0, index), fallbackLanguage)}
<span data-highlighted-chars="">{pattern}</span>
{renderFallbackCodeTokens(
line.slice(index + pattern.length),
fallbackLanguage,
)}
</>
);
}
export function useMarkdownCodeLineHtml({
end,
highlightPattern,
language,
sourceLines,
start,
}: {
end: number;
highlightPattern: string;
language: string;
sourceLines: readonly string[];
start: number;
}) {
const requests = React.useMemo(
() =>
createMarkdownCodeLineHtmlRequests({
end,
highlightPattern,
language,
sourceLines,
start,
}),
[end, highlightPattern, language, sourceLines, start],
);
const subscribe = React.useCallback(
(onStoreChange: () => void) => {
for (const request of requests) {
let subscribers = markdownCodeLineHtmlSubscribers.get(request.key);
if (!subscribers) {
subscribers = new Set();
markdownCodeLineHtmlSubscribers.set(request.key, subscribers);
}
subscribers.add(onStoreChange);
ensureMarkdownCodeLineHtml(request);
}
return () => {
for (const request of requests) {
const subscribers = markdownCodeLineHtmlSubscribers.get(request.key);
if (!subscribers) continue;
subscribers.delete(onStoreChange);
if (!subscribers.size) {
markdownCodeLineHtmlSubscribers.delete(request.key);
}
}
};
},
[requests],
);
const getSnapshot = React.useCallback(
() =>
requests
.map((request) =>
resolvedMarkdownCodeLineHtml.has(request.key) ? "1" : "0",
)
.join(""),
[requests],
);
const resolvedVersion = React.useSyncExternalStore(
subscribe,
getSnapshot,
() => "",
);
return React.useMemo(() => {
void resolvedVersion;
const htmlByIndex = new Map<number, string>();
for (const request of requests) {
const html = resolvedMarkdownCodeLineHtml.get(request.key);
if (typeof html === "string") htmlByIndex.set(request.index, html);
}
return htmlByIndex;
}, [requests, resolvedVersion]);
}
function createMarkdownCodeLineHtmlRequests({
end,
highlightPattern,
language,
sourceLines,
start,
}: {
end: number;
highlightPattern: string;
language: string;
sourceLines: readonly string[];
start: number;
}) {
const languageId = prismLanguageForMarkdownCode(language);
if (!languageId) return [];
const requests: MarkdownCodeLineHtmlRequest[] = [];
const boundedStart = Math.max(0, Math.min(start, sourceLines.length));
const boundedEnd = Math.max(boundedStart, Math.min(end, sourceLines.length));
for (let index = boundedStart; index < boundedEnd; index += 1) {
const line = sourceLines[index] ?? "";
requests.push({
highlightPattern,
index,
key: markdownCodeLineHtmlKey({
highlightPattern,
languageId,
line,
}),
languageId,
line,
});
}
return requests;
}
function ensureMarkdownCodeLineHtml(request: MarkdownCodeLineHtmlRequest) {
if (
resolvedMarkdownCodeLineHtml.has(request.key) ||
pendingMarkdownCodeLineHtml.has(request.key)
) {
return;
}
pendingMarkdownCodeLineHtml.set(request.key, request);
scheduleMarkdownCodeHighlightFlush();
}
function scheduleMarkdownCodeHighlightFlush() {
if (markdownCodeHighlightFlushHandle) return;
markdownCodeHighlightFlushHandle = scheduleMarkdownCodeHighlightTask(() => {
markdownCodeHighlightFlushHandle = null;
flushPendingMarkdownCodeHighlights();
});
}
function flushPendingMarkdownCodeHighlights() {
const firstRequest = pendingMarkdownCodeLineHtml.values().next().value;
if (!firstRequest) return;
const requests: MarkdownCodeLineHtmlRequest[] = [];
for (const request of pendingMarkdownCodeLineHtml.values()) {
if (
request.languageId !== firstRequest.languageId ||
request.highlightPattern !== firstRequest.highlightPattern
) {
continue;
}
pendingMarkdownCodeLineHtml.delete(request.key);
requests.push(request);
if (requests.length >= MARKDOWN_CODE_HIGHLIGHT_BATCH_SIZE) break;
}
if (requests.length === 0) return;
if (canUseMarkdownCodeHighlightWorker()) {
requestMarkdownCodeHighlightWorker(requests);
} else {
void highlightMarkdownCodeLinesOnMainThread(requests);
}
if (pendingMarkdownCodeLineHtml.size > 0)
scheduleMarkdownCodeHighlightFlush();
}
function canUseMarkdownCodeHighlightWorker() {
return (
!isMarkdownCodeHighlightWorkerFailed &&
typeof Worker !== "undefined" &&
typeof window !== "undefined"
);
}
function requestMarkdownCodeHighlightWorker(
requests: readonly MarkdownCodeLineHtmlRequest[],
) {
const worker = getMarkdownCodeHighlightWorker();
if (!worker) {
void highlightMarkdownCodeLinesOnMainThread(requests);
return;
}
markdownCodeHighlightRequestId += 1;
const requestId = markdownCodeHighlightRequestId;
activeWorkerRequests.set(requestId, requests);
const firstRequest = requests[0]!;
const request: MarkdownCodeHighlightWorkerRequest = {
generation: MARKDOWN_CODE_HIGHLIGHT_RENDERER_VERSION,
highlightPattern: firstRequest.highlightPattern,
languageId: firstRequest.languageId,
lines: requests.map(({ index, line }) => ({ index, line })),
requestId,
type: "highlight",
};
worker.postMessage(request);
}
function getMarkdownCodeHighlightWorker() {
if (markdownCodeHighlightWorker) return markdownCodeHighlightWorker;
try {
markdownCodeHighlightWorker = new Worker(
new URL(
"./markdown-greenfield-code-highlight.worker.ts",
import.meta.url,
),
{ type: "module" },
);
markdownCodeHighlightWorker.onmessage = (
event: MessageEvent<MarkdownCodeHighlightWorkerResponse>,
) => handleMarkdownCodeHighlightWorkerMessage(event.data);
markdownCodeHighlightWorker.onerror = () =>
failMarkdownCodeHighlightWorker();
markdownCodeHighlightWorker.onmessageerror = () =>
failMarkdownCodeHighlightWorker();
return markdownCodeHighlightWorker;
} catch {
isMarkdownCodeHighlightWorkerFailed = true;
return null;
}
}
function handleMarkdownCodeHighlightWorkerMessage(
message: MarkdownCodeHighlightWorkerResponse,
) {
const requests = activeWorkerRequests.get(message.requestId);
if (
!requests ||
message.generation !== MARKDOWN_CODE_HIGHLIGHT_RENDERER_VERSION
) {
return;
}
activeWorkerRequests.delete(message.requestId);
if (message.type === "error") {
resolveMarkdownCodeLineHtmlRequests(
requests.map((request) => ({ html: null, index: request.index })),
requests,
);
return;
}
resolveMarkdownCodeLineHtmlRequests(message.results, requests);
}
function failMarkdownCodeHighlightWorker() {
const activeRequests = Array.from(activeWorkerRequests.values()).flat();
activeWorkerRequests.clear();
markdownCodeHighlightWorker?.terminate();
markdownCodeHighlightWorker = null;
isMarkdownCodeHighlightWorkerFailed = true;
if (activeRequests.length) {
void highlightMarkdownCodeLinesOnMainThread(activeRequests);
}
}
async function highlightMarkdownCodeLinesOnMainThread(
requests: readonly MarkdownCodeLineHtmlRequest[],
) {
const firstRequest = requests[0];
if (!firstRequest) return;
try {
await ensureCodePrismLanguage(firstRequest.languageId);
resolveMarkdownCodeLineHtmlRequests(
requests.map((request) => ({
html: shouldTokenizeCodeLine(request.line)
? markdownCodeTokensToHtml({
highlightPattern: request.highlightPattern,
line: request.line,
tokens: tokenizePrismCodeLine(request.languageId, request.line),
})
: null,
index: request.index,
})),
requests,
);
} catch {
resolveMarkdownCodeLineHtmlRequests(
requests.map((request) => ({ html: null, index: request.index })),
requests,
);
}
}
function resolveMarkdownCodeLineHtmlRequests(
results: readonly MarkdownCodeHighlightLineResult[],
requests: readonly MarkdownCodeLineHtmlRequest[],
) {
const requestsByIndex = new Map(
requests.map((request) => [request.index, request] as const),
);
const resolvedKeys: string[] = [];
for (const result of results) {
const request = requestsByIndex.get(result.index);
if (!request) continue;
resolvedMarkdownCodeLineHtml.set(request.key, result.html);
resolvedKeys.push(request.key);
}
trimMarkdownCodeLineHtmlCache();
notifyMarkdownCodeLineHtmlSubscribers(resolvedKeys);
}
function notifyMarkdownCodeLineHtmlSubscribers(keys: readonly string[]) {
const subscribers = new Set<() => void>();
for (const key of keys) {
for (const subscriber of markdownCodeLineHtmlSubscribers.get(key) ?? []) {
subscribers.add(subscriber);
}
}
for (const subscriber of subscribers) subscriber();
}
function trimMarkdownCodeLineHtmlCache() {
while (
resolvedMarkdownCodeLineHtml.size > MARKDOWN_CODE_HIGHLIGHT_CACHE_LIMIT
) {
const oldestKey = resolvedMarkdownCodeLineHtml.keys().next().value;
if (oldestKey === undefined) break;
resolvedMarkdownCodeLineHtml.delete(oldestKey);
}
}
function scheduleMarkdownCodeHighlightTask(callback: () => void) {
if (typeof window === "undefined") {
const id = setTimeout(callback, 0) as unknown as number;
return { id, kind: "timeout" as const };
}
const browserWindow = window as MarkdownCodeHighlightIdleWindow;
if (browserWindow.requestIdleCallback) {
return {
id: browserWindow.requestIdleCallback(callback, { timeout: 120 }),
kind: "idle" as const,
};
}
return {
id: browserWindow.setTimeout(callback, 0),
kind: "timeout" as const,
};
}
export function resetMarkdownCodeHighlightForTests() {
resolvedMarkdownCodeLineHtml.clear();
pendingMarkdownCodeLineHtml.clear();
markdownCodeLineHtmlSubscribers.clear();
activeWorkerRequests.clear();
if (markdownCodeHighlightFlushHandle) {
cancelMarkdownCodeHighlightTask(markdownCodeHighlightFlushHandle);
}
markdownCodeHighlightFlushHandle = null;
markdownCodeHighlightWorker?.terminate();
markdownCodeHighlightWorker = null;
isMarkdownCodeHighlightWorkerFailed = false;
markdownCodeHighlightRequestId = 0;
}
function cancelMarkdownCodeHighlightTask(
handle: MarkdownCodeHighlightTaskHandle,
) {
if (typeof window === "undefined") {
clearTimeout(handle.id);
return;
}
const browserWindow = window as MarkdownCodeHighlightIdleWindow;
if (handle.kind === "idle") {
browserWindow.cancelIdleCallback?.(handle.id);
return;
}
browserWindow.clearTimeout(handle.id);
}
function markdownCodeLineHtmlKey({
highlightPattern,
languageId,
line,
}: {
highlightPattern: string;
languageId: string;
line: string;
}) {
return [
MARKDOWN_CODE_HIGHLIGHT_RENDERER_VERSION,
languageId,
highlightPattern,
line,
].join("\0");
}
function prismLanguageForMarkdownCode(language: string) {
const aliases: Record<string, string> = {
dockerfile: "dockerfile",
js: "javascript",
shell: "bash",
ts: "typescript",
};
const languageId = aliases[language] ?? language;
if (languageId === "text" || languageId === "plaintext") return "";
return languageId;
}
function renderFallbackCodeTokens(line: string, language: string) {
const tokens = tokenizeFallbackCodeLine(line, language);
if (!tokens.length) return " ";
return tokens.map((token, index) =>
token.kind === "plain" ? (
<React.Fragment key={index}>{token.value}</React.Fragment>
) : (
<span
key={index}
className={codeTokenClassName(token.kind)}
data-pretext-code-token={token.kind}
>
{token.value}
</span>
),
);
}
type CodeToken = {
kind: "comment" | "keyword" | "literal" | "number" | "plain" | "string";
value: string;
};
function tokenizeFallbackCodeLine(line: string, language: string): CodeToken[] {
if (!line) return [];
if (language === "diff") return tokenizeDiffLine(line);
if (language === "json") return tokenizeJsonLikeLine(line);
if (language === "yaml") return tokenizeYamlLine(line);
if (
language === "js" ||
language === "jsx" ||
language === "ts" ||
language === "tsx"
) {
return tokenizeCStyleLine(line);
}
if (
language === "shell" ||
language === "bash" ||
language === "dockerfile"
) {
return tokenizeShellLine(line);
}
return [{ kind: "plain", value: line }];
}
function tokenizeCStyleLine(line: string): CodeToken[] {
const commentIndex = line.indexOf("//");
const codePart = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const commentPart = commentIndex >= 0 ? line.slice(commentIndex) : "";
return [
...tokenizeByPattern(
codePart,
/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b(?:as|async|await|break|case|catch|class|const|continue|default|do|else|export|extends|false|finally|for|from|function|if|import|in|instanceof|interface|let|new|null|of|return|satisfies|switch|throw|true|try|type|typeof|undefined|var|while|yield)\b|\b\d+(?:\.\d+)?\b)/g,
classifyCStyleToken,
),
...(commentPart ? [{ kind: "comment" as const, value: commentPart }] : []),
];
}
function classifyCStyleToken(value: string): CodeToken["kind"] {
if (/^["'`]/.test(value)) return "string";
if (/^\d/.test(value)) return "number";
if (/^(?:true|false|null|undefined)$/.test(value)) return "literal";
return "keyword";
}
function tokenizeJsonLikeLine(line: string): CodeToken[] {
return tokenizeByPattern(
line,
/("(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b)/gi,
(value) => {
if (/^"/.test(value)) return "string";
if (/^(?:true|false|null)$/i.test(value)) return "literal";
return "number";
},
);
}
function tokenizeYamlLine(line: string): CodeToken[] {
const commentIndex = line.indexOf("#");
const codePart = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const commentPart = commentIndex >= 0 ? line.slice(commentIndex) : "";
return [
...tokenizeByPattern(
codePart,
/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?\b)/gi,
(value) => {
if (/^["']/.test(value)) return "string";
if (/^(?:true|false|null)$/i.test(value)) return "literal";
return "number";
},
),
...(commentPart ? [{ kind: "comment" as const, value: commentPart }] : []),
];
}
function tokenizeShellLine(line: string): CodeToken[] {
const commentIndex = line.search(/(^|\s)#/);
const codePart = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const commentPart = commentIndex >= 0 ? line.slice(commentIndex) : "";
return [
...tokenizeByPattern(
codePart,
/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:cd|cp|curl|echo|export|git|grep|mkdir|mv|node|npm|pnpm|rm|sed|test|yarn)\b)/g,
(value) => (/^["']/.test(value) ? "string" : "keyword"),
),
...(commentPart ? [{ kind: "comment" as const, value: commentPart }] : []),
];
}
function tokenizeDiffLine(line: string): CodeToken[] {
if (line.startsWith("+") && !line.startsWith("+++")) {
return [{ kind: "literal", value: line }];
}
if (line.startsWith("-") && !line.startsWith("---")) {
return [{ kind: "comment", value: line }];
}
return [{ kind: "plain", value: line }];
}
function tokenizeByPattern(
line: string,
pattern: RegExp,
classify: (value: string) => CodeToken["kind"],
): CodeToken[] {
const tokens: CodeToken[] = [];
let cursor = 0;
for (const match of line.matchAll(pattern)) {
const index = match.index ?? 0;
if (index > cursor) {
tokens.push({ kind: "plain", value: line.slice(cursor, index) });
}
const value = match[0];
tokens.push({ kind: classify(value), value });
cursor = index + value.length;
}
if (cursor < line.length) {
tokens.push({ kind: "plain", value: line.slice(cursor) });
}
return tokens;
}
function codeTokenClassName(kind: CodeToken["kind"]) {
switch (kind) {
case "comment":
return "text-muted-foreground italic";
case "keyword":
return "font-semibold text-sky-700 dark:text-sky-300";
case "literal":
return "text-purple-700 dark:text-purple-300";
case "number":
return "text-amber-700 dark:text-amber-300";
case "string":
return "text-emerald-700 dark:text-emerald-300";
default:
return "";
}
}
registry/new-york-v4/ui/markdown-greenfield-diagram.tsx
"use client";
import * as React from "react";
import { Check, Clipboard } from "lucide-react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import {
MERMAID_VIEWER_STYLES,
describeDiagram,
estimateDiagramBodyHeight,
readDiagramLimitMessage,
renderDiagram,
type DiagramState,
} from "@/components/ui/mermaid-renderer";
export { resetMermaidRendererForTests as resetMarkdownMermaidRendererForTests } from "@/components/ui/mermaid-renderer";
const MERMAID_VIEWPORT_ROOT_MARGIN = "640px 0px";
// A per-instance async store for one diagram's mermaid render. Kept per
// component (not module-global) so instances never share state — no cross-
// render notify fan-out and no shared cache keyed by a render-order id.
type MermaidDiagramStore = {
key: string;
result: DiagramState | null;
listeners: Set<() => void>;
};
export function MarkdownGreenfieldDiagram({
caption,
componentName,
onContentReady,
source,
title,
}: {
caption?: string;
componentName?: string;
onContentReady?: () => void;
source: string;
title?: string;
}) {
const limitMessage = React.useMemo(
() => readDiagramLimitMessage(source),
[source],
);
const diagramId = React.useId().replace(/:/g, "");
const elementId = `markdown-diagram-${diagramId}`;
const description = React.useMemo(() => describeDiagram(source), [source]);
const bodyHeight = React.useMemo(
() => estimateDiagramBodyHeight(source),
[source],
);
const descriptionId = description
? `markdown-diagram-description-${diagramId}`
: undefined;
const captionId = caption
? `markdown-diagram-caption-${diagramId}`
: undefined;
const describedBy =
[descriptionId, captionId].filter(Boolean).join(" ") || undefined;
const [figureRef, isRenderEligible] = useMermaidRenderEligibility({
disabled: Boolean(limitMessage),
source,
});
// Derive the displayed state: a limit message fails immediately; otherwise the
// state follows this instance's async mermaid store (loading until resolved).
const storeRef = React.useRef<MermaidDiagramStore | null>(null);
if (storeRef.current === null) {
storeRef.current = { key: "", listeners: new Set(), result: null };
}
const store = storeRef.current;
const renderKey =
limitMessage || !isRenderEligible ? "" : `${elementId}\0${source}`;
const subscribe = React.useCallback(
(onStoreChange: () => void) => {
store.listeners.add(onStoreChange);
// Kick off (or restart on source change) the render here, in the store
// subscription, so no effect is needed.
if (!limitMessage && store.key !== renderKey) {
store.key = renderKey;
store.result = null;
void renderDiagram(source, elementId).then((result) => {
if (store.key !== renderKey) return;
store.result = result;
for (const listener of store.listeners) listener();
});
}
return () => {
store.listeners.delete(onStoreChange);
};
},
[elementId, limitMessage, renderKey, source, store],
);
const getSnapshot = React.useCallback(
() => (store.key === renderKey ? store.result : null),
[renderKey, store],
);
const resolvedState = React.useSyncExternalStore(
subscribe,
getSnapshot,
() => null,
);
const state: DiagramState = limitMessage
? { status: "failed", message: limitMessage }
: (resolvedState ?? { status: "loading" });
useKeyedLayoutEffect(
joinEffectKey([bodyHeight, onContentReady, state.status]),
() => {
onContentReady?.();
},
);
return (
<figure
ref={figureRef}
aria-describedby={describedBy}
aria-label={title || "Mermaid diagram"}
className="group bg-muted/30 my-5 min-h-40 overflow-hidden rounded-md border"
data-diagram-language="mermaid"
data-diagram-renderer={
state.status === "ready" ? state.renderer : undefined
}
data-diagram-reserved-height={bodyHeight}
data-diagram-state={state.status}
data-pretext-component={componentName}
role="group"
style={
{
"--pretext-diagram-body-height": `${bodyHeight}px`,
} as React.CSSProperties
}
>
<div className="bg-muted/60 flex h-9 items-center gap-1 border-b px-3">
<span className="text-muted-foreground min-w-0 truncate text-xs font-medium">
{title || "mermaid"}
</span>
<DiagramCopyButton
ariaLabel="Copy diagram source"
className="ml-auto"
text={source}
/>
{state.status === "ready" ? (
<DiagramCopyButton ariaLabel="Copy diagram SVG" text={state.svg} />
) : null}
</div>
{description ? (
<p
className="sr-only"
data-pretext-diagram-description=""
id={descriptionId}
>
{description}
</p>
) : null}
<div
aria-label="Mermaid diagram body"
className="h-(--pretext-diagram-body-height) overflow-auto p-4"
data-pretext-diagram-body=""
onKeyDown={(event) => {
const element = event.currentTarget;
if (event.key === "ArrowRight") {
element.scrollLeft += 50;
event.preventDefault();
} else if (event.key === "ArrowLeft") {
element.scrollLeft -= 50;
event.preventDefault();
} else if (event.key === "End") {
element.scrollLeft = Math.max(
0,
element.scrollWidth - element.clientWidth,
);
event.preventDefault();
} else if (event.key === "Home") {
element.scrollLeft = 0;
event.preventDefault();
}
}}
role="region"
tabIndex={0}
>
{state.status === "ready" ? (
<>
<style data-pretext-mermaid-styles="">
{MERMAID_VIEWER_STYLES}
</style>
<div
className="text-foreground"
data-pretext-mermaid-svg=""
dangerouslySetInnerHTML={{ __html: state.svg }}
/>
</>
) : (
<>
{state.status === "failed" ? (
<p
className="border-destructive/25 bg-destructive/10 text-destructive mb-3 rounded border px-3 py-2 text-sm"
data-pretext-diagram-error=""
role="alert"
>
{state.message}
</p>
) : null}
<pre
aria-label="Mermaid diagram source"
className="text-muted-foreground m-0 overflow-x-auto font-mono text-[0.82em] leading-relaxed"
tabIndex={0}
>
{source}
</pre>
</>
)}
</div>
{caption ? (
<figcaption
className="bg-muted/30 text-muted-foreground border-t px-3 py-2 text-sm"
data-pretext-diagram-caption=""
id={captionId}
>
{caption}
</figcaption>
) : null}
</figure>
);
}
function useMermaidRenderEligibility({
disabled,
source,
}: {
disabled: boolean;
source: string;
}) {
const ref = React.useRef<HTMLElement | null>(null);
const [isEligible, setIsEligible] = React.useState(
() => !disabled && typeof IntersectionObserver === "undefined",
);
useKeyedLayoutEffect(joinEffectKey([disabled, source]), () => {
if (disabled) {
setIsEligible(false);
return;
}
if (typeof IntersectionObserver === "undefined") {
setIsEligible(true);
return;
}
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry?.isIntersecting && (entry?.intersectionRatio ?? 0) <= 0) {
return;
}
setIsEligible(true);
observer.disconnect();
},
{ root: null, rootMargin: MERMAID_VIEWPORT_ROOT_MARGIN },
);
observer.observe(element);
return () => observer.disconnect();
});
return [ref, isEligible] as const;
}
function DiagramCopyButton({
ariaLabel,
className,
text,
}: {
ariaLabel: string;
className?: string;
text: string;
}) {
const [isCopied, setIsCopied] = React.useState(false);
return (
<button
aria-label={isCopied ? "Copied" : ariaLabel}
className={[
"text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-ring inline-flex size-7 items-center justify-center rounded-md transition focus-visible:ring-2 focus-visible:outline-none",
className,
]
.filter(Boolean)
.join(" ")}
type="button"
onClick={() => {
void navigator.clipboard?.writeText(text).then(() => {
setIsCopied(true);
window.setTimeout(() => setIsCopied(false), 1200);
});
}}
>
{isCopied ? (
<Check aria-hidden="true" className="size-3.5" />
) : (
<Clipboard aria-hidden="true" className="size-3.5" />
)}
</button>
);
}
registry/new-york-v4/ui/markdown-greenfield-document.ts
"use client";
import type {
MarkdownHastElement,
MarkdownHastNode,
} from "./markdown-hast-types";
import {
markdownSourceRangeFromPosition,
markdownSourceTextForRange,
type MarkdownSourceRange,
} from "./markdown-source-map";
import {
createMarkdownUnifiedDocument,
type MarkdownUnifiedDocument,
} from "./markdown-unified-pipeline";
const TARGET_CHUNK_SOURCE_LINES = 42;
const MAX_CHUNK_SOURCE_LINES = 64;
const HOSTILE_CODE_LINE_COUNT = 400;
const HOSTILE_HAST_NODE_COUNT = 3_000;
const HOSTILE_HAST_DEPTH = 80;
const HOSTILE_TEXT_LENGTH = 20_000;
const HOSTILE_TABLE_CELL_COUNT = 50_000;
const HOSTILE_TABLE_HAST_NODE_COUNT = 120_000;
const HOSTILE_TABLE_TEXT_LENGTH = 1_000_000;
const DOCUMENT_CACHE_LIMIT = 24;
const markdownGreenfieldDocumentCache = new Map<
string,
{
document: MarkdownGreenfieldDocument;
text: string;
}
>();
export type MarkdownGreenfieldBlockKind =
| "blockquote"
| "code"
| "component"
| "diagram"
| "footnotes"
| "frontmatter"
| "heading"
| "html"
| "image"
| "list"
| "math"
| "paragraph"
| "table"
| "thematicBreak"
| "unknown";
export type MarkdownGreenfieldDocument = {
blocks: MarkdownGreenfieldBlock[];
chunks: MarkdownGreenfieldChunk[];
fragmentTargets: MarkdownGreenfieldFragmentTarget[];
headings: MarkdownGreenfieldHeading[];
lineCount: number;
text: string;
unified: MarkdownUnifiedDocument;
wordCount: number;
};
export type MarkdownGreenfieldHeading = {
blockId: string;
id: string;
sourceLine: number;
text: string;
};
export type MarkdownGreenfieldFragmentTarget = {
blockId: string;
id: string;
sourceLine: number;
};
export type MarkdownGreenfieldBlock = {
hastChildren: MarkdownHastNode[];
id: string;
index: number;
isGenerated: boolean;
isHostile: boolean;
kind: MarkdownGreenfieldBlockKind;
sourceLineCount: number;
sourceLineLengths: readonly number[];
sourceRange: MarkdownSourceRange | null;
sourceText: string;
};
export type MarkdownGreenfieldChunk = {
blockIds: string[];
hastChildren: MarkdownHastNode[];
id: string;
index: number;
isHostile: boolean;
nativeFindText: string;
sourceEndLine: number;
sourceLineCount: number;
sourceRange: MarkdownSourceRange | null;
sourceStartLine: number;
sourceText: string;
};
export function createMarkdownGreenfieldDocument(
markdown: string,
): MarkdownGreenfieldDocument {
const text = markdown.length ? markdown : " ";
const cacheKey = markdownGreenfieldDocumentTextKey(text);
const cached = markdownGreenfieldDocumentCache.get(cacheKey);
if (cached?.text === text) {
markdownGreenfieldDocumentCache.delete(cacheKey);
markdownGreenfieldDocumentCache.set(cacheKey, cached);
return cached.document;
}
const document = createUncachedMarkdownGreenfieldDocument(text);
markdownGreenfieldDocumentCache.set(cacheKey, {
document,
text,
});
while (markdownGreenfieldDocumentCache.size > DOCUMENT_CACHE_LIMIT) {
const oldestKey = markdownGreenfieldDocumentCache.keys().next().value;
if (!oldestKey) break;
markdownGreenfieldDocumentCache.delete(oldestKey);
}
return document;
}
function createUncachedMarkdownGreenfieldDocument(
text: string,
): MarkdownGreenfieldDocument {
const unified = createMarkdownUnifiedDocument(text);
normalizeMarkdownGreenfieldHeadingIds(unified.hast.children, unified);
normalizeMarkdownGreenfieldTables(unified.hast.children);
annotateMarkdownGreenfieldSourceMetadata(unified.hast.children, unified);
const blocks = createMarkdownGreenfieldBlocks({ text, unified });
const chunks = createMarkdownGreenfieldChunks({ blocks, text });
const headings = createMarkdownGreenfieldHeadings(blocks);
const fragmentTargets = createMarkdownGreenfieldFragmentTargets({
blocks,
unified,
});
return freezeMarkdownGreenfieldDocument({
blocks,
chunks,
fragmentTargets,
headings,
lineCount: unified.sourceMap.lineCount,
text,
unified,
wordCount: text.trim().split(/\s+/).filter(Boolean).length,
});
}
export function findMarkdownGreenfieldHeadingById(
document: MarkdownGreenfieldDocument,
headingId: string,
) {
const normalizedId = headingId.replace(/^#/, "");
return document.headings.find((heading) => heading.id === normalizedId);
}
export function findMarkdownGreenfieldFragmentTargetById(
document: MarkdownGreenfieldDocument,
fragmentId: string,
) {
const normalizedId = normalizeFragmentTargetId(fragmentId);
return document.fragmentTargets.find((target) => target.id === normalizedId);
}
export function findMarkdownGreenfieldBlockById(
document: MarkdownGreenfieldDocument,
blockId: string,
) {
return document.blocks.find((block) => block.id === blockId) ?? null;
}
export function findMarkdownGreenfieldChunkByBlockId(
document: MarkdownGreenfieldDocument,
blockId: string,
) {
return (
document.chunks.find((chunk) => chunk.blockIds.includes(blockId)) ?? null
);
}
export function findMarkdownGreenfieldBlockBySourceLine(
document: MarkdownGreenfieldDocument,
sourceLine: number,
) {
const line = clampSourceLine(sourceLine, document.lineCount);
return (
document.blocks.find((block) => {
const range = block.sourceRange;
return range && range.startLine <= line && range.endLine >= line;
}) ?? null
);
}
export function findMarkdownGreenfieldChunkBySourceLine(
document: MarkdownGreenfieldDocument,
sourceLine: number,
) {
const block = findMarkdownGreenfieldBlockBySourceLine(document, sourceLine);
if (block) return findMarkdownGreenfieldChunkByBlockId(document, block.id);
const line = clampSourceLine(sourceLine, document.lineCount);
return (
document.chunks.find(
(chunk) => chunk.sourceStartLine <= line && chunk.sourceEndLine >= line,
) ?? null
);
}
export function findMarkdownGreenfieldBlockBySourceOffset(
document: MarkdownGreenfieldDocument,
sourceOffset: number,
) {
const offset = clampSourceOffset(sourceOffset, document.text.length);
return (
document.blocks.find((block) => {
const range = block.sourceRange;
return range && range.startOffset <= offset && range.endOffset > offset;
}) ?? null
);
}
export function findMarkdownGreenfieldChunkBySourceOffset(
document: MarkdownGreenfieldDocument,
sourceOffset: number,
) {
const block = findMarkdownGreenfieldBlockBySourceOffset(
document,
sourceOffset,
);
return block
? findMarkdownGreenfieldChunkByBlockId(document, block.id)
: null;
}
function createMarkdownGreenfieldBlocks({
text,
unified,
}: {
text: string;
unified: MarkdownUnifiedDocument;
}) {
const blocks: MarkdownGreenfieldBlock[] = [];
for (const child of unified.hast.children) {
if (isWhitespaceText(child)) continue;
const directSourceRange = markdownSourceRangeFromPosition({
position: child.position,
sourceMap: unified.sourceMap,
});
const sourceRange =
directSourceRange ?? markdownSyntheticSourceRangeForNode(child, unified);
const kind = markdownBlockKindForHastChild(child);
const sourceText = markdownSourceTextForRange({
range: sourceRange,
sourceMap: unified.sourceMap,
});
const normalizedSourceText =
sourceText ||
(sourceRange
? text.slice(sourceRange.startOffset, sourceRange.endOffset)
: "");
const sourceMetrics =
markdownGreenfieldSourceMetricsForText(normalizedSourceText);
const line = sourceRange?.startLine ?? unified.sourceMap.lineCount;
const block: MarkdownGreenfieldBlock = {
hastChildren: [child],
id: `block-${blocks.length + 1}-${line}-${kind}`,
index: blocks.length,
isGenerated: !directSourceRange,
isHostile: isHostileMarkdownGreenfieldBlock({
child,
kind,
sourceLineCount: sourceMetrics.sourceLineCount,
sourceText: normalizedSourceText,
}),
kind,
sourceLineCount: sourceMetrics.sourceLineCount,
sourceLineLengths: sourceMetrics.sourceLineLengths,
sourceRange,
sourceText: normalizedSourceText,
};
blocks.push(block);
}
if (!blocks.length) {
const sourceMetrics = markdownGreenfieldSourceMetricsForText(text);
blocks.push({
hastChildren: [],
id: "block-1-empty",
index: 0,
isGenerated: true,
isHostile: false,
kind: "paragraph",
sourceLineCount: sourceMetrics.sourceLineCount,
sourceLineLengths: sourceMetrics.sourceLineLengths,
sourceRange: {
endLine: 1,
endOffset: text.length,
startLine: 1,
startOffset: 0,
},
sourceText: text,
});
}
return blocks;
}
function createMarkdownGreenfieldChunks({
blocks,
text,
}: {
blocks: readonly MarkdownGreenfieldBlock[];
text: string;
}) {
const chunks: MarkdownGreenfieldChunk[] = [];
let current: MarkdownGreenfieldBlock[] = [];
const flush = () => {
if (!current.length) return;
chunks.push(createChunk(current, chunks.length, text));
current = [];
};
for (const block of blocks) {
if (block.isHostile) {
flush();
current.push(block);
flush();
continue;
}
const nextLineCount = lineCountForBlocks(current, block);
const startsNewChunk =
current.length > 0 &&
block.kind === "heading" &&
nextLineCount >= TARGET_CHUNK_SOURCE_LINES;
const exceedsMax =
current.length > 0 && nextLineCount > MAX_CHUNK_SOURCE_LINES;
if (startsNewChunk || exceedsMax) flush();
current.push(block);
}
flush();
return chunks;
}
function createChunk(
blocks: readonly MarkdownGreenfieldBlock[],
index: number,
text: string,
): MarkdownGreenfieldChunk {
const ranges = blocks
.map((block) => block.sourceRange)
.filter((range): range is MarkdownSourceRange => Boolean(range));
const sourceRange = ranges.length
? {
endLine: Math.max(...ranges.map((range) => range.endLine)),
endOffset: Math.max(...ranges.map((range) => range.endOffset)),
startLine: Math.min(...ranges.map((range) => range.startLine)),
startOffset: Math.min(...ranges.map((range) => range.startOffset)),
}
: null;
const sourceStartLine =
sourceRange?.startLine ?? blocks[0]?.sourceRange?.startLine ?? 1;
const sourceEndLine =
sourceRange?.endLine ??
blocks[blocks.length - 1]?.sourceRange?.endLine ??
sourceStartLine;
const sourceLineCount = sourceRange
? sourceEndLine - sourceStartLine + 1
: Math.max(
1,
blocks.reduce((sum, block) => sum + block.sourceLineCount, 0),
);
const hastChildren = blocks.flatMap((block) => block.hastChildren);
return {
blockIds: blocks.map((block) => block.id),
hastChildren,
id: `chunk-${index + 1}-${sourceStartLine}`,
index,
isHostile: blocks.some((block) => block.isHostile),
nativeFindText: nativeFindTextForHastChildren(hastChildren),
sourceEndLine,
sourceLineCount,
sourceRange,
sourceStartLine,
sourceText: sourceRange
? text.slice(sourceRange.startOffset, sourceRange.endOffset)
: "",
};
}
function createMarkdownGreenfieldHeadings(
blocks: readonly MarkdownGreenfieldBlock[],
) {
return blocks.flatMap((block) => {
const element = readHastElement(block.hastChildren[0]);
if (!element || !/^h[1-6]$/.test(element.tagName)) return [];
const id = readStringProperty(element.properties?.id);
if (!id) return [];
return [
{
blockId: block.id,
id,
sourceLine: block.sourceRange?.startLine ?? 1,
text: extractHastText(element).trim(),
},
];
});
}
function createMarkdownGreenfieldFragmentTargets({
blocks,
unified,
}: {
blocks: readonly MarkdownGreenfieldBlock[];
unified: MarkdownUnifiedDocument;
}) {
const targets = new Map<string, MarkdownGreenfieldFragmentTarget>();
for (const block of blocks) {
for (const child of block.hastChildren) {
collectFragmentTargets({
block,
node: child,
targets,
unified,
});
}
}
return Array.from(targets.values());
}
function collectFragmentTargets({
block,
node,
targets,
unified,
}: {
block: MarkdownGreenfieldBlock;
node: MarkdownHastNode;
targets: Map<string, MarkdownGreenfieldFragmentTarget>;
unified: MarkdownUnifiedDocument;
}) {
const element = readHastElement(node);
if (!element) return;
const id = readStringProperty(element.properties?.id);
if (id) {
const sourceRange =
markdownSourceRangeFromPosition({
position: element.position,
sourceMap: unified.sourceMap,
}) ?? block.sourceRange;
const target = {
blockId: block.id,
id,
sourceLine: sourceRange?.startLine ?? unified.sourceMap.lineCount,
};
for (const alias of fragmentTargetAliases(id)) {
if (!targets.has(alias)) {
targets.set(alias, { ...target, id: alias });
}
}
}
for (const child of element.children) {
collectFragmentTargets({ block, node: child, targets, unified });
}
}
function markdownSyntheticSourceRangeForNode(
node: MarkdownHastNode,
unified: MarkdownUnifiedDocument,
): MarkdownSourceRange | null {
const ranges = collectMarkdownSourceRanges(node, unified);
if (!ranges.length) return null;
return {
endLine: Math.max(...ranges.map((range) => range.endLine)),
endOffset: Math.max(...ranges.map((range) => range.endOffset)),
startLine: Math.min(...ranges.map((range) => range.startLine)),
startOffset: Math.min(...ranges.map((range) => range.startOffset)),
};
}
function collectMarkdownSourceRanges(
node: MarkdownHastNode,
unified: MarkdownUnifiedDocument,
): MarkdownSourceRange[] {
const range = markdownSourceRangeFromPosition({
position: node.position,
sourceMap: unified.sourceMap,
});
const element = readHastElement(node);
return [
...(range ? [range] : []),
...(element?.children ?? []).flatMap((child) =>
collectMarkdownSourceRanges(child, unified),
),
];
}
function normalizeMarkdownGreenfieldHeadingIds(
nodes: readonly MarkdownHastNode[],
unified: MarkdownUnifiedDocument,
) {
const usedIds = new Map<string, number>();
for (const node of nodes) {
normalizeHeadingIdsInNode(node, usedIds, unified);
}
}
function normalizeHeadingIdsInNode(
node: MarkdownHastNode,
usedIds: Map<string, number>,
unified: MarkdownUnifiedDocument,
) {
const element = readHastElement(node);
if (!element) return;
if (/^h[1-6]$/.test(element.tagName)) {
const visibleText = extractHastText(element);
const markdownText = markdownHeadingTextFromSource(element, unified);
const baseId = safeHeadingIdForText(
markdownText.includes("__proto__") ? markdownText : visibleText,
);
const duplicateCount = usedIds.get(baseId) ?? 0;
usedIds.set(baseId, duplicateCount + 1);
element.properties = {
...element.properties,
id: duplicateCount === 0 ? baseId : `${baseId}-${duplicateCount}`,
};
}
for (const child of element.children) {
normalizeHeadingIdsInNode(child, usedIds, unified);
}
}
function markdownHeadingTextFromSource(
element: MarkdownHastElement,
unified: MarkdownUnifiedDocument,
) {
const range = markdownSourceRangeFromPosition({
position: element.position,
sourceMap: unified.sourceMap,
});
if (!range) return "";
return markdownSourceTextForRange({
range,
sourceMap: unified.sourceMap,
})
.replace(/^\s{0,3}#{1,6}[ \t]*/, "")
.replace(/[ \t]+#*\s*$/, "")
.trim();
}
function safeHeadingIdForText(text: string) {
const slug =
text
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^\w\s-]/g, "")
.trim()
.replace(/\s+/g, "-")
.replace(/-+/g, "-") || "section";
return isDomClobberingId(slug) ? `section-${slug}` : slug;
}
function normalizeFragmentTargetId(fragmentId: string) {
return decodeURIComponent(fragmentId.replace(/^#/, ""));
}
function fragmentTargetAliases(id: string) {
const aliases = new Set<string>([normalizeFragmentTargetId(id)]);
const withoutRepeatedClobberPrefix = id.replace(
/^(user-content-)+/,
"user-content-",
);
aliases.add(withoutRepeatedClobberPrefix);
aliases.add(withoutRepeatedClobberPrefix.replace(/^user-content-/, ""));
return Array.from(aliases).filter(Boolean);
}
function normalizeMarkdownGreenfieldTables(nodes: readonly MarkdownHastNode[]) {
let tableIndex = 0;
for (const node of nodes) {
tableIndex = normalizeTablesInNode(node, tableIndex);
}
}
function normalizeTablesInNode(node: MarkdownHastNode, tableIndex: number) {
const element = readHastElement(node);
if (!element) return tableIndex;
if (element.tagName === "table") {
normalizeTableElement(element, tableIndex);
tableIndex += 1;
}
for (const child of element.children) {
tableIndex = normalizeTablesInNode(child, tableIndex);
}
return tableIndex;
}
function normalizeTableElement(table: MarkdownHastElement, tableIndex: number) {
const rows = tableRows(table);
const headerIds = new Map<number, string>();
rows.forEach((row, rowIndex) => {
row.properties = {
...row.properties,
ariaRowIndex: rowIndex + 1,
dataPretextTableRowIndex: rowIndex + 1,
};
tableCells(row).forEach((cell, columnIndex) => {
const column = columnIndex + 1;
const properties = {
...cell.properties,
ariaColIndex: column,
dataPretextTableColumnIndex: column,
};
if (cell.tagName === "th") {
const id = `markdown-table-${tableIndex + 1}-column-${column}`;
headerIds.set(columnIndex, id);
cell.properties = {
...properties,
id,
scope: "col",
};
} else {
cell.properties = {
...properties,
headers: headerIds.get(columnIndex),
};
}
});
});
}
function annotateMarkdownGreenfieldSourceMetadata(
nodes: readonly MarkdownHastNode[],
unified: MarkdownUnifiedDocument,
) {
for (const node of nodes) {
annotateSourceMetadataInNode(node, unified);
}
}
function annotateSourceMetadataInNode(
node: MarkdownHastNode,
unified: MarkdownUnifiedDocument,
) {
const element = readHastElement(node);
if (!element) return;
const sourceRange = markdownSourceRangeFromPosition({
position: element.position,
sourceMap: unified.sourceMap,
});
if (sourceRange) {
element.properties = {
...element.properties,
dataPretextSourceEndLine: sourceRange.endLine,
dataPretextSourceEndOffset: sourceRange.endOffset,
dataPretextSourceStartLine: sourceRange.startLine,
dataPretextSourceStartOffset: sourceRange.startOffset,
};
}
for (const child of element.children) {
annotateSourceMetadataInNode(child, unified);
}
}
function tableRows(element: MarkdownHastElement) {
const rows: MarkdownHastElement[] = [];
for (const child of element.children) {
const childElement = readHastElement(child);
if (!childElement) continue;
if (childElement.tagName === "tr") {
rows.push(childElement);
} else if (
childElement.tagName === "thead" ||
childElement.tagName === "tbody" ||
childElement.tagName === "tfoot"
) {
rows.push(...tableRows(childElement));
}
}
return rows;
}
function tableCells(row: MarkdownHastElement) {
return row.children
.map(readHastElement)
.filter(
(child): child is MarkdownHastElement =>
child?.tagName === "td" || child?.tagName === "th",
);
}
function isDomClobberingId(id: string) {
return [
"__proto__",
"constructor",
"document",
"forms",
"history",
"location",
"name",
"prototype",
"window",
].includes(id);
}
function markdownBlockKindForHastChild(
child: MarkdownHastNode,
): MarkdownGreenfieldBlockKind {
const element = readHastElement(child);
if (!element) return child.type === "text" ? "paragraph" : "unknown";
if (element.properties?.dataFootnotes != null) return "footnotes";
if (element.properties?.dataMarkdownFrontmatter != null) return "frontmatter";
if (/^h[1-6]$/.test(element.tagName)) return "heading";
if (isMarkdownDiagramElement(element)) return "diagram";
if (isMarkdownComponentElement(element)) return "component";
if (isDisplayMathElement(element)) return "math";
switch (element.tagName) {
case "blockquote":
return "blockquote";
case "hr":
return "thematicBreak";
case "ol":
case "ul":
return "list";
case "p":
if (isDisplayMathElement(element)) return "math";
return firstElementChild(element)?.tagName === "img"
? "image"
: "paragraph";
case "pre":
if (isMermaidCodeElement(element)) return "diagram";
return "code";
case "table":
return "table";
default:
return "html";
}
}
function isMarkdownDiagramElement(element: MarkdownHastElement) {
return (
readStringProperty(element.properties?.dataPretextComponentName) ===
"Diagram"
);
}
function isMarkdownComponentElement(element: MarkdownHastElement) {
return (
element.properties?.dataPretextComponentName != null ||
element.properties?.dataPretextComponentFallback != null ||
element.properties?.dataPretextCalloutKind != null
);
}
function isDisplayMathElement(element: MarkdownHastElement): boolean {
if (
hasClassName(element, "katex-display") ||
hasClassName(element, "math-display")
)
return true;
return element.children
.map(readHastElement)
.some((child) => child != null && isDisplayMathElement(child));
}
function isMermaidCodeElement(element: MarkdownHastElement) {
const code = firstElementChild(element);
return (
code?.tagName === "code" &&
["language-mermaid", "language-mmd", "language-mermaid-js"].some(
(className) => hasClassName(code, className),
)
);
}
function hasClassName(element: MarkdownHastElement, className: string) {
const classes = element.properties?.className;
return Array.isArray(classes)
? classes.includes(className)
: typeof classes === "string" && classes.split(/\s+/).includes(className);
}
function isHostileMarkdownGreenfieldBlock({
child,
kind,
sourceLineCount,
sourceText,
}: {
child: MarkdownHastNode;
kind: MarkdownGreenfieldBlockKind;
sourceLineCount: number;
sourceText: string;
}) {
if (kind === "table") {
if (sourceText.length > HOSTILE_TABLE_TEXT_LENGTH) return true;
if (countTableCells(child) > HOSTILE_TABLE_CELL_COUNT) return true;
if (countHastNodes(child) > HOSTILE_TABLE_HAST_NODE_COUNT) return true;
if (maxHastDepth(child) > HOSTILE_HAST_DEPTH) return true;
return false;
}
if (sourceText.length > HOSTILE_TEXT_LENGTH) return true;
if (countHastNodes(child) > HOSTILE_HAST_NODE_COUNT) return true;
if (maxHastDepth(child) > HOSTILE_HAST_DEPTH) return true;
if (kind === "code" && sourceLineCount > HOSTILE_CODE_LINE_COUNT) {
return true;
}
return false;
}
function countHastNodes(node: MarkdownHastNode): number {
const element = readHastElement(node);
return (
1 +
(element?.children ?? []).reduce(
(sum, child) => sum + countHastNodes(child),
0,
)
);
}
function maxHastDepth(node: MarkdownHastNode): number {
const element = readHastElement(node);
if (!element?.children.length) return 1;
return 1 + Math.max(...element.children.map(maxHastDepth));
}
function lineCountForBlocks(
blocks: readonly MarkdownGreenfieldBlock[],
nextBlock: MarkdownGreenfieldBlock,
) {
let fallbackLineCount = 0;
let sourceEndLine = 0;
let sourceStartLine = 0;
for (const block of blocks) {
fallbackLineCount += block.sourceLineCount;
const range = block.sourceRange;
if (!range) continue;
sourceStartLine = sourceStartLine
? Math.min(sourceStartLine, range.startLine)
: range.startLine;
sourceEndLine = Math.max(sourceEndLine, range.endLine);
}
fallbackLineCount += nextBlock.sourceLineCount;
const nextRange = nextBlock.sourceRange;
if (nextRange) {
sourceStartLine = sourceStartLine
? Math.min(sourceStartLine, nextRange.startLine)
: nextRange.startLine;
sourceEndLine = Math.max(sourceEndLine, nextRange.endLine);
}
if (!sourceStartLine) return Math.max(1, fallbackLineCount);
return sourceEndLine - sourceStartLine + 1;
}
function countTableCells(node: MarkdownHastNode): number {
const element = readHastElement(node);
if (!element) return 0;
const self = element.tagName === "td" || element.tagName === "th" ? 1 : 0;
return (
self +
(element.children ?? []).reduce(
(sum, child) => sum + countTableCells(child),
0,
)
);
}
function markdownGreenfieldSourceMetricsForText(sourceText: string) {
const sourceLineLengths: number[] = [];
let sourceLineLength = 0;
for (let index = 0; index < sourceText.length; index += 1) {
const charCode = sourceText.charCodeAt(index);
if (charCode === 13) {
sourceLineLengths.push(sourceLineLength);
sourceLineLength = 0;
if (sourceText.charCodeAt(index + 1) === 10) index += 1;
continue;
}
if (charCode === 10 || charCode === 0x2028 || charCode === 0x2029) {
sourceLineLengths.push(sourceLineLength);
sourceLineLength = 0;
continue;
}
sourceLineLength += 1;
}
sourceLineLengths.push(sourceLineLength);
return {
sourceLineCount: sourceLineLengths.length,
sourceLineLengths,
};
}
function readHastElement(node: unknown): MarkdownHastElement | null {
return node &&
typeof node === "object" &&
(node as MarkdownHastElement).type === "element"
? (node as MarkdownHastElement)
: null;
}
function firstElementChild(element: MarkdownHastElement) {
return element.children.map(readHastElement).find(Boolean) ?? null;
}
function isWhitespaceText(node: MarkdownHastNode) {
return (
node.type === "text" &&
typeof node.value === "string" &&
node.value.trim() === ""
);
}
function readStringProperty(value: unknown) {
if (typeof value === "string") return value;
if (Array.isArray(value)) return value.filter(Boolean).join(" ");
return "";
}
function extractHastText(node: MarkdownHastNode): string {
if (node.type === "text" && typeof node.value === "string") return node.value;
const element = readHastElement(node);
if (!element) return "";
return element.children.map(extractHastText).join("");
}
function nativeFindTextForHastChildren(children: readonly MarkdownHastNode[]) {
const text: string[] = [];
const stack = [...children].reverse();
while (stack.length) {
const node = stack.pop();
if (!node) continue;
if (node.type === "text" && typeof node.value === "string") {
text.push(node.value);
continue;
}
const element = readHastElement(node);
if (!element) continue;
if (element.tagName === "script" || element.tagName === "style") continue;
for (let index = element.children.length - 1; index >= 0; index -= 1) {
stack.push(element.children[index]!);
}
}
return text.join(" ").trim();
}
function freezeMarkdownHastNode(node: unknown) {
if (!node || typeof node !== "object" || Object.isFrozen(node)) return;
const record = node as Record<string, unknown>;
for (const value of Object.values(record)) {
if (Array.isArray(value)) {
value.forEach(freezeMarkdownHastNode);
Object.freeze(value);
continue;
}
if (value && typeof value === "object") {
freezeMarkdownHastNode(value);
}
}
Object.freeze(node);
}
export function freezeMarkdownGreenfieldDocument(
document: MarkdownGreenfieldDocument,
) {
freezeMarkdownHastNode(document.unified.hast);
for (const block of document.blocks) {
if (block.sourceRange) Object.freeze(block.sourceRange);
Object.freeze(block.hastChildren);
Object.freeze(block.sourceLineLengths);
Object.freeze(block);
}
for (const chunk of document.chunks) {
if (chunk.sourceRange) Object.freeze(chunk.sourceRange);
Object.freeze(chunk.blockIds);
Object.freeze(chunk.hastChildren);
Object.freeze(chunk);
}
for (const heading of document.headings) Object.freeze(heading);
for (const target of document.fragmentTargets) Object.freeze(target);
Object.freeze(document.blocks);
Object.freeze(document.chunks);
Object.freeze(document.headings);
Object.freeze(document.fragmentTargets);
return Object.freeze(document);
}
export function markdownGreenfieldDocumentTextKey(text: string) {
let hash = 2166136261;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return `${text.length}:${(hash >>> 0).toString(36)}`;
}
function clampSourceLine(line: number, lineCount: number) {
if (!Number.isFinite(line)) return 1;
return Math.max(1, Math.min(lineCount, Math.floor(line)));
}
function clampSourceOffset(offset: number, textLength: number) {
if (!Number.isFinite(offset)) return 0;
return Math.max(0, Math.min(textLength, Math.floor(offset)));
}
registry/new-york-v4/ui/markdown-greenfield-document-store.ts
"use client";
import * as React from "react";
import {
createMarkdownGreenfieldDocument,
freezeMarkdownGreenfieldDocument,
markdownGreenfieldDocumentTextKey,
type MarkdownGreenfieldDocument,
} from "./markdown-greenfield-document";
export const MARKDOWN_GREENFIELD_ASYNC_DOCUMENT_MIN_CHARS = 60_000;
export const MARKDOWN_GREENFIELD_DOCUMENT_WORKER_LOCAL_STORAGE_KEY =
"retab:markdown-document-worker";
export const MARKDOWN_GREENFIELD_DOCUMENT_WORKER_SEARCH_PARAM =
"markdownDocumentWorker";
const MARKDOWN_GREENFIELD_DOCUMENT_WORKER_READY_TIMEOUT_MS = 800;
const MARKDOWN_GREENFIELD_DOCUMENT_ENTRY_CACHE_LIMIT = 16;
type MarkdownIdleWindow = Window &
typeof globalThis & {
requestIdleCallback?: Window["requestIdleCallback"];
};
type MarkdownDocumentState =
| { status: "failed"; error: Error }
| { status: "pending" }
| { status: "ready"; document: MarkdownGreenfieldDocument };
type MarkdownDocumentEntry = {
key: string;
listeners: Set<() => void>;
started: boolean;
state: MarkdownDocumentState;
text: string;
};
type MarkdownDocumentWorkerRequest = {
id: number;
text: string;
type: "parse";
};
type MarkdownDocumentWorkerResponse =
| {
type: "ready";
}
| {
document: MarkdownGreenfieldDocument;
id: number;
ok: true;
type: "result";
}
| {
failure: "clone_failed" | "parse_failed";
id: number;
message: string;
ok: false;
type: "result";
};
const markdownDocumentEntries = new Map<string, MarkdownDocumentEntry>();
let nextMarkdownDocumentWorkerRequestId = 1;
export function useMarkdownGreenfieldDocument(text: string) {
const shouldLoadAsync =
text.length >= MARKDOWN_GREENFIELD_ASYNC_DOCUMENT_MIN_CHARS;
const syncDocument = React.useMemo(
() => (shouldLoadAsync ? null : createMarkdownGreenfieldDocument(text)),
[shouldLoadAsync, text],
);
const entry = React.useMemo(
() => (shouldLoadAsync ? getMarkdownDocumentEntry(text) : null),
[shouldLoadAsync, text],
);
const asyncState = React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => {
if (!entry) return () => {};
entry.listeners.add(onStoreChange);
startMarkdownDocumentEntry(entry);
return () => {
entry.listeners.delete(onStoreChange);
};
},
[entry],
),
React.useCallback(() => entry?.state ?? null, [entry]),
() => null,
);
if (syncDocument) return syncDocument;
if (!asyncState || asyncState.status === "pending") return null;
if (asyncState.status === "failed") throw asyncState.error;
return asyncState.document;
}
function getMarkdownDocumentEntry(text: string) {
const key = markdownGreenfieldDocumentTextKey(text);
const existing = markdownDocumentEntries.get(key);
if (existing?.text === text) return existing;
const entry: MarkdownDocumentEntry = {
key,
listeners: new Set(),
started: false,
state: { status: "pending" },
text,
};
markdownDocumentEntries.set(key, entry);
evictMarkdownDocumentEntries(key);
return entry;
}
function evictMarkdownDocumentEntries(currentKey: string) {
for (const [key, entry] of markdownDocumentEntries) {
if (
markdownDocumentEntries.size <=
MARKDOWN_GREENFIELD_DOCUMENT_ENTRY_CACHE_LIMIT
) {
return;
}
if (key === currentKey || entry.listeners.size > 0) continue;
markdownDocumentEntries.delete(key);
}
}
function startMarkdownDocumentEntry(entry: MarkdownDocumentEntry) {
if (entry.started || entry.state.status !== "pending") return;
entry.started = true;
if (typeof Worker !== "undefined" && isMarkdownDocumentWorkerEnabled()) {
try {
startMarkdownDocumentWorker(entry);
return;
} catch {
startMarkdownDocumentFallback(entry);
return;
}
}
startMarkdownDocumentFallback(entry);
}
function startMarkdownDocumentWorker(entry: MarkdownDocumentEntry) {
const worker = new Worker(
new URL("./markdown-greenfield-document.worker.ts", import.meta.url),
{ type: "module" },
);
const id = nextMarkdownDocumentWorkerRequestId++;
let isSettled = false;
const fallBackToMainThread = () => {
if (isSettled) return;
isSettled = true;
window.clearTimeout(readyTimeoutId);
if (!isCurrentMarkdownDocumentEntry(entry)) {
worker.terminate();
return;
}
worker.terminate();
startMarkdownDocumentFallback(entry);
};
const readyTimeoutId = window.setTimeout(() => {
fallBackToMainThread();
}, MARKDOWN_GREENFIELD_DOCUMENT_WORKER_READY_TIMEOUT_MS);
worker.onmessage = (event: MessageEvent<unknown>) => {
const message = markdownDocumentWorkerResponseFromValue(event.data);
if (!message) {
fallBackToMainThread();
return;
}
if (message.type === "ready") {
window.clearTimeout(readyTimeoutId);
try {
worker.postMessage({
id,
text: entry.text,
type: "parse",
} satisfies MarkdownDocumentWorkerRequest);
} catch {
fallBackToMainThread();
}
return;
}
if (message.id !== id || isSettled) {
return;
}
isSettled = true;
window.clearTimeout(readyTimeoutId);
worker.terminate();
if (!isCurrentMarkdownDocumentEntry(entry)) return;
if (message.ok) {
publishMarkdownDocumentState(entry, {
document: freezeMarkdownGreenfieldDocument(message.document),
status: "ready",
});
return;
}
if (message.failure === "clone_failed") {
startMarkdownDocumentFallback(entry);
return;
}
publishMarkdownDocumentState(entry, {
error: new Error(message.message),
status: "failed",
});
};
worker.onerror = fallBackToMainThread;
worker.onmessageerror = fallBackToMainThread;
}
function startMarkdownDocumentFallback(entry: MarkdownDocumentEntry) {
scheduleMarkdownDocumentTask(() => {
if (!isCurrentMarkdownDocumentEntry(entry)) return;
try {
publishMarkdownDocumentState(entry, {
document: createMarkdownGreenfieldDocument(entry.text),
status: "ready",
});
} catch (error) {
publishMarkdownDocumentState(entry, {
error:
error instanceof Error
? error
: new Error("Could not parse Markdown."),
status: "failed",
});
}
});
}
function scheduleMarkdownDocumentTask(callback: () => void) {
if (typeof window === "undefined") return;
const browserWindow = window as MarkdownIdleWindow;
const runWhenIdle = () => {
if (browserWindow.requestIdleCallback) {
browserWindow.requestIdleCallback(callback, { timeout: 120 });
return;
}
browserWindow.setTimeout(callback, 0);
};
if (browserWindow.requestAnimationFrame) {
browserWindow.requestAnimationFrame(() => {
browserWindow.setTimeout(runWhenIdle, 0);
});
return;
}
browserWindow.setTimeout(runWhenIdle, 0);
}
function publishMarkdownDocumentState(
entry: MarkdownDocumentEntry,
state: MarkdownDocumentState,
) {
if (!isCurrentMarkdownDocumentEntry(entry)) return;
entry.state = state;
for (const listener of entry.listeners) listener();
}
function isCurrentMarkdownDocumentEntry(entry: MarkdownDocumentEntry) {
const current = markdownDocumentEntries.get(entry.key);
return current === entry && current.text === entry.text;
}
function isMarkdownDocumentWorkerEnabled() {
if (typeof window === "undefined") return false;
const urlFlag = readMarkdownDocumentWorkerSearchFlag(window.location.search);
if (urlFlag != null) return urlFlag;
return readMarkdownDocumentWorkerStorageFlag() ?? true;
}
function readMarkdownDocumentWorkerSearchFlag(search: string) {
try {
const value = new URLSearchParams(search).get(
MARKDOWN_GREENFIELD_DOCUMENT_WORKER_SEARCH_PARAM,
);
return markdownDocumentWorkerFlagValue(value);
} catch {
return null;
}
}
function readMarkdownDocumentWorkerStorageFlag() {
try {
return markdownDocumentWorkerFlagValue(
window.localStorage.getItem(
MARKDOWN_GREENFIELD_DOCUMENT_WORKER_LOCAL_STORAGE_KEY,
),
);
} catch {
return null;
}
}
function markdownDocumentWorkerFlagValue(value: string | null) {
if (value == null) return null;
switch (value.trim().toLowerCase()) {
case "1":
case "true":
case "yes":
case "on":
case "worker":
return true;
case "0":
case "false":
case "no":
case "off":
case "main":
return false;
default:
return null;
}
}
function markdownDocumentWorkerResponseFromValue(
value: unknown,
): MarkdownDocumentWorkerResponse | null {
if (!value || typeof value !== "object") return null;
const response = value as Partial<MarkdownDocumentWorkerResponse>;
if (response.type === "ready") return { type: "ready" };
if (response.type !== "result" || typeof response.id !== "number") {
return null;
}
if (response.ok === true && response.document) {
return response as MarkdownDocumentWorkerResponse;
}
if (
response.ok === false &&
(response.failure === "clone_failed" ||
response.failure === "parse_failed") &&
typeof response.message === "string"
) {
return response as MarkdownDocumentWorkerResponse;
}
return null;
}
registry/new-york-v4/ui/markdown-greenfield-document.worker.ts
import { createMarkdownGreenfieldDocument } from "./markdown-greenfield-document";
type MarkdownDocumentWorkerRequest = {
id: number;
text: string;
type: "parse";
};
type MarkdownDocumentWorkerResponse =
| {
type: "ready";
}
| {
document: ReturnType<typeof createMarkdownGreenfieldDocument>;
id: number;
ok: true;
type: "result";
}
| {
failure: "clone_failed" | "parse_failed";
id: number;
message: string;
ok: false;
type: "result";
};
self.postMessage({ type: "ready" } satisfies MarkdownDocumentWorkerResponse);
self.onmessage = (event: MessageEvent<MarkdownDocumentWorkerRequest>) => {
const { id, text, type } = event.data;
if (type !== "parse") {
postMarkdownDocumentWorkerError({
failure: "parse_failed",
id,
message: "Markdown document worker received an invalid request.",
});
return;
}
let document: ReturnType<typeof createMarkdownGreenfieldDocument>;
try {
document = createMarkdownGreenfieldDocument(text);
} catch (error) {
postMarkdownDocumentWorkerError({
failure: "parse_failed",
id,
message:
error instanceof Error ? error.message : "Could not parse Markdown.",
});
return;
}
try {
assertStructuredCloneable(document);
self.postMessage({
document,
id,
ok: true,
type: "result",
} satisfies MarkdownDocumentWorkerResponse);
} catch (error) {
postMarkdownDocumentWorkerError({
failure: "clone_failed",
id,
message:
error instanceof Error
? error.message
: "Markdown document worker payload is not structured-clone safe.",
});
}
};
function assertStructuredCloneable(value: unknown) {
if (typeof structuredClone === "function") {
structuredClone(value);
return;
}
if (typeof MessageChannel === "function") {
const channel = new MessageChannel();
try {
channel.port2.postMessage(value);
} finally {
channel.port1.close();
channel.port2.close();
}
return;
}
throw new Error("Structured clone probe is unavailable.");
}
function postMarkdownDocumentWorkerError({
failure,
id,
message,
}: {
failure: "clone_failed" | "parse_failed";
id: number;
message: string;
}) {
self.postMessage({
failure,
id,
message,
ok: false,
type: "result",
} satisfies MarkdownDocumentWorkerResponse);
}
registry/new-york-v4/ui/markdown-greenfield-layout.ts
"use client";
import type {
MarkdownGreenfieldBlock,
MarkdownGreenfieldChunk,
MarkdownGreenfieldDocument,
} from "./markdown-greenfield-document";
// The chunk reading column's max inline size (the old max-w-4xl, 56rem at
// the 16px root). Shared with the surface motion resolver: the align
// translate that hides the close-leg recenter must agree with the markup
// about where the column's margin clamps.
export const MARKDOWN_GREENFIELD_CHUNK_MAX_INLINE_SIZE = 896;
const CHUNK_PADDING_X = 32;
const DOCUMENT_PADDING_TOP = CHUNK_PADDING_X;
const DOCUMENT_PADDING_BOTTOM = CHUNK_PADDING_X;
const MIN_CHUNK_HEIGHT = 64;
// Tuned to the rendered typography (15.5px body at leading-relaxed ≈ 25px,
// 14px mono code at leading-5 = 20px) so estimates track measured heights and
// scroll correction stays minimal.
const BODY_LINE_HEIGHT = 25;
const MONO_LINE_HEIGHT = 20;
const APPROX_BODY_CHAR_WIDTH = 7.9;
const APPROX_MONO_CHAR_WIDTH = 7.8;
export const MARKDOWN_GREENFIELD_LAYOUT_POLICY_VERSION =
"greenfield-layout-rich-blocks-v7";
const MARKDOWN_TABLE_VIRTUALIZATION_ROW_THRESHOLD = 80;
const MARKDOWN_TABLE_VIRTUALIZED_HEIGHT = 640;
const LAYOUT_CACHE_LIMIT = 64;
const ESTIMATED_HEIGHT_CACHE_LIMIT = 12;
const markdownGreenfieldLayoutDocumentIds = new WeakMap<
MarkdownGreenfieldDocument,
number
>();
const markdownGreenfieldLayoutCache = new Map<
string,
{
document: MarkdownGreenfieldDocument;
frame: MarkdownGreenfieldFrame;
}
>();
const markdownGreenfieldEstimatedHeightCaches = new WeakMap<
MarkdownGreenfieldDocument,
Map<string, readonly number[]>
>();
let nextMarkdownGreenfieldLayoutDocumentId = 1;
export type MarkdownGreenfieldFrame = {
chunks: MarkdownGreenfieldChunkFrame[];
totalHeight: number;
width: number;
};
export type MarkdownGreenfieldChunkFrame = {
bottom: number;
height: number;
id: string;
index: number;
measuredHeight: number | null;
sourceEndLine: number;
sourceStartLine: number;
top: number;
};
export type MarkdownGreenfieldMeasuredHeights = {
/** Compact revision key for the current measured-height snapshot. */
cacheKey?: string;
get(
chunk: MarkdownGreenfieldChunk,
context: MarkdownGreenfieldMeasurementContext,
): number | undefined;
};
export type MarkdownGreenfieldMeasurementContext = {
fontScale: number;
policyVersion: string;
width: number;
};
export function layoutMarkdownGreenfieldDocument({
contentWidth,
document,
fontScale,
measuredHeights,
}: {
contentWidth: number;
document: MarkdownGreenfieldDocument;
fontScale: number;
measuredHeights?: MarkdownGreenfieldMeasuredHeights;
}): MarkdownGreenfieldFrame {
const width = Math.max(1, contentWidth);
const context = {
fontScale,
policyVersion: MARKDOWN_GREENFIELD_LAYOUT_POLICY_VERSION,
width,
};
const measuredHeightsCacheKey = measuredHeights?.cacheKey ?? null;
const shouldCacheFrame = !measuredHeights || measuredHeightsCacheKey != null;
const cacheKey = markdownGreenfieldLayoutCacheKey({
context,
document,
measuredHeightsCacheKey,
});
if (shouldCacheFrame) {
const cached = markdownGreenfieldLayoutCache.get(cacheKey);
if (cached?.document === document) {
markdownGreenfieldLayoutCache.delete(cacheKey);
markdownGreenfieldLayoutCache.set(cacheKey, cached);
return cached.frame;
}
}
const estimatedHeights = readMarkdownGreenfieldEstimatedHeights({
context,
document,
});
const chunks: MarkdownGreenfieldChunkFrame[] = [];
let y = DOCUMENT_PADDING_TOP;
for (const [chunkIndex, chunk] of document.chunks.entries()) {
const estimatedHeight = estimatedHeights[chunkIndex] ?? MIN_CHUNK_HEIGHT;
const measuredHeight = readMarkdownGreenfieldMeasuredHeight({
chunk,
context,
measuredHeights,
});
const height = Math.max(
MIN_CHUNK_HEIGHT,
measuredHeight == null ? estimatedHeight : measuredHeight,
);
chunks.push({
bottom: y + height,
height,
id: chunk.id,
index: chunk.index,
measuredHeight,
sourceEndLine: chunk.sourceEndLine,
sourceStartLine: chunk.sourceStartLine,
top: y,
});
y += height;
}
const frame = freezeMarkdownGreenfieldFrame({
chunks,
totalHeight: chunks.length ? y + DOCUMENT_PADDING_BOTTOM : 0,
width,
});
if (shouldCacheFrame) {
markdownGreenfieldLayoutCache.set(cacheKey, { document, frame });
while (markdownGreenfieldLayoutCache.size > LAYOUT_CACHE_LIMIT) {
const oldestKey = markdownGreenfieldLayoutCache.keys().next().value;
if (!oldestKey) break;
markdownGreenfieldLayoutCache.delete(oldestKey);
}
}
return frame;
}
function readMarkdownGreenfieldEstimatedHeights({
context,
document,
}: {
context: MarkdownGreenfieldMeasurementContext;
document: MarkdownGreenfieldDocument;
}) {
const cacheKey = markdownGreenfieldEstimateCacheKey(context);
const cache = markdownGreenfieldEstimateCacheForDocument(document);
const cached = cache.get(cacheKey);
if (cached) {
cache.delete(cacheKey);
cache.set(cacheKey, cached);
return cached;
}
const blocksById = new Map(document.blocks.map((block) => [block.id, block]));
const estimatedHeights = Object.freeze(
document.chunks.map((chunk) =>
estimateMarkdownGreenfieldChunkHeight({
blocks: chunk.blockIds
.map((blockId) => blocksById.get(blockId))
.filter((block): block is MarkdownGreenfieldBlock => Boolean(block)),
fontScale: context.fontScale,
width: context.width,
}),
),
);
cache.set(cacheKey, estimatedHeights);
while (cache.size > ESTIMATED_HEIGHT_CACHE_LIMIT) {
const oldestKey = cache.keys().next().value;
if (!oldestKey) break;
cache.delete(oldestKey);
}
return estimatedHeights;
}
function estimateMarkdownGreenfieldChunkHeight({
blocks,
fontScale,
width,
}: {
blocks: readonly MarkdownGreenfieldBlock[];
fontScale: number;
width: number;
}) {
const textWidth = Math.max(1, width - CHUNK_PADDING_X * 2);
const height = blocks.reduce(
(sum, block) =>
sum +
estimateMarkdownGreenfieldBlockHeight({
block,
fontScale,
textWidth,
}),
0,
);
return Math.max(MIN_CHUNK_HEIGHT, height);
}
function estimateMarkdownGreenfieldBlockHeight({
block,
fontScale,
textWidth,
}: {
block: MarkdownGreenfieldBlock;
fontScale: number;
textWidth: number;
}) {
if (block.kind === "thematicBreak") return 36 * fontScale;
if (block.kind === "frontmatter") {
const lines = Math.max(1, block.sourceLineCount);
return (72 + lines * MONO_LINE_HEIGHT) * fontScale;
}
if (block.kind === "footnotes")
return estimateTextBlock(block, textWidth, fontScale, 0.9);
if (block.kind === "heading") {
return estimateTextBlock(block, textWidth, fontScale, 1.3) + 18 * fontScale;
}
if (block.kind === "diagram") return 360 * fontScale;
if (block.kind === "math") return 120 * fontScale;
if (block.kind === "component") return 160 * fontScale;
if (block.kind === "code") {
const lines = Math.max(1, block.sourceLineCount);
return 48 * fontScale + lines * MONO_LINE_HEIGHT * fontScale;
}
if (block.kind === "table") {
const rows = Math.max(2, block.sourceLineCount);
if (rows >= MARKDOWN_TABLE_VIRTUALIZATION_ROW_THRESHOLD) {
return MARKDOWN_TABLE_VIRTUALIZED_HEIGHT * fontScale;
}
return (48 + rows * 36) * fontScale;
}
if (block.kind === "image") return 280 * fontScale;
if (block.isHostile) return 420 * fontScale;
return estimateTextBlock(block, textWidth, fontScale, 1);
}
function estimateTextBlock(
block: MarkdownGreenfieldBlock,
textWidth: number,
fontScale: number,
multiplier: number,
) {
const charWidth =
block.kind === "code" ? APPROX_MONO_CHAR_WIDTH : APPROX_BODY_CHAR_WIDTH;
const columns = Math.max(12, Math.floor(textWidth / (charWidth * fontScale)));
const visualLines = block.sourceLineLengths.reduce(
(sum, lineLength) => sum + Math.max(1, Math.ceil(lineLength / columns)),
0,
);
return (
16 * fontScale + visualLines * BODY_LINE_HEIGHT * fontScale * multiplier
);
}
function readMarkdownGreenfieldMeasuredHeight({
chunk,
context,
measuredHeights,
}: {
chunk: MarkdownGreenfieldChunk;
context: MarkdownGreenfieldMeasurementContext;
measuredHeights?: MarkdownGreenfieldMeasuredHeights;
}) {
if (!measuredHeights) return null;
const measuredHeight = measuredHeights.get(chunk, context);
if (
typeof measuredHeight === "number" &&
Number.isFinite(measuredHeight) &&
measuredHeight > 0
) {
return measuredHeight;
}
return null;
}
function markdownGreenfieldLayoutCacheKey({
context,
document,
measuredHeightsCacheKey,
}: {
context: MarkdownGreenfieldMeasurementContext;
document: MarkdownGreenfieldDocument;
measuredHeightsCacheKey: string | null;
}) {
return [
markdownGreenfieldLayoutDocumentId(document),
Math.round(context.width * 100) / 100,
context.fontScale.toFixed(4),
context.policyVersion,
measuredHeightsCacheKey ?? "unmeasured",
].join(":");
}
function markdownGreenfieldEstimateCacheKey(
context: MarkdownGreenfieldMeasurementContext,
) {
return [
Math.round(context.width * 100) / 100,
context.fontScale.toFixed(4),
context.policyVersion,
].join(":");
}
function markdownGreenfieldEstimateCacheForDocument(
document: MarkdownGreenfieldDocument,
) {
let cache = markdownGreenfieldEstimatedHeightCaches.get(document);
if (cache) return cache;
cache = new Map();
markdownGreenfieldEstimatedHeightCaches.set(document, cache);
return cache;
}
function markdownGreenfieldLayoutDocumentId(
document: MarkdownGreenfieldDocument,
) {
const existing = markdownGreenfieldLayoutDocumentIds.get(document);
if (existing) return existing;
const next = nextMarkdownGreenfieldLayoutDocumentId++;
markdownGreenfieldLayoutDocumentIds.set(document, next);
return next;
}
function freezeMarkdownGreenfieldFrame(frame: MarkdownGreenfieldFrame) {
for (const chunk of frame.chunks) Object.freeze(chunk);
Object.freeze(frame.chunks);
return Object.freeze(frame);
}
registry/new-york-v4/ui/markdown-greenfield-renderer.tsx
"use client";
import * as React from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import {
BadgeAlert,
Check,
CircleAlert,
Copy,
ExternalLink,
Info,
Lightbulb,
Link2,
TriangleAlert,
} from "lucide-react";
import {
diffLineKind,
isSafeHighlightedCodeLine,
MARKDOWN_CODE_HIGHLIGHT_STYLES,
normalizeCodeLanguage,
renderCodeLine,
useMarkdownCodeLineHtml,
} from "./markdown-greenfield-code-highlight";
import { MarkdownGreenfieldDiagram } from "./markdown-greenfield-diagram";
import type { MarkdownGreenfieldChunk } from "./markdown-greenfield-document";
import type {
MarkdownHastElement,
MarkdownHastNode,
MarkdownHastRoot,
} from "./markdown-hast-types";
import {
sanitizeMarkdownImageUrl,
sanitizeMarkdownMediaUrl,
sanitizeMarkdownUrl,
} from "./markdown-url-policy";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
const MarkdownContentReadyContext = React.createContext<(() => void) | null>(
null,
);
// The rendered body size at 100% zoom. Every other size (headings, code,
// tables, footnotes) is authored in `em` relative to this, so scaling this one
// value with the zoom `fontScale` resizes the whole document as one system.
export const MARKDOWN_GREENFIELD_BASE_FONT_PX = 15.5;
// Tailwind v4's default spacing unit (0.25rem). Scaling it with the zoom
// fontScale keeps padding/margins proportional to the body text.
export const MARKDOWN_GREENFIELD_BASE_SPACING_REM = 0.25;
const MARKDOWN_CODE_VIRTUALIZATION_LINE_THRESHOLD = 100;
const MARKDOWN_CODE_VIRTUALIZED_OVERSCAN_LINES = 12;
const MARKDOWN_CODE_VIRTUALIZED_VIEWPORT_HEIGHT_PX = 512;
const MARKDOWN_CODE_VIRTUALIZED_LINE_HEIGHT_FALLBACK_PX = 24;
const MARKDOWN_TABLE_VIRTUALIZATION_ROW_THRESHOLD = 80;
const MARKDOWN_TABLE_VIRTUALIZATION_CELL_THRESHOLD = 600;
const MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX = 36;
const MARKDOWN_TABLE_VIRTUALIZED_VIEWPORT_HEIGHT_PX = 560;
const MARKDOWN_TABLE_VIRTUALIZED_OVERSCAN_ROWS = 8;
const MARKDOWN_TABLE_NATIVE_FIND_ROWS_PER_ENTRY = 8;
const MARKDOWN_RENDERED_CHUNK_CACHE_LIMIT = 96;
const markdownRenderedChunkCache = new Map<string, React.ReactNode>();
export const MarkdownGreenfieldChunkRenderer = React.memo(
function MarkdownGreenfieldChunkRenderer({
activeMatchOccurrence,
chunk,
fontScale = 1,
onContentReady,
searchQuery,
urlFragmentNavigation = true,
}: {
activeMatchOccurrence?: number;
chunk: MarkdownGreenfieldChunk;
fontScale?: number;
onContentReady?: () => void;
searchQuery?: string;
urlFragmentNavigation?: boolean;
}) {
const ref = React.useRef<HTMLDivElement | null>(null);
const notifyContentReady = React.useCallback(() => {
onContentReady?.();
}, [onContentReady]);
const renderedChildren = React.useMemo(
() =>
renderCachedHastChunkChildren(
chunk,
searchQuery,
activeMatchOccurrence,
{ urlFragmentNavigation },
),
[activeMatchOccurrence, chunk, searchQuery, urlFragmentNavigation],
);
useKeyedLayoutEffect(joinEffectKey([chunk.id, notifyContentReady]), () => {
notifyContentReady();
const element = ref.current;
if (!element || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(notifyContentReady);
observer.observe(element);
return () => observer.disconnect();
});
if (chunk.isHostile) {
return <MarkdownGreenfieldHostileChunk chunk={chunk} />;
}
return (
<MarkdownContentReadyContext.Provider value={notifyContentReady}>
<div
ref={ref}
className="markdown-greenfield-content text-foreground min-w-0 leading-relaxed"
data-slot="markdown-greenfield-content"
// Scale both the font and the spacing scale with zoom so vertical
// rhythm tracks the type size. Tailwind v4 spacing utilities resolve to
// calc(var(--spacing) * n), so overriding --spacing here scales every
// margin/padding/gap inside the document at once.
style={
{
"--spacing": `${(MARKDOWN_GREENFIELD_BASE_SPACING_REM * fontScale).toFixed(5)}rem`,
fontSize: `${MARKDOWN_GREENFIELD_BASE_FONT_PX * fontScale}px`,
} as React.CSSProperties
}
>
{renderedChildren}
</div>
</MarkdownContentReadyContext.Provider>
);
},
);
function renderCachedHastChunkChildren(
chunk: MarkdownGreenfieldChunk,
searchQuery?: string,
activeMatchOccurrence?: number,
options: { urlFragmentNavigation: boolean } = {
urlFragmentNavigation: true,
},
) {
const normalizedQuery = normalizeMarkdownSearchQuery(searchQuery);
const cacheKey = joinEffectKey([
"markdown-rendered-chunk",
chunk,
normalizedQuery,
normalizedQuery ? (activeMatchOccurrence ?? -1) : -1,
options.urlFragmentNavigation,
]);
const cached = readRenderedChunkCache(cacheKey);
if (cached !== undefined || markdownRenderedChunkCache.has(cacheKey)) {
return cached;
}
const rendered = renderHastChildrenUncached({
activeMatchOccurrence,
children: chunk.hastChildren,
normalizedQuery,
urlFragmentNavigation: options.urlFragmentNavigation,
});
writeRenderedChunkCache(cacheKey, rendered);
return rendered;
}
function renderHastChildren(
children: readonly MarkdownHastNode[],
searchQuery?: string,
activeMatchOccurrence?: number,
options: { urlFragmentNavigation: boolean } = {
urlFragmentNavigation: true,
},
) {
return renderHastChildrenUncached({
activeMatchOccurrence,
children,
normalizedQuery: normalizeMarkdownSearchQuery(searchQuery),
urlFragmentNavigation: options.urlFragmentNavigation,
});
}
function renderHastChildrenUncached({
activeMatchOccurrence,
children,
normalizedQuery,
urlFragmentNavigation,
}: {
activeMatchOccurrence?: number;
children: readonly MarkdownHastNode[];
normalizedQuery: string;
urlFragmentNavigation: boolean;
}) {
const root: MarkdownHastRoot = {
type: "root",
children: children.map(cloneHastNode),
};
if (!urlFragmentNavigation) {
suppressDomFragmentIds(root.children);
}
if (normalizedQuery) {
// The counter tracks rendered occurrences in document order so the one at
// activeMatchOccurrence (the chunk-local index of the toolbar's current
// match) can be marked active and styled distinctly from the rest.
highlightMarkdownSearchMatches(root.children, normalizedQuery, {
count: 0,
active: activeMatchOccurrence ?? -1,
});
}
return toJsxRuntime(root as never, {
Fragment,
components: markdownComponents,
ignoreInvalidStyle: true,
jsx,
jsxs,
passKeys: true,
passNode: true,
});
}
function readRenderedChunkCache(key: string) {
if (!markdownRenderedChunkCache.has(key)) return undefined;
const rendered = markdownRenderedChunkCache.get(key);
markdownRenderedChunkCache.delete(key);
markdownRenderedChunkCache.set(key, rendered);
return rendered;
}
function writeRenderedChunkCache(key: string, rendered: React.ReactNode) {
markdownRenderedChunkCache.set(key, rendered);
while (
markdownRenderedChunkCache.size > MARKDOWN_RENDERED_CHUNK_CACHE_LIMIT
) {
const oldestKey = markdownRenderedChunkCache.keys().next().value;
if (!oldestKey) break;
markdownRenderedChunkCache.delete(oldestKey);
}
}
function normalizeMarkdownSearchQuery(searchQuery: string | undefined) {
return searchQuery?.trim().toLowerCase() ?? "";
}
const markdownComponents = {
a: ({
children,
href,
node: _node,
rel: _rel,
target: _target,
...props
}: any) => {
const safeHref = sanitizeMarkdownUrl(href ?? "");
if (!safeHref) return <span>{children}</span>;
const text = reactNodeText(children);
const kind = linkKindForHref(safeHref);
const form = linkFormForHref({
href: safeHref,
text,
});
const external = kind === "external";
return (
<a
{...props}
className={[
"text-primary decoration-muted-foreground/50 visited:text-muted-foreground font-medium [overflow-wrap:anywhere] underline underline-offset-4 hover:decoration-current",
kind === "fragment" ? "decoration-dotted" : "",
form !== "inline" ? "font-mono" : "",
]
.filter(Boolean)
.join(" ")}
data-pretext-link-form={form}
data-pretext-link-kind={kind}
href={safeHref}
aria-label={footnoteLabelForLink({
href: safeHref,
label: props["aria-label"],
text,
})}
rel={external ? "noopener noreferrer" : undefined}
target={external ? "_blank" : undefined}
>
{children}
{external ? (
<ExternalLink className="ml-1 inline size-3" aria-hidden="true" />
) : null}
</a>
);
},
br: ({ node: _node, ...props }: any) => (
<br {...props} data-pretext-line-break="soft" />
),
abbr: ({ node: _node, ...props }: any) => (
<abbr
{...props}
className="cursor-help underline decoration-dotted"
data-pretext-raw-inline=""
/>
),
blockquote: ({ children, node, ...props }: any) => {
const alertKind = readDataProperty(node, "dataPretextAlertKind");
const alertTitle = readDataProperty(node, "dataPretextAlertTitle");
if (alertKind) {
const label = String(alertTitle || alertKind);
const Icon = alertIconForKind(alertKind);
const alertProps = withoutPretextAlertMetadata(props);
return (
<aside
{...alertProps}
aria-label={label}
className="bg-muted/35 my-5 rounded-md border px-4 py-3"
data-pretext-alert-kind={alertKind}
role="note"
>
<div
className="text-foreground mb-2 flex items-center gap-2 text-[0.9em] font-semibold"
data-pretext-alert-title=""
>
<Icon className="size-[1.15em]" aria-hidden="true" />
{label}
</div>
<div
className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
data-pretext-alert-body=""
>
{children}
</div>
</aside>
);
}
return (
<blockquote
{...props}
className="border-border text-muted-foreground my-4 border-l-2 pl-4 italic [&_blockquote]:my-3 [&_ol]:list-[lower-alpha] [&>ul]:my-2"
>
{children}
</blockquote>
);
},
code: ({ children, className, node: _node, ...props }: any) => (
<code
{...props}
className={[
"bg-muted rounded px-1 py-0.5 font-mono text-[0.88em]",
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</code>
),
caption: ({ node: _node, ...props }: any) => (
<caption
{...props}
className="text-muted-foreground caption-top px-3 py-2 text-left text-[0.85em] font-medium"
/>
),
del: ({ node: _node, ...props }: any) => (
<del
{...props}
className="text-muted-foreground decoration-muted-foreground/70 decoration-2"
data-pretext-strikethrough=""
/>
),
details: ({ node: _node, ...props }: any) => (
<details {...props} className="bg-muted/25 my-4 rounded-md border p-3" />
),
dl: ({ node: _node, ...props }: any) => (
<dl {...props} className="my-4 space-y-2" data-pretext-definition-list="" />
),
dt: ({ node: _node, ...props }: any) => (
<dt {...props} className="font-semibold" data-pretext-definition-term="" />
),
dd: ({ node: _node, ...props }: any) => (
<dd
{...props}
className="text-muted-foreground ml-4"
data-pretext-definition-description=""
/>
),
div: ({ children, node, ...props }: any) => {
const calloutKind = readTrustedDataProperty(node, "dataPretextCalloutKind");
if (calloutKind) {
const title =
readTrustedDataProperty(node, "dataPretextCalloutTitle") ||
calloutTitle(calloutKind);
return (
<aside
aria-label={title}
className="bg-muted/35 my-5 rounded-md border px-4 py-3"
data-pretext-callout-kind={calloutKind}
role="note"
>
<div className="mb-2 text-[0.9em] font-semibold">{title}</div>
<div className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{children}
</div>
</aside>
);
}
const componentName = readDataProperty(node, "dataPretextComponentName");
const trusted = isTrustedPretextComponentNode(node);
if (trusted && hasDataProperty(node, "dataPretextComponentFallback")) {
return (
<div
data-pretext-component-fallback=""
data-pretext-component-fallback-name={readDataProperty(
node,
"dataPretextComponentFallbackName",
)}
data-pretext-component-fallback-reason={readDataProperty(
node,
"dataPretextComponentFallbackReason",
)}
data-pretext-component-fallback-source={readDataProperty(
node,
"dataPretextComponentFallbackSource",
)}
>
{children}
</div>
);
}
if (trusted && componentName === "Metric") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
return (
<div
className="bg-muted/25 my-4 w-fit max-w-full min-w-0 rounded-md border px-4 py-3"
data-pretext-component="Metric"
>
<div className="text-muted-foreground text-[0.9em] [overflow-wrap:anywhere]">
{readOptionalString(componentProps.label)}
</div>
<div className="text-[1.55em] font-semibold [overflow-wrap:anywhere]">
{readOptionalString(componentProps.value)}
</div>
</div>
);
}
if (trusted && componentName === "Badge") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
return (
<span
className="bg-muted/35 inline-flex max-w-full items-center rounded-md border px-2 py-0.5 text-[0.9em] font-medium [overflow-wrap:anywhere]"
data-pretext-component="Badge"
>
{readOptionalString(componentProps.label)}
</span>
);
}
if (trusted && componentName === "Callout") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
const kind = readOptionalString(componentProps.kind) ?? "note";
const title =
readOptionalString(componentProps.title) ?? calloutTitle(kind);
return (
<aside
aria-label={title}
className="bg-muted/35 my-5 rounded-md border px-4 py-3"
data-pretext-callout-kind={kind}
data-pretext-component="Callout"
role="note"
>
<div className="mb-2 text-[0.9em] font-semibold">{title}</div>
<div className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{children}
</div>
</aside>
);
}
if (trusted && componentName === "Accordion") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
return (
<details
className="bg-muted/25 my-4 rounded-md border p-3"
data-pretext-component="Accordion"
>
<summary className="cursor-pointer font-medium">
{readOptionalString(componentProps.title)}
</summary>
<div className="mt-3 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{children}
</div>
</details>
);
}
if (trusted && componentName === "Tabs") {
return <MarkdownTabs node={readHastElement(node)} />;
}
if (trusted && componentName === "Image") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
return (
<MarkdownImageSurface
alt={readOptionalString(componentProps.alt) ?? ""}
componentName="Image"
height={readOptionalNumber(componentProps.height)}
src={readOptionalString(componentProps.src) ?? ""}
title={readOptionalString(componentProps.title)}
width={readOptionalNumber(componentProps.width)}
/>
);
}
if (trusted && componentName === "Video") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
return (
<MarkdownVideoSurface
controls={readOptionalBoolean(componentProps.controls) ?? true}
label={readOptionalString(componentProps.label) ?? "Video"}
loop={readOptionalBoolean(componentProps.loop) ?? false}
muted={readOptionalBoolean(componentProps.muted) ?? false}
src={readOptionalString(componentProps.src) ?? ""}
title={readOptionalString(componentProps.title)}
/>
);
}
if (trusted && componentName === "Diagram") {
const componentProps = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
if (
componentProps.type === "mermaid" &&
typeof componentProps.source === "string"
) {
return (
<MarkdownMeasuredDiagram
caption={readOptionalString(componentProps.caption)}
componentName="Diagram"
source={componentProps.source}
title={readOptionalString(componentProps.title)}
/>
);
}
}
return <div {...withoutInternalPretextMetadata(props)}>{children}</div>;
},
h1: headingComponent(
"h1",
"mt-6 mb-3 first:mt-0",
"text-[1.55em] leading-tight font-semibold tracking-tight",
),
h2: headingComponent(
"h2",
"mt-7 mb-3 first:mt-0",
"text-[1.3em] leading-snug font-semibold tracking-tight",
),
h3: headingComponent(
"h3",
"mt-5 mb-2 first:mt-0",
"text-[1.1em] leading-snug font-semibold",
),
h4: headingComponent(
"h4",
"mt-4 mb-2 first:mt-0",
"text-[1em] leading-snug font-semibold",
),
h5: headingComponent(
"h5",
"mt-4 mb-1.5 first:mt-0",
"text-[0.95em] leading-snug font-semibold",
),
h6: headingComponent(
"h6",
"mt-4 mb-1.5 first:mt-0",
"text-[0.9em] leading-snug font-semibold text-muted-foreground",
),
hr: ({ node: _node, ...props }: any) => (
<hr
{...props}
className="border-border my-10 border-0 border-t"
data-pretext-thematic-break=""
/>
),
kbd: ({ node: _node, ...props }: any) => (
<kbd
{...props}
className="bg-muted rounded border px-1.5 py-0.5 font-mono text-[0.85em]"
data-pretext-raw-inline=""
/>
),
mark: ({ node: _node, ...props }: any) => {
const isActiveMatch = "data-pretext-search-match-active" in props;
return (
<mark
{...props}
aria-current={isActiveMatch ? "true" : undefined}
className={[
"text-foreground rounded px-1",
isActiveMatch
? "bg-amber-400 ring-1 ring-amber-500/70 dark:bg-amber-500/70"
: "bg-yellow-200/70 dark:bg-yellow-400/30",
].join(" ")}
data-pretext-raw-inline=""
/>
);
},
img: ({ alt, height, node, src, title, width }: any) => {
if (!hasDataProperty(node, "dataPretextMarkdownImage")) return null;
return (
<MarkdownImageSurface
alt={alt ?? ""}
height={readOptionalNumber(height)}
src={src ?? ""}
title={title}
width={readOptionalNumber(width)}
/>
);
},
input: ({ checked, node: _node, type, ...props }: any) => {
if (type !== "checkbox") return null;
return (
<input
{...props}
aria-label={checked ? "Completed task" : "Incomplete task"}
aria-readonly="true"
checked={checked}
className="border-border accent-primary mr-2 size-3.5 rounded align-[-0.15em]"
data-pretext-task-checkbox={checked ? "checked" : "unchecked"}
disabled
readOnly
type="checkbox"
/>
);
},
li: ({ children, className, node, ...props }: any) => {
const isTask = hasDescendantElement(readHastElement(node), "input");
return (
<li
{...props}
className={[
"leading-relaxed",
isTask ? "list-none pl-0" : "",
"[&>p]:my-1",
className,
]
.filter(Boolean)
.join(" ")}
data-pretext-task-list-item={isTask ? "" : undefined}
>
{children}
</li>
);
},
ol: ({ className, node: _node, ...props }: any) => (
<ol
{...props}
className={[
"my-3 ml-5 list-decimal space-y-1 [&_ol]:list-[lower-alpha]",
className,
]
.filter(Boolean)
.join(" ")}
/>
),
p: ({ node, ...props }: any) => {
const element = readHastElement(node);
const onlyImage =
element?.children.filter((child) => !isWhitespaceText(child)).length ===
1 &&
readHastElement(
element.children.find((child) => !isWhitespaceText(child)),
)?.tagName === "img";
if (onlyImage) {
return (
<div
{...props}
className="my-3 min-w-0 leading-relaxed [overflow-wrap:anywhere]"
/>
);
}
return (
<p
{...props}
className="my-4 min-w-0 leading-7 [overflow-wrap:anywhere]"
/>
);
},
span: ({ className, node: _node, ...props }: any) => {
const classes = String(className ?? "");
if (classes.includes("katex-display")) {
return (
<span
{...props}
aria-label="Math block"
className={[classes, "block overflow-x-auto"]
.filter(Boolean)
.join(" ")}
data-pretext-math-block=""
role="region"
tabIndex={0}
onKeyDown={handleHorizontalScrollKeyDown}
/>
);
}
return (
<span
{...props}
className={classes || undefined}
data-pretext-math-inline={classes.includes("katex") ? "" : undefined}
/>
);
},
pre: ({ children, node, ...props }: any) => {
const code = readPreCodeElement(node);
if (
hasDataProperty(node, "dataMarkdownFrontmatterSource") ||
hasDataProperty(code, "dataMarkdownFrontmatterSource")
) {
return (
<pre
{...props}
className="bg-muted/25 my-4 overflow-x-auto rounded-md border p-4 font-mono text-[0.9em] leading-[1.7] whitespace-pre"
data-markdown-frontmatter-source=""
role="region"
tabIndex={0}
onKeyDown={handleHorizontalScrollKeyDown}
>
<code>{extractHastNodeText(code).replace(/\n$/, "")}</code>
</pre>
);
}
const language = normalizeCodeLanguage(readCodeLanguage(code));
if (language === "mermaid") {
const metadata = readCodeMetadata(code);
return (
<MarkdownMeasuredDiagram
caption={metadata.caption}
source={extractHastNodeText(code).replace(/\n$/, "")}
title={metadata.title}
/>
);
}
return (
<MarkdownCodeBlock
language={language ?? "text"}
metadata={readCodeMetadata(code)}
source={extractHastNodeText(code).replace(/\n$/, "")}
>
{children}
</MarkdownCodeBlock>
);
},
section: ({ children, className, node, ...props }: any) => {
const isFootnotes = hasDataProperty(node, "dataFootnotes");
return (
<section
{...props}
aria-label={isFootnotes ? "Footnotes" : props["aria-label"]}
className={[
isFootnotes
? "text-muted-foreground mt-10 border-t pt-5 text-[0.9em]"
: "my-5",
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</section>
);
},
strong: ({ node: _node, ...props }: any) => (
<strong {...props} className="font-semibold" />
),
summary: ({ node: _node, ...props }: any) => (
<summary {...props} className="cursor-pointer font-medium" />
),
q: ({ cite, node: _node, ...props }: any) => (
<q
{...props}
cite={sanitizeMarkdownUrl(cite ?? "") || undefined}
className="italic"
data-pretext-raw-inline=""
/>
),
ins: ({ cite, node: _node, ...props }: any) => (
<ins
{...props}
cite={sanitizeMarkdownUrl(cite ?? "") || undefined}
className="underline decoration-green-600/60"
data-pretext-raw-inline=""
/>
),
cite: ({ node: _node, ...props }: any) => (
<cite {...props} className="italic" data-pretext-raw-inline="" />
),
dfn: ({ node: _node, ...props }: any) => (
<dfn {...props} className="italic" data-pretext-raw-inline="" />
),
samp: ({ node: _node, ...props }: any) => (
<samp {...props} className="font-mono" data-pretext-raw-inline="" />
),
small: ({ node: _node, ...props }: any) => (
<small
{...props}
className="text-muted-foreground text-[0.85em]"
data-pretext-raw-inline=""
/>
),
sub: ({ node: _node, ...props }: any) => (
<sub {...props} className="align-sub" data-pretext-raw-inline="" />
),
sup: ({ node: _node, ...props }: any) => (
<sup {...props} className="align-super" data-pretext-raw-inline="" />
),
time: ({ node: _node, ...props }: any) => (
<time {...props} data-pretext-raw-inline="" />
),
var: ({ node: _node, ...props }: any) => (
<var {...props} className="font-mono italic" data-pretext-raw-inline="" />
),
table: MarkdownTable,
tbody: ({ node: _node, ...props }: any) => <tbody {...props} />,
td: ({ align, node, ...props }: any) => {
const resolvedAlign = align ?? readHastElement(node)?.properties?.align;
return (
<td
{...props}
align={typeof resolvedAlign === "string" ? resolvedAlign : undefined}
className="border-border border-t px-3 py-1.5 align-top [overflow-wrap:break-word] [&[align=center]]:text-center [&[align=right]]:text-right [&[align=right]]:tabular-nums"
/>
);
},
th: ({ align, node, ...props }: any) => {
const resolvedAlign = align ?? readHastElement(node)?.properties?.align;
return (
<th
{...props}
align={typeof resolvedAlign === "string" ? resolvedAlign : undefined}
className="border-border bg-muted/55 border-b px-3 py-1.5 text-left align-top font-medium [overflow-wrap:break-word] [&[align=center]]:text-center [&[align=right]]:text-right [&[align=right]]:tabular-nums"
scope="col"
/>
);
},
thead: ({ node: _node, ...props }: any) => <thead {...props} />,
tr: ({ node: _node, ...props }: any) => <tr {...props} />,
ul: ({ className, node: _node, ...props }: any) => (
<ul
{...props}
className={[
"my-3 ml-5 list-disc space-y-1 [&_ul]:list-[circle]",
className,
]
.filter(Boolean)
.join(" ")}
/>
),
};
function headingComponent(
Tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6",
blockClassName: string,
textClassName: string,
) {
return function Heading({ children, node, ...props }: any) {
const id =
typeof props.id === "string"
? props.id
: readDataProperty(node, "dataPretextFragmentId");
const text =
extractHastText(readHastElement(node)) || reactNodeText(children);
delete props.dataPretextFragmentId;
delete props["data-pretext-fragment-id"];
if (!id) {
return (
<Tag {...props} className={`${blockClassName} ${textClassName}`}>
{children}
</Tag>
);
}
return (
<div className={`group/heading relative ${blockClassName}`}>
<Tag {...props} className={textClassName}>
{children}
</Tag>
<HeadingAnchor id={id} text={text} />
</div>
);
};
}
// A GitHub-style anchor that appears in the left gutter on hover/focus and
// copies a deep link to the heading. Kept as a sibling of the heading (not a
// child) so it never leaks into the heading's accessible name.
function HeadingAnchor({ id, text }: { id: string; text: string }) {
const [copied, setCopied] = React.useState(false);
return (
<button
aria-label={`Copy link to ${text}`}
className="text-muted-foreground hover:text-foreground absolute top-1/2 -left-7 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded opacity-0 transition-opacity group-hover/heading:opacity-100 focus-visible:opacity-100"
type="button"
onClick={() => {
copyHeadingLink(id);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
}}
>
{copied ? (
<Check className="size-4" aria-hidden="true" />
) : (
<Link2 className="size-4" aria-hidden="true" />
)}
</button>
);
}
function MarkdownGreenfieldHostileChunk({
chunk,
}: {
chunk: MarkdownGreenfieldChunk;
}) {
const sourceLines = React.useMemo(
() => chunk.sourceText.split(/\r\n|[\n\r\u2028\u2029]/),
[chunk.sourceText],
);
const [scrollTop, setScrollTop] = React.useState(0);
const lineHeight = 24;
const viewportHeight = 576;
const start = Math.max(0, Math.floor(scrollTop / lineHeight) - 12);
const end = Math.min(
sourceLines.length,
Math.ceil((scrollTop + viewportHeight) / lineHeight) + 12,
);
const mountedLines = sourceLines.slice(start, end);
const omittedLines = Math.max(0, sourceLines.length - mountedLines.length);
return (
<section
aria-label="Large Markdown block"
className="bg-muted/25 text-muted-foreground overflow-hidden rounded-md border text-sm"
data-markdown-hostile-fallback=""
data-markdown-hostile-line-count={sourceLines.length}
data-markdown-hostile-mounted-lines={mountedLines.length}
data-markdown-hostile-omitted-lines={omittedLines}
data-markdown-hostile-virtualized=""
>
<div className="bg-muted/55 text-foreground flex items-center justify-between gap-3 border-b px-3 py-2 font-medium">
Large Markdown block
<button
aria-label="Copy large Markdown block source"
className="text-muted-foreground text-xs underline underline-offset-4"
type="button"
onClick={() => void navigator.clipboard?.writeText(chunk.sourceText)}
>
Copy
</button>
</div>
<pre
aria-label="Large Markdown source preview"
className="bg-background/60 max-h-[36rem] overflow-auto font-mono text-[13px] whitespace-pre"
data-markdown-hostile-preview=""
role="region"
tabIndex={0}
onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
>
<code
className="relative block min-w-max"
data-markdown-hostile-scroll-canvas=""
style={{ height: Math.max(sourceLines.length, 1) * lineHeight }}
>
{mountedLines.map((line, index) => {
const lineNumber = start + index + 1;
return (
<span
key={lineNumber}
className="absolute inset-x-0 grid grid-cols-[4rem_minmax(0,1fr)] px-4"
data-markdown-hostile-line={lineNumber}
style={{
height: lineHeight,
lineHeight: `${lineHeight}px`,
top: (lineNumber - 1) * lineHeight,
}}
>
<span
aria-hidden="true"
className="text-muted-foreground pr-4 text-right select-none"
>
{lineNumber}
</span>
<span>{line || " "}</span>
</span>
);
})}
</code>
</pre>
</section>
);
}
function MarkdownCodeBlock({
children: _children,
language,
metadata,
source,
}: {
children: React.ReactNode;
language: string;
metadata: ReturnType<typeof readCodeMetadata>;
source: string;
}) {
const [copyFailed, setCopyFailed] = React.useState(false);
const title = metadata.title || language;
const sourceLines = React.useMemo(() => source.split("\n"), [source]);
const lineNumberStart = metadata.lineNumberStart ?? 1;
const lineNumberMaxDigits = String(
lineNumberStart + Math.max(0, sourceLines.length - 1),
).length;
const copy = React.useCallback(
async (selectedOnly: boolean) => {
const selection = window.getSelection();
const selectedText =
selectedOnly && selection?.rangeCount
? selection.toString().trimEnd()
: "";
try {
await navigator.clipboard?.writeText(selectedText || source);
setCopyFailed(false);
} catch {
setCopyFailed(true);
}
},
[source],
);
return (
<figure
aria-label={`${title} code block`}
// text-[1em] keeps the code block on the document's scaling em cascade:
// a host stylesheet (e.g. the demo's rehype-pretty-code rule) may pin
// [data-rehype-pretty-code-figure] to a fixed font-size, which would stop
// the block from following the zoom control. A utility-layer size wins.
className="bg-muted/25 my-5 overflow-hidden rounded-md border text-[1em]"
data-pretext-code-language={language}
data-rehype-pretty-code-figure=""
role="group"
>
<figcaption className="bg-muted/55 flex items-center justify-between gap-3 border-b px-3 py-2 text-sm">
<span
className="text-muted-foreground font-mono text-xs font-medium"
data-pretext-code-title={metadata.title || undefined}
>
{title}
</span>
<span className="flex items-center gap-2">
{copyFailed ? (
<span aria-label="Copy failed" className="text-destructive text-xs">
Copy failed
</span>
) : null}
<button
aria-label="Copy code block"
className="text-muted-foreground text-xs underline underline-offset-4"
type="button"
onClick={() => void copy(false)}
>
Copy
</button>
<button
aria-label="Copy selected code or block"
className="sr-only"
type="button"
onClick={() => void copy(true)}
/>
</span>
</figcaption>
<MarkdownCodeSource
language={language}
lineNumberMaxDigits={lineNumberMaxDigits}
lineNumberStart={lineNumberStart}
metadata={metadata}
sourceLines={sourceLines}
/>
{metadata.caption ? (
<figcaption
className="text-muted-foreground border-t px-3 py-2 text-[0.9em]"
data-pretext-code-caption=""
>
{metadata.caption}
</figcaption>
) : null}
</figure>
);
}
type MarkdownCodeMetadata = ReturnType<typeof readCodeMetadata>;
const MarkdownCodeSource = React.memo(function MarkdownCodeSource({
language,
lineNumberMaxDigits,
lineNumberStart,
metadata,
sourceLines,
}: {
language: string;
lineNumberMaxDigits: number;
lineNumberStart: number;
metadata: MarkdownCodeMetadata;
sourceLines: readonly string[];
}) {
const isVirtualized =
sourceLines.length > MARKDOWN_CODE_VIRTUALIZATION_LINE_THRESHOLD;
const lineHtmlByIndex = useMarkdownCodeLineHtml({
end: isVirtualized ? 0 : sourceLines.length,
highlightPattern: metadata.highlightPattern,
language,
sourceLines,
start: 0,
});
if (isVirtualized) {
return (
<MarkdownVirtualizedCodeSource
language={language}
lineNumberMaxDigits={lineNumberMaxDigits}
lineNumberStart={lineNumberStart}
metadata={metadata}
sourceLines={sourceLines}
/>
);
}
return (
<>
<style data-pretext-code-highlight-styles="">
{MARKDOWN_CODE_HIGHLIGHT_STYLES}
</style>
<pre
aria-label={`${language} code source`}
className="overflow-x-auto p-3 [overflow-wrap:normal] [&_code]:min-w-max"
data-pretext-code-source=""
role="region"
tabIndex={0}
onKeyDown={handleHorizontalScrollKeyDown}
>
<code
{...markdownCodeElementProps({
language,
lineNumberMaxDigits,
lineNumberStart,
metadata,
})}
>
{sourceLines.map((line, index) => (
<MarkdownCodeLine
key={index}
index={index}
language={language}
line={line}
lineHtml={lineHtmlByIndex.get(index)}
lineNumberStart={lineNumberStart}
metadata={metadata}
/>
))}
</code>
</pre>
</>
);
});
function MarkdownVirtualizedCodeSource({
language,
lineNumberMaxDigits,
lineNumberStart,
metadata,
sourceLines,
}: {
language: string;
lineNumberMaxDigits: number;
lineNumberStart: number;
metadata: MarkdownCodeMetadata;
sourceLines: readonly string[];
}) {
const preRef = React.useRef<HTMLPreElement | null>(null);
const codeRef = React.useRef<HTMLElement | null>(null);
const [scrollTop, setScrollTop] = React.useState(0);
const [lineHeight, setLineHeight] = React.useState(
MARKDOWN_CODE_VIRTUALIZED_LINE_HEIGHT_FALLBACK_PX,
);
const [viewportHeight, setViewportHeight] = React.useState(
MARKDOWN_CODE_VIRTUALIZED_VIEWPORT_HEIGHT_PX,
);
const start = Math.max(
0,
Math.floor(scrollTop / lineHeight) -
MARKDOWN_CODE_VIRTUALIZED_OVERSCAN_LINES,
);
const end = Math.min(
sourceLines.length,
Math.ceil((scrollTop + viewportHeight) / lineHeight) +
MARKDOWN_CODE_VIRTUALIZED_OVERSCAN_LINES,
);
const mountedLines = sourceLines.slice(start, end);
const lineWidthCh = React.useMemo(
() => widestMarkdownCodeLineWidthCh(sourceLines),
[sourceLines],
);
const lineHtmlByIndex = useMarkdownCodeLineHtml({
end,
highlightPattern: metadata.highlightPattern,
language,
sourceLines,
start,
});
useKeyedLayoutEffect(joinEffectKey([language, sourceLines.length]), () => {
const updateMetrics = () => {
const measuredLineHeight = measureMarkdownCodeLineHeight(codeRef.current);
if (measuredLineHeight > 0) {
setLineHeight((current) =>
Math.abs(current - measuredLineHeight) > 0.5
? measuredLineHeight
: current,
);
}
const measuredViewportHeight = preRef.current?.clientHeight ?? 0;
if (measuredViewportHeight > 0) {
setViewportHeight((current) =>
Math.abs(current - measuredViewportHeight) > 0.5
? measuredViewportHeight
: current,
);
}
};
updateMetrics();
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(updateMetrics);
if (preRef.current) observer.observe(preRef.current);
if (codeRef.current) observer.observe(codeRef.current);
return () => observer.disconnect();
});
return (
<>
<style data-pretext-code-highlight-styles="">
{MARKDOWN_CODE_HIGHLIGHT_STYLES}
</style>
<pre
ref={preRef}
aria-label={`${language} code source`}
className="max-h-[32rem] overflow-auto overflow-x-auto p-3 [overflow-wrap:normal] [&_code]:min-w-max"
data-pretext-code-line-count={sourceLines.length}
data-pretext-code-mounted-lines={mountedLines.length}
data-pretext-code-source=""
data-pretext-code-virtualized=""
role="region"
tabIndex={0}
onKeyDown={handleHorizontalScrollKeyDown}
onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
>
<code
ref={codeRef}
{...markdownCodeElementProps({
language,
lineNumberMaxDigits,
lineNumberStart,
metadata,
start,
virtualized: true,
})}
style={markdownVirtualizedCodeCanvasStyle({
language,
lineNumberStart,
lineWidthCh,
metadata,
start,
totalLines: sourceLines.length,
})}
>
{mountedLines.map((line, offset) => {
const index = start + offset;
return (
<MarkdownCodeLine
key={index}
index={index}
language={language}
line={line}
lineHtml={lineHtmlByIndex.get(index)}
lineNumberStart={lineNumberStart}
metadata={metadata}
totalLines={sourceLines.length}
virtualized
/>
);
})}
</code>
</pre>
</>
);
}
function MarkdownCodeLine({
index,
language,
line,
lineHtml,
lineNumberStart,
metadata,
totalLines,
virtualized = false,
}: {
index: number;
language: string;
line: string;
lineHtml: string | undefined;
lineNumberStart: number;
metadata: MarkdownCodeMetadata;
totalLines?: number;
virtualized?: boolean;
}) {
const lineNumber = lineNumberStart + index;
const diffKind = diffLineKind(line);
return (
<span
aria-label={metadata.showLineNumbers ? `Line ${lineNumber}` : undefined}
aria-posinset={
metadata.showLineNumbers && virtualized ? index + 1 : undefined
}
aria-setsize={
metadata.showLineNumbers && virtualized ? totalLines : undefined
}
className={[
markdownCodeLineClassName(diffKind),
virtualized ? "absolute right-0 left-0" : "",
]
.filter(Boolean)
.join(" ")}
data-highlighted-line={
metadata.highlightedLines.has(index + 1) ? "" : undefined
}
data-line=""
data-pretext-code-diff-line={diffKind ?? undefined}
data-pretext-code-line-number={
metadata.showLineNumbers ? lineNumber : undefined
}
role={metadata.showLineNumbers ? "listitem" : undefined}
style={
virtualized
? {
height: "var(--markdown-code-line-height)",
top: `calc(${index} * var(--markdown-code-line-height))`,
}
: undefined
}
>
{renderCodeLine({
fallbackLanguage: language,
line,
lineHtml,
pattern: metadata.highlightPattern,
})}
</span>
);
}
function markdownCodeElementProps({
language,
lineNumberMaxDigits,
lineNumberStart,
metadata,
start = 0,
virtualized = false,
}: {
language: string;
lineNumberMaxDigits: number;
lineNumberStart: number;
metadata: MarkdownCodeMetadata;
start?: number;
virtualized?: boolean;
}) {
return {
"aria-label": metadata.showLineNumbers
? `${language} numbered code lines`
: undefined,
className: [
"block font-mono text-[0.9em] leading-[1.45]",
virtualized ? "relative" : "",
metadata.showLineNumbers
? "[counter-reset:line] before:content-[counter(line)]"
: "",
metadata.highlightedLines.size
? "[&>[data-highlighted-line]]:bg-primary/10"
: "",
metadata.highlightPattern
? "[&_[data-highlighted-chars]]:bg-primary/20 [&_[data-highlighted-chars]]:rounded"
: "",
]
.filter(Boolean)
.join(" "),
"data-language": language,
"data-line-numbers": metadata.showLineNumbers ? "" : undefined,
"data-line-numbers-max-digits": metadata.showLineNumbers
? lineNumberMaxDigits
: undefined,
"data-pretext-code-virtualized": virtualized ? "" : undefined,
role: metadata.showLineNumbers ? "list" : undefined,
style: metadata.showLineNumbers
? { counterSet: `line ${lineNumberStart + start - 1}` }
: undefined,
};
}
function markdownVirtualizedCodeCanvasStyle({
language,
lineNumberStart,
lineWidthCh,
metadata,
start,
totalLines,
}: {
language: string;
lineNumberStart: number;
lineWidthCh: number;
metadata: MarkdownCodeMetadata;
start: number;
totalLines: number;
}): React.CSSProperties {
return {
"--markdown-code-line-height": markdownCodeLineHeightCss(language),
counterSet: metadata.showLineNumbers
? `line ${lineNumberStart + start - 1}`
: undefined,
height: `calc(${Math.max(totalLines, 1)} * var(--markdown-code-line-height))`,
minWidth: markdownCodeMinWidth(lineWidthCh, metadata.showLineNumbers),
} as React.CSSProperties;
}
function markdownCodeLineClassName(diffKind: ReturnType<typeof diffLineKind>) {
return [
"block min-h-5 box-border whitespace-pre",
diffKind === "add" ? "bg-emerald-500/10" : "",
diffKind === "remove" ? "bg-red-500/10" : "",
]
.filter(Boolean)
.join(" ");
}
function markdownCodeLineHeightCss(language: string) {
return language === "text" || language === "plaintext"
? "0.95em"
: "calc(1.45em + var(--spacing) * 1)";
}
function markdownCodeMinWidth(lineWidthCh: number, showLineNumbers: boolean) {
const contentWidth = `${Math.max(1, lineWidthCh)}ch`;
return showLineNumbers
? `max(100%, calc(${contentWidth} + var(--spacing) * 22))`
: `max(100%, ${contentWidth})`;
}
function widestMarkdownCodeLineWidthCh(lines: readonly string[]) {
return lines.reduce(
(widest, line) => Math.max(widest, markdownCodeLineWidthCh(line)),
1,
);
}
function markdownCodeLineWidthCh(line: string) {
let width = 0;
for (const character of line) {
if (character === "\t") {
width += 4 - (width % 4);
} else {
width += 1;
}
}
return width;
}
function measureMarkdownCodeLineHeight(code: HTMLElement | null) {
if (!code) return 0;
const sample = document.createElement("span");
sample.className = markdownCodeLineClassName(null);
sample.dataset.line = "";
sample.style.position = "absolute";
sample.style.visibility = "hidden";
sample.textContent = " ";
code.appendChild(sample);
const height = sample.getBoundingClientRect().height || sample.offsetHeight;
sample.remove();
return height;
}
function MarkdownMeasuredDiagram({
caption,
componentName,
source,
title,
}: {
caption?: string;
componentName?: string;
source: string;
title?: string;
}) {
const notifyContentReady = React.useContext(MarkdownContentReadyContext);
return (
<MarkdownGreenfieldDiagram
caption={caption}
componentName={componentName}
onContentReady={notifyContentReady ?? undefined}
source={source}
title={title}
/>
);
}
function MarkdownImageSurface({
alt,
componentName,
height,
src,
title,
width,
}: {
alt: string;
componentName?: string;
height?: number;
src: string;
title?: string;
width?: number;
}) {
const notifyContentReady = React.useContext(MarkdownContentReadyContext);
const safeSrc = sanitizeMarkdownImageUrl(src);
const explicitAspectRatio =
width && height ? `${width} / ${height}` : undefined;
const [state, setState] = React.useState<
"blocked" | "failed" | "loading" | "ready"
>(safeSrc ? "loading" : "blocked");
const [aspectRatio, setAspectRatio] = React.useState(
() => explicitAspectRatio ?? "",
);
const captionId = React.useId();
// Reset load state when the source/aspect-ratio inputs change by adjusting
// state during render (React's prop-change pattern) instead of in an effect.
const sourceResetKey = `${safeSrc}::${explicitAspectRatio ?? ""}`;
const [prevSourceResetKey, setPrevSourceResetKey] =
React.useState(sourceResetKey);
if (sourceResetKey !== prevSourceResetKey) {
setPrevSourceResetKey(sourceResetKey);
setState(safeSrc ? "loading" : "blocked");
setAspectRatio(explicitAspectRatio ?? "");
}
useKeyedLayoutEffect(
joinEffectKey([aspectRatio, notifyContentReady, state]),
() => {
notifyContentReady?.();
},
);
return (
<figure
aria-label={
state === "failed" ? `Image failed: ${alt}` : alt || title || "Image"
}
className="my-5 w-fit max-w-full"
data-pretext-component={componentName}
data-pretext-image-height={height}
data-pretext-image-state={state}
data-pretext-image-src={safeSrc || undefined}
data-pretext-image-width={width}
role="group"
style={aspectRatio ? { aspectRatio } : undefined}
>
{safeSrc ? (
<div
className="bg-muted/25 relative flex min-h-48 max-w-full items-center justify-center overflow-hidden rounded-md border"
data-pretext-image-frame=""
style={aspectRatio ? { aspectRatio } : undefined}
>
{state === "loading" ? (
<span className="text-muted-foreground absolute inset-x-4 top-1/2 -translate-y-1/2 text-center text-sm">
Loading image
</span>
) : null}
{state === "failed" ? (
<div
className="text-muted-foreground absolute inset-0 flex items-center justify-center px-4 text-center text-sm"
role="alert"
>
Could not load image{alt ? `: ${alt}` : ""}
</div>
) : null}
{state === "failed" ? (
<div
aria-label={alt || "Image"}
className="text-muted-foreground text-sm"
data-pretext-image-state="failed"
role="img"
>
Image failed to load: {alt}
<button
className="ml-2 text-xs underline underline-offset-4"
type="button"
onClick={() => setState("loading")}
>
Retry image
</button>
</div>
) : (
<img
alt={alt}
aria-describedby={title ? captionId : undefined}
className={[
"block max-h-[70vh] max-w-full object-contain transition-opacity",
state === "loading" ? "opacity-0" : "",
]
.filter(Boolean)
.join(" ")}
decoding="async"
loading="lazy"
src={safeSrc}
title={title}
onError={(event) => {
event.currentTarget.setAttribute(
"data-pretext-image-state",
"failed",
);
setState("failed");
}}
onLoad={(event) => {
const image = event.currentTarget;
if (image.naturalWidth && image.naturalHeight) {
setAspectRatio(
`${image.naturalWidth} / ${image.naturalHeight}`,
);
}
setState("ready");
}}
/>
)}
</div>
) : (
<span
aria-label={alt || "Blocked image"}
className="bg-muted/35 text-muted-foreground flex min-h-24 items-center rounded-md border border-dashed px-4 text-sm"
role="img"
>
{alt || "Blocked image"}
</span>
)}
{title ? (
<figcaption
id={captionId}
className="text-muted-foreground mt-2 text-[0.9em]"
data-pretext-image-caption=""
>
{title}
</figcaption>
) : null}
</figure>
);
}
function MarkdownVideoSurface({
controls = true,
label,
loop = false,
muted = false,
src,
title,
}: {
controls?: boolean;
label: string;
loop?: boolean;
muted?: boolean;
src: string;
title?: string;
}) {
const notifyContentReady = React.useContext(MarkdownContentReadyContext);
const safeSrc = sanitizeMarkdownMediaUrl(src);
const [failed, setFailed] = React.useState(false);
useKeyedLayoutEffect(
joinEffectKey([failed, notifyContentReady, safeSrc]),
() => {
notifyContentReady?.();
},
);
if (!safeSrc) {
return (
<div
aria-label={`Video blocked: ${label}`}
className="bg-muted/35 text-muted-foreground my-5 rounded-md border border-dashed p-4 text-sm"
data-pretext-component="Video"
data-pretext-video-state="blocked"
role="group"
>
{label}
</div>
);
}
return (
<figure
aria-label={failed ? `Video failed to load: ${label}` : label}
className="my-5 max-w-full"
data-pretext-component="Video"
data-pretext-video-state={failed ? "failed" : "ready"}
role="group"
>
<video
className="bg-muted block max-h-[70vh] max-w-full rounded-md border"
controls={controls}
loop={loop}
muted={muted}
preload="metadata"
src={safeSrc}
title={title}
onError={() => setFailed(true)}
/>
{title ? (
<figcaption className="text-muted-foreground mt-2 text-[0.9em]">
{title}
</figcaption>
) : null}
</figure>
);
}
function MarkdownTabs({ node }: { node: MarkdownHastElement | null }) {
const props = readComponentProps(
readDataProperty(node, "dataPretextComponentProps"),
);
const tabs = (node?.children ?? [])
.map(readHastElement)
.filter((child): child is MarkdownHastElement => {
return (
child?.tagName === "div" &&
readDataProperty(child, "dataPretextComponentName") === "Tab"
);
});
const [selected, setSelected] = React.useState(0);
const baseId = React.useId();
const label = readOptionalString(props.label) ?? "Tabs";
const select = (index: number) =>
setSelected((index + tabs.length) % tabs.length);
return (
<div className="my-5" data-pretext-component="Tabs">
<div aria-label={label} className="flex gap-1 border-b" role="tablist">
{tabs.map((tab, index) => {
const tabProps = readComponentProps(
readDataProperty(tab, "dataPretextComponentProps"),
);
const title =
readOptionalString(tabProps.title) ?? `Tab ${index + 1}`;
const active = selected === index;
return (
<button
key={index}
aria-controls={`${baseId}-panel-${index}`}
aria-selected={active}
className="px-3 py-2 text-sm font-medium"
id={`${baseId}-tab-${index}`}
role="tab"
tabIndex={active ? 0 : -1}
type="button"
onClick={() => select(index)}
onKeyDown={(event) => {
if (event.key === "ArrowRight") {
event.preventDefault();
const nextIndex = (index + 1) % tabs.length;
select(nextIndex);
document
.getElementById(`${baseId}-tab-${nextIndex}`)
?.focus();
} else if (event.key === "End") {
event.preventDefault();
select(tabs.length - 1);
document
.getElementById(`${baseId}-tab-${tabs.length - 1}`)
?.focus();
} else if (event.key === "Home") {
event.preventDefault();
select(0);
document.getElementById(`${baseId}-tab-0`)?.focus();
}
}}
>
{title}
</button>
);
})}
</div>
{tabs.map((tab, index) => (
<div
key={index}
aria-labelledby={`${baseId}-tab-${index}`}
hidden={selected !== index}
id={`${baseId}-panel-${index}`}
className="pt-3"
role="tabpanel"
>
{renderHastChildren(tab.children)}
</div>
))}
</div>
);
}
// Tags whose text is verbatim source (code) or already a highlight; wrapping a
// match inside them would corrupt the rendered output, so they are skipped.
const MARKDOWN_SEARCH_SKIP_TAGS = new Set([
"code",
"pre",
"mark",
"script",
"style",
"textarea",
]);
// Browser-find-style highlighting for the in-app search: wraps every case-
// insensitive occurrence of the active query in a <mark> so matches are visible
// where the search navigates. Mutates the freshly cloned chunk tree in place,
// matching the same trimmed-substring semantics as the toolbar match count.
type MarkdownSearchHighlightContext = { active: number; count: number };
function highlightMarkdownSearchMatches(
nodes: MarkdownHastNode[],
lowerQuery: string,
context: MarkdownSearchHighlightContext,
) {
for (let index = 0; index < nodes.length; index += 1) {
const node = nodes[index];
if (node.type === "text" && typeof node.value === "string") {
const replacement = splitMarkdownTextForSearch(
node.value,
lowerQuery,
context,
);
if (replacement) {
nodes.splice(index, 1, ...replacement);
index += replacement.length - 1;
}
continue;
}
if (node.type === "element" && Array.isArray(node.children)) {
const tagName = (node as MarkdownHastElement).tagName.toLowerCase();
if (MARKDOWN_SEARCH_SKIP_TAGS.has(tagName)) continue;
highlightMarkdownSearchMatches(node.children, lowerQuery, context);
}
}
}
function splitMarkdownTextForSearch(
value: string,
lowerQuery: string,
context: MarkdownSearchHighlightContext,
): MarkdownHastNode[] | null {
const lowerValue = value.toLowerCase();
let matchStart = lowerValue.indexOf(lowerQuery);
if (matchStart === -1) return null;
const out: MarkdownHastNode[] = [];
let cursor = 0;
while (matchStart !== -1) {
if (matchStart > cursor) {
out.push({ type: "text", value: value.slice(cursor, matchStart) });
}
const matchEnd = matchStart + lowerQuery.length;
const isActive = context.count === context.active;
context.count += 1;
out.push({
type: "element",
tagName: "mark",
properties: isActive
? { dataPretextSearchMatch: "", dataPretextSearchMatchActive: "" }
: { dataPretextSearchMatch: "" },
children: [{ type: "text", value: value.slice(matchStart, matchEnd) }],
});
cursor = matchEnd;
matchStart = lowerValue.indexOf(lowerQuery, cursor);
}
if (cursor < value.length) {
out.push({ type: "text", value: value.slice(cursor) });
}
return out;
}
function cloneHastNode<T extends MarkdownHastNode>(node: T): T {
if (!("children" in node) || !Array.isArray(node.children)) {
return { ...node };
}
return {
...node,
children: node.children.map((child) =>
cloneHastNode(child as MarkdownHastNode),
),
properties: readHastElement(node)?.properties
? { ...readHastElement(node)!.properties }
: undefined,
} as T;
}
function suppressDomFragmentIds(nodes: MarkdownHastNode[]) {
for (const node of nodes) {
const element = readHastElement(node);
if (!element) continue;
const id = element.properties?.id;
if (
typeof id === "string" &&
id &&
shouldSuppressDomFragmentId(element, id)
) {
element.properties = {
...element.properties,
dataPretextFragmentId: id,
};
delete element.properties.id;
}
suppressDomFragmentIds(element.children);
}
}
function shouldSuppressDomFragmentId(element: MarkdownHastElement, id: string) {
return /^h[1-6]$/.test(element.tagName) || /^user-content-fn/.test(id);
}
function readDataProperty(node: unknown, property: string) {
const element = readHastElement(node);
const value =
element?.properties?.[property] ?? element?.properties?.[toKebab(property)];
return typeof value === "string" ? value : "";
}
function readStringProperty(value: unknown) {
return typeof value === "string" ? value : null;
}
function readTrustedDataProperty(node: unknown, property: string) {
if (!isTrustedPretextComponentNode(node)) return "";
return readDataProperty(node, property);
}
function isTrustedPretextComponentNode(node: unknown) {
return readHastElement(node)?.properties?.pretextComponentTrusted === true;
}
function hasDataProperty(node: unknown, property: string) {
const element = readHastElement(node);
return (
element?.properties != null &&
(Object.hasOwn(element.properties, property) ||
Object.hasOwn(element.properties, toKebab(property)))
);
}
function withoutPretextAlertMetadata(props: Record<string, unknown>) {
const next = { ...props };
delete next.dataPretextAlertKind;
delete next.dataPretextAlertTitle;
delete next["data-pretext-alert-kind"];
delete next["data-pretext-alert-title"];
return next;
}
function withoutInternalPretextMetadata(props: Record<string, unknown>) {
const next = { ...props };
for (const key of Object.keys(next)) {
if (/^dataPretext(?:Component|Callout|Heading)/.test(key)) {
delete next[key];
}
if (/^data-pretext-(?:component|callout|heading)/.test(key)) {
delete next[key];
}
if (key === "pretextComponentTrusted") {
delete next[key];
}
}
return next;
}
function readHastElement(node: unknown): MarkdownHastElement | null {
return node &&
typeof node === "object" &&
(node as MarkdownHastElement).type === "element"
? (node as MarkdownHastElement)
: null;
}
function readPreCodeElement(node: unknown): MarkdownHastElement | null {
const pre = readHastElement(node);
if (pre?.tagName !== "pre") return null;
const code =
pre.children
.map(readHastElement)
.find((child): child is MarkdownHastElement =>
Boolean(child && child.tagName === "code"),
) ?? null;
return code;
}
function readCodeLanguage(code: MarkdownHastElement | null) {
const className = code?.properties?.className;
const classes = Array.isArray(className) ? className : [className];
const languageClass = classes.find(
(value): value is string =>
typeof value === "string" && value.startsWith("language-"),
);
const language = languageClass?.slice("language-".length).toLowerCase();
if (language === "mmd" || language === "mermaid-js") return "mermaid";
return language ?? null;
}
function readCodeMetadata(code: MarkdownHastElement | null) {
return parseCodeMetadata(readDataProperty(code, "dataPretextCodeMeta"));
}
function parseCodeMetadata(meta: string) {
const result: {
caption?: string;
highlightedLines: Set<number>;
highlightPattern: string;
lineNumberStart?: number;
showLineNumbers: boolean;
title?: string;
} = {
highlightedLines: new Set<number>(),
highlightPattern: "",
showLineNumbers: false,
};
for (const match of meta.matchAll(
/(?:^|\s)(title|caption)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/g,
)) {
const key = match[1] as "caption" | "title";
result[key] = match[2] ?? match[3] ?? match[4] ?? "";
}
const lineNumbers = /(?:^|\s)showLineNumbers(?:\{(\d+)\})?(?=\s|$)/i.exec(
meta,
);
if (lineNumbers) {
result.showLineNumbers = true;
result.lineNumberStart = lineNumbers[1] ? Number(lineNumbers[1]) : 1;
}
for (const match of meta.matchAll(/\{(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)\}/g)) {
for (const value of match[1]!.split(",")) {
addHighlightedCodeLineSpec(result.highlightedLines, value);
}
}
const highlightPattern = /\/([^/\n]+)\//.exec(meta);
result.highlightPattern = highlightPattern?.[1] ?? "";
return result;
}
function addHighlightedCodeLineSpec(lines: Set<number>, spec: string) {
const range = /^(\d+)-(\d+)$/.exec(spec);
if (range) {
const start = Number(range[1]);
const end = Number(range[2]);
if (!isSafeHighlightedCodeLine(start) || !isSafeHighlightedCodeLine(end)) {
return;
}
if (end < start || end - start > 500) return;
for (let line = start; line <= end; line += 1) lines.add(line);
return;
}
const line = Number(spec);
if (isSafeHighlightedCodeLine(line)) lines.add(line);
}
function toKebab(value: string) {
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
}
function alertIconForKind(kind: string) {
switch (kind) {
case "note":
return Info;
case "tip":
return Lightbulb;
case "warning":
return TriangleAlert;
case "caution":
return CircleAlert;
default:
return BadgeAlert;
}
}
function calloutTitle(kind: string) {
const titles: Record<string, string> = {
caution: "Caution",
important: "Important",
note: "Note",
tip: "Tip",
warning: "Warning",
};
return titles[kind] ?? "Note";
}
function linkKindForHref(href: string) {
if (href.startsWith("#")) return "fragment";
if (href.startsWith("/")) return "root";
if (/^mailto:/i.test(href)) return "email";
if (/^(?:https?:)?\/\//i.test(href)) return "external";
return "relative";
}
function linkFormForHref({ href, text }: { href: string; text: string }) {
if (/^mailto:/i.test(href) && href.slice("mailto:".length) === text) {
return "email-autolink";
}
if (
/^https?:\/\/www\./i.test(href) &&
href.replace(/^https?:\/\//i, "") === text
) {
return "autolink";
}
if (href === text && /^(?:https?:)?\/\//i.test(href)) return "autolink";
return "inline";
}
function footnoteLabelForLink({
href,
label,
text,
}: {
href: string;
label: unknown;
text: string;
}) {
const display = footnoteDisplayForHref(href, text);
if (/^#(?:user-content-)?fnref-/i.test(href)) {
return "Back to footnote reference ↩";
}
if (/^#(?:user-content-)?fn-/i.test(href)) {
return `Footnote${display ? ` ${display}` : ""}`;
}
if (typeof label === "string" && label) return label;
return undefined;
}
function footnoteDisplayForHref(href: string, text: string) {
const visibleText = text.trim().replace(/^\[|\]$/g, "");
if (/^\d+[a-z]?$/i.test(visibleText)) return visibleText;
const match = /^#(?:user-content-)?fn(?:ref)?-([A-Za-z0-9_-]+)/i.exec(href);
if (!match) return "";
return match[1]!.replace(/-/g, " ");
}
function reactNodeText(node: React.ReactNode): string {
if (typeof node === "string" || typeof node === "number") return String(node);
if (Array.isArray(node)) return node.map(reactNodeText).join("");
return "";
}
function hasDescendantElement(
element: MarkdownHastElement | null,
tagName: string,
): boolean {
if (!element) return false;
return element.children.some((child) => {
const childElement = readHastElement(child);
return (
childElement?.tagName === tagName ||
hasDescendantElement(childElement, tagName)
);
});
}
function isWhitespaceText(node: MarkdownHastNode) {
return (
node.type === "text" &&
typeof node.value === "string" &&
node.value.trim() === ""
);
}
function extractHastText(element: MarkdownHastElement | null): string {
return extractHastNodeText(element);
}
function extractHastNodeText(
node: MarkdownHastNode | null | undefined,
): string {
if (!node) return "";
if (node.type === "text" && typeof node.value === "string") return node.value;
const element = readHastElement(node);
if (!element) return "";
return element.children.map(extractHastNodeText).join("");
}
type MarkdownTableModel = {
caption: MarkdownHastElement | null;
captionText: string;
cellCount: number;
columnCount: number;
rows: MarkdownTableRowModel[];
tsv: string;
};
type MarkdownTableRowModel = {
cells: MarkdownTableCellModel[];
key: string;
rowIndex: number;
};
type MarkdownTableCellModel = {
align: MarkdownTableCellAlign | null;
children: readonly MarkdownHastNode[];
columnIndex: number;
headerId: string | null;
headers: string | null;
isHeader: boolean;
key: string;
text: string;
};
type MarkdownTableCellAlign = "center" | "char" | "justify" | "left" | "right";
type MarkdownTableFindEntry = {
key: string;
rowIndex: number;
text: string;
};
function MarkdownTable({ node, style, ...props }: any) {
const table = readHastElement(node);
const model = React.useMemo(() => readMarkdownTableModel(table), [table]);
if (model && shouldVirtualizeMarkdownTable(model)) {
return <MarkdownVirtualTable model={model} style={style} />;
}
const ariaColumnCount = model?.columnCount ?? tableColumnCount(table);
const columnCount = ariaColumnCount ?? 0;
return (
<div
aria-label="Markdown table"
className="group relative my-4 overflow-hidden rounded-lg border"
data-markdown-table-region=""
role="region"
tabIndex={0}
onKeyDown={handleHorizontalScrollKeyDown}
>
<TableCopyButton copyText={model?.tsv} />
<div className="overflow-x-auto" data-markdown-table-scroll="">
<table
{...props}
aria-colcount={ariaColumnCount}
aria-rowcount={model?.rows.length ?? tableRowCount(table)}
className="w-full border-collapse text-[0.85em]"
data-markdown-table=""
style={{
...style,
minWidth: markdownTableMinWidth(columnCount, style?.minWidth),
}}
/>
</div>
</div>
);
}
function MarkdownVirtualTable({
model,
style,
}: {
model: MarkdownTableModel;
style?: React.CSSProperties;
}) {
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const scrollFrameRef = React.useRef<number | null>(null);
const [viewport, setViewport] = React.useState({
height: MARKDOWN_TABLE_VIRTUALIZED_VIEWPORT_HEIGHT_PX,
scrollTop: 0,
});
const visibleWindow = React.useMemo(
() =>
markdownVirtualTableVisibleWindow({
rowCount: model.rows.length,
scrollTop: viewport.scrollTop,
viewportHeight: viewport.height,
}),
[model.rows.length, viewport.height, viewport.scrollTop],
);
const visibleRows = React.useMemo(
() => model.rows.slice(visibleWindow.start, visibleWindow.end),
[model.rows, visibleWindow.end, visibleWindow.start],
);
const findEntries = React.useMemo(
() => markdownTableFindEntries(model.rows),
[model.rows],
);
const updateViewport = React.useCallback(() => {
const element = scrollRef.current;
if (!element) return;
setViewport({
height:
element.clientHeight || MARKDOWN_TABLE_VIRTUALIZED_VIEWPORT_HEIGHT_PX,
scrollTop: element.scrollTop,
});
}, []);
const handleScroll = React.useCallback(() => {
if (scrollFrameRef.current != null) return;
scrollFrameRef.current = requestAnimationFrame(() => {
scrollFrameRef.current = null;
updateViewport();
});
}, [updateViewport]);
const revealRow = React.useCallback((rowIndex: number) => {
const top = rowIndex * MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX;
scrollRef.current?.scrollTo?.({ behavior: "auto", top });
if (scrollRef.current) scrollRef.current.scrollTop = top;
setViewport((current) => ({
height: scrollRef.current?.clientHeight || current.height,
scrollTop: top,
}));
}, []);
useKeyedLayoutEffect(
joinEffectKey(["markdown-virtual-table-viewport", model.rows.length]),
() => {
updateViewport();
return () => {
if (scrollFrameRef.current != null) {
cancelAnimationFrame(scrollFrameRef.current);
}
};
},
);
return (
<div
aria-label="Markdown table"
className="group relative my-4 overflow-hidden rounded-lg border"
data-markdown-table-region=""
data-markdown-table-virtualized-region=""
role="region"
tabIndex={0}
onKeyDown={handleHorizontalScrollKeyDown}
>
<TableCopyButton copyText={model.tsv} />
<div
ref={scrollRef}
className="max-h-[560px] overflow-auto"
data-markdown-table-scroll=""
onScroll={handleScroll}
>
<table
aria-colcount={model.columnCount}
aria-rowcount={model.rows.length}
className="w-full border-collapse text-[0.85em]"
data-markdown-table=""
data-markdown-table-mounted-rows={visibleRows.length}
data-markdown-table-total-rows={model.rows.length}
data-markdown-table-virtualized=""
style={{
...style,
minWidth: markdownTableMinWidth(model.columnCount, style?.minWidth),
}}
>
{model.caption ? (
<caption className="text-muted-foreground bg-background sticky top-0 z-20 caption-top px-3 py-2 text-left font-medium">
{renderHastChildren(model.caption.children)}
</caption>
) : null}
<tbody>
{visibleWindow.start > 0 ? (
<MarkdownVirtualTableSpacerRow
columnCount={model.columnCount}
height={
visibleWindow.start * MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX
}
/>
) : null}
{visibleRows.map((row) => (
<MarkdownVirtualTableRow key={row.key} row={row} />
))}
{visibleWindow.end < model.rows.length ? (
<MarkdownVirtualTableSpacerRow
columnCount={model.columnCount}
height={
(model.rows.length - visibleWindow.end) *
MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX
}
/>
) : null}
</tbody>
</table>
<MarkdownVirtualTableNativeFindIndex
entries={findEntries}
revealRow={revealRow}
/>
</div>
</div>
);
}
function MarkdownVirtualTableRow({ row }: { row: MarkdownTableRowModel }) {
return (
<tr
aria-rowindex={row.rowIndex + 1}
className="border-b"
data-markdown-table-row=""
data-pretext-table-row-index={row.rowIndex + 1}
style={{
height: MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX,
}}
>
{row.cells.map((cell) => (
<MarkdownVirtualTableCell key={cell.key} cell={cell} />
))}
</tr>
);
}
function MarkdownVirtualTableCell({ cell }: { cell: MarkdownTableCellModel }) {
const Cell = cell.isHeader ? "th" : "td";
return (
<Cell
align={cell.align ?? undefined}
aria-colindex={cell.columnIndex + 1}
className={markdownTableCellClassName({
align: cell.align,
isHeader: cell.isHeader,
})}
data-markdown-table-cell=""
data-pretext-table-column-index={cell.columnIndex + 1}
headers={cell.isHeader ? undefined : (cell.headers ?? undefined)}
id={cell.headerId ?? undefined}
scope={cell.isHeader ? "col" : undefined}
>
<span className="block truncate">
{renderHastChildren(cell.children)}
</span>
</Cell>
);
}
function MarkdownVirtualTableSpacerRow({
columnCount,
height,
}: {
columnCount: number;
height: number;
}) {
return (
<tr aria-hidden="true" data-markdown-table-spacer-row="">
<td colSpan={columnCount} style={{ height, padding: 0 }} />
</tr>
);
}
function MarkdownVirtualTableNativeFindIndex({
entries,
revealRow,
}: {
entries: readonly MarkdownTableFindEntry[];
revealRow: (rowIndex: number) => void;
}) {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute top-0 left-0 h-px w-px overflow-hidden opacity-0"
data-markdown-table-native-find-index=""
data-markdown-table-native-find-entries={entries.length}
>
{entries.map((entry) => (
<MarkdownVirtualTableNativeFindEntry
key={entry.key}
entry={entry}
revealRow={revealRow}
/>
))}
</div>
);
}
function MarkdownVirtualTableNativeFindEntry({
entry,
revealRow,
}: {
entry: MarkdownTableFindEntry;
revealRow: (rowIndex: number) => void;
}) {
const ref = React.useRef<HTMLSpanElement | null>(null);
useKeyedLayoutEffect(joinEffectKey([entry.key, entry.rowIndex]), () => {
const element = ref.current;
if (!element) return;
element.setAttribute("hidden", "until-found");
const handleBeforeMatch = () => {
revealRow(entry.rowIndex);
requestAnimationFrame(() => {
element.setAttribute("hidden", "until-found");
});
};
element.addEventListener("beforematch", handleBeforeMatch);
return () => {
element.removeEventListener("beforematch", handleBeforeMatch);
};
});
return (
<span
ref={ref}
className="absolute top-0 left-0 block h-px w-px overflow-hidden whitespace-pre"
data-markdown-table-native-find-entry=""
data-markdown-table-native-find-row={entry.rowIndex + 1}
>
{entry.text || " "}
</span>
);
}
function readMarkdownTableModel(
table: MarkdownHastElement | null,
): MarkdownTableModel | null {
if (!table) return null;
const caption = table.children
.map(readHastElement)
.find(
(child): child is MarkdownHastElement => child?.tagName === "caption",
);
const rows = tableRows(table).map((row, rowIndex) =>
readMarkdownTableRowModel(row, rowIndex),
);
if (!rows.length) return null;
const columnCount = rows.reduce(
(max, row) => Math.max(max, row.cells.length),
0,
);
const cellCount = rows.reduce((sum, row) => sum + row.cells.length, 0);
return {
caption: caption ?? null,
captionText: normalizeTableCellText(extractHastNodeText(caption)),
cellCount,
columnCount,
rows,
tsv: serializeMarkdownTableModelAsTsv(rows),
};
}
function readMarkdownTableRowModel(
row: MarkdownHastElement,
rowIndex: number,
): MarkdownTableRowModel {
return {
cells: tableCells(row).map((cell, columnIndex) =>
readMarkdownTableCellModel(cell, rowIndex, columnIndex),
),
key: `row-${rowIndex}`,
rowIndex,
};
}
function readMarkdownTableCellModel(
cell: MarkdownHastElement,
rowIndex: number,
columnIndex: number,
): MarkdownTableCellModel {
return {
align: readMarkdownTableCellAlign(cell.properties?.align),
children: cell.children,
columnIndex,
headerId: readStringProperty(cell.properties?.id),
headers: readStringProperty(cell.properties?.headers),
isHeader: cell.tagName === "th",
key: `cell-${rowIndex}-${columnIndex}`,
text: normalizeTableCellText(extractHastNodeText(cell)),
};
}
function readMarkdownTableCellAlign(
value: unknown,
): MarkdownTableCellAlign | null {
return value === "center" ||
value === "char" ||
value === "justify" ||
value === "left" ||
value === "right"
? value
: null;
}
function shouldVirtualizeMarkdownTable(model: MarkdownTableModel) {
return (
model.rows.length >= MARKDOWN_TABLE_VIRTUALIZATION_ROW_THRESHOLD ||
model.cellCount >= MARKDOWN_TABLE_VIRTUALIZATION_CELL_THRESHOLD
);
}
function markdownVirtualTableVisibleWindow({
rowCount,
scrollTop,
viewportHeight,
}: {
rowCount: number;
scrollTop: number;
viewportHeight: number;
}) {
const visibleStart = Math.max(
0,
Math.floor(scrollTop / MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX) -
MARKDOWN_TABLE_VIRTUALIZED_OVERSCAN_ROWS,
);
const visibleCount =
Math.ceil(viewportHeight / MARKDOWN_TABLE_VIRTUALIZED_ROW_HEIGHT_PX) +
MARKDOWN_TABLE_VIRTUALIZED_OVERSCAN_ROWS * 2;
return {
end: Math.min(rowCount, visibleStart + Math.max(1, visibleCount)),
start: visibleStart,
};
}
function markdownTableFindEntries(
rows: readonly MarkdownTableRowModel[],
): MarkdownTableFindEntry[] {
const entries: MarkdownTableFindEntry[] = [];
for (
let rowIndex = 0;
rowIndex < rows.length;
rowIndex += MARKDOWN_TABLE_NATIVE_FIND_ROWS_PER_ENTRY
) {
const chunkRows = rows.slice(
rowIndex,
rowIndex + MARKDOWN_TABLE_NATIVE_FIND_ROWS_PER_ENTRY,
);
entries.push({
key: `table-find-${rowIndex}`,
rowIndex,
text: chunkRows
.map((row) => row.cells.map((cell) => cell.text).join("\t"))
.join("\n"),
});
}
return entries;
}
function serializeMarkdownTableModelAsTsv(
rows: readonly MarkdownTableRowModel[],
) {
return rows
.map((row) => row.cells.map((cell) => cell.text).join("\t"))
.join("\n");
}
function markdownTableMinWidth(
columnCount: number,
fallback: React.CSSProperties["minWidth"],
) {
return columnCount >= 4 ? `${Math.max(640, columnCount * 160)}px` : fallback;
}
function markdownTableCellClassName({
align,
isHeader,
}: {
align: string | null;
isHeader: boolean;
}) {
return [
"min-w-0 border-r px-3 py-1.5 last:border-r-0",
isHeader
? "border-b bg-muted/55 text-left font-medium"
: "border-t align-top",
align === "center" ? "text-center" : "",
align === "right" ? "text-right tabular-nums" : "",
]
.filter(Boolean)
.join(" ");
}
function handleHorizontalScrollKeyDown(
event: React.KeyboardEvent<HTMLElement>,
) {
const element =
event.currentTarget.querySelector<HTMLElement>(
"[data-markdown-table-scroll]",
) ?? event.currentTarget;
if (event.key === "ArrowRight") {
element.scrollLeft += 50;
event.preventDefault();
} else if (event.key === "ArrowLeft") {
element.scrollLeft -= 50;
event.preventDefault();
} else if (event.key === "End") {
element.scrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
event.preventDefault();
} else if (event.key === "Home") {
element.scrollLeft = 0;
event.preventDefault();
}
}
function tableRowCount(table: MarkdownHastElement | null) {
if (!table) return undefined;
return countDescendantElements(table, "tr");
}
function tableColumnCount(table: MarkdownHastElement | null) {
const firstRow = findDescendantElement(table, "tr");
if (!firstRow) return undefined;
return firstRow.children.filter((child) => {
const element = readHastElement(child);
return element?.tagName === "td" || element?.tagName === "th";
}).length;
}
function tableRows(element: MarkdownHastElement) {
const rows: MarkdownHastElement[] = [];
for (const child of element.children) {
const childElement = readHastElement(child);
if (!childElement) continue;
if (childElement.tagName === "tr") {
rows.push(childElement);
} else if (
childElement.tagName === "thead" ||
childElement.tagName === "tbody" ||
childElement.tagName === "tfoot"
) {
rows.push(...tableRows(childElement));
}
}
return rows;
}
function tableCells(row: MarkdownHastElement) {
return row.children
.map(readHastElement)
.filter(
(child): child is MarkdownHastElement =>
child?.tagName === "td" || child?.tagName === "th",
);
}
function countDescendantElements(
element: MarkdownHastElement,
tagName: string,
): number {
return element.children.reduce((sum, child) => {
const childElement = readHastElement(child);
if (!childElement) return sum;
return (
sum +
(childElement.tagName === tagName ? 1 : 0) +
countDescendantElements(childElement, tagName)
);
}, 0);
}
function findDescendantElement(
element: MarkdownHastElement | null,
tagName: string,
): MarkdownHastElement | null {
if (!element) return null;
for (const child of element.children) {
const childElement = readHastElement(child);
if (!childElement) continue;
if (childElement.tagName === tagName) return childElement;
const found = findDescendantElement(childElement, tagName);
if (found) return found;
}
return null;
}
// A subtle hover copy affordance in the table's top-right corner, replacing the
// persistent chrome bar so the table reads as a clean document table.
function TableCopyButton({ copyText }: { copyText?: string }) {
const [copied, setCopied] = React.useState(false);
return (
<button
aria-label="Copy table as TSV"
className="bg-background/90 text-muted-foreground hover:text-foreground absolute top-2 right-2 z-10 inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium opacity-0 shadow-sm backdrop-blur-sm transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
type="button"
onClick={(event) => {
copyTable(event.currentTarget, copyText);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
}}
>
{copied ? (
<Check className="size-3.5" aria-hidden="true" />
) : (
<Copy className="size-3.5" aria-hidden="true" />
)}
{copied ? "Copied" : "Copy"}
</button>
);
}
function copyTable(button: HTMLButtonElement, copyText?: string) {
const region = button.closest('[role="region"]');
const selection = window.getSelection();
const selectedText =
selection &&
selection.rangeCount > 0 &&
region?.contains(selection.anchorNode)
? selection.toString()
: "";
if (selectedText.trim()) {
void navigator.clipboard?.writeText(selectedText.trim());
return;
}
if (copyText != null) {
void navigator.clipboard?.writeText(copyText);
return;
}
const table = region?.querySelector("table");
if (!table) return;
void navigator.clipboard?.writeText(serializeTableAsTsv(table));
}
function serializeTableAsTsv(table: HTMLTableElement) {
return Array.from(table.querySelectorAll("tr"))
.map((row) =>
Array.from(row.querySelectorAll("th,td"))
.map((cell) => normalizeTableCellText(cell.textContent ?? ""))
.join("\t"),
)
.join("\n");
}
function normalizeTableCellText(value: string) {
return value.trim().replace(/[\t\r\n ]+/g, " ");
}
function copyHeadingLink(id: string) {
const base = `${window.location.origin}${window.location.pathname}${window.location.search}`;
void navigator.clipboard?.writeText(`${base}#${id}`);
}
function readComponentProps(value: string): Record<string, unknown> {
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object"
? (parsed as Record<string, unknown>)
: {};
} catch {
return {};
}
}
function readOptionalString(value: unknown) {
return typeof value === "string" && value ? value : undefined;
}
function readOptionalNumber(value: unknown) {
const parsed =
typeof value === "number"
? value
: typeof value === "string"
? Number(value)
: NaN;
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
function readOptionalBoolean(value: unknown) {
if (typeof value === "boolean") return value;
if (typeof value !== "string") return undefined;
if (value === "true") return true;
if (value === "false") return false;
return undefined;
}
registry/new-york-v4/ui/markdown-greenfield-virtualizer.ts
"use client";
import type { MarkdownGreenfieldChunk } from "./markdown-greenfield-document";
import type { MarkdownGreenfieldChunkFrame } from "./markdown-greenfield-layout";
export type MarkdownGreenfieldScrollAnchor = {
chunkId: string;
/** Negative when the viewport is in document padding before the chunk. */
offsetWithinChunkPx: number;
chunkHeightPx: number;
};
export type MarkdownGreenfieldVisibleRange = {
start: number;
end: number;
};
export type MarkdownGreenfieldVisibleProjection = {
frameIds: readonly string[];
range: MarkdownGreenfieldVisibleRange;
};
export function getMarkdownGreenfieldVisibleRange({
frames,
overscanPx,
scrollTop,
viewportHeight,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
overscanPx: number;
scrollTop: number;
viewportHeight: number;
}): MarkdownGreenfieldVisibleRange {
if (!frames.length) return { start: 0, end: 0 };
const start = firstFrameWithBottomAfter(
frames,
Math.max(0, scrollTop - overscanPx),
);
const end = firstFrameWithTopAtOrAfter(
frames,
scrollTop + viewportHeight + overscanPx,
);
return { start, end: Math.max(start + 1, end) };
}
export function createMarkdownGreenfieldVisibleProjection({
frames,
range,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
range: MarkdownGreenfieldVisibleRange;
}): MarkdownGreenfieldVisibleProjection {
return {
frameIds: frames.slice(range.start, range.end).map((frame) => frame.id),
range,
};
}
export function getMarkdownGreenfieldVisibleProjection({
frames,
overscanPx,
scrollTop,
viewportHeight,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
overscanPx: number;
scrollTop: number;
viewportHeight: number;
}): MarkdownGreenfieldVisibleProjection {
return createMarkdownGreenfieldVisibleProjection({
frames,
range: getMarkdownGreenfieldVisibleRange({
frames,
overscanPx,
scrollTop,
viewportHeight,
}),
});
}
export function getMarkdownGreenfieldProjectedVisibleFrames({
frames,
projection,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
projection: MarkdownGreenfieldVisibleProjection;
}) {
const rangedFrames = frames.slice(
projection.range.start,
projection.range.end,
);
if (frameIdsEqual(rangedFrames, projection.frameIds)) return rangedFrames;
const framesById = new Map(frames.map((frame) => [frame.id, frame]));
return projection.frameIds.flatMap((frameId) => {
const frame = framesById.get(frameId);
return frame ? [frame] : [];
});
}
export function isMarkdownGreenfieldVisibleProjectionSameWindow({
frames,
projection,
range,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
projection: MarkdownGreenfieldVisibleProjection;
range: MarkdownGreenfieldVisibleRange;
}) {
const nextLength = range.end - range.start;
if (projection.frameIds.length !== nextLength) return false;
for (let offset = 0; offset < nextLength; offset += 1) {
if (frames[range.start + offset]?.id !== projection.frameIds[offset]) {
return false;
}
}
return true;
}
export function getMarkdownGreenfieldVisibleFrames({
frames,
overscanPx,
scrollTop,
viewportHeight,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
overscanPx: number;
scrollTop: number;
viewportHeight: number;
}) {
const range = getMarkdownGreenfieldVisibleRange({
frames,
overscanPx,
scrollTop,
viewportHeight,
});
return frames.slice(range.start, range.end);
}
export function getMarkdownGreenfieldScrollAnchor({
frames,
scrollTop,
}: {
frames: readonly MarkdownGreenfieldChunkFrame[];
scrollTop: number;
}): MarkdownGreenfieldScrollAnchor | null {
if (!frames.length) return null;
const frame =
frames[
Math.min(firstFrameWithBottomAfter(frames, scrollTop), frames.length - 1)
];
if (!frame) return null;
return {
chunkId: frame.id,
chunkHeightPx: frame.height,
offsetWithinChunkPx: scrollTop - frame.top,
};
}
export function resolveMarkdownGreenfieldScrollAnchor({
anchor,
frames,
}: {
anchor: MarkdownGreenfieldScrollAnchor;
frames: readonly MarkdownGreenfieldChunkFrame[];
}) {
const frame = frames.find((item) => item.id === anchor.chunkId);
if (!frame) return null;
if (anchor.offsetWithinChunkPx < 0) {
return Math.max(0, frame.top + anchor.offsetWithinChunkPx);
}
// Preserve the position *within* the anchored chunk proportionally. When the
// chunk keeps its height this reduces to the original pixel offset; when an
// over-estimated chunk shrinks after measurement, the viewport stays at the
// same relative content instead of snapping toward the chunk's top (which is
// what `min(offset, height - 1)` did, yanking the reader upward).
const fraction =
anchor.chunkHeightPx > 0
? Math.min(
1,
Math.max(0, anchor.offsetWithinChunkPx / anchor.chunkHeightPx),
)
: 0;
return frame.top + fraction * frame.height;
}
export function getMarkdownGreenfieldScrollTopForLineRange({
chunks,
frames,
preferredChunkId,
range,
viewportHeight,
}: {
chunks: readonly Pick<
MarkdownGreenfieldChunk,
"id" | "index" | "sourceEndLine" | "sourceStartLine"
>[];
frames: readonly MarkdownGreenfieldChunkFrame[];
preferredChunkId?: string | null;
range: { end: number; start: number } | null;
viewportHeight: number;
}) {
if (!range) return null;
const preferredChunk =
preferredChunkId == null
? null
: chunks.find((item) => item.id === preferredChunkId);
const chunk =
preferredChunk ??
chunks.find(
(item) =>
item.sourceStartLine <= range.start &&
item.sourceEndLine >= range.start,
) ??
chunks[0];
const frame = chunk
? frames.find((candidate) => candidate.index === chunk.index)
: null;
if (!frame) return null;
const lineCount = Math.max(
1,
frame.sourceEndLine - frame.sourceStartLine + 1,
);
const lineOffset =
(Math.max(0, range.start - frame.sourceStartLine) / lineCount) *
frame.height || 0;
return Math.max(0, frame.top + lineOffset - viewportHeight * 0.25);
}
export function getMarkdownGreenfieldSourceLineForScrollTop({
chunks,
frames,
scrollTop,
}: {
chunks: readonly Pick<
MarkdownGreenfieldChunk,
"index" | "sourceEndLine" | "sourceStartLine"
>[];
frames: readonly MarkdownGreenfieldChunkFrame[];
scrollTop: number;
}) {
if (!frames.length) return 1;
const frame =
frames[
Math.min(firstFrameWithBottomAfter(frames, scrollTop), frames.length - 1)
];
if (!frame) return 1;
const chunk = chunks.find((item) => item.index === frame.index);
if (!chunk) return frame.sourceStartLine;
const lineCount = Math.max(
1,
chunk.sourceEndLine - chunk.sourceStartLine + 1,
);
const ratio = Math.max(
0,
Math.min(1, (scrollTop - frame.top) / frame.height),
);
return (
chunk.sourceStartLine +
Math.min(lineCount - 1, Math.floor(ratio * lineCount))
);
}
function firstFrameWithBottomAfter(
frames: readonly MarkdownGreenfieldChunkFrame[],
y: number,
) {
let low = 0;
let high = frames.length;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (frames[mid]!.bottom > y) high = mid;
else low = mid + 1;
}
return low;
}
function firstFrameWithTopAtOrAfter(
frames: readonly MarkdownGreenfieldChunkFrame[],
y: number,
) {
let low = 0;
let high = frames.length;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (frames[mid]!.top >= y) high = mid;
else low = mid + 1;
}
return low;
}
function frameIdsEqual(
frames: readonly MarkdownGreenfieldChunkFrame[],
frameIds: readonly string[],
) {
if (frames.length !== frameIds.length) return false;
for (let index = 0; index < frames.length; index += 1) {
if (frames[index]?.id !== frameIds[index]) return false;
}
return true;
}
registry/new-york-v4/ui/markdown-hast-types.ts
"use client";
export type MarkdownPoint = {
column?: number;
line?: number;
offset?: number;
};
export type MarkdownPosition = {
end?: MarkdownPoint;
start?: MarkdownPoint;
};
export type MarkdownHastText = {
position?: MarkdownPosition;
type: "text";
value: string;
};
export type MarkdownHastElement = {
children: MarkdownHastNode[];
position?: MarkdownPosition;
properties?: Record<string, unknown>;
tagName: string;
type: "element";
};
export type MarkdownHastRoot = {
children: MarkdownHastNode[];
type: "root";
};
export type MarkdownHastNode =
| MarkdownHastElement
| MarkdownHastText
| {
children?: MarkdownHastNode[];
position?: MarkdownPosition;
type: string;
value?: unknown;
};
registry/new-york-v4/ui/markdown-source-map.ts
"use client";
import type { MarkdownPosition } from "./markdown-hast-types";
export interface MarkdownSourceRange {
endLine: number;
endOffset: number;
startLine: number;
startOffset: number;
}
export interface MarkdownSourceMap {
lineCount: number;
lineStarts: readonly number[];
text: string;
}
export function createMarkdownSourceMap(text: string): MarkdownSourceMap {
const lineStarts = [0];
for (const match of text.matchAll(/\r\n|[\n\r\u2028\u2029]/g)) {
lineStarts.push(match.index + match[0].length);
}
return {
lineCount: Math.max(1, lineStarts.length),
lineStarts,
text,
};
}
export function markdownSourceRangeFromPosition({
position,
sourceMap,
}: {
position: MarkdownPosition | null | undefined;
sourceMap: MarkdownSourceMap;
}): MarkdownSourceRange | null {
if (!position?.start || !position.end) return null;
const startLine = clampLine(position.start.line, sourceMap);
const endLine = clampLine(position.end.line, sourceMap);
const startOffset =
typeof position.start.offset === "number"
? clampOffset(position.start.offset, sourceMap.text.length)
: offsetFromLineColumn({
column: position.start.column,
line: startLine,
sourceMap,
});
const endOffset =
typeof position.end.offset === "number"
? clampOffset(position.end.offset, sourceMap.text.length)
: offsetFromLineColumn({
column: position.end.column,
line: endLine,
sourceMap,
});
return {
endLine: Math.max(startLine, endLine),
endOffset: Math.max(startOffset, endOffset),
startLine,
startOffset,
};
}
export function markdownSourceTextForRange({
range,
sourceMap,
}: {
range: MarkdownSourceRange | null;
sourceMap: MarkdownSourceMap;
}) {
if (!range) return "";
return sourceMap.text.slice(range.startOffset, range.endOffset);
}
export function markdownRangesIntersect({
a,
b,
}: {
a: MarkdownSourceRange | null;
b: MarkdownSourceRange | null;
}) {
if (!a || !b) return false;
return a.startOffset < b.endOffset && a.endOffset > b.startOffset;
}
export function markdownLineRangeIntersects({
endLine,
range,
startLine,
}: {
endLine: number;
range: { end: number; start: number } | null;
startLine: number;
}) {
if (!range) return false;
return startLine <= range.end && endLine >= range.start;
}
function offsetFromLineColumn({
column,
line,
sourceMap,
}: {
column: number | undefined;
line: number;
sourceMap: MarkdownSourceMap;
}) {
const lineStart = sourceMap.lineStarts[line - 1] ?? 0;
return clampOffset(
lineStart + Math.max(0, (column ?? 1) - 1),
sourceMap.text.length,
);
}
function clampLine(line: number | undefined, sourceMap: MarkdownSourceMap) {
if (!Number.isFinite(line)) return 1;
return Math.max(1, Math.min(sourceMap.lineCount, Math.trunc(line ?? 1)));
}
function clampOffset(offset: number, textLength: number) {
return Math.max(0, Math.min(textLength, offset));
}
registry/new-york-v4/ui/markdown-unified-pipeline.ts
"use client";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import rehypeSlug from "rehype-slug";
import remarkBreaks from "remark-breaks";
import remarkDirective from "remark-directive";
import remarkGemoji from "remark-gemoji";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import remarkSmartypants from "remark-smartypants";
import { unified } from "unified";
import { VFile } from "vfile";
import type {
MarkdownHastElement,
MarkdownHastNode,
MarkdownHastRoot,
} from "./markdown-hast-types";
import { createMarkdownSourceMap } from "./markdown-source-map";
export type MarkdownUnifiedDocument = {
hast: MarkdownHastRoot;
mdast: MarkdownMdastRoot;
messages: MarkdownUnifiedMessage[];
sourceMap: ReturnType<typeof createMarkdownSourceMap>;
};
export type MarkdownUnifiedMessage = {
column?: number;
fatal?: boolean | null;
line?: number;
reason: string;
ruleId?: string | null;
source?: string | null;
};
export type MarkdownUnifiedOptions = {
gfm?: MarkdownGfmOptions;
};
export const MARKDOWN_REMARK_PLUGINS = [
"remark-parse",
"remark-directive",
"remark-gfm",
"remark-breaks",
"remark-math",
"remark-gemoji",
"remark-markdown-prose-transforms",
"remark-smartypants",
"remark-markdown-github-alerts",
"remark-markdown-definition-lists",
"remark-markdown-components",
"remark-markdown-code-metadata",
"remark-markdown-trusted-images",
] as const;
export const MARKDOWN_REHYPE_PLUGINS = [
"remark-rehype",
"rehype-raw",
"rehype-slug",
"rehype-sanitize",
"rehype-markdown-trusted-metadata",
"rehype-markdown-safe-inputs",
"rehype-katex",
] as const;
type MarkdownGfmOptions = {
singleTilde?: boolean;
stringLength?: (value: string) => number;
tableCellPadding?: boolean;
tablePipeAlign?: boolean;
};
type MarkdownMdastNode = {
children?: MarkdownMdastNode[];
data?: {
hProperties?: Record<string, unknown>;
[key: string]: unknown;
};
lang?: string;
meta?: string;
position?: {
end?: {
column?: number;
line?: number;
offset?: number;
};
start?: {
column?: number;
line?: number;
offset?: number;
};
};
type: string;
value?: string;
};
type MarkdownMdastRoot = MarkdownMdastNode & {
children: MarkdownMdastNode[];
type: "root";
};
type MarkdownMdastParagraph = MarkdownMdastNode & {
children: MarkdownMdastNode[];
type: "paragraph";
};
type MarkdownComponent = {
children?: MarkdownMdastNode[];
name: string;
props: Record<string, string>;
};
const GITHUB_ALERT_LABELS = {
caution: "Caution",
important: "Important",
note: "Note",
tip: "Tip",
warning: "Warning",
} as const;
const MARKDOWN_KATEX_OPTIONS = {
maxExpand: 1000,
maxSize: 10,
strict: "ignore",
trust: false,
} as const;
const MARKDOWN_KATEX_RENDER_CACHE_LIMIT = 512;
const MARKDOWN_KATEX_RENDER_CACHE_RENDERER = "rehype-katex@7.0.1";
const MARKDOWN_KATEX_RENDER_CACHE_CONFIG = markdownKatexConfigKey(
MARKDOWN_KATEX_OPTIONS,
);
type MarkdownHastParent = MarkdownHastElement | MarkdownHastRoot;
type MarkdownKatexCacheMode = "display" | "inline";
type MarkdownKatexCacheEntry = {
message?: MarkdownKatexCacheMessage;
nodes: MarkdownHastNode[];
};
type MarkdownKatexCacheMessage = {
reason: string;
ruleId?: string | null;
source?: string | null;
};
type MarkdownKatexMatch = {
displayMode: boolean;
messagePosition?: MarkdownHastElement["position"];
mode: MarkdownKatexCacheMode;
scope: MarkdownHastElement;
source: string;
};
type MarkdownKatexRenderMiss = {
endMarkerId: string;
key: string;
messagePosition?: MarkdownHastElement["position"];
startMarkerId: string;
};
type MarkdownKatexRenderPlaceholder = {
key: string;
messagePosition?: MarkdownHastElement["position"];
placeholderId: string;
};
type MarkdownKatexCachedMessageReplay = {
message: MarkdownKatexCacheMessage;
messagePosition?: MarkdownHastElement["position"];
};
type MarkdownKatexRenderState = {
cachedMessages: MarkdownKatexCachedMessageReplay[];
firstMissByKey: Map<string, MarkdownKatexRenderMiss>;
firstMisses: MarkdownKatexRenderMiss[];
nextMarkerId: number;
placeholders: MarkdownKatexRenderPlaceholder[];
renderedByKey: Map<string, MarkdownKatexCacheEntry>;
};
type MarkdownMdastProcessor = ReturnType<typeof createMarkdownMdastProcessor>;
type MarkdownHastProcessor = ReturnType<typeof createMarkdownHastProcessor>;
let defaultMarkdownMdastProcessor: MarkdownMdastProcessor | null = null;
let defaultMarkdownHastProcessor: MarkdownHastProcessor | null = null;
let markdownUnifiedSanitizeSchema: ReturnType<
typeof createUncachedMarkdownUnifiedSanitizeSchema
> | null = null;
const markdownKatexRenderCache = new Map<string, MarkdownKatexCacheEntry>();
const markdownKatexRenderCacheStats = {
hits: 0,
misses: 0,
sameDocumentHits: 0,
writes: 0,
};
export function createMarkdownUnifiedDocument(
markdown: string,
options: MarkdownUnifiedOptions = {},
): MarkdownUnifiedDocument {
const file = new VFile({ value: markdown });
const mdastProcessor = getMarkdownMdastProcessor(options);
const parsedMdast = mdastProcessor.parse(file) as MarkdownMdastRoot;
const mdast = mdastProcessor.runSync(
parsedMdast as never,
file,
) as MarkdownMdastRoot;
const hastProcessor = getMarkdownHastProcessor();
const hast = hastProcessor.runSync(mdast as never, file) as MarkdownHastRoot;
const sourceMap = createMarkdownSourceMap(markdown);
injectMarkdownFrontmatter(hast, sourceMap.text);
return {
hast,
mdast,
messages: file.messages.map(markdownUnifiedMessageFromVFileMessage),
sourceMap,
};
}
function getMarkdownMdastProcessor(
options: MarkdownUnifiedOptions,
): MarkdownMdastProcessor {
if (options.gfm) return createMarkdownMdastProcessor(options);
if (!defaultMarkdownMdastProcessor) {
defaultMarkdownMdastProcessor = createMarkdownMdastProcessor({});
}
return defaultMarkdownMdastProcessor;
}
function createMarkdownMdastProcessor(options: MarkdownUnifiedOptions) {
return unified()
.use(remarkParse)
.use(remarkDirective)
.use(remarkGfm, options.gfm)
.use(remarkBreaks)
.use(remarkMath)
.use(remarkGemoji)
.use(remarkMarkdownProseTransforms)
.use(remarkSmartypants)
.use(remarkMarkdownGithubAlerts)
.use(remarkMarkdownDefinitionLists)
.use(remarkMarkdownComponents)
.use(remarkMarkdownCodeMetadata)
.use(remarkMarkdownTrustedImages);
}
function getMarkdownHastProcessor(): MarkdownHastProcessor {
if (!defaultMarkdownHastProcessor) {
defaultMarkdownHastProcessor = createMarkdownHastProcessor();
}
return defaultMarkdownHastProcessor;
}
function createMarkdownHastProcessor() {
return unified()
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSlug)
.use(rehypeSanitize, getMarkdownUnifiedSanitizeSchema())
.use(rehypeMarkdownTrustedMetadata)
.use(rehypeMarkdownSafeInputs)
.use(rehypeMarkdownCachedKatex, MARKDOWN_KATEX_OPTIONS);
}
function getMarkdownUnifiedSanitizeSchema() {
if (!markdownUnifiedSanitizeSchema) {
markdownUnifiedSanitizeSchema =
createUncachedMarkdownUnifiedSanitizeSchema();
}
return markdownUnifiedSanitizeSchema;
}
function createUncachedMarkdownUnifiedSanitizeSchema() {
return {
...defaultSchema,
clobberPrefix: "user-content-",
attributes: {
...defaultSchema.attributes,
"*": [
...(defaultSchema.attributes?.["*"] ?? []),
"ariaDescribedBy",
"ariaHidden",
"ariaLabel",
"ariaLabelledBy",
"className",
"dataFootnoteBackref",
"dataFootnoteRef",
"dataFootnotes",
"dataPretextComponentFallback",
"dataPretextComponentFallbackName",
"dataPretextComponentFallbackReason",
"dataPretextComponentFallbackSource",
"dataPretextComponentName",
"dataPretextComponentProps",
"dataPretextMarkdownImage",
"dataPretextAlertKind",
"dataPretextAlertTitle",
"dataPretextCalloutKind",
"dataPretextCalloutTitle",
"id",
],
a: [
...(defaultSchema.attributes?.a ?? []),
"ariaDescribedBy",
"dataFootnoteBackref",
"dataFootnoteRef",
"href",
"id",
"title",
],
code: [
...(defaultSchema.attributes?.code ?? []),
"className",
"dataPretextCodeMeta",
],
img: [
...(defaultSchema.attributes?.img ?? []),
"dataPretextMarkdownImage",
],
ins: [...(defaultSchema.attributes?.ins ?? []), "cite"],
input: ["checked", "disabled", "type"],
li: [...(defaultSchema.attributes?.li ?? []), "className"],
ol: [...(defaultSchema.attributes?.ol ?? []), "start"],
section: [
...(defaultSchema.attributes?.section ?? []),
"className",
"dataFootnotes",
],
time: [...(defaultSchema.attributes?.time ?? []), "dateTime"],
sup: [...(defaultSchema.attributes?.sup ?? []), "id"],
td: [...(defaultSchema.attributes?.td ?? []), "align"],
th: [...(defaultSchema.attributes?.th ?? []), "align"],
q: [...(defaultSchema.attributes?.q ?? []), "cite"],
},
tagNames: [
...(defaultSchema.tagNames ?? []),
"abbr",
"caption",
"cite",
"dd",
"details",
"dfn",
"dl",
"dt",
"figcaption",
"figure",
"input",
"ins",
"kbd",
"mark",
"q",
"samp",
"section",
"small",
"summary",
"time",
"var",
],
};
}
function markdownUnifiedMessageFromVFileMessage(message: {
column?: number;
fatal?: boolean | null;
line?: number;
reason: string;
ruleId?: string | null;
source?: string | null;
}): MarkdownUnifiedMessage {
return {
column: message.column,
fatal: message.fatal,
line: message.line,
reason: message.reason,
ruleId: message.ruleId,
source: message.source,
};
}
function injectMarkdownFrontmatter(hast: MarkdownHastRoot, markdown: string) {
const frontmatter = readMarkdownFrontmatter(markdown);
if (!frontmatter) return;
hast.children = [
createFrontmatterElement(frontmatter),
...hast.children.filter((child) => {
const line = child.position?.start?.line ?? Number.POSITIVE_INFINITY;
return line > frontmatter.endLine;
}),
];
}
function readMarkdownFrontmatter(markdown: string) {
const lines = markdown.split(/\r\n|[\n\r\u2028\u2029]/);
const first = lines[0]?.trim();
const kind = first === "---" ? "yaml" : first === "+++" ? "toml" : "";
if (!kind) return null;
const closeIndex = lines.findIndex(
(line, index) => index > 0 && line.trim() === first,
);
if (closeIndex <= 0) return null;
const body = lines.slice(1, closeIndex);
if (!body.some((line) => line.trim())) return null;
return {
body,
endLine: closeIndex + 1,
kind,
raw: lines.slice(0, closeIndex + 1).join("\n"),
};
}
function createFrontmatterElement(frontmatter: {
body: string[];
endLine: number;
kind: string;
raw: string;
}): MarkdownHastElement {
const lastLine =
frontmatter.raw.split(/\r\n|[\n\r\u2028\u2029]/).at(-1) ?? "";
return {
type: "element",
tagName: "div",
position: {
start: { line: 1, column: 1, offset: 0 },
end: {
line: frontmatter.endLine,
column: lastLine.length + 1,
offset: frontmatter.raw.length,
},
},
properties: {
dataMarkdownFrontmatter: frontmatter.kind,
},
children: [
{
type: "element",
tagName: "pre",
properties: { dataMarkdownFrontmatterSource: "" },
children: [
{
type: "element",
tagName: "code",
properties: { dataMarkdownFrontmatterSource: "" },
children: [{ type: "text", value: frontmatter.raw }],
},
],
},
{
type: "element",
tagName: "dl",
properties: { dataMarkdownFrontmatterMetadata: "" },
children: frontmatterEntries(frontmatter).flatMap(
([key, value, kind]) => [
{
type: "element" as const,
tagName: "dt",
properties: {},
children: [{ type: "text" as const, value: key }],
},
{
type: "element" as const,
tagName: "dd",
properties: { dataFrontmatterValueKind: kind },
children: [{ type: "text" as const, value }],
},
],
),
},
],
};
}
function frontmatterEntries(frontmatter: { body: string[]; kind: string }) {
return frontmatter.kind === "toml"
? tomlFrontmatterEntries(frontmatter.body)
: yamlFrontmatterEntries(frontmatter.body);
}
function yamlFrontmatterEntries(
lines: string[],
): Array<[string, string, string]> {
const entries: Array<[string, string, string]> = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index]!;
const match = /^([A-Za-z0-9_.-]+):\s*(.*)$/.exec(line);
if (!match) continue;
const key = match[1]!;
const value = match[2]!.trim();
if (value.startsWith("{") || value.startsWith("[{")) continue;
if (/^\[.*\]$/.test(value)) {
entries.push([
key,
value
.slice(1, -1)
.split(",")
.map((item) => item.trim())
.join(", "),
"list",
]);
} else if (value) {
entries.push([key, value.replace(/^["']|["']$/g, ""), "scalar"]);
} else {
const items: string[] = [];
while (/^\s+-\s+/.test(lines[index + 1] ?? "")) {
index += 1;
items.push((lines[index] ?? "").replace(/^\s+-\s+/, "").trim());
}
if (items.length) entries.push([key, items.join(", "), "list"]);
}
}
return entries;
}
function tomlFrontmatterEntries(
lines: string[],
): Array<[string, string, string]> {
const entries: Array<[string, string, string]> = [];
let section = "";
for (const line of lines) {
const sectionMatch = /^\[([^\]]+)\]$/.exec(line.trim());
if (sectionMatch) {
section = `${sectionMatch[1]}.`;
continue;
}
const match = /^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/.exec(line.trim());
if (!match) continue;
const key = `${section}${match[1]}`;
const value = match[2]!.trim();
if (/^\[.*\]$/.test(value)) {
entries.push([
key,
value
.slice(1, -1)
.split(",")
.map((item) => item.trim().replace(/^["']|["']$/g, ""))
.join(", "),
"list",
]);
} else {
entries.push([key, value.replace(/^["']|["']$/g, ""), "scalar"]);
}
}
return entries;
}
function remarkMarkdownGithubAlerts() {
return function transform(tree: unknown) {
for (const node of (tree as MarkdownMdastRoot).children) {
transformGithubAlertBlockquote(node);
}
};
}
function remarkMarkdownProseTransforms() {
return function transform(tree: unknown) {
visitMarkdownMdastNodes(tree as MarkdownMdastRoot, (node) => {
if (node.type !== "text" || typeof node.value !== "string") return;
node.value = node.value
.replace(/--/g, "—")
.replace(/\.\.\./g, "…")
.replace(/->/g, "→")
.replace(/\b1\/2\b/g, "½");
});
};
}
function transformGithubAlertBlockquote(node: MarkdownMdastNode) {
if (node.type !== "blockquote") return;
const blockquote = node;
const first = blockquote.children?.[0];
if (!first || first.type !== "paragraph") return;
const alert = readGithubAlertMarker(first as MarkdownMdastParagraph);
if (!alert) return;
blockquote.data = {
...blockquote.data,
hProperties: {
...(blockquote.data?.hProperties as Record<string, unknown> | undefined),
dataPretextAlertKind: alert.kind,
dataPretextAlertTitle: alert.title,
},
};
}
function remarkMarkdownCodeMetadata() {
return function transform(tree: unknown) {
visitMarkdownMdastNodes(tree as MarkdownMdastRoot, (node) => {
if (node.type !== "code" || !node.meta) return;
node.data = {
...node.data,
hProperties: {
...(node.data?.hProperties as Record<string, unknown> | undefined),
dataPretextCodeMeta: node.meta,
},
};
});
};
}
function remarkMarkdownTrustedImages() {
return function transform(tree: unknown) {
visitMarkdownMdastNodes(tree as MarkdownMdastRoot, (node) => {
if (node.type !== "image" && node.type !== "imageReference") return;
node.data = {
...node.data,
hProperties: {
...(node.data?.hProperties as Record<string, unknown> | undefined),
dataPretextMarkdownImage: "",
},
};
});
};
}
function visitMarkdownMdastNodes(
node: MarkdownMdastNode,
visitor: (node: MarkdownMdastNode) => void,
) {
visitor(node);
for (const child of node.children ?? []) {
visitMarkdownMdastNodes(child, visitor);
}
}
function rehypeMarkdownCachedKatex(options: typeof MARKDOWN_KATEX_OPTIONS) {
const renderKatex = rehypeKatex(options);
return function transform(tree: MarkdownHastRoot, file: VFile) {
const state: MarkdownKatexRenderState = {
cachedMessages: [],
firstMissByKey: new Map(),
firstMisses: [],
nextMarkerId: 1,
placeholders: [],
renderedByKey: new Map(),
};
prepareMarkdownKatexRenderCache(tree, state);
const firstKatexMessageIndex = file.messages.length;
renderKatex(tree as never, file);
finishMarkdownKatexRenderCache(tree, file, state, firstKatexMessageIndex);
};
}
function prepareMarkdownKatexRenderCache(
parent: MarkdownHastParent,
state: MarkdownKatexRenderState,
) {
let index = 0;
while (index < parent.children.length) {
const child = parent.children[index]!;
const element = readHastElement(child);
if (!element) {
index += 1;
continue;
}
const match = readMarkdownKatexMatch(element);
if (!match) {
prepareMarkdownKatexRenderCache(element, state);
index += 1;
continue;
}
const key = markdownKatexRenderCacheKey(match);
const cached = readMarkdownKatexRenderCache(key);
if (cached) {
parent.children.splice(index, 1, ...cloneMarkdownHastNodes(cached.nodes));
if (cached.message) {
state.cachedMessages.push({
message: cached.message,
messagePosition: match.messagePosition,
});
}
markdownKatexRenderCacheStats.hits += 1;
index += cached.nodes.length;
continue;
}
if (state.firstMissByKey.has(key)) {
const placeholder = createMarkdownKatexPlaceholder(state);
parent.children[index] = placeholder;
state.placeholders.push({
key,
messagePosition: match.messagePosition,
placeholderId: readStringProperty(
placeholder.properties?.dataPretextKatexCachePlaceholder,
),
});
markdownKatexRenderCacheStats.sameDocumentHits += 1;
index += 1;
continue;
}
const startMarker = createMarkdownKatexMarker(state, "start");
const endMarker = createMarkdownKatexMarker(state, "end");
const miss: MarkdownKatexRenderMiss = {
endMarkerId: readStringProperty(
endMarker.properties?.dataPretextKatexCacheMarker,
),
key,
messagePosition: match.messagePosition,
startMarkerId: readStringProperty(
startMarker.properties?.dataPretextKatexCacheMarker,
),
};
state.firstMissByKey.set(key, miss);
state.firstMisses.push(miss);
markdownKatexRenderCacheStats.misses += 1;
parent.children.splice(index, 1, startMarker, match.scope, endMarker);
index += 3;
}
}
function finishMarkdownKatexRenderCache(
tree: MarkdownHastRoot,
file: VFile,
state: MarkdownKatexRenderState,
firstKatexMessageIndex: number,
) {
const katexMessages = file.messages
.slice(firstKatexMessageIndex)
.filter((message) => message.source === "rehype-katex");
const usedMessageIndexes = new Set<number>();
for (const miss of state.firstMisses) {
const renderedNodes = readRenderedMarkdownKatexNodes(tree, miss);
if (!renderedNodes) continue;
const message = readMarkdownKatexMessageForMiss({
messages: katexMessages,
miss,
usedMessageIndexes,
});
const entry = {
message,
nodes: cloneMarkdownHastNodes(renderedNodes),
};
state.renderedByKey.set(miss.key, entry);
writeMarkdownKatexRenderCache(miss.key, entry);
}
for (const placeholder of state.placeholders) {
const cached =
state.renderedByKey.get(placeholder.key) ??
readMarkdownKatexRenderCache(placeholder.key);
if (!cached) continue;
replaceMarkdownKatexPlaceholder(
tree,
placeholder.placeholderId,
cloneMarkdownHastNodes(cached.nodes),
);
replayMarkdownKatexCacheMessage(file, placeholder, cached.message);
}
for (const cachedMessage of state.cachedMessages) {
replayMarkdownKatexCacheMessage(file, cachedMessage, cachedMessage.message);
}
}
function readMarkdownKatexMatch(
element: MarkdownHastElement,
): MarkdownKatexMatch | null {
if (element.tagName === "pre") {
const code = element.children.map(readHastElement).find((child) => {
return (
child?.tagName === "code" && hasArrayClassName(child, "language-math")
);
});
if (code) {
return {
displayMode: true,
messagePosition: code.position,
mode: "display",
scope: element,
source: extractMarkdownKatexText(element),
};
}
}
const languageMath = hasArrayClassName(element, "language-math");
const mathDisplay = hasArrayClassName(element, "math-display");
const mathInline = hasArrayClassName(element, "math-inline");
if (!languageMath && !mathDisplay && !mathInline) return null;
return {
displayMode: mathDisplay,
messagePosition: element.position,
mode: mathDisplay ? "display" : "inline",
scope: element,
source: extractMarkdownKatexText(element),
};
}
function createMarkdownKatexMarker(
state: MarkdownKatexRenderState,
side: "end" | "start",
): MarkdownHastElement {
const id = `${side}-${state.nextMarkerId}`;
state.nextMarkerId += 1;
return {
type: "element",
tagName: "span",
properties: {
dataPretextKatexCacheMarker: id,
hidden: true,
},
children: [],
};
}
function createMarkdownKatexPlaceholder(
state: MarkdownKatexRenderState,
): MarkdownHastElement {
const id = `placeholder-${state.nextMarkerId}`;
state.nextMarkerId += 1;
return {
type: "element",
tagName: "span",
properties: {
dataPretextKatexCachePlaceholder: id,
hidden: true,
},
children: [],
};
}
function readRenderedMarkdownKatexNodes(
parent: MarkdownHastParent,
miss: MarkdownKatexRenderMiss,
): MarkdownHastNode[] | null {
const startIndex = parent.children.findIndex((child) =>
isMarkdownKatexMarker(child, miss.startMarkerId),
);
if (startIndex >= 0) {
const endIndex = parent.children.findIndex((child, index) => {
return (
index > startIndex && isMarkdownKatexMarker(child, miss.endMarkerId)
);
});
if (endIndex < 0) return null;
const renderedNodes = parent.children.slice(startIndex + 1, endIndex);
parent.children.splice(
startIndex,
endIndex - startIndex + 1,
...renderedNodes,
);
return renderedNodes;
}
for (const child of parent.children) {
const element = readHastElement(child);
if (!element) continue;
const renderedNodes = readRenderedMarkdownKatexNodes(element, miss);
if (renderedNodes) return renderedNodes;
}
return null;
}
function replaceMarkdownKatexPlaceholder(
parent: MarkdownHastParent,
placeholderId: string,
nodes: MarkdownHastNode[],
): boolean {
const index = parent.children.findIndex((child) =>
isMarkdownKatexPlaceholder(child, placeholderId),
);
if (index >= 0) {
parent.children.splice(index, 1, ...nodes);
return true;
}
for (const child of parent.children) {
const element = readHastElement(child);
if (
element &&
replaceMarkdownKatexPlaceholder(element, placeholderId, nodes)
) {
return true;
}
}
return false;
}
function readMarkdownKatexMessageForMiss({
messages,
miss,
usedMessageIndexes,
}: {
messages: readonly VFile["messages"][number][];
miss: MarkdownKatexRenderMiss;
usedMessageIndexes: Set<number>;
}): MarkdownKatexCacheMessage | undefined {
const start = miss.messagePosition?.start;
const exactIndex = messages.findIndex((message, index) => {
return (
!usedMessageIndexes.has(index) &&
message.line === start?.line &&
(start?.column == null || message.column === start.column)
);
});
const hasPosition = start?.line != null || start?.column != null;
if (exactIndex < 0 && hasPosition) return undefined;
const fallbackIndex =
exactIndex >= 0
? exactIndex
: messages.findIndex((_, index) => !usedMessageIndexes.has(index));
if (fallbackIndex < 0) return undefined;
usedMessageIndexes.add(fallbackIndex);
const message = messages[fallbackIndex]!;
return {
reason: message.reason,
ruleId: message.ruleId,
source: message.source,
};
}
function replayMarkdownKatexCacheMessage(
file: VFile,
target: { messagePosition?: MarkdownHastElement["position"] },
message: MarkdownKatexCacheMessage | undefined,
) {
if (!message) return;
file.message(message.reason, {
place: target.messagePosition as never,
ruleId: message.ruleId ?? undefined,
source: message.source ?? undefined,
});
}
function isMarkdownKatexMarker(node: MarkdownHastNode, id: string) {
return (
readStringProperty(
readHastElement(node)?.properties?.dataPretextKatexCacheMarker,
) === id
);
}
function isMarkdownKatexPlaceholder(node: MarkdownHastNode, id: string) {
return (
readStringProperty(
readHastElement(node)?.properties?.dataPretextKatexCachePlaceholder,
) === id
);
}
function readMarkdownKatexRenderCache(key: string) {
const entry = markdownKatexRenderCache.get(key);
if (!entry) return null;
markdownKatexRenderCache.delete(key);
markdownKatexRenderCache.set(key, entry);
return entry;
}
function writeMarkdownKatexRenderCache(
key: string,
entry: MarkdownKatexCacheEntry,
) {
markdownKatexRenderCache.set(key, entry);
markdownKatexRenderCacheStats.writes += 1;
while (markdownKatexRenderCache.size > MARKDOWN_KATEX_RENDER_CACHE_LIMIT) {
const oldestKey = markdownKatexRenderCache.keys().next().value;
if (!oldestKey) break;
markdownKatexRenderCache.delete(oldestKey);
}
}
function markdownKatexRenderCacheKey(match: MarkdownKatexMatch) {
return JSON.stringify([
MARKDOWN_KATEX_RENDER_CACHE_RENDERER,
MARKDOWN_KATEX_RENDER_CACHE_CONFIG,
match.mode,
match.displayMode,
match.source,
]);
}
function markdownKatexConfigKey(options: Record<string, unknown>) {
return JSON.stringify(
Object.fromEntries(
Object.entries(options).sort(([left], [right]) =>
left.localeCompare(right),
),
),
);
}
function extractMarkdownKatexText(node: MarkdownHastNode): string {
if (node.type === "text" && typeof node.value === "string") return node.value;
const element = readHastElement(node);
if (!element) return "";
return element.children.map(extractMarkdownKatexText).join("");
}
function cloneMarkdownHastNodes(nodes: readonly MarkdownHastNode[]) {
return nodes.map(cloneMarkdownHastNode);
}
function cloneMarkdownHastNode(node: MarkdownHastNode): MarkdownHastNode {
if (node.type === "text") {
return {
...node,
position: node.position
? cloneMarkdownPosition(node.position)
: undefined,
};
}
const element = readHastElement(node);
if (element) {
return {
...element,
children: cloneMarkdownHastNodes(element.children),
position: element.position
? cloneMarkdownPosition(element.position)
: undefined,
properties: element.properties
? cloneMarkdownProperties(element.properties)
: undefined,
};
}
const children = "children" in node ? node.children : undefined;
return {
...node,
children: children ? cloneMarkdownHastNodes(children) : undefined,
position: node.position ? cloneMarkdownPosition(node.position) : undefined,
};
}
function cloneMarkdownPosition(
position: NonNullable<MarkdownHastNode["position"]>,
) {
return {
end: position.end ? { ...position.end } : undefined,
start: position.start ? { ...position.start } : undefined,
};
}
function cloneMarkdownProperties(properties: Record<string, unknown>) {
const next: Record<string, unknown> = {};
for (const [key, value] of Object.entries(properties)) {
next[key] = Array.isArray(value) ? [...value] : value;
}
return next;
}
function readHastElement(node: unknown): MarkdownHastElement | null {
return node &&
typeof node === "object" &&
(node as MarkdownHastElement).type === "element"
? (node as MarkdownHastElement)
: null;
}
function hasArrayClassName(element: MarkdownHastElement, className: string) {
const value = element.properties?.className;
return Array.isArray(value) && value.includes(className);
}
function readStringProperty(value: unknown) {
if (typeof value === "string") return value;
if (Array.isArray(value)) return value.filter(Boolean).join(" ");
return "";
}
export function resetMarkdownMathRenderCacheForTests() {
markdownKatexRenderCache.clear();
markdownKatexRenderCacheStats.hits = 0;
markdownKatexRenderCacheStats.misses = 0;
markdownKatexRenderCacheStats.sameDocumentHits = 0;
markdownKatexRenderCacheStats.writes = 0;
}
export function getMarkdownMathRenderCacheStatsForTests() {
return {
...markdownKatexRenderCacheStats,
size: markdownKatexRenderCache.size,
};
}
function readGithubAlertMarker(paragraph: MarkdownMdastParagraph) {
const first = paragraph.children[0];
if (!first || first.type !== "text") return null;
const match =
/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][\t ]*(?:\r?\n)?/i.exec(
first.value ?? "",
);
if (!match) return null;
const kind = match[1]!.toLowerCase() as keyof typeof GITHUB_ALERT_LABELS;
first.value = (first.value ?? "").slice(match[0].length);
if (!first.value) {
paragraph.children.shift();
}
return {
kind,
title: GITHUB_ALERT_LABELS[kind],
};
}
function remarkMarkdownComponents() {
return function transform(tree: unknown, file: VFile) {
transformMarkdownComponentChildren(tree as MarkdownMdastRoot, file);
};
}
function remarkMarkdownDefinitionLists() {
return function transform(tree: unknown) {
const root = tree as MarkdownMdastRoot;
const nextChildren: MarkdownMdastNode[] = [];
for (const child of root.children) {
const definitionList = markdownDefinitionListFromParagraph(child);
nextChildren.push(definitionList ?? child);
}
root.children = nextChildren;
};
}
function markdownDefinitionListFromParagraph(
node: MarkdownMdastNode,
): MarkdownMdastNode | null {
if (node.type !== "paragraph" || !node.children?.length) return null;
const lines = splitMdastParagraphLines(node.children);
if (lines.length < 2) return null;
const term = trimMdastLine(lines[0] ?? []);
const definitions = lines
.slice(1)
.map(trimDefinitionLine)
.filter(
(definition): definition is MarkdownMdastNode[] => definition != null,
);
if (!term.length || !definitions.length) return null;
return {
type: "list",
data: {
hName: "dl",
hProperties: {
dataPretextDefinitionList: "",
},
},
children: [
{
type: "listItem",
data: {
hName: "dt",
hProperties: {
dataPretextDefinitionTerm: "",
},
},
children: term,
},
...definitions.map((definition) => ({
type: "listItem",
data: {
hName: "dd",
hProperties: {
dataPretextDefinitionDescription: "",
},
},
children: definition,
})),
],
};
}
function splitMdastParagraphLines(children: MarkdownMdastNode[]) {
const lines: MarkdownMdastNode[][] = [[]];
for (const child of children) {
if (child.type === "break") {
lines.push([]);
continue;
}
if (child.type === "text" && typeof child.value === "string") {
const parts = child.value.split(/\r?\n/);
parts.forEach((part, index) => {
if (index > 0) lines.push([]);
if (part) lines[lines.length - 1]!.push({ ...child, value: part });
});
continue;
}
lines[lines.length - 1]!.push(child);
}
return lines;
}
function trimMdastLine(line: MarkdownMdastNode[]) {
return trimMdastLineEnd(trimMdastLineStart(line));
}
function trimDefinitionLine(line: MarkdownMdastNode[]) {
const trimmed = trimMdastLine(line);
const first = trimmed[0];
if (
!first ||
first.type !== "text" ||
typeof first.value !== "string" ||
!first.value.startsWith(":")
) {
return null;
}
first.value = first.value.replace(/^:\s*/, "");
return trimMdastLine(trimmed);
}
function trimMdastLineStart(line: MarkdownMdastNode[]) {
const next = line.map((node) => ({ ...node }));
while (
next[0]?.type === "text" &&
typeof next[0].value === "string" &&
!next[0].value.trim()
) {
next.shift();
}
if (next[0]?.type === "text" && typeof next[0].value === "string") {
next[0].value = next[0].value.replace(/^\s+/, "");
if (!next[0].value) next.shift();
}
return next;
}
function trimMdastLineEnd(line: MarkdownMdastNode[]) {
const next = line.map((node) => ({ ...node }));
while (
next.at(-1)?.type === "text" &&
typeof next.at(-1)?.value === "string" &&
!String(next.at(-1)?.value).trim()
) {
next.pop();
}
const last = next.at(-1);
if (last?.type === "text" && typeof last.value === "string") {
last.value = last.value.replace(/\s+$/, "");
if (!last.value) next.pop();
}
return next;
}
function rehypeMarkdownSafeInputs() {
return function transform(tree: MarkdownHastRoot) {
removeUnsafeInputChildren(tree);
};
}
function rehypeMarkdownTrustedMetadata() {
return function transform(tree: MarkdownHastRoot) {
trustGeneratedMarkdownMetadata(tree);
};
}
function trustGeneratedMarkdownMetadata(
node: MarkdownHastElement | MarkdownHastRoot,
) {
for (const child of node.children) {
const element = child as MarkdownHastElement;
if (!element || element.type !== "element") continue;
if (hasMarkdownInternalMetadata(element)) {
if (element.position) {
stripMarkdownInternalMetadata(element);
} else if (hasMarkdownTrustedComponentMetadata(element)) {
element.properties ??= {};
element.properties.pretextComponentTrusted = true;
}
}
trustGeneratedMarkdownMetadata(element);
}
}
function hasMarkdownInternalMetadata(element: MarkdownHastElement) {
return Object.keys(element.properties ?? {}).some((key) =>
/^(?:dataPretextComponent|dataPretextCallout|dataFootnotes|dataFootnoteBackref|pretextComponentTrusted)/.test(
key,
),
);
}
function hasMarkdownTrustedComponentMetadata(element: MarkdownHastElement) {
return Object.keys(element.properties ?? {}).some((key) =>
/^(?:dataPretextComponent|dataPretextCallout|pretextComponentTrusted)/.test(
key,
),
);
}
function stripMarkdownInternalMetadata(element: MarkdownHastElement) {
for (const key of Object.keys(element.properties ?? {})) {
if (
/^(?:dataPretextComponent|data-pretext-component|dataPretextCallout|data-pretext-callout|dataFootnotes|data-footnotes|dataFootnoteBackref|data-footnote-backref|pretextComponentTrusted)/.test(
key,
)
) {
delete element.properties?.[key];
}
}
}
function removeUnsafeInputChildren(
parent: MarkdownHastElement | MarkdownHastRoot,
) {
if (!Array.isArray(parent.children)) return;
parent.children = parent.children.filter((child) => {
const element = child as MarkdownHastElement;
if (!element || element.type !== "element") return true;
if (element.tagName !== "input") return true;
return (
isHastElement(parent) &&
parent.tagName === "li" &&
hasClassName(parent, "task-list-item") &&
element.properties?.type === "checkbox"
);
});
for (const child of parent.children) {
const element = child as MarkdownHastElement;
if (element?.type === "element") removeUnsafeInputChildren(element);
}
}
function isHastElement(
node: MarkdownHastElement | MarkdownHastRoot,
): node is MarkdownHastElement {
return node.type === "element";
}
function hasClassName(element: MarkdownHastElement, className: string) {
const value = element.properties?.className;
return Array.isArray(value) && value.includes(className);
}
function transformMarkdownComponentChildren(
parent: MarkdownMdastNode,
file: VFile,
) {
const children = parent.children;
if (!children) return;
transformMarkdownHtmlContainers(parent, file);
for (let index = 0; index < children.length; index += 1) {
const child = children[index]!;
if (child.type === "html" && typeof child.value === "string") {
const component = parseMarkdownComponentHtml(child.value);
if (component) {
children[index] = createMarkdownComponentNode(component);
continue;
}
// CommonMark merges consecutive component tags (no blank line between)
// into one HTML block; split it so each tag renders as its own component
// (or its own fallback) instead of the whole run falling through to text.
const multiple = splitMarkdownComponentHtml(child.value, file);
if (multiple) {
children.splice(index, 1, ...multiple);
index += multiple.length - 1;
continue;
}
if (isMarkdownComponentHtml(child.value)) {
const reason = fallbackReasonForHtml(child.value);
emitMarkdownComponentFallbackMessage({
file,
node: child,
reason,
});
children[index] = createMarkdownComponentFallbackNode({
name: componentNameFromHtml(child.value) ?? "Component",
reason,
source: child.value.trim(),
});
continue;
}
}
const paragraphComponentSource = componentSourceFromParagraph(child, file);
if (paragraphComponentSource) {
const reason = fallbackReasonForHtml(paragraphComponentSource);
emitMarkdownComponentFallbackMessage({
file,
node: child,
reason,
});
children[index] = createMarkdownComponentFallbackNode({
name: componentNameFromHtml(paragraphComponentSource) ?? "Component",
reason,
source: paragraphComponentSource,
});
continue;
}
if (
child.type === "containerDirective" &&
isMarkdownContainerComponentName(readDirectiveName(child))
) {
transformMarkdownComponentChildren(child, file);
const component = parseMarkdownDirectiveComponent(child);
if (component) {
children[index] = createMarkdownComponentNode(component);
continue;
}
if (isMarkdownContainerComponentName(readDirectiveName(child))) {
const reason = "Unsupported component directive props";
emitMarkdownComponentFallbackMessage({
file,
node: child,
reason,
});
children[index] = createMarkdownComponentFallbackNode({
name:
componentNameForDirective(readDirectiveName(child)) ?? "Component",
reason,
source: directiveSourceForUnsafeComponent(child),
children: child.children ?? [],
});
continue;
}
}
if (
child.type === "containerDirective" &&
isCalloutKind(readDirectiveName(child))
) {
children[index] = createMarkdownCalloutNode(child);
continue;
}
if (child.type === "leafDirective" || child.type === "textDirective") {
const component = parseMarkdownDirectiveComponent(child);
if (component) {
children[index] = createMarkdownComponentNode(component);
continue;
}
if (componentNameForDirective(readDirectiveName(child))) {
const reason = "Unsupported component directive props";
emitMarkdownComponentFallbackMessage({
file,
node: child,
reason,
});
children[index] = createMarkdownComponentFallbackNode({
name:
componentNameForDirective(readDirectiveName(child)) ?? "Component",
reason,
source: directiveSourceForUnsafeComponent(child),
children:
child.type === "textDirective" ? (child.children ?? []) : [],
});
continue;
}
}
transformMarkdownComponentChildren(child, file);
}
}
function componentSourceFromParagraph(node: MarkdownMdastNode, file: VFile) {
if (node.type !== "paragraph") return null;
const nonWhitespaceChildren = (node.children ?? []).filter(
(child) => !(child.type === "text" && !String(child.value ?? "").trim()),
);
if (nonWhitespaceChildren.length !== 1) return null;
const onlyChild = nonWhitespaceChildren[0]!;
if (onlyChild.type !== "text" || typeof onlyChild.value !== "string") {
return null;
}
const source = sourceTextForMdastNode(node, file).trim() || onlyChild.value;
return isMarkdownComponentHtml(source) ? source : null;
}
function transformMarkdownHtmlContainers(
parent: MarkdownMdastNode,
file: VFile,
) {
const children = parent.children;
if (!children) return;
for (let index = 0; index < children.length; index += 1) {
const child = children[index]!;
if (child.type !== "html" || typeof child.value !== "string") continue;
const start = /^<([A-Z][A-Za-z0-9]*)\b([^>]*)>$/.exec(child.value.trim());
if (!start || !["Accordion", "Callout"].includes(start[1]!)) continue;
const name = start[1]!;
const closeIndex = children.findIndex(
(candidate, candidateIndex) =>
candidateIndex > index &&
candidate.type === "html" &&
typeof candidate.value === "string" &&
candidate.value.trim() === `</${name}>`,
);
if (closeIndex < 0) continue;
const propsText = start[2]!;
const source = child.value.trim();
const inner = children.slice(index + 1, closeIndex);
const component = hasEventHandlerAttribute(propsText)
? null
: parseMarkdownComponentProps(
name,
parseComponentAttributes(propsText),
inner,
);
if (!component) {
emitMarkdownComponentFallbackMessage({
file,
node: child,
reason: fallbackReasonForHtml(source),
});
}
children.splice(
index,
closeIndex - index + 1,
component
? createMarkdownComponentNode(component)
: createMarkdownComponentFallbackNode({
name,
reason: fallbackReasonForHtml(source),
source,
children: inner,
}),
);
}
}
function emitMarkdownComponentFallbackMessage({
file,
node,
reason,
}: {
file: VFile;
node: MarkdownMdastNode;
reason: string;
}) {
file.message(
new Error(reason),
markdownMessagePoint(node),
"markdown:component-fallback",
);
}
function markdownMessagePoint(node: MarkdownMdastNode) {
const point = node.position?.start;
return typeof point?.line === "number" && typeof point.column === "number"
? { column: point.column, line: point.line }
: null;
}
function createMarkdownComponentNode(
component: MarkdownComponent,
): MarkdownMdastNode {
return {
type: "pretextComponent",
data: {
hName: "div",
hProperties: {
dataPretextComponentName: component.name,
dataPretextComponentProps: JSON.stringify(component.props),
},
},
children: component.children ?? [],
};
}
function createMarkdownComponentFallbackNode({
children = [],
name,
reason,
source,
}: {
children?: MarkdownMdastNode[];
name: string;
reason: string;
source: string;
}): MarkdownMdastNode {
return {
type: "pretextComponentFallback",
data: {
hName: "div",
hProperties: {
dataPretextComponentFallback: "",
dataPretextComponentFallbackName: name,
dataPretextComponentFallbackReason: reason,
dataPretextComponentFallbackSource: source,
},
},
children: [{ type: "text", value: source }, ...children],
};
}
function createMarkdownCalloutNode(node: MarkdownMdastNode): MarkdownMdastNode {
const kind = calloutKind(readDirectiveName(node));
const attrs = readDirectiveAttributes(node) ?? {};
const title =
typeof attrs.title === "string" && attrs.title
? attrs.title
: calloutTitle(kind);
return {
type: "pretextCallout",
data: {
hName: "div",
hProperties: {
dataPretextCalloutKind: kind,
dataPretextCalloutTitle: title,
},
},
children: node.children ?? [],
};
}
// Splits an HTML block holding several self-closing component tags (only
// whitespace between them) into one node per tag: a component node when the tag
// parses, otherwise a fallback node — so one invalid tag never blanks the whole
// run. Returns null only when the block isn't entirely component tags (then
// normal HTML handling applies).
function splitMarkdownComponentHtml(value: string, file: VFile) {
const trimmed = value.trim();
// Quote-aware so `>` inside an attribute value doesn't end a tag early.
const tagPattern = /<[A-Z][A-Za-z0-9]*\b(?:[^>"']|"[^"]*"|'[^']*')*\/>/g;
const matches = [...trimmed.matchAll(tagPattern)];
if (matches.length < 2) return null;
let cursor = 0;
for (const match of matches) {
if (trimmed.slice(cursor, match.index).trim() !== "") return null;
cursor = match.index + match[0].length;
}
if (trimmed.slice(cursor).trim() !== "") return null;
// Only treat the block as components when every tag names a known component.
if (
!matches.every((match) => {
const name = componentNameFromHtml(match[0]);
return name !== null && isMarkdownLeafComponentName(name);
})
) {
return null;
}
return matches.map((match) => {
const tag = match[0];
const component = parseMarkdownComponentHtml(tag);
if (component) return createMarkdownComponentNode(component);
const reason = fallbackReasonForHtml(tag);
emitMarkdownComponentFallbackMessage({
file,
node: { type: "html", value: tag } as MarkdownMdastNode,
reason,
});
return createMarkdownComponentFallbackNode({
name: componentNameFromHtml(tag) ?? "Component",
reason,
source: tag,
});
});
}
function parseMarkdownComponentHtml(value: string) {
// Match exactly one self-closing tag: attribute chars are either non-quote/
// non-`>` or fully-quoted strings, so `>` inside an attribute (e.g. a mermaid
// `source="graph TD; A-->B"`) is allowed, while a multi-tag HTML block fails
// here and is handled by splitMarkdownComponentHtml.
const match = /^<([A-Z][A-Za-z0-9]*)\b((?:[^>"']|"[^"]*"|'[^']*')*)\/>$/.exec(
value.trim(),
);
if (!match) return null;
const name = match[1]!;
const propsText = match[2]!;
if (!isMarkdownLeafComponentName(name)) {
return null;
}
return parseMarkdownComponentProps(
name,
parseComponentAttributes(propsText),
[],
);
}
function parseMarkdownDirectiveComponent(node: MarkdownMdastNode) {
const name = componentNameForDirective(readDirectiveName(node));
if (!name) return null;
return parseMarkdownComponentProps(
name,
readDirectiveAttributes(node),
node.children ?? [],
);
}
function parseMarkdownComponentProps(
name: string,
props: Record<string, unknown> | null,
children: MarkdownMdastNode[],
): MarkdownComponent | null {
if (!props) return null;
if (Object.keys(props).some((key) => /^on/i.test(key))) return null;
if (name === "Diagram") {
if (props.type !== "mermaid" || typeof props.source !== "string")
return null;
return {
name,
props: {
caption: readPropString(props.caption),
source: normalizeMarkdownDiagramSource(props.source),
title: readPropString(props.title),
type: "mermaid",
},
};
}
if (name === "Metric") {
if (!props.label || !props.value || props.tone) return null;
return {
name,
props: {
label: readPropString(props.label),
value: readPropString(props.value),
},
};
}
if (name === "Badge") {
const label = readPropString(props.label) || mdastText(children);
const tone = readPropString(props.tone);
if (!label || (tone && !["default", "success", "warning"].includes(tone))) {
return null;
}
return { name, props: { label, tone } };
}
if (name === "Image") {
if (!props.src || !props.alt) return null;
return {
name,
props: {
alt: readPropString(props.alt),
height: readPropString(props.height),
src: readPropString(props.src),
title: readPropString(props.title),
width: readPropString(props.width),
},
};
}
if (name === "Video") {
if (!props.src || !props.label) return null;
return {
name,
props: {
controls: readPropString(props.controls),
label: readPropString(props.label),
loop: readPropString(props.loop),
muted: readPropString(props.muted),
src: readPropString(props.src),
title: readPropString(props.title),
},
};
}
if (name === "Accordion") {
if (!props.title) return null;
return { name, props: { title: readPropString(props.title) }, children };
}
if (name === "Callout") {
const kind = calloutKind(readPropString(props.kind) || "note");
return {
name,
props: {
kind,
title: readPropString(props.title) || calloutTitle(kind),
},
children,
};
}
if (name === "Tabs" || name === "Tab") {
return {
name,
props: {
label: readPropString(props.label),
title: readPropString(props.title),
},
children,
};
}
return null;
}
function normalizeMarkdownDiagramSource(source: string) {
return source
.split(/;\s*/)
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
}
function readDirectiveName(node: MarkdownMdastNode) {
const directive = node as unknown as { name?: unknown };
return typeof directive.name === "string" ? directive.name : "";
}
function readDirectiveAttributes(node: MarkdownMdastNode) {
const attributes = (node as { attributes?: unknown }).attributes;
return attributes && typeof attributes === "object"
? (attributes as Record<string, unknown>)
: null;
}
function parseQuotedAttributes(value: string): Record<string, string> {
return Object.fromEntries(
Array.from(value.matchAll(/\s*([A-Za-z][A-Za-z0-9_]*)="([^"]*)"/g)).map(
(item) => [item[1]!, item[2]!],
),
);
}
function parseComponentAttributes(value: string) {
if (/\{\s*\.\.\./.test(value)) return null;
const attributes: Record<string, string> = {};
const consumed: Array<[number, number]> = [];
for (const match of value.matchAll(
/\s+([A-Za-z][A-Za-z0-9_]*)=(?:"([^"]*)"|\{(\d+(?:\.\d+)?|true|false)\})/g,
)) {
attributes[match[1]!] = match[2] ?? match[3] ?? "";
consumed.push([match.index ?? 0, (match.index ?? 0) + match[0].length]);
}
for (const match of value.matchAll(/\s+([A-Za-z][A-Za-z0-9_]*)(?=\s|$)/g)) {
const index = match.index ?? 0;
if (consumed.some(([start, end]) => index >= start && index < end)) {
continue;
}
attributes[match[1]!] = "true";
consumed.push([index, index + match[0].length]);
}
if (removeRanges(value, consumed).trim()) return null;
return attributes;
}
function removeRanges(value: string, ranges: Array<[number, number]>) {
let result = "";
let offset = 0;
for (const [start, end] of ranges.sort((a, b) => a[0] - b[0])) {
result += value.slice(offset, start);
offset = end;
}
return result + value.slice(offset);
}
function readPropString(value: unknown) {
return typeof value === "string" ? value : "";
}
function mdastText(nodes: readonly MarkdownMdastNode[]): string {
return nodes
.map((node) =>
typeof node.value === "string"
? node.value
: mdastText(node.children ?? []),
)
.join("");
}
function componentNameForDirective(name: string) {
const normalized = name.toLowerCase();
const names: Record<string, string> = {
accordion: "Accordion",
badge: "Badge",
callout: "Callout",
diagram: "Diagram",
image: "Image",
metric: "Metric",
tab: "Tab",
tabs: "Tabs",
video: "Video",
};
return names[normalized] ?? null;
}
function isMarkdownLeafComponentName(name: string) {
return ["Badge", "Diagram", "Image", "Metric", "Video"].includes(name);
}
function isMarkdownContainerComponentName(name: string) {
return Boolean(componentNameForDirective(name));
}
function isMarkdownComponentHtml(value: string) {
return /^<\/?[A-Z][A-Za-z0-9.]*(?:\b|\.)/.test(value.trim());
}
function componentNameFromHtml(value: string) {
return /^<\/?([A-Z][A-Za-z0-9.]*)/.exec(value.trim())?.[1] ?? null;
}
function hasEventHandlerAttribute(value: string) {
return /\son[A-Za-z]+\s*=/.test(value);
}
function fallbackReasonForHtml(value: string) {
const name = componentNameFromHtml(value);
if (name?.includes(".")) {
return "Remote or namespaced components are not supported";
}
if (!name || !componentNameForDirective(name)) return "Unsupported component";
if (hasEventHandlerAttribute(value))
return "Event handler props are not supported";
if (value.includes("{")) return "Component props must be literal values";
return "Unsupported component";
}
function sourceTextForMdastNode(node: MarkdownMdastNode, file: VFile) {
const value = String(file.value ?? "");
const start = node.position?.start?.offset;
const end = node.position?.end?.offset;
if (
typeof start !== "number" ||
typeof end !== "number" ||
start < 0 ||
end < start
) {
return "";
}
return value.slice(start, end);
}
function isCalloutKind(name: string) {
return ["caution", "important", "note", "success", "tip", "warning"].includes(
name.toLowerCase(),
);
}
function calloutKind(value: string) {
const normalized = value.toLowerCase();
if (normalized === "success") return "tip";
if (isCalloutKind(normalized)) return normalized;
return "note";
}
function calloutTitle(kind: string) {
const titles: Record<string, string> = {
caution: "Caution",
important: "Important",
note: "Note",
tip: "Tip",
warning: "Warning",
};
return titles[kind] ?? "Note";
}
function directiveSourceForUnsafeComponent(node: MarkdownMdastNode) {
const name = readDirectiveName(node);
const attrs = readDirectiveAttributes(node) ?? {};
const attrSource = Object.entries(attrs)
.map(([key, value]) => `${key}="${String(value)}"`)
.join(" ");
const marker = node.type === "textDirective" ? ":" : "::";
return `${marker}${name}${attrSource ? `{${attrSource}}` : ""}`;
}
registry/new-york-v4/ui/markdown-url-policy.ts
"use client";
const URL_CONTROL_CHARACTER_PATTERN = /[\u0000-\u001F\u007F]/;
const URL_SCHEME_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):/;
const URL_BACKSLASH_PATTERN = /[\\\uFE68\uFF3C\u2216]/;
const URL_CONFUSABLE_DELIMITER_PATTERN =
/[\u02D0\u0589\u05C3\u0703\u0704\u16EC\u1803\u1809\u205A\u2236\uA789\uFE13\uFE55\uFF1A\u2044\u2215\u2571\u27CB\u29F8\uFF0F]/;
const ALLOWED_LINK_PROTOCOLS = new Set(["http", "https", "mailto"]);
export function sanitizeMarkdownUrl(value: string) {
const trimmed = value.trim();
if (!trimmed) return "";
if (URL_CONTROL_CHARACTER_PATTERN.test(trimmed)) return "";
if (URL_BACKSLASH_PATTERN.test(trimmed)) return "";
if (URL_CONFUSABLE_DELIMITER_PATTERN.test(trimmed)) return "";
const decoded = decodeMarkdownUrl(trimmed).trim();
if (!decoded || URL_CONTROL_CHARACTER_PATTERN.test(decoded)) return "";
if (URL_BACKSLASH_PATTERN.test(decoded)) return "";
if (URL_CONFUSABLE_DELIMITER_PATTERN.test(decoded)) return "";
const decodedScheme = getMarkdownUrlScheme(decoded);
const rawScheme = getMarkdownUrlScheme(trimmed);
if (decodedScheme && decodedScheme !== rawScheme) return "";
if (decodedScheme && !ALLOWED_LINK_PROTOCOLS.has(decodedScheme)) return "";
if (trimmed.startsWith("#")) return trimmed;
if (trimmed.startsWith("/")) return trimmed.startsWith("//") ? "" : trimmed;
try {
const url = new URL(trimmed, "https://retab.local");
if (ALLOWED_LINK_PROTOCOLS.has(url.protocol.slice(0, -1))) {
return rawScheme ? url.href : trimmed;
}
} catch {
return "";
}
return "";
}
export function sanitizeMarkdownImageUrl(value: string) {
const safeUrl = sanitizeMarkdownUrl(value);
if (!safeUrl || safeUrl.startsWith("mailto:") || safeUrl.startsWith("#")) {
return "";
}
if (isMarkdownSvgResourceUrl(safeUrl)) return "";
return safeUrl;
}
export function sanitizeMarkdownMediaUrl(value: string) {
// Image URLs already reject SVG resources, so media inherits that policy.
return sanitizeMarkdownImageUrl(value);
}
function decodeMarkdownUrl(value: string) {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function getMarkdownUrlScheme(value: string) {
return URL_SCHEME_PATTERN.exec(value)?.[1]?.toLowerCase() ?? null;
}
export function isMarkdownSvgResourceUrl(value: string) {
const decoded = decodeMarkdownUrl(value).trim();
try {
const url = new URL(decoded, "https://retab.local");
return /\.(?:svg|svgz)$/i.test(url.pathname);
} catch {
const pathname = decoded.split(/[?#]/, 1)[0] ?? decoded;
return /\.(?:svg|svgz)$/i.test(pathname);
}
}
registry/new-york-v4/ui/text-viewer-virtualization.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
const DEFAULT_VIEWPORT_HEIGHT = 600;
const DEFAULT_VIEWPORT_WIDTH = 800;
const DEFAULT_OVERSCAN = 6;
const MAX_VIRTUAL_ITEMS = 500;
export interface TextVirtualItem {
index: number;
key: React.Key;
start: number;
size: number;
end: number;
}
export interface TextVirtualOffsets {
starts: number[];
totalSize: number;
}
export interface TextVirtualViewport {
scrollTop: number;
clientHeight: number;
clientWidth: number;
}
export interface TextScrollAnchor {
index: number;
offsetWithinLine: number;
}
export interface TextFrameVirtualItem {
end: number;
index: number;
key: React.Key;
size: number;
start: number;
}
export interface TextFrameGeometry {
bottom: number;
height: number;
top: number;
}
export interface TextFrameScrollAnchor {
index: number;
offsetWithinFrame: number;
}
export interface TextInverseStickyWindow {
afterHeight: number;
beforeHeight: number;
renderedBottom: number;
renderedHeight: number;
renderedTop: number;
stickyOffset: number;
}
export function buildTextVirtualOffsets({
itemSizes,
paddingStart = 0,
paddingEnd = 0,
}: {
itemSizes: readonly number[];
paddingStart?: number;
paddingEnd?: number;
}): TextVirtualOffsets {
const starts: number[] = [];
let offset = safeSize(paddingStart);
for (const itemSize of itemSizes) {
starts.push(offset);
offset += safeSize(itemSize);
}
return {
starts,
totalSize: offset + safeSize(paddingEnd),
};
}
export function getTextVirtualItems({
itemSizes,
offsets,
scrollTop,
viewportHeight,
overscan = DEFAULT_OVERSCAN,
}: {
itemSizes: readonly number[];
offsets: TextVirtualOffsets;
scrollTop: number;
viewportHeight: number;
overscan?: number;
}): TextVirtualItem[] {
const count = itemSizes.length;
if (count === 0) return [];
const safeScrollTop = safeOffset(scrollTop);
const safeViewportHeight = Math.max(1, safeSize(viewportHeight));
const safeOverscan = safeCount(overscan);
const visibleStartIndex = findFirstItemEndingAfter({
itemSizes,
starts: offsets.starts,
offset: safeScrollTop,
});
const visibleEndExclusive = findFirstItemStartingAtOrAfter({
starts: offsets.starts,
offset: safeScrollTop + safeViewportHeight,
});
const start = Math.max(0, visibleStartIndex - safeOverscan);
const end = Math.min(
count,
Math.max(visibleStartIndex + 1, visibleEndExclusive) + safeOverscan,
);
const cappedEnd = Math.min(count, start + MAX_VIRTUAL_ITEMS);
return Array.from(
{ length: Math.min(end, cappedEnd) - start },
(_, localIndex) => {
const index = start + localIndex;
const size = safeSize(itemSizes[index]);
const itemStart = offsets.starts[index] ?? 0;
return {
index,
key: index,
start: itemStart,
size,
end: itemStart + size,
};
},
);
}
export function textScrollTopForItem({
itemIndex,
itemSizes,
offsets,
viewportHeight,
align = "center",
}: {
itemIndex: number;
itemSizes: readonly number[];
offsets: TextVirtualOffsets;
viewportHeight: number;
align?: "start" | "center" | "end";
}) {
if (!Number.isSafeInteger(itemIndex) || itemIndex < 0) return 0;
const itemStart = offsets.starts[itemIndex];
if (itemStart == null) return 0;
const itemSize = safeSize(itemSizes[itemIndex]);
const safeViewportHeight = safeSize(viewportHeight);
if (align === "end") {
return Math.max(0, itemStart - safeViewportHeight + itemSize);
}
if (align === "center") {
return Math.max(0, itemStart - safeViewportHeight / 2 + itemSize / 2);
}
return Math.max(0, itemStart);
}
export function getTextScrollAnchor({
itemSizes,
offsets,
scrollTop,
}: {
itemSizes: readonly number[];
offsets: Pick<TextVirtualOffsets, "starts">;
scrollTop: number;
}): TextScrollAnchor | null {
if (itemSizes.length === 0) return null;
const safeScrollTop = safeOffset(scrollTop);
let low = 0;
let high = itemSizes.length - 1;
let index = 0;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const start = offsets.starts[mid] ?? 0;
const end = start + safeSize(itemSizes[mid]);
if (end > safeScrollTop) {
index = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return {
index,
offsetWithinLine: Math.max(0, safeScrollTop - (offsets.starts[index] ?? 0)),
};
}
export function useTextVariableVirtualizer({
itemSizes,
overscan = DEFAULT_OVERSCAN,
paddingStart = 0,
paddingEnd = 0,
scrollRef,
}: {
itemSizes: readonly number[];
overscan?: number;
paddingStart?: number;
paddingEnd?: number;
scrollRef: React.RefObject<HTMLElement | null>;
}) {
const viewport = useTextVirtualViewport(scrollRef);
const offsets = React.useMemo(
() => buildTextVirtualOffsets({ itemSizes, paddingStart, paddingEnd }),
[itemSizes, paddingEnd, paddingStart],
);
const viewportHeight = viewport.clientHeight || DEFAULT_VIEWPORT_HEIGHT;
const viewportWidth = viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH;
const virtualItems = React.useMemo(
() =>
getTextVirtualItems({
itemSizes,
offsets,
overscan,
scrollTop: viewport.scrollTop,
viewportHeight,
}),
[itemSizes, offsets, overscan, viewport.scrollTop, viewportHeight],
);
return {
offsets,
totalSize: offsets.totalSize,
viewportHeight,
viewportWidth,
virtualItems,
};
}
export function getTextFrameVirtualItems({
frames,
maxItems = MAX_VIRTUAL_ITEMS,
overscanPx = 0,
scrollTop,
viewportHeight,
}: {
frames: readonly TextFrameGeometry[];
maxItems?: number;
overscanPx?: number;
scrollTop: number;
viewportHeight: number;
}): TextFrameVirtualItem[] {
if (frames.length === 0) return [];
const safeScrollTop = safeOffset(scrollTop);
const safeViewportHeight = Math.max(1, safeSize(viewportHeight));
const safeOverscanPx = safeSize(overscanPx);
const visibleStart = findFirstFrameEndingAfter(frames, safeScrollTop);
const visibleEnd = Math.max(
visibleStart + 1,
findFirstFrameStartingAtOrAfter(
frames,
safeScrollTop + safeViewportHeight,
visibleStart,
),
);
const start = findFirstFrameEndingAfter(
frames,
Math.max(0, safeScrollTop - safeOverscanPx),
);
const end = Math.max(
visibleEnd,
findFirstFrameStartingAtOrAfter(
frames,
safeScrollTop + safeViewportHeight + safeOverscanPx,
visibleStart,
),
);
const safeMaxItems = Math.max(visibleEnd - start, safeCount(maxItems));
const cappedEnd = Math.min(frames.length, end, start + safeMaxItems);
return Array.from({ length: cappedEnd - start }, (_, localIndex) => {
const index = start + localIndex;
const frame = frames[index]!;
return {
end: frame.bottom,
index,
key: index,
size: safeSize(frame.height),
start: frame.top,
};
});
}
export function getTextInverseStickyWindow({
renderedBottom,
renderedTop,
totalHeight,
viewportHeight,
}: {
renderedBottom: number;
renderedTop: number;
totalHeight: number;
viewportHeight: number;
}): TextInverseStickyWindow {
const safeTotalHeight = safeOffset(totalHeight);
const safeViewportHeight = Math.max(1, safeSize(viewportHeight));
const top = clamp(safeOffset(renderedTop), 0, safeTotalHeight);
const bottom = clamp(
Math.max(top, safeOffset(renderedBottom)),
top,
safeTotalHeight,
);
const renderedHeight = bottom - top;
return {
afterHeight: Math.max(0, safeTotalHeight - bottom),
beforeHeight: top,
renderedBottom: bottom,
renderedHeight,
renderedTop: top,
stickyOffset: -Math.max(0, renderedHeight - safeViewportHeight),
};
}
export function getTextFrameScrollAnchor({
frames,
scrollTop,
}: {
frames: readonly TextFrameGeometry[];
scrollTop: number;
}): TextFrameScrollAnchor | null {
if (frames.length === 0) return null;
const safeScrollTop = safeOffset(scrollTop);
const index = findFirstFrameEndingAfter(frames, safeScrollTop);
const frame = frames[index] ?? frames[frames.length - 1];
if (!frame) return null;
return {
index,
offsetWithinFrame: Math.max(0, safeScrollTop - frame.top),
};
}
export function useTextVirtualViewport(
scrollRef: React.RefObject<HTMLElement | null>,
): TextVirtualViewport {
const [viewport, setViewport] = React.useState<TextVirtualViewport>({
scrollTop: 0,
clientHeight: 0,
clientWidth: 0,
});
useKeyedLayoutEffect(joinEffectKey([scrollRef]), () => {
const scrollElement = scrollRef.current;
if (!scrollElement) return;
let frame = 0;
const readViewport = () => {
frame = 0;
const next = {
scrollTop: safeOffset(scrollElement.scrollTop),
clientHeight: safeSize(scrollElement.clientHeight),
clientWidth: safeSize(scrollElement.clientWidth),
};
setViewport((current) =>
current.scrollTop === next.scrollTop &&
current.clientHeight === next.clientHeight &&
current.clientWidth === next.clientWidth
? current
: next,
);
};
const scheduleRead = () => {
if (frame) return;
frame = requestAnimationFrame(readViewport);
};
readViewport();
scrollElement.addEventListener("scroll", scheduleRead, { passive: true });
const observer =
typeof ResizeObserver !== "undefined"
? new ResizeObserver(scheduleRead)
: null;
observer?.observe(scrollElement);
return () => {
if (frame) cancelAnimationFrame(frame);
scrollElement.removeEventListener("scroll", scheduleRead);
observer?.disconnect();
};
});
return viewport;
}
function findFirstFrameEndingAfter(
frames: readonly TextFrameGeometry[],
offset: number,
) {
let low = 0;
let high = frames.length - 1;
let result = frames.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if ((frames[mid]?.bottom ?? 0) > offset) {
result = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return result;
}
function findFirstFrameStartingAtOrAfter(
frames: readonly TextFrameGeometry[],
offset: number,
start: number,
) {
let low = Math.max(0, start);
let high = frames.length - 1;
let result = frames.length;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if ((frames[mid]?.top ?? 0) >= offset) {
result = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return result;
}
function findFirstItemEndingAfter({
itemSizes,
starts,
offset,
}: {
itemSizes: readonly number[];
starts: readonly number[];
offset: number;
}) {
let low = 0;
let high = itemSizes.length - 1;
let result = itemSizes.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const end = (starts[mid] ?? 0) + safeSize(itemSizes[mid]);
if (end > offset) {
result = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return result;
}
function findFirstItemStartingAtOrAfter({
starts,
offset,
}: {
starts: readonly number[];
offset: number;
}) {
let low = 0;
let high = starts.length - 1;
let result = starts.length;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if ((starts[mid] ?? 0) >= offset) {
result = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return result;
}
function safeCount(value: number) {
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
}
function safeOffset(value: number) {
return Number.isFinite(value) && value > 0 ? value : 0;
}
function safeSize(value: number | undefined) {
return Number.isFinite(value) && value != null && 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/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);
}
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-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/markdown-greenfield-code-highlight-html.ts
import type { CodeTokenLeaf } from "./code-viewer-syntax-protocol";
export function markdownCodeTokensToHtml({
highlightPattern,
line,
tokens,
}: {
highlightPattern: string;
line: string;
tokens: readonly CodeTokenLeaf[] | null;
}) {
if (!tokens) return null;
if (!tokens.length) return " ";
const highlightStart = highlightPattern ? line.indexOf(highlightPattern) : -1;
const highlightEnd =
highlightStart >= 0 ? highlightStart + highlightPattern.length : -1;
let cursor = 0;
let html = "";
for (const token of tokens) {
const tokenStart = cursor;
const tokenEnd = cursor + token.text.length;
cursor = tokenEnd;
html += tokenToHtml({
highlightEnd,
highlightStart,
token,
tokenEnd,
tokenStart,
});
}
return html || " ";
}
function tokenToHtml({
highlightEnd,
highlightStart,
token,
tokenEnd,
tokenStart,
}: {
highlightEnd: number;
highlightStart: number;
token: CodeTokenLeaf;
tokenEnd: number;
tokenStart: number;
}) {
const inner = tokenTextToHtml({
highlightEnd,
highlightStart,
text: token.text,
textEnd: tokenEnd,
textStart: tokenStart,
});
if (!token.kind || !/^[a-z-]+$/.test(token.kind)) return inner;
return `<span class="cv-token-${token.kind}" data-pretext-code-token="${token.kind}">${inner}</span>`;
}
function tokenTextToHtml({
highlightEnd,
highlightStart,
text,
textEnd,
textStart,
}: {
highlightEnd: number;
highlightStart: number;
text: string;
textEnd: number;
textStart: number;
}) {
if (
highlightStart < 0 ||
textEnd <= highlightStart ||
textStart >= highlightEnd
) {
return escapeHtml(text);
}
const before = text.slice(0, Math.max(0, highlightStart - textStart));
const highlighted = text.slice(
Math.max(0, highlightStart - textStart),
Math.min(text.length, highlightEnd - textStart),
);
const after = text.slice(Math.min(text.length, highlightEnd - textStart));
return [
escapeHtml(before),
highlighted
? `<span data-highlighted-chars="">${escapeHtml(highlighted)}</span>`
: "",
escapeHtml(after),
].join("");
}
function escapeHtml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
registry/new-york-v4/ui/markdown-greenfield-code-highlight-protocol.ts
export const MARKDOWN_CODE_HIGHLIGHT_BATCH_SIZE = 64;
export const MARKDOWN_CODE_HIGHLIGHT_CACHE_LIMIT = 4096;
export const MARKDOWN_CODE_HIGHLIGHT_RENDERER_VERSION = 1;
export type MarkdownCodeHighlightLineRequest = {
index: number;
line: string;
};
export type MarkdownCodeHighlightLineResult = {
html: string | null;
index: number;
};
export type MarkdownCodeHighlightWorkerRequest = {
generation: number;
highlightPattern: string;
languageId: string;
lines: MarkdownCodeHighlightLineRequest[];
requestId: number;
type: "highlight";
};
export type MarkdownCodeHighlightWorkerResponse =
| {
generation: number;
languageId: string;
requestId: number;
results: MarkdownCodeHighlightLineResult[];
type: "highlighted";
}
| {
generation: number;
languageId: string;
message: string;
requestId: number;
type: "error";
};
registry/new-york-v4/ui/markdown-greenfield-code-highlight.worker.ts
import { shouldTokenizeCodeLine } from "./code-viewer-syntax-protocol";
import {
ensureCodePrismLanguage,
tokenizeCodeLine,
} from "./code-viewer-syntax-prism";
import { markdownCodeTokensToHtml } from "./markdown-greenfield-code-highlight-html";
import type {
MarkdownCodeHighlightWorkerRequest,
MarkdownCodeHighlightWorkerResponse,
} from "./markdown-greenfield-code-highlight-protocol";
const workerSelf = self as unknown as {
onmessage:
| ((event: MessageEvent<MarkdownCodeHighlightWorkerRequest>) => void)
| null;
postMessage(message: MarkdownCodeHighlightWorkerResponse): void;
};
function post(message: MarkdownCodeHighlightWorkerResponse) {
workerSelf.postMessage(message);
}
async function highlightInWorker(request: MarkdownCodeHighlightWorkerRequest) {
await ensureCodePrismLanguage(request.languageId);
post({
generation: request.generation,
languageId: request.languageId,
requestId: request.requestId,
results: request.lines.map(({ index, line }) => ({
html: shouldTokenizeCodeLine(line)
? markdownCodeTokensToHtml({
highlightPattern: request.highlightPattern,
line,
tokens: tokenizeCodeLine(request.languageId, line),
})
: null,
index,
})),
type: "highlighted",
});
}
workerSelf.onmessage = (
event: MessageEvent<MarkdownCodeHighlightWorkerRequest>,
) => {
const request = event.data;
if (request.type !== "highlight") return;
void highlightInWorker(request).catch((error) => {
post({
generation: request.generation,
languageId: request.languageId,
message: error instanceof Error ? error.message : String(error),
requestId: request.requestId,
type: "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/mermaid-renderer.ts
const MERMAID_MAX_LINES = 160;
const MERMAID_MAX_SOURCE_LENGTH = 12_000;
const MERMAID_CACHE_LIMIT = 64;
const MERMAID_SCOPED_CACHE_LIMIT = 128;
const MMDR_VERSION = "0.2.2";
const MMDR_WASM_URL = "/vendor/mmdr/typst_mmdr.wasm";
const MMDR_BASE_THEME = "modern";
const MMDR_LAYOUT = "";
const MMDR_THEME = {
background: "transparent",
cluster_bkg: "transparent",
cluster_border: "var(--mmdr-border)",
edge_label_background: "transparent",
font_family: "Inter, ui-sans-serif, system-ui, sans-serif",
font_size: 16,
line_color: "var(--mmdr-line)",
primary_border_color: "var(--mmdr-border)",
primary_color: "var(--mmdr-node-fill)",
primary_text_color: "var(--mmdr-text)",
secondary_color: "var(--mmdr-node-fill)",
tertiary_color: "var(--mmdr-node-fill)",
} as const;
const MMDR_THEME_JSON = JSON.stringify(MMDR_THEME);
const MERMAID_CONFIG = {
flowchart: {
htmlLabels: false,
useMaxWidth: true,
},
securityLevel: "strict",
sequence: {
useMaxWidth: true,
},
startOnLoad: false,
suppressErrorRendering: true,
theme: "base",
themeVariables: {
background: "transparent",
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
lineColor: "#64748b",
primaryBorderColor: "#94a3b8",
primaryColor: "#f8fafc",
primaryTextColor: "#0f172a",
secondaryColor: "#ecfeff",
tertiaryColor: "#f0fdf4",
},
} as const;
const DIAGRAM_CACHE_CONFIG_KEY = JSON.stringify({
mermaid: MERMAID_CONFIG,
mmdr: {
baseTheme: MMDR_BASE_THEME,
theme: MMDR_THEME,
version: MMDR_VERSION,
},
});
const MERMAID_CACHE_CONFIG_KEY = JSON.stringify(MERMAID_CONFIG);
export const MERMAID_VIEWER_STYLES = `
[data-pretext-mermaid-svg] {
--mmdr-border: var(--border);
--mmdr-line: var(--muted-foreground);
--mmdr-node-fill: color-mix(in srgb, var(--background) 88%, var(--foreground) 12%);
--mmdr-text: var(--foreground);
}
[data-pretext-mermaid-svg] svg {
color: var(--foreground);
display: block;
height: auto;
margin-inline: auto;
max-width: 100%;
}
[data-pretext-mermaid-svg] rect.background {
fill: transparent;
stroke: none;
}
[data-pretext-mermaid-svg] .node rect,
[data-pretext-mermaid-svg] .node polygon,
[data-pretext-mermaid-svg] .node circle,
[data-pretext-mermaid-svg] .node ellipse,
[data-pretext-mermaid-svg] .node path,
[data-pretext-mermaid-svg] .basic.label-container,
[data-pretext-mermaid-svg] .label-container,
[data-pretext-mermaid-svg] .actor,
[data-pretext-mermaid-svg] .entityBox,
[data-pretext-mermaid-svg] .state,
[data-pretext-mermaid-svg] .classGroup rect,
[data-pretext-mermaid-svg] .requirement,
[data-pretext-mermaid-svg] .requirementBox,
[data-pretext-mermaid-svg] .element,
[data-pretext-mermaid-svg] .cluster rect,
[data-pretext-mermaid-svg] .note,
[data-pretext-mermaid-svg] .task {
fill: color-mix(in srgb, var(--background) 88%, var(--foreground) 12%);
stroke: var(--border);
stroke-width: 1px;
}
[data-pretext-mermaid-svg] .cluster rect,
[data-pretext-mermaid-svg] .section,
[data-pretext-mermaid-svg] .grid .tick line {
fill: color-mix(in srgb, var(--background) 94%, var(--foreground) 6%);
stroke: var(--border);
}
[data-pretext-mermaid-svg] text,
[data-pretext-mermaid-svg] tspan,
[data-pretext-mermaid-svg] .label,
[data-pretext-mermaid-svg] .label text,
[data-pretext-mermaid-svg] .nodeLabel,
[data-pretext-mermaid-svg] .edgeLabel,
[data-pretext-mermaid-svg] .actor,
[data-pretext-mermaid-svg] .messageText,
[data-pretext-mermaid-svg] .noteText,
[data-pretext-mermaid-svg] .taskText,
[data-pretext-mermaid-svg] .sectionTitle,
[data-pretext-mermaid-svg] .titleText {
color: var(--foreground);
fill: var(--foreground);
}
[data-pretext-mermaid-svg] .edgePath path,
[data-pretext-mermaid-svg] .flowchart-link,
[data-pretext-mermaid-svg] .messageLine0,
[data-pretext-mermaid-svg] .messageLine1,
[data-pretext-mermaid-svg] .actor-line,
[data-pretext-mermaid-svg] .relation,
[data-pretext-mermaid-svg] .transition,
[data-pretext-mermaid-svg] line {
fill: none;
stroke: var(--muted-foreground);
}
[data-pretext-mermaid-svg] marker path,
[data-pretext-mermaid-svg] marker polygon,
[data-pretext-mermaid-svg] marker circle,
[data-pretext-mermaid-svg] .marker,
[data-pretext-mermaid-svg] .arrowMarkerPath {
fill: var(--muted-foreground);
stroke: var(--muted-foreground);
}
`;
const mermaidDiagramCache = new Map<string, Promise<DiagramState>>();
const mermaidScopedSvgCache = new Map<string, string>();
type MermaidApi = {
initialize?: (config: typeof MERMAID_CONFIG) => void;
render: (id: string, source: string) => Promise<{ svg: string }>;
};
type MmdrApi = {
render: (source: string) => string;
};
type MmdrExports = {
memory: WebAssembly.Memory;
render: (
sourceLength: number,
baseThemeLength: number,
themeLength: number,
layoutLength: number,
) => number;
};
type MermaidRenderJob = {
reject: (reason: unknown) => void;
resolve: (state: DiagramState) => void;
run: () => Promise<DiagramState>;
};
type MermaidIdleWindow = Window &
typeof globalThis & {
requestIdleCallback?: (
callback: () => void,
options?: { timeout?: number },
) => number;
};
let mermaidApiPromise: Promise<MermaidApi> | null = null;
let mmdrApiPromise: Promise<MmdrApi> | null = null;
let isMmdrUnavailable = false;
let mermaidInitializedConfigKey = "";
let isMermaidRenderQueueRunning = false;
const mermaidRenderQueue: MermaidRenderJob[] = [];
export type DiagramRenderer = "basic" | "mermaid" | "mmdr";
export type DiagramState =
| { status: "failed"; message: string }
| { status: "loading" }
| { renderer: DiagramRenderer; status: "ready"; svg: string };
type ReadyDiagramState = Extract<DiagramState, { status: "ready" }>;
export function resetMermaidRendererForTests() {
mermaidDiagramCache.clear();
mermaidScopedSvgCache.clear();
mermaidRenderQueue.length = 0;
mermaidApiPromise = null;
mmdrApiPromise = null;
isMmdrUnavailable = false;
mermaidInitializedConfigKey = "";
isMermaidRenderQueueRunning = false;
}
export async function renderDiagram(
source: string,
id: string,
): Promise<DiagramState> {
const state = await getCachedDiagram(source);
if (state.status !== "ready") return state;
const scopedKey = `${DIAGRAM_CACHE_CONFIG_KEY}\0${state.renderer}\0${id}\0${source}`;
let svg = mermaidScopedSvgCache.get(scopedKey);
if (!svg) {
svg = scopeCachedMermaidSvg(state.svg, id);
mermaidScopedSvgCache.set(scopedKey, svg);
trimMapToLimit(mermaidScopedSvgCache, MERMAID_SCOPED_CACHE_LIMIT);
}
return { renderer: state.renderer, status: "ready", svg };
}
function getCachedDiagram(source: string) {
const key = `${DIAGRAM_CACHE_CONFIG_KEY}\0${source}`;
let cached = mermaidDiagramCache.get(key);
if (!cached) {
cached = enqueueMermaidRender(() => loadDiagram(source));
mermaidDiagramCache.set(key, cached);
trimMapToLimit(mermaidDiagramCache, MERMAID_CACHE_LIMIT);
}
return cached;
}
function enqueueMermaidRender(run: () => Promise<DiagramState>) {
return new Promise<DiagramState>((resolve, reject) => {
mermaidRenderQueue.push({ reject, resolve, run });
void drainMermaidRenderQueue();
});
}
async function drainMermaidRenderQueue() {
if (isMermaidRenderQueueRunning) return;
isMermaidRenderQueueRunning = true;
try {
for (;;) {
const job = mermaidRenderQueue.shift();
if (!job) return;
try {
await waitForMermaidRenderSlot();
job.resolve(await job.run());
} catch (error) {
job.reject(error);
}
}
} finally {
isMermaidRenderQueueRunning = false;
}
}
function waitForMermaidRenderSlot() {
if (typeof window === "undefined") return Promise.resolve();
const idleWindow = window as MermaidIdleWindow;
if (typeof idleWindow.requestIdleCallback === "function") {
return new Promise<void>((resolve) => {
idleWindow.requestIdleCallback?.(() => resolve(), { timeout: 250 });
});
}
if (typeof window.requestAnimationFrame === "function") {
return new Promise<void>((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
return new Promise<void>((resolve) => {
window.setTimeout(resolve, 0);
});
}
async function loadDiagram(source: string): Promise<DiagramState> {
const mmdrState = await loadMmdrDiagram(source);
if (mmdrState.status === "ready") return mmdrState;
return loadMermaidDiagram(source);
}
async function loadMmdrDiagram(source: string): Promise<DiagramState> {
try {
const mmdr = await loadMmdrApi();
return {
renderer: "mmdr",
status: "ready",
svg: sanitizeSvg(mmdr.render(source)),
};
} catch {
return { status: "failed", message: "mmdr unavailable" };
}
}
async function loadMermaidDiagram(source: string): Promise<DiagramState> {
try {
const mermaid = await loadMermaidApi();
const result = await mermaid.render(
`markdown-diagram-cache-${hashMermaidSource(source)}`,
source,
);
return {
renderer: "mermaid",
status: "ready",
svg: sanitizeSvg(result.svg),
};
} catch (error) {
const message = error instanceof Error ? error.message : "Invalid diagram";
if (!/getBBox|layout|force-basic-fallback/i.test(message)) {
return { status: "failed", message };
}
return renderBasicMermaidDiagram(source);
}
}
async function loadMmdrApi() {
if (!canUseMmdrRenderer()) {
throw new Error("mmdr is unavailable in this runtime");
}
if (isMmdrUnavailable) {
throw new Error("mmdr failed to load");
}
mmdrApiPromise ??= instantiateMmdrApi().catch((error) => {
isMmdrUnavailable = true;
throw error;
});
return mmdrApiPromise;
}
async function loadMermaidApi() {
mermaidApiPromise ??= import("mermaid").then(
(mermaidModule) => mermaidModule.default as MermaidApi,
);
const mermaid = await mermaidApiPromise;
if (mermaidInitializedConfigKey !== MERMAID_CACHE_CONFIG_KEY) {
mermaid.initialize?.(MERMAID_CONFIG);
mermaidInitializedConfigKey = MERMAID_CACHE_CONFIG_KEY;
}
return mermaid;
}
function canUseMmdrRenderer() {
return (
typeof window !== "undefined" &&
typeof window.fetch === "function" &&
typeof WebAssembly !== "undefined" &&
typeof TextDecoder !== "undefined" &&
typeof TextEncoder !== "undefined"
);
}
async function instantiateMmdrApi(): Promise<MmdrApi> {
let instance: WebAssembly.Instance | null = null;
let currentArgs: Uint8Array[] = [];
let currentResult: Uint8Array | null = null;
const imports = {
typst_env: {
wasm_minimal_protocol_send_result_to_host(
pointer: number,
length: number,
) {
if (!instance) return;
const memory = new Uint8Array(
(instance.exports as unknown as MmdrExports).memory.buffer,
);
currentResult = memory.slice(pointer, pointer + length);
},
wasm_minimal_protocol_write_args_to_buffer(pointer: number) {
if (!instance) return;
const memory = new Uint8Array(
(instance.exports as unknown as MmdrExports).memory.buffer,
);
let offset = pointer;
for (const arg of currentArgs) {
memory.set(arg, offset);
offset += arg.length;
}
},
},
};
const response = await window.fetch(MMDR_WASM_URL);
if (!response.ok) {
throw new Error(`Failed to load mmdr renderer: ${response.status}`);
}
const wasm = await WebAssembly.instantiate(
await response.arrayBuffer(),
imports,
);
instance = wasm.instance;
const exports = instance.exports as unknown as MmdrExports;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
return {
render(source: string) {
currentArgs = [
encoder.encode(source),
encoder.encode(MMDR_BASE_THEME),
encoder.encode(MMDR_THEME_JSON),
encoder.encode(MMDR_LAYOUT),
];
currentResult = null;
const status = exports.render(
currentArgs[0]?.length ?? 0,
currentArgs[1]?.length ?? 0,
currentArgs[2]?.length ?? 0,
currentArgs[3]?.length ?? 0,
);
const result = decoder.decode(currentResult ?? new Uint8Array());
currentArgs = [];
currentResult = null;
if (status !== 0) {
throw new Error(result || "mmdr failed to render the diagram");
}
return result;
},
};
}
function scopeCachedMermaidSvg(svg: string, idPrefix: string) {
if (typeof DOMParser === "undefined") return svg;
const document = new DOMParser().parseFromString(svg, "image/svg+xml");
const root = document.documentElement;
if (!root || root.tagName.toLowerCase() !== "svg") return svg;
const ids = new Map<string, string>();
for (const element of Array.from(root.querySelectorAll("[id]"))) {
const id = element.getAttribute("id");
if (!id) continue;
const nextId = `${idPrefix}-${id}`;
ids.set(id, nextId);
element.setAttribute("id", nextId);
}
if (!ids.size) return new XMLSerializer().serializeToString(root);
for (const element of Array.from(root.querySelectorAll("*"))) {
for (const attribute of Array.from(element.attributes)) {
const value = attribute.value;
let nextValue = value;
for (const [oldId, nextId] of ids) {
nextValue = nextValue
.replaceAll(`url(#${oldId})`, `url(#${nextId})`)
.replaceAll(`#${oldId}`, `#${nextId}`);
}
if (nextValue !== value) element.setAttribute(attribute.name, nextValue);
}
}
return new XMLSerializer().serializeToString(root);
}
function hashMermaidSource(source: string) {
let hash = 2166136261;
for (let index = 0; index < source.length; index += 1) {
hash ^= source.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(36);
}
function renderBasicMermaidDiagram(source: string): ReadyDiagramState {
const kind = diagramKind(source);
const lines = readableDiagramLines(source);
const escaped = escapeHtml(lines.join(" | ") || describeDiagram(source));
const piePaths =
kind === "pie"
? '<path d="M20 20h20v20H20z"/><path d="M44 20h20v20H44z"/><path d="M68 20h20v20H68z"/>'
: "";
return {
renderer: "basic",
status: "ready",
svg: `<svg role="img" aria-label="Mermaid diagram" data-pretext-basic-mermaid="${kind}" viewBox="0 0 720 160" width="100%" height="160" xmlns="http://www.w3.org/2000/svg"><rect width="720" height="160" rx="8" fill="currentColor" opacity="0.05"/>${piePaths}<text x="24" y="82" fill="currentColor" font-family="monospace" font-size="16">${escaped}</text></svg>`,
};
}
function sanitizeSvg(svg: string) {
if (typeof DOMParser === "undefined") return svg;
const document = new DOMParser().parseFromString(svg, "image/svg+xml");
const root = document.documentElement;
if (!root || root.tagName.toLowerCase() !== "svg") {
const fallback = renderBasicMermaidDiagram("");
return fallback.status === "ready" ? fallback.svg : "";
}
root.setAttribute("role", "img");
if (!root.getAttribute("aria-label")) {
root.setAttribute("aria-label", "Mermaid diagram");
}
root.setAttribute("data-pretext-sanitized-mermaid", "");
for (const element of Array.from(root.querySelectorAll("*"))) {
const tagName = element.tagName.toLowerCase();
if (
tagName === "script" ||
tagName === "a" ||
tagName === "animate" ||
tagName === "foreignobject" ||
tagName === "image" ||
tagName === "iframe" ||
tagName === "object" ||
tagName === "embed" ||
tagName === "style" ||
tagName === "use"
) {
element.remove();
continue;
}
for (const attribute of Array.from(element.attributes)) {
const name = attribute.name.toLowerCase();
const value = attribute.value.trim();
if (name.startsWith("on") || name === "style") {
element.removeAttribute(attribute.name);
continue;
}
if ((name === "id" || name === "name") && isDomClobberingId(value)) {
element.setAttribute(attribute.name, `user-content-${value}`);
continue;
}
if (
(name === "href" || name.endsWith(":href") || name === "src") &&
!isSafeSvgReference(value)
) {
element.removeAttribute(attribute.name);
}
}
}
for (const attribute of Array.from(root.attributes)) {
const name = attribute.name.toLowerCase();
if (name.startsWith("on") || name === "style") {
root.removeAttribute(attribute.name);
continue;
}
if (
(name === "id" || name === "name") &&
isDomClobberingId(attribute.value)
) {
root.setAttribute(attribute.name, `user-content-${attribute.value}`);
}
}
return new XMLSerializer()
.serializeToString(root)
.replace(/^<svg\b([^>]*)\brole="([^"]*)"([^>]*)>/, '<svg role="$2"$1$3>');
}
function trimMapToLimit<K, V>(map: Map<K, V>, limit: number) {
while (map.size > limit) {
const oldestKey = map.keys().next().value;
if (oldestKey === undefined) break;
map.delete(oldestKey);
}
}
function isSafeSvgReference(value: string) {
return (
value === "" ||
value.startsWith("#") ||
value.startsWith("data:image/") ||
/^https?:\/\//i.test(value)
);
}
export function readDiagramLimitMessage(source: string) {
if (source.length > MERMAID_MAX_SOURCE_LENGTH) {
return "Mermaid diagram too large to render safely. Copy the source and render it in a dedicated diagram tool.";
}
if (source.split(/\r\n|[\n\r\u2028\u2029]/).length > MERMAID_MAX_LINES) {
return "Mermaid diagram has too many lines to render safely. Copy the source and render it in a dedicated diagram tool.";
}
return null;
}
export function describeDiagram(source: string) {
const lines = semanticDiagramLines(source);
const kind = diagramKind(source);
if (kind === "graph") {
const direction = lines[0]
?.match(/\b(?:graph|flowchart)\s+([A-Z]{2})\b/i)?.[1]
?.toUpperCase();
const directionText = direction === "LR" ? "left to right" : "top down";
const edges = lines.filter((line) => /-->|---|-.->|==>/.test(line)).length;
const nodes = new Set<string>();
for (const line of lines) {
for (const match of line.matchAll(/\b([A-Za-z][\w-]*)\b/g)) {
const value = match[1]!;
if (
!["graph", "flowchart", "TD", "LR", "TB", "BT", "RL"].includes(value)
) {
nodes.add(value);
}
}
}
return `Mermaid graph diagram flowing ${directionText}, with ${nodes.size} nodes and ${edges} edge${edges === 1 ? "" : "s"}.`;
}
if (kind === "sequence") {
const messages = lines.filter((line) =>
/->>|-->>|->|-->/.test(line),
).length;
const participants = new Set<string>();
for (const line of lines) {
const participant = /^participant\s+\S+(?:\s+as\s+(.+))?/i.exec(line);
if (participant)
participants.add(participant[1] ?? line.split(/\s+/)[1]!);
for (const side of line.split(/->>|-->>|->|-->/)) {
const name = side.split(":")[0]?.trim();
if (name && !/^sequenceDiagram/i.test(name)) participants.add(name);
}
}
return `Mermaid sequence diagram with ${participants.size} participants and ${messages} message${messages === 1 ? "" : "s"}.`;
}
if (kind === "state") {
const transitions = lines.filter((line) => /-->/.test(line)).length;
const states = new Set<string>();
for (const line of lines) {
const stateAlias = /^state\s+"([^"]+)"\s+as\s+(\w+)/.exec(line);
if (stateAlias) {
states.add(stateAlias[1]!);
continue;
}
if (!/-->/.test(line)) continue;
for (const side of line.split(/-->/)) {
const name = side
.split(":")[0]!
.replace(/\[\*\]/g, "")
.trim();
if (name && !/^stateDiagram/.test(name)) {
states.add(name);
}
}
}
return `Mermaid state diagram with ${states.size} states and ${transitions} transitions.`;
}
if (kind === "class") {
const relationships = lines.filter((line) =>
/<\|--|--|<--|\.\./.test(line),
).length;
const classes = new Set<string>();
for (const line of lines) {
for (const match of line.matchAll(/\b([A-Z][A-Za-z0-9_]*)\b/g))
classes.add(match[1]!);
}
return `Mermaid class diagram with ${classes.size} classes and ${relationships} relationship${relationships === 1 ? "" : "s"}.`;
}
if (kind === "er") {
const relationships = lines.filter((line) =>
/\|\||o\{|\|\{/.test(line),
).length;
const entities = new Set<string>();
for (const line of lines) {
for (const match of line.matchAll(/\b([A-Z][A-Z0-9_]*)\b/g)) {
if (!["ERDIAGRAM"].includes(match[1]!)) entities.add(match[1]!);
}
}
return `Mermaid entity relationship diagram with ${entities.size} entities and ${relationships} relationships.`;
}
if (kind === "journey")
return `Mermaid journey diagram with ${countLinesStarting(lines, "section ")} sections and ${lines.filter((line) => /:\s*\d+\s*:/.test(line)).length} tasks.`;
if (kind === "gantt")
return `Mermaid Gantt chart with ${countLinesStarting(lines, "section ")} sections and ${lines.filter((line) => /:/.test(line) && !/^dateFormat/i.test(line) && !/^title\b/i.test(line)).length} tasks.`;
if (kind === "gitGraph")
return `Mermaid Git graph with ${countLinesStarting(lines, "branch ")} branch, ${countExact(lines, "commit")} commits, and ${countLinesStarting(lines, "merge ")} merge.`;
if (kind === "timeline")
return `Mermaid timeline with ${countLinesStarting(lines, "section ")} sections and ${lines.filter((line) => /^\d/.test(line)).length} events.`;
if (kind === "mindmap")
return `Mermaid mind map with ${Math.max(0, lines.length - 1)} nodes.`;
if (kind === "quadrantChart")
return `Mermaid quadrant chart with ${lines.filter((line) => /:\s*\[[^\]]+\]/.test(line)).length} points.`;
if (kind === "requirementDiagram")
return `Mermaid requirement diagram with ${countLinesStarting(lines, "requirement ")} requirement, ${countLinesStarting(lines, "element ")} element, and ${lines.filter((line) => /-\s*\w+\s*->/.test(line)).length} relationship.`;
if (kind === "xychart") {
const series = lines.filter((line) => /^(bar|line)\s+\[/.test(line));
const values = series.reduce(
(sum, line) => sum + (line.match(/-?\d+(?:\.\d+)?/g)?.length ?? 0),
0,
);
return `Mermaid XY chart with ${series.length} series and ${values} values.`;
}
if (kind === "sankey") {
const flows = lines.filter((line) => line.includes(","));
const nodes = new Set(
flows.flatMap((line) =>
line
.split(",")
.slice(0, 2)
.map((item) => item.trim()),
),
);
return `Mermaid Sankey diagram with ${nodes.size} nodes and ${flows.length} flows.`;
}
if (kind === "c4")
return `Mermaid C4 diagram with ${lines.filter((line) => /^(Person|System)\(/.test(line)).length} nodes and ${countLinesStarting(lines, "Rel(")} relationship.`;
if (kind === "pie") {
const values = lines
.flatMap((line) => line.match(/:\s*(\d+(?:\.\d+)?)/)?.[1] ?? [])
.map(Number);
const total = values.reduce((sum, value) => sum + value, 0);
return `Mermaid pie chart with ${values.length} slices and total value ${total}.`;
}
return "Mermaid diagram";
}
export function estimateDiagramBodyHeight(source: string) {
switch (diagramKind(source)) {
case "graph":
return 286;
case "sequence":
return 160;
case "state":
return 238;
case "class":
return 220;
case "er":
return 292;
case "journey":
return 270;
case "gantt":
return 300;
case "gitGraph":
return 212;
case "timeline":
return 270;
case "mindmap":
return 232;
case "quadrantChart":
return 296;
case "requirementDiagram":
return 230;
case "xychart":
return 334;
case "sankey":
return 310;
case "c4":
return 236;
case "pie":
return 224;
default:
return 180;
}
}
function diagramKind(source: string) {
const first = semanticDiagramLines(source)[0] ?? "";
if (/^(?:graph|flowchart)\b/i.test(first)) return "graph";
if (/^sequenceDiagram\b/i.test(first)) return "sequence";
if (/^stateDiagram/i.test(first)) return "state";
if (/^classDiagram\b/i.test(first)) return "class";
if (/^erDiagram\b/i.test(first)) return "er";
if (/^journey\b/i.test(first)) return "journey";
if (/^gantt\b/i.test(first)) return "gantt";
if (/^gitGraph\b/i.test(first)) return "gitGraph";
if (/^timeline\b/i.test(first)) return "timeline";
if (/^mindmap\b/i.test(first)) return "mindmap";
if (/^quadrantChart\b/i.test(first)) return "quadrantChart";
if (/^requirementDiagram\b/i.test(first)) return "requirementDiagram";
if (/^xychart/i.test(first)) return "xychart";
if (/^sankey/i.test(first)) return "sankey";
if (/^C4/i.test(first)) return "c4";
if (/^pie\b/i.test(first)) return "pie";
return "source";
}
function semanticDiagramLines(source: string) {
return stripMermaidFrontmatter(source)
.split(/\r\n|[\n\r\u2028\u2029]/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("%%"));
}
function readableDiagramLines(source: string) {
const lines = semanticDiagramLines(source);
if (diagramKind(source) === "pie") {
return lines.map((line) => {
const match = /"([^"]+)"\s*:\s*(\d+(?:\.\d+)?)/.exec(line);
return match ? `${match[1]} ${match[2]} (${match[2]}%)` : line;
});
}
return lines;
}
function stripMermaidFrontmatter(source: string) {
const lines = source.split(/\r\n|[\n\r\u2028\u2029]/);
if (lines[0]?.trim() !== "---") return source;
const end = lines.findIndex(
(line, index) => index > 0 && line.trim() === "---",
);
return end === -1 ? source : lines.slice(end + 1).join("\n");
}
function countLinesStarting(lines: readonly string[], prefix: string) {
return lines.filter((line) => line.startsWith(prefix)).length;
}
function countExact(lines: readonly string[], value: string) {
return lines.filter((line) => line === value).length;
}
function isDomClobberingId(id: string) {
return ["constructor", "forms", "images", "location", "__proto__"].includes(
id,
);
}
function escapeHtml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
registry/new-york-v4/ui/file-viewer-context.tsx
"use client";
import * as React from "react";
import type { FileCategory } from "@/lib/viewer-source";
import type { FileViewerElementRegistry } from "./file-viewer-elements";
import type { FileViewerMotionKernel } from "./file-viewer-motion-kernel";
export type FileViewerSidebarMode = "inline" | "overlay";
export type FileViewerSidebarRequestedMode = "auto" | FileViewerSidebarMode;
export type FileViewerSidebarState = "expanded" | "collapsed";
export type FileViewerSidebarSide = "left" | "right";
export type FileViewerSidebarCollapsible = "offcanvas" | "none";
export type FileViewerHeaderMode = "inline" | "outlets";
export const DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH = "10rem";
export type FileViewerSetSidebarOpen = (
value: boolean | ((isSidebarOpen: boolean) => boolean),
) => void;
export type FileViewerSidebarOpenProps = {
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
open?: boolean;
};
export type FileViewerContextValue = {
headerMode: FileViewerHeaderMode;
hasHeaderOutlets: boolean;
isInsideFileViewer: boolean;
resourceCategory: FileCategory;
sidebarOpenProps: FileViewerSidebarOpenProps;
};
export type FileViewerSidebarRegistration = {
collapsible: FileViewerSidebarCollapsible;
id: string;
side: FileViewerSidebarSide;
width: string;
widthPixels: number;
};
export type FileViewerSidebarValue = {
canToggleSidebar: boolean;
isSidebarInteractive: boolean;
isSidebarOpen: boolean;
mode: FileViewerSidebarMode;
side: FileViewerSidebarSide;
sidebarId: string;
sidebarState: FileViewerSidebarState;
setSidebarOpen: FileViewerSetSidebarOpen;
toggleSidebar: () => void;
};
export type FileViewerShellStaticContextValue = {
canToggleSidebar: boolean;
collapsible: FileViewerSidebarCollapsible;
elementRegistry: FileViewerElementRegistry;
mode: FileViewerSidebarMode;
motionDurationMs: number;
motionKernel: FileViewerMotionKernel;
registerSidebar: (registration: FileViewerSidebarRegistration) => () => void;
rootId: string;
setSidebarOpen: FileViewerSetSidebarOpen;
side: FileViewerSidebarSide;
sidebarId: string;
sidebarWidth: string;
toggleSidebar: () => void;
};
export type FileViewerSidebarDynamicContextValue = {
isSidebarInteractive: boolean;
isSidebarOpen: boolean;
isSidebarTransitioning: boolean;
sidebarState: FileViewerSidebarState;
};
export const FileViewerContext = React.createContext<FileViewerContextValue>({
headerMode: "inline",
hasHeaderOutlets: false,
isInsideFileViewer: false,
resourceCategory: "unsupported",
sidebarOpenProps: {},
});
export const FileViewerShellStaticContext =
React.createContext<FileViewerShellStaticContextValue | null>(null);
export const FileViewerSidebarDynamicContext =
React.createContext<FileViewerSidebarDynamicContextValue | null>(null);
export function useFileViewerContext() {
return React.useContext(FileViewerContext);
}
export function useOptionalFileViewerShellStatic() {
return React.useContext(FileViewerShellStaticContext);
}
export function useFileViewerShellStatic(consumer: string) {
const context = React.useContext(FileViewerShellStaticContext);
if (!context) {
throw new Error(`${consumer} must be rendered inside FileViewer.`);
}
return context;
}
export function useOptionalFileViewerShell() {
const staticContext = React.useContext(FileViewerShellStaticContext);
const sidebarContext = React.useContext(FileViewerSidebarDynamicContext);
return React.useMemo(
() =>
staticContext && sidebarContext
? { ...staticContext, ...sidebarContext }
: null,
[sidebarContext, staticContext],
);
}
export function useFileViewerShell(consumer: string) {
const context = useOptionalFileViewerShell();
if (!context) {
throw new Error(`${consumer} must be rendered inside FileViewer.`);
}
return context;
}
export function useFileViewerSidebar(): FileViewerSidebarValue {
const fileViewerContext = React.useContext(FileViewerContext);
const shellContext = useOptionalFileViewerShell();
if (!fileViewerContext.isInsideFileViewer || !shellContext) {
throw new Error("useFileViewerSidebar must be used within FileViewer.");
}
return shellContext;
}
registry/new-york-v4/ui/file-viewer-elements.ts
"use client";
import type {
FileViewerDocumentSurface,
FileViewerMotionKernel,
} from "./file-viewer-motion-kernel";
import type { FileViewerMotionFrame } from "./file-viewer-motion-plan";
export const FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT =
"file-viewer:before-layout-motion";
// The kernel dispatches the before-layout-motion event as a CustomEvent whose
// detail is its LIVE interactive frame, so renderers can capture pre-commit
// anchors against what is actually on screen (a mid-flight retarget sees the
// settled layout plus the in-flight transform).
export function readFileViewerBeforeLayoutMotionFrame(
event: Event,
): FileViewerMotionFrame | null {
if (!(event instanceof CustomEvent)) return null;
const detail: unknown = event.detail;
return detail != null && typeof detail === "object"
? (detail as FileViewerMotionFrame)
: null;
}
export type FileViewerElements = {
documentSurfaceElement: HTMLElement | null;
getDocumentSurfaceMotionProbeElement: (() => HTMLElement | null) | null;
sidebarElement: HTMLElement | null;
sidebarGapElement: HTMLDivElement | null;
sidebarTriggerElement: HTMLElement | null;
viewerShellElement: HTMLDivElement | null;
};
export type FileViewerElementRegistry = {
getElements: () => FileViewerElements;
registerDocumentSurface: (surface: FileViewerDocumentSurface) => () => void;
registerSidebarElement: (element: HTMLElement | null) => void;
registerSidebarGapElement: (element: HTMLDivElement | null) => void;
registerSidebarTriggerElement: (element: HTMLElement | null) => void;
registerViewerShellElement: (element: HTMLDivElement | null) => void;
};
export function createFileViewerElementRegistry({
motionKernel,
onViewerShellElementChange,
}: {
motionKernel: FileViewerMotionKernel;
onViewerShellElementChange: (element: HTMLDivElement | null) => void;
}): FileViewerElementRegistry {
const elements: FileViewerElements = {
documentSurfaceElement: null,
getDocumentSurfaceMotionProbeElement: null,
sidebarElement: null,
sidebarGapElement: null,
sidebarTriggerElement: null,
viewerShellElement: null,
};
let documentSurfaceRegistration = 0;
return {
getElements: () => elements,
registerDocumentSurface: (surface) => {
documentSurfaceRegistration += 1;
const registration = documentSurfaceRegistration;
elements.documentSurfaceElement = surface.element;
elements.getDocumentSurfaceMotionProbeElement =
surface.getMotionProbeElement ?? null;
motionKernel.setDocumentSurface(surface);
return () => {
if (documentSurfaceRegistration !== registration) return;
elements.documentSurfaceElement = null;
elements.getDocumentSurfaceMotionProbeElement = null;
motionKernel.setDocumentSurface(null);
};
},
registerSidebarElement: (element) => {
if (elements.sidebarElement === element) return;
elements.sidebarElement = element;
},
registerSidebarGapElement: (element) => {
if (elements.sidebarGapElement === element) return;
elements.sidebarGapElement = element;
motionKernel.setSidebarGapElement(element);
},
registerSidebarTriggerElement: (element) => {
elements.sidebarTriggerElement = element;
},
registerViewerShellElement: (element) => {
if (elements.viewerShellElement === element) return;
elements.viewerShellElement = element;
onViewerShellElementChange(element);
},
};
}
registry/new-york-v4/ui/file-viewer-motion-kernel.ts
"use client";
import * as React from "react";
import { flushSync } from "react-dom";
import { FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT } from "./file-viewer-elements";
import {
easeFileViewerMotion,
FILE_VIEWER_MOTION_DURATION_MS,
areFileViewerMotionFramesEqual,
areFileViewerMotionRestFramesEqual,
createFileViewerIdleMotionFrame,
createFileViewerMotionPlan,
createFileViewerMotionRestFrame,
type FileViewerMotionFrame,
type FileViewerMotionPlan,
type FileViewerMotionRestFrame,
type FileViewerMotionTarget,
} from "./file-viewer-motion-plan";
// The kernel is the single owner of sidebar motion. It holds the clock (one
// rAF loop from slide start through settle), writes the continuous inline
// styles, and publishes to React subscribers only at phase edges
// (idle → sliding → settling → idle). Everything discrete — data attributes,
// inert/aria, the overlay translate classes — is owned by React renders.
//
// Commit-then-relax ordering: the first sliding frame is flushed
// synchronously inside the toggle's own task, so renderers commit their
// TARGET layout and rebase scroll before anything paints; the per-tick style
// writes then only relax the counter-transform to identity. Settle removes a
// no-op transform — it never commits layout, so no flushSync runs inside rAF.
export type FileViewerMotionKernel = {
getFlightRecords: () => readonly FileViewerMotionFlightRecord[];
getInteractiveSnapshot: () => FileViewerMotionFrame;
getSnapshot: () => FileViewerMotionFrame;
setDocumentSurface: (surface: FileViewerDocumentSurface | null) => void;
setSidebarGapElement: (element: HTMLElement | null) => void;
startMotion: (target: FileViewerMotionTarget) => void;
subscribe: (listener: () => void) => () => void;
syncTarget: (target: FileViewerMotionTarget) => void;
};
// Always-on flight recorder: every motion leaves a bounded trace (per-tick
// widths, phase edges, settle holds, inter-frame gaps) so a blink report is
// diagnosable after the fact without re-instrumenting.
export type FileViewerMotionFlightRecord = {
fromInlineSize: number;
id: number;
interrupted: boolean;
maxTickGapMs: number;
open: boolean;
settleHoldFrameCount: number;
startedAt: number;
ticks: FileViewerMotionFlightTick[];
toInlineSize: number;
};
export type FileViewerMotionFlightTick = {
elapsedMs: number;
phase: FileViewerMotionFrame["phase"];
sidebarInlineSize: number;
};
const FILE_VIEWER_FLIGHT_RECORD_LIMIT = 8;
const FILE_VIEWER_FLIGHT_TICK_LIMIT = 240;
export type FileViewerDocumentSurface = {
element: HTMLElement;
getMotionProbeElement?: (() => HTMLElement | null) | null;
readSettleSnapshot?: FileViewerDocumentSurfaceSettleSnapshotReader | null;
resolveMotionStyle?: FileViewerDocumentSurfaceMotionResolver | null;
};
export type FileViewerDocumentSurfaceSettleSnapshotReader = () =>
| readonly number[]
| null
| undefined;
// Layout reads are quarantined outside the kernel (viewer-measurement / the
// frame controller layer). The kernel owns time and style writes only, so the
// settle-hold rect reader is injected by its creator rather than imported.
export type FileViewerElementRectSnapshotReader = (
element: HTMLElement | null,
) => readonly number[];
export type FileViewerMotionKernelOptions = {
readElementRectSnapshot?: FileViewerElementRectSnapshotReader | null;
};
export type FileViewerDocumentSurfaceMotionStyle = {
customProperties?: Readonly<Record<string, string | null>>;
transform: string;
transformOrigin: string;
willChange: string;
};
export type FileViewerDocumentSurfaceMotionResolver = (
frame: FileViewerMotionFrame,
) => FileViewerDocumentSurfaceMotionStyle | null;
type FileViewerActiveMotion = {
durationMs: number;
from: FileViewerMotionRestFrame;
// The clock re-anchors to the first tick's vsync frame time: startedAt is
// stamped inside the toggle's task, but the synchronous slide-start commit
// can burn 10ms+ before anything paints, and an ease anchored at the click
// lands its first painted frame that deep into the curve.
hasFrameClockAnchor: boolean;
id: number;
startedAt: number;
to: FileViewerMotionRestFrame;
};
type FileViewerSettleRelease = {
idleFrame: FileViewerMotionFrame;
lastSnapshot: readonly number[];
remainingFrameCount: number;
settlingFrame: FileViewerMotionFrame;
stableFrameCount: number;
};
const FILE_VIEWER_SETTLE_SCROLL_EPSILON_PX = 0.25;
const FILE_VIEWER_SETTLE_STABLE_FRAME_COUNT = 2;
const FILE_VIEWER_SETTLE_MAX_HOLD_FRAMES = 6;
const FILE_VIEWER_SUBPIXEL_ENDPOINT_EPSILON_PX = 1;
export const DEFAULT_FILE_VIEWER_MOTION_FRAME: FileViewerMotionFrame = {
shellInlineSize: 0,
durationMs: FILE_VIEWER_MOTION_DURATION_MS,
fromInlineSize: 0,
layoutInlineSize: 0,
mode: "overlay",
motionId: null,
motionProgress: 1,
open: false,
phase: "idle",
side: "left",
sidebarInlineSize: 0,
sidebarWidth: 0,
toInlineSize: 0,
};
export function createFileViewerMotionKernel({
readElementRectSnapshot = null,
}: FileViewerMotionKernelOptions = {}): FileViewerMotionKernel {
const listeners = new Set<() => void>();
let contractFrame = DEFAULT_FILE_VIEWER_MOTION_FRAME;
let interactiveFrame = DEFAULT_FILE_VIEWER_MOTION_FRAME;
let target: FileViewerMotionTarget = {
shellInlineSize: 0,
durationMs: DEFAULT_FILE_VIEWER_MOTION_FRAME.durationMs,
mode: DEFAULT_FILE_VIEWER_MOTION_FRAME.mode,
open: DEFAULT_FILE_VIEWER_MOTION_FRAME.open,
side: DEFAULT_FILE_VIEWER_MOTION_FRAME.side,
sidebarWidth: 0,
};
let documentSurface: FileViewerDocumentSurface | null = null;
let documentSurfaceCustomProperties = new Set<string>();
let sidebarGapElement: HTMLElement | null = null;
let activeMotion: FileViewerActiveMotion | null = null;
let settleRelease: FileViewerSettleRelease | null = null;
let rafHandle = 0;
let settleReleaseHandle = 0;
let motionSequence = 0;
const flightRecords: FileViewerMotionFlightRecord[] = [];
let activeFlightRecord: FileViewerMotionFlightRecord | null = null;
let lastFlightTickAt = 0;
const beginFlightRecord = (motion: FileViewerActiveMotion) => {
if (activeFlightRecord && activeFlightRecord.id !== motion.id) {
activeFlightRecord.interrupted = true;
}
activeFlightRecord = {
fromInlineSize: motion.from.layoutInlineSize,
id: motion.id,
interrupted: false,
maxTickGapMs: 0,
open: motion.to.open,
settleHoldFrameCount: 0,
startedAt: motion.startedAt,
ticks: [],
toInlineSize: motion.to.layoutInlineSize,
};
lastFlightTickAt = motion.startedAt;
flightRecords.push(activeFlightRecord);
if (flightRecords.length > FILE_VIEWER_FLIGHT_RECORD_LIMIT) {
flightRecords.splice(
0,
flightRecords.length - FILE_VIEWER_FLIGHT_RECORD_LIMIT,
);
}
};
const recordFlightTick = (frame: FileViewerMotionFrame, now = readNow()) => {
const record = activeFlightRecord;
if (!record || frame.motionId !== record.id) return;
record.maxTickGapMs = Math.max(record.maxTickGapMs, now - lastFlightTickAt);
lastFlightTickAt = now;
if (frame.phase === "settling") record.settleHoldFrameCount += 1;
if (record.ticks.length >= FILE_VIEWER_FLIGHT_TICK_LIMIT) return;
record.ticks.push({
elapsedMs: Math.max(0, now - record.startedAt),
phase: frame.phase,
sidebarInlineSize: frame.sidebarInlineSize,
});
};
const notify = () => {
for (const listener of listeners) listener();
};
const publishContractFrame = (
nextFrame: FileViewerMotionFrame,
{ flushSubscribers = false }: { flushSubscribers?: boolean } = {},
) => {
if (areFileViewerMotionFramesEqual(contractFrame, nextFrame)) return;
contractFrame = nextFrame;
if (flushSubscribers) {
flushSync(notify);
return;
}
notify();
};
// The gap's inline size and the document surface's counter-scale must land
// in the same frame: two independent CSS transitions (width on the gap,
// transform on the surface) can desync under main-thread jank, letting the
// document edge drift off the sidebar edge mid-slide. The kernel therefore
// writes both here, once per tick.
const writeElementStyles = (nextFrame: FileViewerMotionFrame) => {
writeSidebarGapStyle(nextFrame);
writeDocumentSurfaceStyle(nextFrame);
};
const commit = (
nextFrame: FileViewerMotionFrame,
{ publish = true }: { publish?: boolean } = {},
) => {
writeElementStyles(nextFrame);
interactiveFrame = nextFrame;
if (publish) publishContractFrame(nextFrame);
};
const cancelTick = () => {
if (rafHandle === 0) return;
getCancelAnimationFrame()(rafHandle);
rafHandle = 0;
};
const cancelSettleRelease = () => {
settleRelease = null;
if (settleReleaseHandle === 0) return;
getCancelAnimationFrame()(settleReleaseHandle);
settleReleaseHandle = 0;
};
const readMotionSample = (
motion: FileViewerActiveMotion,
now = readNow(),
): FileViewerMotionFrame => {
const rawTimeProgress =
motion.durationMs <= 0
? 1
: clamp((now - motion.startedAt) / motion.durationMs, 0, 1);
const rawMotionProgress = easeFileViewerMotion(rawTimeProgress);
const rawSidebarInlineSize = lerp(
motion.from.sidebarInlineSize,
motion.to.sidebarInlineSize,
rawMotionProgress,
);
const isSubpixelEndpoint =
rawMotionProgress > 0.98 &&
Math.abs(rawSidebarInlineSize - motion.to.sidebarInlineSize) <=
FILE_VIEWER_SUBPIXEL_ENDPOINT_EPSILON_PX;
const motionProgress = isSubpixelEndpoint ? 1 : rawMotionProgress;
const sidebarInlineSize = isSubpixelEndpoint
? motion.to.sidebarInlineSize
: rawSidebarInlineSize;
const layoutInlineSize = Math.max(
0,
motion.to.shellInlineSize - sidebarInlineSize,
);
const fromInlineSize = motion.from.layoutInlineSize;
return {
shellInlineSize: motion.to.shellInlineSize,
durationMs: motion.durationMs,
fromInlineSize,
layoutInlineSize,
mode: motion.to.mode,
motionId: motion.id,
motionProgress,
open: motion.to.open,
phase: motionProgress < 1 ? "sliding" : "settling",
side: motion.to.side,
sidebarInlineSize,
sidebarWidth: motion.to.sidebarWidth,
toInlineSize: motion.to.layoutInlineSize,
};
};
const settle = () => {
if (!activeMotion) return;
const finishedMotion = activeMotion;
activeMotion = null;
cancelTick();
const idleFrame = createFileViewerIdleMotionFrame(finishedMotion.to);
const settlingFrame: FileViewerMotionFrame = {
...idleFrame,
fromInlineSize: finishedMotion.from.layoutInlineSize,
motionId: finishedMotion.id,
phase: "settling",
};
// Layout and scroll were committed at slide start; settling only clears
// the (now identity) counter-transform and holds until shell geometry
// stops moving. Nothing here re-renders geometry, so no flushSync in rAF.
commit(settlingFrame, { publish: false });
recordFlightTick(settlingFrame);
publishContractFrame(settlingFrame);
scheduleSettleRelease(settlingFrame, idleFrame);
};
// Ticks sample the clock at the rAF FRAME timestamp, never the callback's
// execution time: the frame time is the vsync the paint belongs to, and a
// callback running late in a janky frame would otherwise write a position
// ahead of the frame's own time axis — a real paint-side velocity excess
// (the probes' rule 11, applied to the writer). The first tick also
// re-anchors startedAt to its frame time, so the ease starts at the first
// paintable frame rather than at the click that precedes the slide-start
// commit.
const tick = (frameTime: number) => {
rafHandle = 0;
if (!activeMotion) return;
const now = Number.isFinite(frameTime) ? frameTime : readNow();
if (!activeMotion.hasFrameClockAnchor) {
activeMotion.hasFrameClockAnchor = true;
activeMotion.startedAt = now;
}
const sample = readMotionSample(activeMotion, now);
if (sample.motionProgress >= 1) {
settle();
return;
}
commit(sample, { publish: false });
recordFlightTick(sample, now);
scheduleTick();
};
const scheduleTick = () => {
if (rafHandle !== 0) return;
rafHandle = getRequestAnimationFrame()(tick);
};
const scheduleSettleRelease = (
settlingFrame: FileViewerMotionFrame,
idleFrame: FileViewerMotionFrame,
) => {
cancelSettleRelease();
settleRelease = {
idleFrame,
lastSnapshot: readSettleSnapshot(),
remainingFrameCount: FILE_VIEWER_SETTLE_MAX_HOLD_FRAMES,
settlingFrame,
stableFrameCount: 0,
};
scheduleSettleReleaseFrame();
};
const scheduleSettleReleaseFrame = () => {
if (settleReleaseHandle !== 0) return;
settleReleaseHandle = getRequestAnimationFrame()(holdSettleRelease);
};
const holdSettleRelease = () => {
settleReleaseHandle = 0;
if (!settleRelease) return;
commit(settleRelease.settlingFrame, { publish: false });
recordFlightTick(settleRelease.settlingFrame);
const snapshot = readSettleSnapshot();
const stableFrameCount = areSettleSnapshotsEqual(
settleRelease.lastSnapshot,
snapshot,
)
? settleRelease.stableFrameCount + 1
: 0;
const remainingFrameCount = settleRelease.remainingFrameCount - 1;
if (
stableFrameCount >= FILE_VIEWER_SETTLE_STABLE_FRAME_COUNT ||
remainingFrameCount <= 0
) {
const idleFrame = settleRelease.idleFrame;
settleRelease = null;
// Natural completion: close the flight record so the next motion does
// not mark this one interrupted.
activeFlightRecord = null;
commit(idleFrame);
return;
}
settleRelease = {
...settleRelease,
lastSnapshot: snapshot,
remainingFrameCount,
stableFrameCount,
};
scheduleSettleReleaseFrame();
};
// The event carries the kernel's LIVE frame so renderers can capture their
// pre-commit anchor against what is actually on screen — during a mid-flight
// retarget that is the settled layout PLUS the in-flight transform, not the
// settled layout alone.
const dispatchBeforeLayoutMotion = (currentFrame: FileViewerMotionFrame) => {
documentSurface?.element.dispatchEvent(
new CustomEvent<FileViewerMotionFrame>(
FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,
{ detail: currentFrame },
),
);
};
const interruptActiveFlightRecord = () => {
if (!activeFlightRecord) return;
activeFlightRecord.interrupted = true;
activeFlightRecord = null;
};
const retarget = (nextTarget: FileViewerMotionTarget, animate: boolean) => {
cancelSettleRelease();
// Continuity is with what is PAINTED, not with the clock: mid-flight the
// screen shows the last tick's commit (`interactiveFrame`), which can be
// a frame behind a fresh clock sample. Planning (and the before-motion
// capture renderers do off the event detail) from the painted frame keeps
// the retarget hand-off pixel-continuous; the new motion simply re-lerps
// from the painted geometry.
const currentFrame =
interactiveFrame.shellInlineSize > 0
? interactiveFrame
: createFileViewerIdleMotionFrame(
createFileViewerMotionRestFrame(target),
);
const plan = createFileViewerMotionPlan({
animate: animate && !prefersReducedMotion(),
currentFrame,
nextTarget,
});
if (shouldDispatchBeforeLayoutMotion(plan)) {
dispatchBeforeLayoutMotion(currentFrame);
}
target = plan.resolvedTarget;
if (!plan.shouldAnimate) {
if (activeMotion) interruptActiveFlightRecord();
activeMotion = null;
cancelTick();
commit(createFileViewerIdleMotionFrame(plan.nextRestFrame));
return;
}
motionSequence += 1;
activeMotion = {
durationMs: plan.resolvedTarget.durationMs,
from: { ...plan.currentRestFrame, layoutInlineSize: plan.fromInlineSize },
hasFrameClockAnchor: false,
id: motionSequence,
startedAt: readNow(),
to: plan.nextRestFrame,
};
beginFlightRecord(activeMotion);
const startFrame = readMotionSample(activeMotion, activeMotion.startedAt);
// Commit the discontinuity while it cannot be seen: flush the first
// sliding frame synchronously (inside the toggle's own task) so renderers
// lay out at the target width and rebase scroll before first paint, hidden
// behind the counter-transform written above in the same task.
writeElementStyles(startFrame);
interactiveFrame = startFrame;
recordFlightTick(startFrame, activeMotion.startedAt);
publishContractFrame(startFrame, { flushSubscribers: true });
scheduleTick();
};
const syncTarget = (nextTarget: FileViewerMotionTarget) => {
const nextRestFrame = createFileViewerMotionRestFrame(nextTarget);
if (activeMotion) {
target = nextTarget;
if (areFileViewerMotionRestFramesEqual(activeMotion.to, nextRestFrame)) {
return;
}
// A mode flip mid-motion (breakpoint crossing during the slide) cannot
// be animated: React re-renders the new mode immediately, so an inline
// slide continuing against overlay DOM (or vice versa) double-moves the
// surface. Snap to the new rest geometry instead.
if (nextRestFrame.mode !== activeMotion.to.mode) {
interruptActiveFlightRecord();
activeMotion = null;
cancelTick();
cancelSettleRelease();
commit(createFileViewerIdleMotionFrame(nextRestFrame));
return;
}
retarget(nextTarget, true);
return;
}
cancelSettleRelease();
target = nextTarget;
commit(createFileViewerIdleMotionFrame(nextRestFrame));
};
return {
getFlightRecords: () => flightRecords.slice(),
getInteractiveSnapshot: () =>
activeMotion ? readMotionSample(activeMotion) : interactiveFrame,
getSnapshot: () => contractFrame,
setDocumentSurface: (surface) => {
const previousSurface = documentSurface;
if (
previousSurface &&
(!surface || previousSurface.element !== surface.element)
) {
clearDocumentSurfaceStyle(previousSurface.element);
}
documentSurface = surface;
writeDocumentSurfaceStyle(interactiveFrame);
},
setSidebarGapElement: (element) => {
sidebarGapElement = element;
writeSidebarGapStyle(interactiveFrame);
},
startMotion: (nextTarget) => retarget(nextTarget, true),
subscribe: (listener) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
syncTarget,
};
function writeSidebarGapStyle(nextFrame: FileViewerMotionFrame) {
if (!sidebarGapElement) return;
// Overlay motion is CSS-owned; relinquish the gap so its `w-0` class is
// the only writer outside inline mode.
if (nextFrame.mode !== "inline") {
sidebarGapElement.style.width = "";
sidebarGapElement.style.flexBasis = "";
return;
}
sidebarGapElement.style.width = `${nextFrame.sidebarInlineSize}px`;
sidebarGapElement.style.flexBasis = `${nextFrame.sidebarInlineSize}px`;
}
function writeDocumentSurfaceStyle(nextFrame: FileViewerMotionFrame) {
if (!documentSurface) return;
const { element, resolveMotionStyle } = documentSurface;
const resolvedStyle = resolveMotionStyle?.(nextFrame);
if (resolvedStyle) {
writeDocumentSurfaceCustomProperties(
element,
resolvedStyle.customProperties,
);
element.style.transform = resolvedStyle.transform;
element.style.transformOrigin = resolvedStyle.transformOrigin;
element.style.willChange = resolvedStyle.willChange;
return;
}
// Default (no motion resolver): identity. Fit-width renderers register
// the shared commit-then-relax resolver (file-viewer-fit-width-motion);
// a surface without one either tracks the live DOM width on its own or
// opts out of shell motion entirely, and must not be transformed here.
writeDocumentSurfaceCustomProperties(element, null);
element.style.transform = "";
element.style.transformOrigin = "";
element.style.willChange = "";
}
function writeDocumentSurfaceCustomProperties(
element: HTMLElement,
customProperties:
| Readonly<Record<string, string | null>>
| null
| undefined,
) {
const nextNames = new Set(Object.keys(customProperties ?? {}));
for (const name of documentSurfaceCustomProperties) {
if (!nextNames.has(name)) {
element.style.removeProperty(name);
}
}
for (const [name, value] of Object.entries(customProperties ?? {})) {
if (value == null) {
element.style.removeProperty(name);
} else {
element.style.setProperty(name, value);
}
}
documentSurfaceCustomProperties = nextNames;
}
function clearDocumentSurfaceStyle(element: HTMLElement) {
element.style.transform = "";
element.style.transformOrigin = "";
element.style.willChange = "";
for (const name of documentSurfaceCustomProperties) {
element.style.removeProperty(name);
}
documentSurfaceCustomProperties = new Set();
}
function readSettleSnapshot(): readonly number[] {
const values: number[] = [];
appendElementRectSnapshot(values, sidebarGapElement);
appendElementRectSnapshot(values, documentSurface?.element ?? null);
try {
const surfaceSnapshot = documentSurface?.readSettleSnapshot?.();
if (surfaceSnapshot) {
values.push(...surfaceSnapshot.map(toSettleSnapshotNumber));
}
} catch {
// A renderer snapshot is diagnostic, not correctness-critical. If a
// renderer unmounts while settling, fall back to shell geometry.
}
return values.length > 0 ? values : [0];
}
function appendElementRectSnapshot(
values: number[],
element: HTMLElement | null,
) {
if (!readElementRectSnapshot || !element) return;
for (const value of readElementRectSnapshot(element)) {
values.push(toSettleSnapshotNumber(value));
}
}
}
function areSettleSnapshotsEqual(
previous: readonly number[],
next: readonly number[],
) {
if (previous.length !== next.length) return false;
return previous.every(
(value, index) =>
Math.abs(value - next[index]) <= FILE_VIEWER_SETTLE_SCROLL_EPSILON_PX,
);
}
function toSettleSnapshotNumber(value: number) {
return Number.isFinite(value) ? value : 0;
}
function shouldDispatchBeforeLayoutMotion({
currentRestFrame,
nextRestFrame,
}: FileViewerMotionPlan) {
return (
currentRestFrame.mode === "inline" &&
nextRestFrame.mode === "inline" &&
Math.abs(
currentRestFrame.layoutInlineSize - nextRestFrame.layoutInlineSize,
) > 0.001
);
}
export function useFileViewerMotionFrame(
kernel: FileViewerMotionKernel | null | undefined,
): FileViewerMotionFrame {
const subscribe = React.useCallback(
(listener: () => void) => kernel?.subscribe(listener) ?? (() => {}),
[kernel],
);
const getSnapshot = React.useCallback(
() => kernel?.getSnapshot() ?? DEFAULT_FILE_VIEWER_MOTION_FRAME,
[kernel],
);
return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
function prefersReducedMotion() {
return (
typeof matchMedia === "function" &&
matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
function readNow() {
return typeof performance !== "undefined" &&
typeof performance.now === "function"
? performance.now()
: Date.now();
}
function getRequestAnimationFrame() {
return (
globalThis.requestAnimationFrame ??
((callback: FrameRequestCallback) =>
window.setTimeout(() => callback(readNow()), 16))
);
}
function getCancelAnimationFrame() {
return (
globalThis.cancelAnimationFrame ??
((id: number) => {
window.clearTimeout(id);
})
);
}
function lerp(from: number, to: number, progress: number) {
return from + (to - from) * progress;
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
registry/new-york-v4/ui/file-viewer-renderer-contract.ts
"use client";
import {
getFileViewerMotionRasterInlineSize,
type FileViewerMotionFrame,
type FileViewerMotionPhase,
} from "./file-viewer-motion-plan";
import type { ViewerDocumentTransition } from "./viewer-types";
export type FileViewerDocumentAlign = "start" | "center" | "end";
// Physical inline direction of the document frame (computed CSS `direction`).
// The fit-width motion transform works on the physical X axis, so it needs
// the direction to model where auto-margin alignment actually puts the stage.
export type FileViewerInlineDirection = "ltr" | "rtl";
// `phase` is the motion clock's state; `documentTransition` is the single
// spelling of the policies derived from it. Renderers read layout/scroll/
// visual decisions from the transition, never from duplicated top-level
// fields. `isTransitioning` is shorthand for `phase === "sliding"`.
export type FileViewerRendererFrame = {
align: FileViewerDocumentAlign;
canToggleSidebar: boolean;
direction: FileViewerInlineDirection;
documentTransition: ViewerDocumentTransition;
element: HTMLDivElement | null;
fromInlineSize: number | null;
isTransitioning: boolean;
layoutInlineSize: number | null;
motionDurationMs: number;
phase: FileViewerMotionPhase;
rasterInlineSize: number | null;
settledInlineSize: number | null;
shellInlineSize: number | null;
toInlineSize: number | null;
usesShellGeometry: boolean;
};
export function resolveFileViewerRendererLayoutInlineSize({
fallbackInlineSize,
rendererFrame,
}: {
fallbackInlineSize: number | null;
rendererFrame: FileViewerRendererFrame;
}) {
const fallbackSize = resolveMeasuredInlineSize(fallbackInlineSize);
// Commit-then-relax: the renderer lays out at the motion's TARGET width for
// the entire motion (layoutPolicy "target" from the first sliding frame).
// The in-flight visual is the surface motion transform reprojecting that
// settled layout, so settle never commits layout.
if (
rendererFrame.documentTransition.layoutPolicy === "target" &&
rendererFrame.toInlineSize != null
) {
return rendererFrame.toInlineSize;
}
return rendererFrame.layoutInlineSize ?? fallbackSize;
}
export function createFileViewerRendererFrame({
align,
canToggleSidebar,
direction = "ltr",
element,
fallbackInlineSize,
motionFrame,
motionDurationMs,
usesShellGeometry,
}: {
align: FileViewerDocumentAlign;
canToggleSidebar: boolean;
direction?: FileViewerInlineDirection;
element: HTMLDivElement | null;
fallbackInlineSize: number | null;
motionFrame: FileViewerMotionFrame;
motionDurationMs: number;
usesShellGeometry: boolean;
}): FileViewerRendererFrame {
const measuredInlineSize = resolveMeasuredInlineSize(fallbackInlineSize);
const shellInlineSize = usesShellGeometry
? motionFrame.shellInlineSize
: null;
const layoutInlineSize = usesShellGeometry
? motionFrame.layoutInlineSize
: measuredInlineSize;
const settledInlineSize = usesShellGeometry
? motionFrame.toInlineSize
: measuredInlineSize;
const rasterInlineSize = usesShellGeometry
? getFileViewerMotionRasterInlineSize(motionFrame)
: layoutInlineSize;
const fromInlineSize = usesShellGeometry
? motionFrame.fromInlineSize
: settledInlineSize;
const toInlineSize = usesShellGeometry
? motionFrame.toInlineSize
: settledInlineSize;
const documentTransition = createFileViewerRendererTransition({
motionFrame,
usesShellGeometry,
});
const phase = usesShellGeometry ? motionFrame.phase : "idle";
return {
align,
canToggleSidebar,
direction,
documentTransition,
element,
fromInlineSize,
isTransitioning: phase === "sliding",
layoutInlineSize,
motionDurationMs,
phase,
rasterInlineSize,
settledInlineSize: settledInlineSize ?? layoutInlineSize,
shellInlineSize,
toInlineSize: toInlineSize ?? layoutInlineSize,
usesShellGeometry,
};
}
function resolveMeasuredInlineSize(inlineSize: number | null | undefined) {
return inlineSize != null && Number.isFinite(inlineSize) && inlineSize > 0
? inlineSize
: null;
}
function createFileViewerRendererTransition({
motionFrame,
usesShellGeometry,
}: {
motionFrame: FileViewerMotionFrame;
usesShellGeometry: boolean;
}): ViewerDocumentTransition {
if (!usesShellGeometry) {
return {
layoutPolicy: "live",
scrollPolicy: "preserve",
source: "none",
transitionId: null,
visualPolicy: "none",
};
}
switch (motionFrame.phase) {
// Sliding commits the TARGET layout immediately (inside the toggle's own
// task, before first paint) and rebases scroll to the reading anchor in
// the same commit; the shell transform hides the jump. Settling then has
// no layout or scroll work left — it only clears the identity transform.
case "sliding":
return {
layoutPolicy: "target",
scrollPolicy: "rebase",
source: "viewer-shell",
transitionId: motionFrame.motionId,
visualPolicy: "shell-transform",
};
case "settling":
return {
layoutPolicy: "target",
scrollPolicy: "rebase",
source: "viewer-shell",
transitionId: motionFrame.motionId,
visualPolicy: "shell-transform",
};
case "idle":
return {
layoutPolicy: "live",
scrollPolicy: "preserve",
source: "none",
transitionId: null,
visualPolicy: "none",
};
}
}
registry/new-york-v4/ui/file-viewer-renderer-frame.tsx
"use client";
import * as React from "react";
import {
useOptionalFileViewerShell,
useOptionalFileViewerShellStatic,
useFileViewerShellStatic,
} from "./file-viewer-context";
import type { FileViewerDocumentSurface } from "./file-viewer-motion-kernel";
import {
createFileViewerRendererFrame,
type FileViewerDocumentAlign,
type FileViewerRendererFrame,
} from "./file-viewer-renderer-contract";
import { useFileViewerMotionFrame } from "./file-viewer-motion-kernel";
import { useViewerInlineDirection } from "./viewer-measurement";
export type FileViewerRendererEnvironment = {
registerDocumentSurface: (surface: FileViewerDocumentSurface) => () => void;
usesShellGeometry: boolean;
};
export type FileViewerDocumentFrameState = {
align: FileViewerDocumentAlign;
element: HTMLDivElement | null;
inlineSize: number | null;
};
const FileViewerDocumentFrameContext =
React.createContext<FileViewerDocumentFrameState | null>(null);
export function FileViewerDocumentFrameProvider({
children,
value,
}: {
children: React.ReactNode;
value: FileViewerDocumentFrameState;
}) {
return (
<FileViewerDocumentFrameContext.Provider value={value}>
{children}
</FileViewerDocumentFrameContext.Provider>
);
}
export function useOptionalFileViewerDocumentFrame(): FileViewerDocumentFrameState | null {
return React.useContext(FileViewerDocumentFrameContext);
}
export function useOptionalFileViewerRendererEnvironment(): FileViewerRendererEnvironment {
const { elementRegistry, usesShellGeometry } =
useFileViewerRendererEnvironmentState();
const registerDocumentSurface = React.useCallback(
(surface: FileViewerDocumentSurface) =>
elementRegistry?.registerDocumentSurface(surface) ?? (() => {}),
[elementRegistry],
);
return React.useMemo(
() => ({
registerDocumentSurface,
usesShellGeometry,
}),
[registerDocumentSurface, usesShellGeometry],
);
}
export type FileViewerSidebarMotion = {
/** True when the shell animates the sidebar (inline mode with a toggle). */
isMotionManaged: boolean;
isSidebarInteractive: boolean;
isSidebarOpen: boolean;
isSidebarTransitioning: boolean;
};
export function useOptionalFileViewerSidebarMotion(): FileViewerSidebarMotion | null {
const shell = useOptionalFileViewerShell();
return React.useMemo(
() =>
shell
? {
isMotionManaged: shell.mode === "inline" && shell.canToggleSidebar,
isSidebarInteractive: shell.isSidebarInteractive,
isSidebarOpen: shell.isSidebarOpen,
isSidebarTransitioning: shell.isSidebarTransitioning,
}
: null,
[shell],
);
}
export function useFileViewerRendererFrame({
fallbackInlineSize,
}: {
fallbackInlineSize?: number | null;
} = {}): FileViewerRendererFrame {
useFileViewerShellStatic("useFileViewerRendererFrame");
return useResolvedFileViewerRendererFrame({
fallbackInlineSize,
required: true,
});
}
export function useOptionalFileViewerRendererFrame({
fallbackInlineSize,
}: {
fallbackInlineSize?: number | null;
} = {}): FileViewerRendererFrame {
return useResolvedFileViewerRendererFrame({
fallbackInlineSize,
required: false,
});
}
function useResolvedFileViewerRendererFrame({
fallbackInlineSize,
required,
}: {
fallbackInlineSize?: number | null;
required: boolean;
}): FileViewerRendererFrame {
const { motionFrame, shell, usesShellGeometry } =
useFileViewerRendererEnvironmentState();
const documentFrame = useOptionalFileViewerDocumentFrame();
if (required && !documentFrame) {
throw new Error(
"useFileViewerRendererFrame must be used within FileViewerInset.",
);
}
const fallbackSize =
fallbackInlineSize != null && Number.isFinite(fallbackInlineSize)
? fallbackInlineSize
: null;
// The fit-width motion transform is a physical-X computation, so renderers
// need the frame's computed CSS `direction` alongside its logical align.
const direction = useViewerInlineDirection(documentFrame?.element ?? null);
return React.useMemo(
() =>
createFileViewerRendererFrame({
align: documentFrame?.align ?? "center",
canToggleSidebar: shell?.canToggleSidebar ?? false,
direction,
element: documentFrame?.element ?? null,
fallbackInlineSize: documentFrame?.inlineSize ?? fallbackSize,
motionFrame,
motionDurationMs: shell?.motionDurationMs ?? 0,
usesShellGeometry,
}),
[
direction,
documentFrame?.align,
documentFrame?.element,
documentFrame?.inlineSize,
fallbackSize,
shell?.canToggleSidebar,
motionFrame,
shell?.motionDurationMs,
usesShellGeometry,
],
);
}
function useFileViewerRendererEnvironmentState() {
const shell = useOptionalFileViewerShellStatic();
const motionFrame = useFileViewerMotionFrame(shell?.motionKernel);
const usesShellGeometry = Boolean(
shell &&
motionFrame.shellInlineSize > 0 &&
shell.mode === "inline" &&
(shell.canToggleSidebar || shell.collapsible === "none"),
);
return React.useMemo(
() => ({
elementRegistry: shell?.elementRegistry,
motionFrame,
shell,
usesShellGeometry,
}),
[motionFrame, shell, usesShellGeometry],
);
}
registry/new-york-v4/ui/file-viewer-motion-plan.ts
"use client";
import type {
FileViewerSidebarMode,
FileViewerSidebarSide,
} from "./file-viewer-context";
export const FILE_VIEWER_MOTION_EPSILON = 0.5;
// The one duration for every sidebar motion timeline — the kernel clock, the
// motion targets, and the overlay panel's CSS transition all read this value.
export const FILE_VIEWER_MOTION_DURATION_MS = 150;
// The one easing for every sidebar motion timeline. Cubic ease-out: the slide
// decelerates into rest. Linear progress ends at full velocity, and content
// far from the reading anchor (the bottom of a tall fit-width document)
// travels several pixels per millisecond straight into a hard stop — a
// visible jolt the anchor line never shows.
export function easeFileViewerMotion(timeProgress: number) {
return 1 - (1 - timeProgress) ** 3;
}
export type FileViewerMotionPhase = "idle" | "sliding" | "settling";
export type FileViewerMotionTarget = {
shellInlineSize: number;
durationMs: number;
mode: FileViewerSidebarMode;
open: boolean;
side: FileViewerSidebarSide;
sidebarWidth: number;
};
export type FileViewerMotionFrame = {
shellInlineSize: number;
durationMs: number;
fromInlineSize: number;
layoutInlineSize: number;
mode: FileViewerSidebarMode;
motionId: number | null;
motionProgress: number;
open: boolean;
phase: FileViewerMotionPhase;
side: FileViewerSidebarSide;
sidebarInlineSize: number;
sidebarWidth: number;
toInlineSize: number;
};
export type FileViewerMotionRestFrame = Pick<
FileViewerMotionFrame,
| "shellInlineSize"
| "durationMs"
| "layoutInlineSize"
| "mode"
| "open"
| "side"
| "sidebarInlineSize"
| "sidebarWidth"
>;
export type FileViewerMotionPlan = {
currentRestFrame: FileViewerMotionRestFrame;
fromInlineSize: number;
nextRestFrame: FileViewerMotionRestFrame;
resolvedTarget: FileViewerMotionTarget;
shouldAnimate: boolean;
};
export function createFileViewerMotionRestFrame(
target: FileViewerMotionTarget,
): FileViewerMotionRestFrame {
const shellInlineSize = target.shellInlineSize;
const sidebarInlineSize =
target.mode === "inline" && target.open
? Math.min(target.sidebarWidth, shellInlineSize)
: 0;
return {
shellInlineSize,
durationMs: target.durationMs,
layoutInlineSize: Math.max(0, shellInlineSize - sidebarInlineSize),
mode: target.mode,
open: target.open,
side: target.side,
sidebarInlineSize,
sidebarWidth: target.sidebarWidth,
};
}
export function createFileViewerIdleMotionFrame(
restFrame: FileViewerMotionRestFrame,
): FileViewerMotionFrame {
return {
...restFrame,
fromInlineSize: restFrame.layoutInlineSize,
motionId: null,
motionProgress: 1,
phase: "idle",
toInlineSize: restFrame.layoutInlineSize,
};
}
export function getFileViewerMotionRasterInlineSize(
frame: Pick<
FileViewerMotionFrame,
"fromInlineSize" | "layoutInlineSize" | "toInlineSize"
>,
): number {
return Math.max(
frame.fromInlineSize,
frame.toInlineSize,
frame.layoutInlineSize,
);
}
export function createFileViewerMotionPlan({
animate,
currentFrame,
nextTarget,
}: {
animate: boolean;
currentFrame: FileViewerMotionFrame;
nextTarget: FileViewerMotionTarget;
}): FileViewerMotionPlan {
const resolvedTarget = resolveFileViewerMotionTarget({
currentFrame,
nextTarget,
});
const currentRestFrame = pickFileViewerMotionRestFrame(currentFrame);
const nextRestFrame = createFileViewerMotionRestFrame(resolvedTarget);
// The motion's visual origin is what is on screen RIGHT NOW: for a fresh
// motion that is the rest layout; for a mid-flight retarget it is the live
// interpolated width, so the new motion's first frame (and every renderer's
// anchor solve) continues from the picture the reader is looking at rather
// than the interrupted motion's origin.
const fromInlineSize =
currentFrame.phase === "sliding"
? currentFrame.layoutInlineSize
: currentRestFrame.layoutInlineSize;
const shouldAnimate =
animate &&
resolvedTarget.mode === "inline" &&
currentFrame.shellInlineSize > 0 &&
Math.abs(
currentRestFrame.sidebarInlineSize - nextRestFrame.sidebarInlineSize,
) > FILE_VIEWER_MOTION_EPSILON &&
resolvedTarget.durationMs > 0;
return {
currentRestFrame,
fromInlineSize,
nextRestFrame,
resolvedTarget,
shouldAnimate,
};
}
export function areFileViewerMotionRestFramesEqual(
previous: FileViewerMotionRestFrame,
next: FileViewerMotionRestFrame,
) {
return (
areFileViewerMotionNumbersEqual(
previous.shellInlineSize,
next.shellInlineSize,
) &&
previous.durationMs === next.durationMs &&
areFileViewerMotionNumbersEqual(
previous.layoutInlineSize,
next.layoutInlineSize,
) &&
previous.mode === next.mode &&
previous.open === next.open &&
previous.side === next.side &&
areFileViewerMotionNumbersEqual(
previous.sidebarInlineSize,
next.sidebarInlineSize,
) &&
areFileViewerMotionNumbersEqual(previous.sidebarWidth, next.sidebarWidth)
);
}
export function areFileViewerMotionFramesEqual(
previous: FileViewerMotionFrame,
next: FileViewerMotionFrame,
) {
return (
areFileViewerMotionRestFramesEqual(previous, next) &&
previous.motionId === next.motionId &&
areFileViewerMotionNumbersEqual(
previous.motionProgress,
next.motionProgress,
) &&
previous.phase === next.phase &&
areFileViewerMotionNumbersEqual(
previous.fromInlineSize,
next.fromInlineSize,
) &&
areFileViewerMotionNumbersEqual(previous.toInlineSize, next.toInlineSize)
);
}
function pickFileViewerMotionRestFrame(
frame: FileViewerMotionFrame,
): FileViewerMotionRestFrame {
return {
shellInlineSize: frame.shellInlineSize,
durationMs: frame.durationMs,
layoutInlineSize: frame.layoutInlineSize,
mode: frame.mode,
open: frame.open,
side: frame.side,
sidebarInlineSize: frame.sidebarInlineSize,
sidebarWidth: frame.sidebarWidth,
};
}
function resolveFileViewerMotionTarget({
currentFrame,
nextTarget,
}: {
currentFrame: FileViewerMotionFrame;
nextTarget: FileViewerMotionTarget;
}): FileViewerMotionTarget {
if (
nextTarget.mode !== "overlay" ||
currentFrame.mode !== "inline" ||
currentFrame.shellInlineSize <= 0
) {
return nextTarget;
}
return {
...nextTarget,
shellInlineSize:
nextTarget.shellInlineSize > 0
? nextTarget.shellInlineSize
: currentFrame.shellInlineSize,
mode: "inline",
};
}
function areFileViewerMotionNumbersEqual(previous: number, next: number) {
return Math.abs(previous - next) <= 0.001;
}
registry/new-york-v4/ui/markdown-greenfield-renderer-frame.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT } from "./file-viewer-elements";
import { createFileViewerAlignTranslateSurfaceMotionResolver } from "./file-viewer-fit-width-motion";
import {
resolveFileViewerRendererLayoutInlineSize,
type FileViewerDocumentAlign,
} from "./file-viewer-renderer-contract";
import {
useOptionalFileViewerRendererEnvironment,
useOptionalFileViewerRendererFrame,
} from "./file-viewer-renderer-frame";
import { MARKDOWN_GREENFIELD_CHUNK_MAX_INLINE_SIZE } from "./markdown-greenfield-layout";
export type MarkdownGreenfieldRendererFrame = {
setDocumentSurfaceElement: React.RefCallback<HTMLDivElement>;
transformOrigin: string;
usesShellGeometry: boolean;
viewportInlineSize: number;
};
export function useMarkdownGreenfieldRendererFrame({
fallbackViewportInlineSize,
onBeforeLayoutMotion,
}: {
fallbackViewportInlineSize: number;
onBeforeLayoutMotion: () => void;
}): MarkdownGreenfieldRendererFrame {
const { registerDocumentSurface, usesShellGeometry } =
useOptionalFileViewerRendererEnvironment();
const rendererFrame = useOptionalFileViewerRendererFrame({
fallbackInlineSize: fallbackViewportInlineSize,
});
const viewportInlineSize =
resolveFileViewerRendererLayoutInlineSize({
fallbackInlineSize: fallbackViewportInlineSize,
rendererFrame,
}) ?? fallbackViewportInlineSize;
const surfaceRef = React.useRef<HTMLDivElement | null>(null);
const [surfaceElement, setSurfaceElement] =
React.useState<HTMLDivElement | null>(null);
const handleBeforeLayoutMotion = React.useCallback(() => {
onBeforeLayoutMotion();
}, [onBeforeLayoutMotion]);
const setDocumentSurfaceElement = React.useCallback(
(element: HTMLDivElement | null) => {
const previousElement = surfaceRef.current;
if (previousElement === element) return;
previousElement?.removeEventListener(
FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,
handleBeforeLayoutMotion,
);
surfaceRef.current = element;
setSurfaceElement((previous) =>
previous === element ? previous : element,
);
if (!element) return;
element.addEventListener(
FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,
handleBeforeLayoutMotion,
);
},
[handleBeforeLayoutMotion],
);
// Commit-then-relax's relax half. The canvas commits the motion's TARGET
// width via minWidth from the first sliding frame, so on a widening pane
// the centered chunks would recenter synchronously with the click; the
// resolver translates them back to the live width's align margin and eases
// to identity. The chunk column is CLAMPED (max inline size), not
// fit-width — its content width does not scale with the pane — so the
// reprojection is translate-only, never a scale.
//
// The align is the CHUNK COLUMN's, not rendererFrame.align: the shell
// declares the document frame "start" (inert — the frame is w-full), while
// the column centers itself inside the canvas by its own markup (left-1/2
// -translate-x-1/2, physical and direction-independent). Deriving from the
// shell align here made the resolver a silent no-op and the close-leg snap
// survived it.
const resolveSurfaceMotionStyle = React.useMemo(
() =>
createFileViewerAlignTranslateSurfaceMotionResolver({
align: "center",
direction: rendererFrame.direction,
maxStageInlineSize: MARKDOWN_GREENFIELD_CHUNK_MAX_INLINE_SIZE,
}),
[rendererFrame.direction],
);
const documentSurfaceKey = surfaceElement
? joinEffectKey([
"markdown-document-surface",
surfaceElement,
registerDocumentSurface,
resolveSurfaceMotionStyle,
])
: null;
useKeyedLayoutEffect(documentSurfaceKey, () => {
if (!surfaceElement) return;
return registerDocumentSurface({
element: surfaceElement,
resolveMotionStyle: resolveSurfaceMotionStyle,
});
});
return React.useMemo(
() => ({
setDocumentSurfaceElement,
transformOrigin: getMarkdownGreenfieldDocumentTransformOrigin(
rendererFrame.align,
),
usesShellGeometry,
viewportInlineSize,
}),
[
rendererFrame.align,
setDocumentSurfaceElement,
usesShellGeometry,
viewportInlineSize,
],
);
}
function getMarkdownGreenfieldDocumentTransformOrigin(
align: FileViewerDocumentAlign,
) {
switch (align) {
case "start":
return "left top";
case "end":
return "right top";
case "center":
return "center top";
}
}
registry/new-york-v4/ui/viewer-types.ts
import type * as React from "react";
export type ViewerSidebarMode = "inline" | "overlay";
export type ViewerSidebarRequestedMode = "auto" | ViewerSidebarMode;
export type ViewerSidebarGapTransition = "width" | "none";
export type ViewerSidebarState = "expanded" | "collapsed";
export type ViewerSidebarSide = "left" | "right";
export type ViewerSidebarCollapsible = "offcanvas" | "none";
export type ViewerDocumentFrameAlign = "start" | "center" | "end";
export type ViewerGeometryTransitionPhase = "idle" | "sliding";
export type ViewerDocumentReadingAnchorInput = {
scrollTop: number;
viewportBlockSize: number;
};
export type ViewerDocumentReadingAnchorTarget<Anchor> = {
anchor: Anchor;
viewportBlockSize: number;
};
export type ViewerDocumentTransitionSource =
| "none"
| "viewer-shell"
| "document-layout";
// Commit-then-relax: layout commits its target inside the motion's first
// frame and scroll rebases in the same commit, so there is no frozen layout
// and no deferred scroll left in the vocabulary.
export type ViewerDocumentLayoutPolicy = "live" | "target";
export type ViewerDocumentScrollPolicy = "preserve" | "rebase";
export type ViewerDocumentVisualPolicy =
| "none"
| "document-flip"
| "shell-transform";
export type ViewerDocumentTransition = {
layoutPolicy: ViewerDocumentLayoutPolicy;
scrollPolicy: ViewerDocumentScrollPolicy;
source: ViewerDocumentTransitionSource;
transitionId: number | string | null;
visualPolicy: ViewerDocumentVisualPolicy;
};
export type ViewerDocumentLayoutModel<Anchor> = {
blockSize: number;
captureReadingAnchor: (
input: ViewerDocumentReadingAnchorInput,
) => Anchor | null;
getReadingAnchorScrollTop: (
target: ViewerDocumentReadingAnchorTarget<Anchor>,
) => number | null;
inlineSize: number;
isTransitioning?: boolean;
transition?: ViewerDocumentTransition;
};
export type ViewerDocumentPhysicalScrollPosition = {
physicalScrollTop: number;
scrollPageOffset: number;
};
export type ViewerDocumentResolvedScrollTarget = {
left?: number;
top: number;
};
export type ViewerDocumentScrollMapper = {
getLogicalScrollTop: (input: {
blockSize: number;
physicalScrollTop: number;
scrollPageOffset: number;
viewportBlockSize: number;
}) => number;
getPhysicalScrollSize: (input: {
blockSize: number;
viewportBlockSize: number;
}) => number;
resolvePhysicalScrollPosition: (input: {
blockSize: number;
logicalScrollTop: number;
scrollPageOffset: number;
viewportBlockSize: number;
}) => ViewerDocumentPhysicalScrollPosition;
};
export type ViewerDocumentScrollMetrics = {
physicalScrollSize: number;
physicalScrollTop: number;
scrollPageOffset: number;
scrollTop: number;
viewportBlockSize: number;
};
export type ViewerDocumentScrollTargetResolver<Anchor, Target> = (input: {
layout: ViewerDocumentLayoutModel<Anchor>;
scrollTop: number;
target: Target;
viewportElement: HTMLDivElement;
}) => ViewerDocumentResolvedScrollTarget | null;
// A zoom step is the one geometry change whose intent is "zoom the camera",
// not "keep my reading position": it re-anchors the viewport CENTER on both
// axes and relaxes a FLIP about that fixed point. `capture` runs in the zoom
// gesture's own task against the pre-zoom layout and painted DOM;
// `resolveScrollTarget` and `play` run inside the geometry commit against the
// post-zoom layout (commit-then-relax).
export type ViewerDocumentZoomMotionBypassReason =
| "resolve-failed"
| "shell-transition"
| "stale-intent";
export type ViewerDocumentZoomMotionController<Transaction = unknown> = {
capture: (input: {
scrollTop: number;
viewportElement: HTMLDivElement;
}) => Transaction | null;
/**
* Telemetry tap: a captured zoom intent reached a geometry commit but the
* zoom lane declined it. Without this the bypass is invisible — the commit
* falls back to the reading-anchor restore and no flight is recorded.
*/
noteBypass?: (reason: ViewerDocumentZoomMotionBypassReason) => void;
resolveScrollTarget: (input: {
transaction: Transaction;
viewportElement: HTMLDivElement;
}) => ViewerDocumentResolvedScrollTarget | null;
play: (input: {
transaction: Transaction;
viewportElement: HTMLDivElement;
}) => (() => void) | null;
};
export type ViewerGeometrySnapshot = {
bodyInlineSize: number;
documentInlineSize: number;
hasMeasuredBody: boolean;
isTransitioning: boolean;
mode: ViewerSidebarMode;
open: boolean;
progress: number;
sidebarGapTransition: ViewerSidebarGapTransition;
sidebarInlineSize: number;
sidebarWidth: number;
side: ViewerSidebarSide;
state: ViewerSidebarState;
transitionPhase: ViewerGeometryTransitionPhase;
};
export type ViewerGeometryStore = {
getSnapshot: () => ViewerGeometrySnapshot;
setTarget: (target: ViewerGeometryTarget) => void;
subscribe: (listener: () => void) => () => void;
};
export type ViewerGeometryTarget = {
bodyElement: HTMLElement | null;
mode: ViewerSidebarMode;
open: boolean;
rootElement: HTMLElement | null;
sidebarElement: HTMLElement | null;
sidebarGapTransition: ViewerSidebarGapTransition;
sidebarWidth: number;
side: ViewerSidebarSide;
state: ViewerSidebarState;
};
export type ViewerSidebarStateValue = {
state: ViewerSidebarState;
open: boolean;
setOpen: (value: boolean | ((open: boolean) => boolean)) => void;
toggleSidebar: () => void;
canToggleSidebar: boolean;
mode: ViewerSidebarMode;
side: ViewerSidebarSide;
};
export type ViewerRootProps = React.ComponentProps<"div"> & {
defaultOpen?: boolean;
inlineBreakpoint?: number;
mode?: ViewerSidebarRequestedMode;
onOpenChange?: (open: boolean) => void;
open?: boolean;
sidebarCollapsible?: ViewerSidebarCollapsible;
sidebarGapTransition?: ViewerSidebarGapTransition;
sidebarSide?: ViewerSidebarSide;
stateNamespace?: ViewerStateAttributeNamespace;
};
export type ViewerFrameProps = React.ComponentProps<"div">;
export type ViewerHeaderProps = React.ComponentProps<"div">;
export type ViewerBodyProps = React.ComponentProps<"div">;
export type ViewerSurfaceProps = React.ComponentProps<"div">;
export type ViewerViewportProps = React.ComponentProps<"div">;
export type ViewerDocumentFrameProps = React.ComponentProps<"div"> & {
align?: ViewerDocumentFrameAlign;
maxInlineSize?: React.CSSProperties["maxInlineSize"];
};
export type ViewerStateAttributeNamespace = {
prefix: string;
slots?: {
body?: boolean;
root?: boolean;
sidebar?: boolean;
};
};
export type ViewerSidebarRegistration = {
collapsible: ViewerSidebarCollapsible;
element: HTMLElement;
id: string;
instanceId: string;
side: ViewerSidebarSide;
width: string;
widthPixels: number;
};
export type ViewerPortalContainmentAttributes = {
"data-viewer-portal-root-id": string;
};
export type ViewerSidebarRegistrationState = {
defaultSidebarCollapsible: ViewerSidebarCollapsible;
defaultSidebarSide: ViewerSidebarSide;
geometryStore: ViewerGeometryStore;
getRootElement: () => HTMLElement | null;
hasSidebar: boolean;
registerBody: (element: HTMLElement) => () => void;
registerSidebar: (registration: ViewerSidebarRegistration) => () => void;
rootId: string;
sidebarId: string;
sidebarGapTransition: ViewerSidebarGapTransition;
sidebarSide: ViewerSidebarSide;
setLastTriggerElement: (element: HTMLElement | null) => void;
stateNamespace?: ViewerStateAttributeNamespace;
};
export type ViewerRootDiagnostics = {
getRootElement: () => HTMLElement | null;
layoutSignature: string;
rootId: string;
};
export type ViewerSurfaceMeasurement = {
hasMeasured: boolean;
setViewportElement: React.RefCallback<HTMLDivElement>;
viewportElement: HTMLDivElement | null;
viewportHeight: number | null;
viewportWidth: number | null;
};
export type ViewerSidebarSlotNames = {
container?: string;
gap?: string;
inner?: string;
};
export type ViewerStateAttributeSlot = "body" | "root" | "sidebar";
export type ViewerStateAttributeValues = {
hasSidebar?: boolean;
sidebarCollapsible?: ViewerSidebarCollapsible;
sidebarMode?: ViewerSidebarMode;
sidebarOpen?: boolean;
sidebarSide?: ViewerSidebarSide;
sidebarState?: ViewerSidebarState;
};
export type ViewerDataAttributes = Record<`data-${string}`, string | undefined>;
export type ViewerSidebarProps = React.ComponentProps<"aside"> &
ViewerDataAttributes & {
side?: ViewerSidebarSide;
collapsible?: ViewerSidebarCollapsible;
innerClassName?: string;
namespacedSlot?: string;
namespacedSlotNames?: ViewerSidebarSlotNames;
slotNames?: ViewerSidebarSlotNames;
width?: string;
};
registry/new-york-v4/ui/viewer-measurement.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
export type StableElementSize<Element extends HTMLElement = HTMLElement> = {
element: Element | null;
hasMeasured: boolean;
height: number | null;
setElement: React.RefCallback<Element>;
width: number | null;
};
export type StableElementSizeOptions = {
enabled?: boolean;
observe?: boolean;
retainLastNonZero?: boolean;
};
export type StableCssLengthOptions = {
element: HTMLElement | null;
retainLastNonZero?: boolean;
value: string;
};
type MeasuredSize = {
height: number | null;
width: number | null;
};
type RawMeasuredSize = {
height: number;
width: number;
};
// DOM layout reads for viewer chrome are quarantined in this module. The
// file-viewer motion kernel (time + style writes only) receives this reader by
// injection from the frame controller instead of touching layout APIs itself.
export function readElementRectSnapshot(
element: HTMLElement | null,
): readonly number[] {
if (!element) return [];
const rect = element.getBoundingClientRect();
return [rect.left, rect.top, rect.width, rect.height];
}
// Computed CSS inline direction of an element, sampled when it attaches (a
// runtime `dir` flip is picked up on the next mount). The fit-width motion
// transform works on the physical X axis, so the renderer frame needs to
// know which edge auto-margin alignment pins the stage to.
export function useViewerInlineDirection(
element: HTMLElement | null,
): "ltr" | "rtl" {
const [direction, setDirection] = React.useState<"ltr" | "rtl">("ltr");
useKeyedLayoutEffect(element ? joinEffectKey([element]) : null, () => {
if (!element) return;
setDirection(getComputedStyle(element).direction === "rtl" ? "rtl" : "ltr");
});
return direction;
}
function readElementSize(element: HTMLElement): RawMeasuredSize {
const rect =
typeof element.getBoundingClientRect === "function"
? element.getBoundingClientRect()
: null;
return {
height: rect?.height || element.clientHeight,
width: rect?.width || element.clientWidth,
};
}
function resolveMeasuredElementSize({
currentSize,
nextSize,
retainLastNonZero,
}: {
currentSize: MeasuredSize;
nextSize: RawMeasuredSize;
retainLastNonZero: boolean;
}): MeasuredSize {
const width =
Number.isFinite(nextSize.width) &&
(!retainLastNonZero || nextSize.width > 0)
? nextSize.width
: currentSize.width;
const height =
Number.isFinite(nextSize.height) &&
(!retainLastNonZero || nextSize.height > 0)
? nextSize.height
: currentSize.height;
if (currentSize.width === width && currentSize.height === height) {
return currentSize;
}
return { height, width };
}
export function useStableElementSize<Element extends HTMLElement = HTMLElement>(
options: StableElementSizeOptions = {},
): StableElementSize<Element> {
const enabled = options.enabled ?? true;
const observe = options.observe ?? true;
const retainLastNonZero = options.retainLastNonZero ?? false;
const [element, setElementState] = React.useState<Element | null>(null);
const [size, setSize] = React.useState<MeasuredSize>({
height: null,
width: null,
});
const hasMeasured = size.height !== null || size.width !== null;
const setElement = React.useCallback((nextElement: Element | null) => {
setElementState(nextElement);
}, []);
useKeyedLayoutEffect(enabled ? null : "reset", () => {
setSize({ height: null, width: null });
});
useKeyedLayoutEffect(
enabled && element
? joinEffectKey([element, observe, retainLastNonZero])
: null,
() => {
if (!element) return;
setSize((currentSize) =>
resolveMeasuredElementSize({
currentSize,
nextSize: readElementSize(element),
retainLastNonZero,
}),
);
const ResizeObserverConstructor = observe
? globalThis.ResizeObserver
: undefined;
if (typeof ResizeObserverConstructor === "undefined") return;
let frame = 0;
let latestSize = readElementSize(element);
const observer = new ResizeObserverConstructor((entries) => {
for (const entry of entries) {
latestSize = readElementSize(entry.target as HTMLElement);
}
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
setSize((currentSize) =>
resolveMeasuredElementSize({
currentSize,
nextSize: latestSize,
retainLastNonZero,
}),
);
});
});
observer.observe(element);
return () => {
if (frame) cancelAnimationFrame(frame);
observer.disconnect();
};
},
);
return React.useMemo(
() => ({
element,
hasMeasured,
height: size.height,
setElement,
width: size.width,
}),
[element, hasMeasured, setElement, size.height, size.width],
);
}
export function useStableCssLength({
element,
retainLastNonZero = true,
value,
}: StableCssLengthOptions) {
const [resolvedLength, setResolvedLength] = React.useState(0);
useKeyedLayoutEffect(
value ? joinEffectKey([element, retainLastNonZero, value]) : null,
() => {
const nextLength = resolveCssLength(value, element);
setResolvedLength((currentLength) => {
if (retainLastNonZero && nextLength <= 0) return currentLength;
return areCssLengthsEqual(currentLength, nextLength)
? currentLength
: nextLength;
});
},
);
return resolvedLength;
}
function resolveCssLength(value: string, element: HTMLElement | null) {
const trimmedValue = value.trim();
const pixelMatch = trimmedValue.match(/^(-?\d+(?:\.\d+)?)px$/);
if (pixelMatch) return Math.max(0, Number(pixelMatch[1]));
if (typeof window === "undefined") return 0;
const remMatch = trimmedValue.match(/^(-?\d+(?:\.\d+)?)rem$/);
if (remMatch) {
return (
Math.max(0, Number(remMatch[1])) *
readComputedFontSize(window.document.documentElement)
);
}
const emMatch = trimmedValue.match(/^(-?\d+(?:\.\d+)?)em$/);
if (emMatch) {
return Math.max(0, Number(emMatch[1])) * readComputedFontSize(element);
}
const measuringElement = window.document.createElement("div");
measuringElement.style.contain = "strict";
measuringElement.style.inlineSize = trimmedValue;
measuringElement.style.position = "absolute";
measuringElement.style.visibility = "hidden";
(element ?? window.document.body).appendChild(measuringElement);
const width = readElementInlineSize(measuringElement);
measuringElement.remove();
return width;
}
function readElementInlineSize(element: HTMLElement) {
const rect =
typeof element.getBoundingClientRect === "function"
? element.getBoundingClientRect()
: null;
const width = rect?.width || element.clientWidth || 0;
return Number.isFinite(width) && width > 0 ? width : 0;
}
function readComputedFontSize(element: Element | null) {
if (typeof window === "undefined") return 16;
const fontSize = element ? window.getComputedStyle(element).fontSize : "16px";
const value = Number.parseFloat(fontSize);
return Number.isFinite(value) && value > 0 ? value : 16;
}
function areCssLengthsEqual(previous: number, next: number) {
return Math.abs(previous - next) <= 0.001;
}
registry/new-york-v4/ui/file-viewer-fit-width-motion.ts
"use client";
import type { FileViewerDocumentSurfaceMotionResolver } from "./file-viewer-motion-kernel";
import type { FileViewerMotionFrame } from "./file-viewer-motion-plan";
import type {
FileViewerDocumentAlign,
FileViewerInlineDirection,
} from "./file-viewer-renderer-contract";
export const FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY =
"--file-viewer-fit-width-anchor-block";
// Commit-then-relax: the renderer lays out at the motion's TARGET width from
// the first commit, and this resolver reprojects that settled layout to the
// in-flight visual width with one uniform transform. The transform terminates
// on identity, so settle removes a no-op style instead of committing layout.
//
// The anchor custom property is the reading line's block offset in the settled
// stage's own coordinates (post-rebase scrollTop + marker offset). It is read
// live via var(), so the renderer writes it once per motion (in a layout
// effect after the slide-start scroll rebase) without re-entering the kernel.
export function createFileViewerFitWidthSurfaceMotionResolver({
align,
anchorBlockProperty = FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY,
direction = "ltr",
isFitWidth,
stageInlineSize,
stageOuterInlinePadding = 0,
stageInlinePadding = 0,
stageInlineSlope = 1,
stageBlockSlope = stageInlineSlope,
}: {
align: FileViewerDocumentAlign;
anchorBlockProperty?: string;
direction?: FileViewerInlineDirection;
isFitWidth: boolean;
stageInlineSize: number;
/** Constant symmetric padding around the transformed stage. */
stageOuterInlinePadding?: number;
/** Constant symmetric inline padding inside the transformed stage. */
stageInlinePadding?: number;
stageInlineSlope?: number;
/**
* Slope for the BLOCK axis when it differs from the inline one. A stage
* whose inline box carries constant padding while its block stack scales
* with the content (the image viewer: fit subtracts the horizontal
* padding, vertical gaps/padding scale) has two different affine models —
* X tracks the pane 1:1 while Y scales by the content ratio — and a
* uniform scale cannot land both axes exactly. Defaults to the inline
* slope (uniform scale) for fully proportional stages like the PDF.
*/
stageBlockSlope?: number;
}): FileViewerDocumentSurfaceMotionResolver {
return (frame) => {
if (!isFitWidth || frame.phase !== "sliding") {
return {
transform: "",
transformOrigin: "",
willChange: "",
};
}
return {
transform: getFileViewerFitWidthSurfaceMotionTransform({
align,
anchorBlockProperty,
direction,
frame,
stageInlineSize,
stageOuterInlinePadding,
stageInlinePadding,
stageInlineSlope,
stageBlockSlope,
}),
transformOrigin: "0px 0px",
willChange: "transform",
};
};
}
// Commit-then-relax for a CLAMPED reading column rather than a fit-width
// stage: the stage's inline size is min(canvas, maxStageInlineSize), so it
// does not scale with the pane — the only thing a width change moves is the
// align margin. The canvas commits the motion's TARGET width from the first
// sliding frame (minWidth under layoutPolicy "target"), which means a
// widening pane's chunks land at the settled margin synchronously with the
// click; this resolver reprojects them back to the live width's margin with
// a translate that terminates on identity. A narrowing pane never engages it
// (the canvas tracks the live width above its minWidth, so live and settled
// margins agree) — exactly the leg that already glides on layout.
export function createFileViewerAlignTranslateSurfaceMotionResolver({
align,
direction = "ltr",
maxStageInlineSize,
}: {
align: FileViewerDocumentAlign;
direction?: FileViewerInlineDirection;
/** The column's max inline size (the chunk's max-width, in px). */
maxStageInlineSize: number;
}): FileViewerDocumentSurfaceMotionResolver {
return (frame) => {
if (frame.phase !== "sliding") {
return {
transform: "",
transformOrigin: "",
willChange: "",
};
}
return {
transform: getFileViewerAlignTranslateSurfaceMotionTransform({
align,
direction,
frame,
maxStageInlineSize,
}),
transformOrigin: "0px 0px",
willChange: "transform",
};
};
}
function getFileViewerAlignTranslateSurfaceMotionTransform({
align,
direction,
frame,
maxStageInlineSize,
}: {
align: FileViewerDocumentAlign;
direction: FileViewerInlineDirection;
frame: FileViewerMotionFrame;
maxStageInlineSize: number;
}) {
if (
!Number.isFinite(maxStageInlineSize) ||
maxStageInlineSize <= 0 ||
frame.layoutInlineSize <= 0 ||
frame.toInlineSize <= 0
) {
return "";
}
// The canvas lays out at max(live, target): minWidth holds the committed
// target under a still-narrow pane, and a pane wider than the target just
// fills. The stage (reading column) centers/aligns INSIDE the canvas, and
// an overflowing canvas itself pins to the pane's start edge — left in
// LTR, right in RTL — so the stage's pane-space position carries the
// canvas offset too.
const canvasInlineSize = Math.max(frame.layoutInlineSize, frame.toInlineSize);
const stageInlineSize = Math.min(canvasInlineSize, maxStageInlineSize);
const canvasInlineOffset =
direction === "rtl"
? Math.min(0, frame.layoutInlineSize - canvasInlineSize)
: 0;
const settledStageLeft =
canvasInlineOffset +
getFileViewerStageInlineMargin({
align,
availableInlineSize: canvasInlineSize,
direction,
stageInlineSize,
});
const liveStageLeft = getFileViewerStageInlineMargin({
align,
availableInlineSize: frame.layoutInlineSize,
direction,
stageInlineSize,
});
const translateX = liveStageLeft - settledStageLeft;
if (Math.abs(translateX) <= 0.001) return "";
return `translate3d(${formatFileViewerMotionPixel(translateX)}px, 0px, 0)`;
}
export function getFileViewerFitWidthScale({
availableInlineSize,
contentInlineSize,
stageInlinePadding = 0,
}: {
availableInlineSize: number;
contentInlineSize: number;
stageInlinePadding?: number;
}) {
if (availableInlineSize <= 0 || contentInlineSize <= 0) return 1;
const contentAvailableInlineSize = Math.max(
1,
availableInlineSize - stageInlinePadding,
);
return contentAvailableInlineSize / contentInlineSize;
}
// The visual scale the resolver renders for a given live width — the same
// affine reprojection as the transform itself. Renderers use it to reason
// about the on-screen state (anchor capture/solve) without duplicating the
// formula.
//
// stageInlineSlope is how many stage pixels the settled stage grows per pane
// pixel. It is 1 whenever the stage IS the fit-width content (image, docx,
// pptx, uniform-width PDFs: stage = pane − constant padding), but a stage
// that is WIDER than its fit basis grows faster than the pane — a PDF fits
// its FIRST page while the stage spans its WIDEST page, so a mixed-width
// document has slope maxBase/fitBase > 1. A unit-slope assumption there
// under-scales the first frame by (slope − 1)·delta/stage — measured as a
// ~7px content step at the anchor-hold frame of a 355-page prospectus.
export function getFileViewerFitWidthVisualScale({
liveInlineSize,
stageInlineSize,
stageInlineSlope = 1,
targetInlineSize,
}: {
liveInlineSize: number;
stageInlineSize: number;
stageInlineSlope?: number;
targetInlineSize: number;
}) {
if (
stageInlineSize <= 0 ||
!Number.isFinite(liveInlineSize) ||
!Number.isFinite(targetInlineSize)
) {
return 1;
}
const slope =
Number.isFinite(stageInlineSlope) && stageInlineSlope > 0
? stageInlineSlope
: 1;
return (
Math.max(1, stageInlineSize + slope * (liveInlineSize - targetInlineSize)) /
stageInlineSize
);
}
// Capture side of the motion anchor: the probe content line's on-screen block
// offset relative to the scroll box, taken just before a motion (or retarget)
// commits. When a motion is already in flight the DOM is the settled layout
// PLUS the live transform, so the capture applies that transform — otherwise
// a retarget would solve continuity against a picture the reader never saw.
export function captureFileViewerFitWidthAnchorScreenOffset({
lastAnchorBlock,
liveFrame,
probeStageOffset,
scrollTop,
stageInlineSize,
stageInlinePadding = 0,
stageBlockSlope = 1,
}: {
lastAnchorBlock: number | null;
liveFrame: FileViewerMotionFrame | null;
probeStageOffset: number;
scrollTop: number;
stageInlineSize: number;
stageInlinePadding?: number;
/** The BLOCK-axis slope — anchor capture/solve is block-axis math. */
stageBlockSlope?: number;
}) {
const untransformed = probeStageOffset - scrollTop;
if (!liveFrame || liveFrame.phase !== "sliding") return untransformed;
const liveScale = getFileViewerFitWidthVisualScale({
liveInlineSize: liveFrame.layoutInlineSize,
stageInlineSize: getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
}),
stageInlineSlope: stageBlockSlope,
targetInlineSize: liveFrame.toInlineSize,
});
if (Math.abs(1 - liveScale) <= 0.001) return untransformed;
return (
liveScale * probeStageOffset +
(1 - liveScale) * (lastAnchorBlock ?? 0) -
scrollTop
);
}
// Solve side: the anchor block offset that puts the probe content line back on
// its captured screen position under the NEW layout model at the motion's
// first-frame scale. Exact regardless of how the rebase clamped or how the
// old/new layout models relate (measured page sizes, constant gaps/padding).
// Returns null when the motion is degenerate (caller falls back to the live
// reading marker).
export function resolveFileViewerFitWidthMotionAnchorBlock({
fromInlineSize,
probeScreenOffset,
probeStageOffset,
scrollTop,
stageInlineSize,
stageInlinePadding = 0,
stageBlockSlope = 1,
toInlineSize,
}: {
fromInlineSize: number | null;
probeScreenOffset: number;
probeStageOffset: number;
scrollTop: number;
stageInlineSize: number;
stageInlinePadding?: number;
/** The BLOCK-axis slope — anchor capture/solve is block-axis math. */
stageBlockSlope?: number;
toInlineSize: number | null;
}) {
if (fromInlineSize == null || toInlineSize == null) return null;
const startScale = getFileViewerFitWidthVisualScale({
liveInlineSize: fromInlineSize,
stageInlineSize: getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
}),
stageInlineSlope: stageBlockSlope,
targetInlineSize: toInlineSize,
});
if (!Number.isFinite(startScale) || Math.abs(1 - startScale) <= 0.001) {
return null;
}
return (
(probeScreenOffset + scrollTop - startScale * probeStageOffset) /
(1 - startScale)
);
}
function getFileViewerFitWidthSurfaceMotionTransform({
align,
anchorBlockProperty,
direction,
frame,
stageInlineSize,
stageOuterInlinePadding,
stageInlinePadding,
stageInlineSlope,
stageBlockSlope,
}: {
align: FileViewerDocumentAlign;
anchorBlockProperty: string;
direction: FileViewerInlineDirection;
frame: FileViewerMotionFrame;
stageInlineSize: number;
stageOuterInlinePadding: number;
stageInlinePadding: number;
stageInlineSlope: number;
stageBlockSlope: number;
}) {
if (
stageInlineSize <= 0 ||
frame.layoutInlineSize <= 0 ||
frame.toInlineSize <= 0
) {
return "";
}
// Fit-width renderers size their stage as an affine function of the
// available width (stage = slope × width − constant padding), so the
// in-flight visual stage is the settled stage plus the scaled live width
// delta. At the first frame this resolves to exactly the pre-toggle stage
// size, and at the last frame to the settled stage — identity. Each axis
// carries its own slope: they differ when the stage's inline box holds
// constant padding while its block stack scales with the content.
const contentInlineSize = getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
});
const inlineScale = getFileViewerFitWidthVisualScale({
liveInlineSize: frame.layoutInlineSize,
stageInlineSize: contentInlineSize,
stageInlineSlope,
targetInlineSize: frame.toInlineSize,
});
const blockScale = getFileViewerFitWidthVisualScale({
liveInlineSize: frame.layoutInlineSize,
stageInlineSize: contentInlineSize,
stageInlineSlope: stageBlockSlope,
targetInlineSize: frame.toInlineSize,
});
const visualStageInlineSize =
inlineScale * contentInlineSize + stageInlinePadding;
const availableStageInlineSize = Math.max(
1,
frame.layoutInlineSize - Math.max(0, stageOuterInlinePadding),
);
const settledMargin = getFileViewerStageInlineMargin({
align,
availableInlineSize: availableStageInlineSize,
direction,
stageInlineSize,
});
const visualMargin = getFileViewerStageInlineMargin({
align,
availableInlineSize: availableStageInlineSize,
direction,
stageInlineSize: visualStageInlineSize,
});
// The padding is constant in both endpoint layouts. Scaling the outer stage
// would scale that inset too, making the visible page briefly too wide or
// narrow on the first frame. Rebase the symmetric start inset so the inner
// content edge, not the transparent wrapper edge, is pixel-continuous.
const inlinePaddingStart = stageInlinePadding / 2;
const translateX =
visualMargin - settledMargin + (1 - inlineScale) * inlinePaddingStart;
if (Math.abs(frame.layoutInlineSize - frame.toInlineSize) <= 0.001) {
return "";
}
const formattedInlineScale = formatFileViewerMotionScale(inlineScale);
const formattedBlockScale = formatFileViewerMotionScale(blockScale);
const formattedTranslateX = formatFileViewerMotionPixel(translateX);
// Scale about the stage origin; the anchor term keeps the reading line
// fixed on the block axis: y' = s·y + (1 − s)·anchor equals y at
// y = anchor.
const translateY = `calc((1 - ${formattedBlockScale}) * var(${anchorBlockProperty}, 0px))`;
const formattedScale =
formattedInlineScale === formattedBlockScale
? formattedInlineScale
: `${formattedInlineScale}, ${formattedBlockScale}`;
return `translate3d(${formattedTranslateX}px, ${translateY}, 0) scale(${formattedScale})`;
}
function getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
}: {
stageInlinePadding: number;
stageInlineSize: number;
}) {
const padding = Number.isFinite(stageInlinePadding)
? Math.max(0, stageInlinePadding)
: 0;
return Math.max(1, stageInlineSize - padding);
}
// Physical LEFT offset of the stage box inside the available inline size —
// translateX shifts along the physical X axis, so the model must speak
// physical-left in both directions. Stages align with physical auto margins
// (mx-auto for center, ml-auto for end, plain flow for start), so:
// - free space ≥ 0: center splits it; end pins right in both directions
// (ml-auto is physical); start follows flow (left in LTR, right in RTL).
// - free space < 0 (the settled stage overflows the live container — the
// close leg's early frames): auto margins collapse to 0 and CSS resolves
// the over-constraint against the direction's end edge, pinning the box to
// the start edge — left edge at 0 in LTR, at the negative free space in
// RTL. The old unconditional max(0, …) clamp encoded only the LTR half and
// made the RTL close leg overshoot by the overflow amount.
function getFileViewerStageInlineMargin({
align,
availableInlineSize,
direction,
stageInlineSize,
}: {
align: FileViewerDocumentAlign;
availableInlineSize: number;
direction: FileViewerInlineDirection;
stageInlineSize: number;
}) {
const freeInlineSize = availableInlineSize - stageInlineSize;
if (freeInlineSize < 0) return direction === "rtl" ? freeInlineSize : 0;
switch (align) {
case "start":
return direction === "rtl" ? freeInlineSize : 0;
case "end":
return freeInlineSize;
case "center":
return freeInlineSize / 2;
}
}
function formatFileViewerMotionPixel(value: number) {
return Number.isFinite(value) ? Number(value.toFixed(3)) : 0;
}
function formatFileViewerMotionScale(value: number) {
return Number.isFinite(value) ? String(Number(value.toFixed(6))) : "1";
}