Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269import { ReadTransaction, Replicache } from "replicache";import type { Fact, ReplicacheMutators } from "src/replicache";import { scanIndex } from "src/replicache/utils";import { renderToStaticMarkup } from "react-dom/server";import { RenderYJSFragment } from "components/Blocks/TextBlock/RenderYJSFragment";import { Block } from "components/Blocks/Block";import { List, parseBlocksToList } from "./parseBlocksToList";import Katex from "katex";
// Only used to fill the clipboard on copy, so comment anchors are omitted// (renderComments={false}) — see stripCommentMarks in components/Blocks/TextBlockexport async function getBlocksAsHTML( rep: Replicache<ReplicacheMutators>, selectedBlocks: Block[],) { let data = await rep?.query(async (tx) => { let result: string[] = []; let parsed = parseBlocksToList(selectedBlocks); for (let pb of parsed) { if (pb.type === "block") result.push(await renderBlock(pb.block, tx)); else { // Check if the first child is an ordered list let isOrdered = pb.children[0]?.block.listData?.listStyle === "ordered"; let tag = isOrdered ? "ol" : "ul"; result.push( `<${tag}>${( await Promise.all( pb.children.map(async (c) => await renderList(c, tx)), ) ).join("\n")} </${tag}>`, ); } } return result; }); return data;}
async function renderList(l: List, tx: ReadTransaction): Promise<string> { let children = ( await Promise.all(l.children.map(async (c) => await renderList(c, tx))) ).join("\n"); let [checked] = await scanIndex(tx).eav(l.block.value, "block/check-list");
// Check if nested children are ordered or unordered let isOrdered = l.children[0]?.block.listData?.listStyle === "ordered"; let tag = isOrdered ? "ol" : "ul";
return `<li ${checked ? `data-checked=${checked.data.value}` : ""}>${await renderBlock(l.block, tx)} ${ l.children.length > 0 ? ` <${tag}>${children}</${tag}> ` : "" }</li>`;}
async function getAllFacts( tx: ReadTransaction, entity: string,): Promise<Array<Fact<any>>> { let facts = await scanIndex(tx).eav(entity, ""); let childFacts = ( await Promise.all( facts.map((f) => { if ( f.data.type === "reference" || f.data.type === "ordered-reference" || f.data.type === "spatial-reference" ) { return getAllFacts(tx, f.data.value); } return []; }), ) ).flat(); return [...facts, ...childFacts];}
const BlockTypeToHTML: { [K in Fact<"block/type">["data"]["value"]]: ( b: Block, tx: ReadTransaction, alignment?: Fact<"block/text-alignment">["data"]["value"], ) => Promise<React.ReactNode>;} = { datetime: async () => null, rsvp: async () => null, mailbox: async () => null, poll: async () => null, embed: async () => null, signup: async () => null, "bluesky-post": async (b, tx) => { let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); if (!post) return null; return ( <div data-type="bluesky-post" data-bluesky-post={JSON.stringify(post.data.value)} /> ); }, "standard-site-post": async (b, tx) => { let [uri] = await scanIndex(tx).eav(b.value, "block/standard-site-post"); if (!uri) return null; return <div data-type="standard-site-post" data-uri={uri.data.value} />; }, "standard-site-publication": async (b, tx) => { let [uri] = await scanIndex(tx).eav( b.value, "block/standard-site-publication", ); if (!uri) return null; return ( <div data-type="standard-site-publication" data-uri={uri.data.value} /> ); }, math: async (b, tx, a) => { let [math] = await scanIndex(tx).eav(b.value, "block/math"); const html = Katex.renderToString(math?.data.value || "", { displayMode: true, throwOnError: false, macros: { "\\f": "#1f(#2)", }, }); // Return the element, not renderToStaticMarkup's string — renderBlock // renders it once. Pre-rendering here would get escaped by that second // pass, so the clipboard would hold the markup as literal text and math // would paste as text instead of a math block. return ( <div data-type="math" data-tex={math?.data.value} data-alignment={a} dangerouslySetInnerHTML={{ __html: html }} /> ); }, "horizontal-rule": async () => <hr />, "members-only-delimiter": async () => null, image: async (b, tx, a) => { let [src] = await scanIndex(tx).eav(b.value, "block/image"); if (!src) return ""; return <img src={src.data.src} data-alignment={a} />; }, "image-gallery": async (b, tx, a) => { let images = (await scanIndex(tx).eav(b.value, "gallery/image")).sort( (x, y) => (x.data.position > y.data.position ? 1 : -1), ); let sources = await Promise.all( images.map(async (img) => { let [src] = await scanIndex(tx).eav(img.data.value, "block/image"); let [alt] = await scanIndex(tx).eav(img.data.value, "image/alt"); return src ? { src: src.data.src, alt: alt?.data.value } : null; }), ); return ( <div data-type="image-gallery"> {sources.map((s, i) => s ? <img key={i} src={s.src} alt={s.alt} /> : null, )} </div> ); }, code: async (b, tx, a) => { let [code] = await scanIndex(tx).eav(b.value, "block/code"); let [lang] = await scanIndex(tx).eav(b.value, "block/code-language"); let language = lang?.data.value; // data-lang preserves the exact language for Leaflet→Leaflet paste; the // language-* class on <code> is what htmlToMarkdown turns into a fenced // code block's language, so the language survives the text/plain path too. return ( <pre data-lang={language}> <code className={language ? `language-${language}` : undefined}> {code?.data.value || ""} </code> </pre> ); }, button: async (b, tx, a) => { let [text] = await scanIndex(tx).eav(b.value, "button/text"); let [url] = await scanIndex(tx).eav(b.value, "button/url"); if (!text || !url) return ""; return ( <a href={url.data.value} data-type="button" data-alignment={a}> {text.data.value} </a> ); }, blockquote: async (b, tx, a) => { let [value] = await scanIndex(tx).eav(b.value, "block/text"); return ( <RenderYJSFragment value={value?.data.value} attrs={{ "data-alignment": a, }} wrapper={"blockquote"} renderComments={false} /> ); }, heading: async (b, tx, a) => { let [value] = await scanIndex(tx).eav(b.value, "block/text"); let [headingLevel] = await scanIndex(tx).eav( b.value, "block/heading-level", ); let wrapper = ("h" + (headingLevel?.data.value || 1)) as "h1" | "h2" | "h3"; return ( <RenderYJSFragment value={value?.data.value} attrs={{ "data-alignment": a, }} wrapper={wrapper} renderComments={false} /> ); }, link: async (b, tx, a) => { let [url] = await scanIndex(tx).eav(b.value, "link/url"); let [title] = await scanIndex(tx).eav(b.value, "link/title"); if (!url) return ""; return ( <a href={url.data.value} target="_blank"> {title.data.value} </a> ); }, card: async (b, tx, a) => { let [card] = await scanIndex(tx).eav(b.value, "block/card"); let facts = await getAllFacts(tx, card.data.value); return ( <div data-type="card" data-facts={JSON.stringify(facts)} data-entityid={card.data.value} /> ); }, text: async (b, tx, a) => { let [value] = await scanIndex(tx).eav(b.value, "block/text"); let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size");
return ( <RenderYJSFragment value={value?.data.value} attrs={{ "data-alignment": a, "data-text-size": textSize?.data.value, }} wrapper="p" renderComments={false} /> ); }, "posts-list": async () => null,};
async function renderBlock(b: Block, tx: ReadTransaction) { let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment"); let toHtml = BlockTypeToHTML[b.type]; let element = await toHtml(b, tx, alignment?.data.value); return renderToStaticMarkup(element);}