import React, { useMemo, useRef } from "react";
import { useDidResolution } from "../hooks/useDidResolution";
import { useBlob } from "../hooks/useBlob";
import { useAtProto } from "../providers/AtProtoProvider";
import {
parseAtUri,
formatDidForLabel,
toBlueskyPostUrl,
leafletRkeyUrl,
normalizeLeafletBasePath,
} from "../utils/at-uri";
import { BlueskyPost } from "../components/BlueskyPost";
import type {
LeafletDocumentRecord,
LeafletLinearDocumentPage,
LeafletLinearDocumentBlock,
LeafletBlock,
LeafletTextBlock,
LeafletHeaderBlock,
LeafletBlockquoteBlock,
LeafletImageBlock,
LeafletUnorderedListBlock,
LeafletListItem,
LeafletWebsiteBlock,
LeafletIFrameBlock,
LeafletMathBlock,
LeafletCodeBlock,
LeafletBskyPostBlock,
LeafletAlignmentValue,
LeafletRichTextFacet,
LeafletRichTextFeature,
LeafletPublicationRecord,
} from "../types/leaflet";
export interface LeafletDocumentRendererProps {
record: LeafletDocumentRecord;
loading: boolean;
error?: Error;
did: string;
rkey: string;
canonicalUrl?: string;
publicationBaseUrl?: string;
publicationRecord?: LeafletPublicationRecord;
}
export const LeafletDocumentRenderer: React.FC<
LeafletDocumentRendererProps
> = ({
record,
loading,
error,
did,
rkey,
canonicalUrl,
publicationBaseUrl,
publicationRecord,
}) => {
const { blueskyAppBaseUrl } = useAtProto();
const authorDid = record.author?.startsWith("did:")
? record.author
: undefined;
const publicationUri = useMemo(
() => parseAtUri(record.publication),
[record.publication],
);
const postUrl = useMemo(() => {
const postRefUri = record.postRef?.uri;
if (!postRefUri) return undefined;
const parsed = parseAtUri(postRefUri);
return parsed ? toBlueskyPostUrl(parsed) : undefined;
}, [record.postRef?.uri]);
const { handle: publicationHandle } = useDidResolution(publicationUri?.did);
const fallbackAuthorLabel = useAuthorLabel(record.author, authorDid);
const resolvedPublicationLabel =
publicationRecord?.name?.trim() ??
(publicationHandle
? `@${publicationHandle}`
: publicationUri
? formatDidForLabel(publicationUri.did)
: undefined);
const authorLabel = resolvedPublicationLabel ?? fallbackAuthorLabel;
const authorHref = publicationUri
? `${blueskyAppBaseUrl}/profile/${publicationUri.did}`
: undefined;
if (error)
return (
Failed to load leaflet.
);
if (loading && !record)
return Loading leaflet…
;
if (!record)
return (
Leaflet record missing.
);
const publishedAt = record.publishedAt
? new Date(record.publishedAt)
: undefined;
const publishedLabel = publishedAt
? publishedAt.toLocaleString(undefined, {
dateStyle: "long",
timeStyle: "short",
})
: undefined;
const fallbackLeafletUrl = `${blueskyAppBaseUrl}/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
const publicationRoot =
publicationBaseUrl ?? publicationRecord?.base_path ?? undefined;
const resolvedPublicationRoot = publicationRoot
? normalizeLeafletBasePath(publicationRoot)
: undefined;
const publicationLeafletUrl = leafletRkeyUrl(publicationRoot, rkey);
const viewUrl =
canonicalUrl ??
publicationLeafletUrl ??
postUrl ??
(publicationUri
? `${blueskyAppBaseUrl}/profile/${publicationUri.did}`
: undefined) ??
fallbackLeafletUrl;
const metaItems: React.ReactNode[] = [];
if (authorLabel) {
const authorNode = authorHref ? (
{authorLabel}
) : (
authorLabel
);
metaItems.push(By {authorNode});
}
if (publishedLabel)
metaItems.push(
,
);
if (resolvedPublicationRoot) {
metaItems.push(
{resolvedPublicationRoot.replace(/^https?:\/\//, "")}
,
);
}
if (viewUrl) {
metaItems.push(
View source
,
);
}
return (
{record.pages?.map((page, pageIndex) => (
))}
);
};
const LeafletPageRenderer: React.FC<{
page: LeafletLinearDocumentPage;
documentDid: string;
}> = ({ page, documentDid }) => {
if (!page.blocks?.length) return null;
return (
{page.blocks.map((blockWrapper, idx) => (
))}
);
};
interface LeafletBlockRendererProps {
wrapper: LeafletLinearDocumentBlock;
documentDid: string;
isFirst?: boolean;
}
const LeafletBlockRenderer: React.FC = ({
wrapper,
documentDid,
isFirst,
}) => {
const block = wrapper.block;
if (!block || !("$type" in block) || !block.$type) {
return null;
}
const alignment = alignmentValue(wrapper.alignment);
switch (block.$type) {
case "pub.leaflet.blocks.header":
return (
);
case "pub.leaflet.blocks.blockquote":
return (
);
case "pub.leaflet.blocks.image":
return (
);
case "pub.leaflet.blocks.unorderedList":
return (
);
case "pub.leaflet.blocks.website":
return (
);
case "pub.leaflet.blocks.iframe":
return (
);
case "pub.leaflet.blocks.math":
return (
);
case "pub.leaflet.blocks.code":
return (
);
case "pub.leaflet.blocks.horizontalRule":
return (
);
case "pub.leaflet.blocks.bskyPost":
return (
);
case "pub.leaflet.blocks.text":
default:
return (
);
}
};
const LeafletTextBlockView: React.FC<{
block: LeafletTextBlock;
alignment?: React.CSSProperties["textAlign"];
isFirst?: boolean;
}> = ({ block, alignment, isFirst }) => {
const segments = useMemo(
() => createFacetedSegments(block.plaintext, block.facets),
[block.plaintext, block.facets],
);
const textContent = block.plaintext ?? "";
if (!textContent.trim() && segments.length === 0) {
return null;
}
const style: React.CSSProperties = {
...base.paragraph,
color: `var(--atproto-color-text)`,
...(alignment ? { textAlign: alignment } : undefined),
...(isFirst ? { marginTop: 0 } : undefined),
};
return (
{segments.map((segment, idx) => (
{renderSegment(segment)}
))}
);
};
const LeafletHeaderBlockView: React.FC<{
block: LeafletHeaderBlock;
alignment?: React.CSSProperties["textAlign"];
isFirst?: boolean;
}> = ({ block, alignment, isFirst }) => {
const level =
block.level && block.level >= 1 && block.level <= 6 ? block.level : 2;
const segments = useMemo(
() => createFacetedSegments(block.plaintext, block.facets),
[block.plaintext, block.facets],
);
const normalizedLevel = Math.min(Math.max(level, 1), 6) as
| 1
| 2
| 3
| 4
| 5
| 6;
const headingTag = (["h1", "h2", "h3", "h4", "h5", "h6"] as const)[
normalizedLevel - 1
];
const style: React.CSSProperties = {
...base.heading,
color: `var(--atproto-color-text)`,
fontSize: normalizedLevel === 1 ? 30 : normalizedLevel === 2 ? 28 : normalizedLevel === 3 ? 24 : normalizedLevel === 4 ? 20 : normalizedLevel === 5 ? 18 : 16,
...(alignment ? { textAlign: alignment } : undefined),
...(isFirst ? { marginTop: 0 } : undefined),
};
return React.createElement(
headingTag,
{ style },
segments.map((segment, idx) => (
{renderSegment(segment)}
)),
);
};
const LeafletBlockquoteBlockView: React.FC<{
block: LeafletBlockquoteBlock;
alignment?: React.CSSProperties["textAlign"];
isFirst?: boolean;
}> = ({ block, alignment, isFirst }) => {
const segments = useMemo(
() => createFacetedSegments(block.plaintext, block.facets),
[block.plaintext, block.facets],
);
const textContent = block.plaintext ?? "";
if (!textContent.trim() && segments.length === 0) {
return null;
}
return (
{segments.map((segment, idx) => (
{renderSegment(segment)}
))}
);
};
const LeafletImageBlockView: React.FC<{
block: LeafletImageBlock;
alignment?: React.CSSProperties["textAlign"];
documentDid: string;
}> = ({ block, alignment, documentDid }) => {
const cid = block.image?.ref?.$link ?? block.image?.cid;
const { url, loading, error } = useBlob(documentDid, cid);
const aspectRatio =
block.aspectRatio?.height && block.aspectRatio?.width
? `${block.aspectRatio.width} / ${block.aspectRatio.height}`
: undefined;
return (
{url && !error ? (

) : (
{loading
? "Loading image…"
: error
? "Image unavailable"
: "No image"}
)}
{block.alt && block.alt.trim().length > 0 && (
{block.alt}
)}
);
};
const LeafletListBlockView: React.FC<{
block: LeafletUnorderedListBlock;
alignment?: React.CSSProperties["textAlign"];
documentDid: string;
}> = ({ block, alignment, documentDid }) => {
return (
{block.children?.map((child, idx) => (
))}
);
};
const LeafletListItemRenderer: React.FC<{
item: LeafletListItem;
documentDid: string;
alignment?: React.CSSProperties["textAlign"];
}> = ({ item, documentDid, alignment }) => {
return (
{item.children && item.children.length > 0 && (
{item.children.map((child, idx) => (
))}
)}
);
};
const LeafletInlineBlock: React.FC<{
block: LeafletBlock;
documentDid: string;
alignment?: React.CSSProperties["textAlign"];
}> = ({ block, documentDid, alignment }) => {
switch (block.$type) {
case "pub.leaflet.blocks.header":
return (
);
case "pub.leaflet.blocks.blockquote":
return (
);
case "pub.leaflet.blocks.image":
return (
);
default:
return (
);
}
};
const LeafletWebsiteBlockView: React.FC<{
block: LeafletWebsiteBlock;
alignment?: React.CSSProperties["textAlign"];
documentDid: string;
}> = ({ block, alignment, documentDid }) => {
const previewCid =
block.previewImage?.ref?.$link ?? block.previewImage?.cid;
const { url, loading, error } = useBlob(documentDid, previewCid);
return (
{url && !error ? (
) : (
{loading ? "Loading preview…" : "Open link"}
)}
{block.title && (
{block.title}
)}
{block.description && (
{block.description}
)}
{block.src}
);
};
const LeafletIframeBlockView: React.FC<{
block: LeafletIFrameBlock;
alignment?: React.CSSProperties["textAlign"];
}> = ({ block, alignment }) => {
return (
);
};
const LeafletMathBlockView: React.FC<{
block: LeafletMathBlock;
alignment?: React.CSSProperties["textAlign"];
}> = ({ block, alignment }) => {
return (
{block.tex}
);
};
const LeafletCodeBlockView: React.FC<{
block: LeafletCodeBlock;
alignment?: React.CSSProperties["textAlign"];
}> = ({ block, alignment }) => {
const codeRef = useRef(null);
const langClass = block.language
? `language-${block.language.toLowerCase()}`
: undefined;
return (
{block.plaintext}
);
};
const LeafletHorizontalRuleBlockView: React.FC<{
alignment?: React.CSSProperties["textAlign"];
}> = ({ alignment }) => {
return (
);
};
const LeafletBskyPostBlockView: React.FC<{
block: LeafletBskyPostBlock;
}> = ({ block }) => {
const parsed = parseAtUri(block.postRef?.uri);
if (!parsed) {
return (
Referenced post unavailable.
);
}
return (
);
};
function alignmentValue(
value?: LeafletAlignmentValue,
): React.CSSProperties["textAlign"] | undefined {
if (!value) return undefined;
let normalized = value.startsWith("#") ? value.slice(1) : value;
if (normalized.includes("#")) {
normalized = normalized.split("#").pop() ?? normalized;
}
if (normalized.startsWith("lex:")) {
normalized = normalized.split(":").pop() ?? normalized;
}
switch (normalized) {
case "textAlignLeft":
return "left";
case "textAlignCenter":
return "center";
case "textAlignRight":
return "right";
case "textAlignJustify":
return "justify";
default:
return undefined;
}
}
function useAuthorLabel(
author: string | undefined,
authorDid: string | undefined,
): string | undefined {
const { handle } = useDidResolution(authorDid);
if (!author) return undefined;
if (handle) return `@${handle}`;
if (authorDid) return formatDidForLabel(authorDid);
return author;
}
interface Segment {
text: string;
features: LeafletRichTextFeature[];
}
function createFacetedSegments(
plaintext: string,
facets?: LeafletRichTextFacet[],
): Segment[] {
if (!facets?.length) {
return [{ text: plaintext, features: [] }];
}
const prefix = buildBytePrefix(plaintext);
const startEvents = new Map();
const endEvents = new Map();
const boundaries = new Set([0, prefix.length - 1]);
for (const facet of facets) {
const { byteStart, byteEnd } = facet.index ?? {};
if (
typeof byteStart !== "number" ||
typeof byteEnd !== "number" ||
byteStart >= byteEnd
)
continue;
const start = byteOffsetToCharIndex(prefix, byteStart);
const end = byteOffsetToCharIndex(prefix, byteEnd);
if (start >= end) continue;
boundaries.add(start);
boundaries.add(end);
if (facet.features?.length) {
startEvents.set(start, [
...(startEvents.get(start) ?? []),
...facet.features,
]);
endEvents.set(end, [
...(endEvents.get(end) ?? []),
...facet.features,
]);
}
}
const sortedBounds = Array.from(boundaries).sort((a, b) => a - b);
const segments: Segment[] = [];
let active: LeafletRichTextFeature[] = [];
for (let i = 0; i < sortedBounds.length - 1; i++) {
const boundary = sortedBounds[i];
const next = sortedBounds[i + 1];
const endFeatures = endEvents.get(boundary);
if (endFeatures?.length) {
active = active.filter((feature) => !endFeatures.includes(feature));
}
const startFeatures = startEvents.get(boundary);
if (startFeatures?.length) {
active = [...active, ...startFeatures];
}
if (boundary === next) continue;
const text = sliceByCharRange(plaintext, boundary, next);
segments.push({ text, features: active.slice() });
}
return segments;
}
function buildBytePrefix(text: string): number[] {
const encoder = new TextEncoder();
const prefix: number[] = [0];
let byteCount = 0;
for (let i = 0; i < text.length; ) {
const codePoint = text.codePointAt(i)!;
const char = String.fromCodePoint(codePoint);
const encoded = encoder.encode(char);
byteCount += encoded.length;
prefix.push(byteCount);
i += codePoint > 0xffff ? 2 : 1;
}
return prefix;
}
function byteOffsetToCharIndex(prefix: number[], byteOffset: number): number {
for (let i = 0; i < prefix.length; i++) {
if (prefix[i] === byteOffset) return i;
if (prefix[i] > byteOffset) return Math.max(0, i - 1);
}
return prefix.length - 1;
}
function sliceByCharRange(text: string, start: number, end: number): string {
if (start <= 0 && end >= text.length) return text;
let result = "";
let charIndex = 0;
for (let i = 0; i < text.length && charIndex < end; ) {
const codePoint = text.codePointAt(i)!;
const char = String.fromCodePoint(codePoint);
if (charIndex >= start && charIndex < end) result += char;
i += codePoint > 0xffff ? 2 : 1;
charIndex++;
}
return result;
}
function renderSegment(
segment: Segment,
): React.ReactNode {
const parts = segment.text.split("\n");
return parts.flatMap((part, idx) => {
const key = `${segment.text}-${idx}-${part.length}`;
const wrapped = applyFeatures(
part.length ? part : "\u00a0",
segment.features,
key,
);
if (idx === parts.length - 1) return wrapped;
return [wrapped,
];
});
}
function applyFeatures(
content: React.ReactNode,
features: LeafletRichTextFeature[],
key: string,
): React.ReactNode {
if (!features?.length)
return {content};
return (
{features.reduce(
(child, feature, idx) =>
wrapFeature(
child,
feature,
`${key}-feature-${idx}`,
),
content,
)}
);
}
function wrapFeature(
child: React.ReactNode,
feature: LeafletRichTextFeature,
key: string,
): React.ReactNode {
switch (feature.$type) {
case "pub.leaflet.richtext.facet#link":
return (
{child}
);
case "pub.leaflet.richtext.facet#code":
return (
{child}
);
case "pub.leaflet.richtext.facet#highlight":
return (
{child}
);
case "pub.leaflet.richtext.facet#underline":
return (
{child}
);
case "pub.leaflet.richtext.facet#strikethrough":
return (
{child}
);
case "pub.leaflet.richtext.facet#bold":
return {child};
case "pub.leaflet.richtext.facet#italic":
return {child};
case "pub.leaflet.richtext.facet#id":
return (
{child}
);
default:
return {child};
}
}
const base: Record = {
container: {
display: "flex",
flexDirection: "column",
gap: 24,
padding: "24px 28px",
borderRadius: 20,
borderWidth: "1px",
borderStyle: "solid",
borderColor: "transparent",
maxWidth: 720,
width: "100%",
fontFamily:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
},
header: {
display: "flex",
flexDirection: "column",
gap: 16,
},
headerContent: {
display: "flex",
flexDirection: "column",
gap: 8,
},
title: {
fontSize: 32,
margin: 0,
lineHeight: 1.15,
},
subtitle: {
margin: 0,
fontSize: 16,
lineHeight: 1.5,
},
meta: {
display: "flex",
flexWrap: "wrap",
gap: 8,
alignItems: "center",
fontSize: 14,
},
body: {
display: "flex",
flexDirection: "column",
gap: 18,
},
page: {
display: "flex",
flexDirection: "column",
gap: 18,
},
paragraph: {
margin: "1em 0 0",
lineHeight: 1.65,
fontSize: 16,
},
heading: {
margin: "0.5em 0 0",
fontWeight: 700,
},
blockquote: {
margin: "1em 0 0",
padding: "0.6em 1em",
borderLeftWidth: "4px",
borderLeftStyle: "solid",
},
figure: {
margin: "1.2em 0 0",
display: "flex",
flexDirection: "column",
gap: 12,
},
imageWrapper: {
borderRadius: 16,
overflow: "hidden",
width: "100%",
position: "relative",
background: "#e2e8f0",
},
image: {
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
},
imagePlaceholder: {
width: "100%",
padding: "24px 16px",
textAlign: "center",
},
caption: {
fontSize: 13,
lineHeight: 1.4,
},
list: {
paddingLeft: 28,
margin: "1em 0 0",
listStyleType: "disc",
listStylePosition: "outside",
},
nestedList: {
paddingLeft: 20,
marginTop: 8,
listStyleType: "circle",
listStylePosition: "outside",
},
listItem: {
marginTop: 8,
display: "list-item",
},
linkCard: {
borderRadius: 16,
borderWidth: "1px",
borderStyle: "solid",
display: "flex",
flexDirection: "column",
overflow: "hidden",
textDecoration: "none",
},
linkPreview: {
width: "100%",
height: 180,
objectFit: "cover",
},
linkPreviewPlaceholder: {
width: "100%",
height: 180,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 14,
},
linkContent: {
display: "flex",
flexDirection: "column",
gap: 6,
padding: "16px 18px",
},
iframe: {
width: "100%",
height: 360,
border: "1px solid #cbd5f5",
borderRadius: 16,
},
math: {
margin: "1em 0 0",
padding: "14px 16px",
borderRadius: 12,
fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace',
overflowX: "auto",
},
code: {
margin: "1em 0 0",
padding: "14px 16px",
borderRadius: 12,
overflowX: "auto",
fontSize: 14,
},
hr: {
border: 0,
borderTopWidth: "1px",
borderTopStyle: "solid",
margin: "24px 0 0",
},
embedFallback: {
padding: "12px 16px",
borderRadius: 12,
border: "1px solid #e2e8f0",
fontSize: 14,
},
};
export default LeafletDocumentRenderer;