import { 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/TextBlock export async function getBlocksAsHTML( rep: Replicache, 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")} `, ); } } return result; }); return data; } async function renderList(l: List, tx: ReadTransaction): Promise { 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 `
  • ${await renderBlock(l.block, tx)} ${ l.children.length > 0 ? ` <${tag}>${children} ` : "" }
  • `; } async function getAllFacts( tx: ReadTransaction, entity: string, ): Promise>> { 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; } = { 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 (
    ); }, "standard-site-post": async (b, tx) => { let [uri] = await scanIndex(tx).eav(b.value, "block/standard-site-post"); if (!uri) return null; return
    ; }, "standard-site-publication": async (b, tx) => { let [uri] = await scanIndex(tx).eav( b.value, "block/standard-site-publication", ); if (!uri) return null; return (
    ); }, 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 (
    ); }, "horizontal-rule": async () =>
    , "members-only-delimiter": async () => null, image: async (b, tx, a) => { let [src] = await scanIndex(tx).eav(b.value, "block/image"); if (!src) return ""; return ; }, "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 (
    {sources.map((s, i) => s ? {s.alt} : null, )}
    ); }, 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 is what htmlToMarkdown turns into a fenced // code block's language, so the language survives the text/plain path too. return (
            
              {code?.data.value || ""}
            
          
    ); }, 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 ( {text.data.value} ); }, blockquote: async (b, tx, a) => { let [value] = await scanIndex(tx).eav(b.value, "block/text"); return ( ); }, 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 ( ); }, 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 ( {title.data.value} ); }, card: async (b, tx, a) => { let [card] = await scanIndex(tx).eav(b.value, "block/card"); let facts = await getAllFacts(tx, card.data.value); return (
    ); }, 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 ( ); }, "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); }