Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213import { blobRefToSrc } from "src/utils/blobRefToSrc";import { richtextToYjsText } from "src/utils/richtextToYjs";import type { FactInput } from "src/utils/createLeaflet";
// The facts for one block, in the order factsToPagesRecord reads them back.export type BlockFacts = { facts: FactInput[] };
/** * Rebuild editor facts from a published document's blocks. * * The inverse of the block-emitting half of `factsToPagesRecord`. A document * that arrives already in Leaflet's content format has the record but no facts, * so there is nothing for the editor to open; this reconstructs them. * * Returns null when the document contains a block this cannot faithfully * rebuild. That is deliberate and the whole document is refused: adopting it * would create a leaflet whose next save republishes from facts, and any block * missing from those facts would be dropped from the author's published post. * No edit button is a much better outcome than one that quietly truncates. * * Blocks that can't appear here in practice are the ones only Leaflet can * author — polls, page embeds, posts lists, signup forms — since a document * converted from another platform came from a platform with no way to render * them. */export function documentToBlockFacts( blocks: unknown, did: string, baseUrl: string,): BlockFacts[] | null { if (!Array.isArray(blocks)) return null;
const out: BlockFacts[] = []; for (const entry of blocks) { const block = (entry as { block?: unknown })?.block; const facts = blockToFacts(block, did, baseUrl); if (!facts) return null; out.push({ facts }); } return out;}
function blockToFacts( block: unknown, did: string, baseUrl: string,): FactInput[] | null { if (!block || typeof block !== "object") return null; const b = block as Record<string, any>;
const type = (data: string): FactInput => ({ attribute: "block/type", data: { type: "block-type-union", value: data }, }); const text = (): FactInput => ({ attribute: "block/text", data: { type: "text", value: richtextToYjsText(b.plaintext ?? "", b.facets), }, }); const string = (attribute: string, value: unknown): FactInput[] => typeof value === "string" && value ? [{ attribute, data: { type: "string", value } }] : []; const number = (attribute: string, value: unknown): FactInput[] => typeof value === "number" ? [{ attribute, data: { type: "number", value } }] : [];
switch (b.$type) { case "pub.leaflet.blocks.blockquote": { return [type("blockquote"), text()]; }
case "pub.leaflet.blocks.button": { if (!b.text || !b.url) return null; return [ type("button"), ...string("button/text", b.text), ...string("button/url", b.url), ]; }
case "pub.leaflet.blocks.code": { return [ type("code"), ...string("block/code", b.plaintext ?? ""), ...string("block/code-language", b.language), ]; }
case "pub.leaflet.blocks.header": { return [ type("heading"), text(), { attribute: "block/heading-level", data: { type: "number", value: Math.floor(b.level ?? 1) }, }, ]; }
case "pub.leaflet.blocks.horizontalRule": { return [type("horizontal-rule")]; }
case "pub.leaflet.blocks.html": { if (!b.html) return null; return [ type("html"), ...string("embed/html", b.html), ...number("embed/height", b.height), ]; }
case "pub.leaflet.blocks.iframe": { if (!b.url) return null; return [ type("embed"), ...string("embed/url", b.url), ...number("embed/height", b.height), ...aspectRatioFact(b.aspectRatio), ]; }
case "pub.leaflet.blocks.image": { const src = imageSrc(b.image, did, baseUrl); // Width and height are required by the image fact; without the record's // aspect ratio the editor has no intrinsic size to lay out from. if (!src || !b.aspectRatio?.width || !b.aspectRatio?.height) return null; return [ type("image"), { attribute: "block/image", data: { type: "image", src, height: Math.floor(b.aspectRatio.height), width: Math.floor(b.aspectRatio.width), }, }, ...string("image/alt", b.alt), ]; }
case "pub.leaflet.blocks.math": { return [type("math"), ...string("block/math", b.tex ?? "")]; }
case "pub.leaflet.blocks.text": { return [ type("text"), text(), ...(b.textSize ? [ { attribute: "block/text-size", data: { type: "text-size-union", value: b.textSize }, }, ] : []), ]; }
case "pub.leaflet.blocks.website": { if (!b.src) return null; return [ type("link"), ...string("link/url", b.src), ...string("link/title", b.title), ...string("link/description", b.description), // The preview image is deliberately left off: `link/preview` wants a // src Leaflet can re-upload, and a bookmark card renders without it. ]; }
// Lists, galleries, polls, page embeds, posts lists and signup forms are // not rebuilt. Refusing the document is the safe half of that choice. default: { return null; } }}
function aspectRatioFact(ratio: unknown): FactInput[] { const r = ratio as { width?: number; height?: number } | undefined; if (!r?.width || !r?.height) return []; return [ { attribute: "embed/aspect-ratio", data: { type: "string", value: `${r.width}/${r.height}` }, }, ];}
/** * A fetchable URL for a record's image blob. * * `uploadImage` re-fetches whatever `src` holds when the post is next * published, so the proxy URL round-trips: the blob is read back out of the * author's own repo and re-uploaded. */function imageSrc(image: unknown, did: string, baseUrl: string): string | null { const ref = (image as { ref?: unknown })?.ref; if (!ref) return null; try { return blobRefToSrc(ref as never, did, baseUrl); } catch { return null; }}