a tool for shared writing and social publishing
1import { ReadTransaction, Replicache } from "replicache"; 2import type { Fact, ReplicacheMutators } from "src/replicache"; 3import { scanIndex } from "src/replicache/utils"; 4import { renderToStaticMarkup } from "react-dom/server"; 5import { RenderYJSFragment } from "components/Blocks/TextBlock/RenderYJSFragment"; 6import { Block } from "components/Blocks/Block"; 7import { List, parseBlocksToList } from "./parseBlocksToList"; 8import Katex from "katex"; 9 10export async function getBlocksAsHTML( 11 rep: Replicache<ReplicacheMutators>, 12 selectedBlocks: Block[], 13) { 14 let data = await rep?.query(async (tx) => { 15 let result: string[] = []; 16 let parsed = parseBlocksToList(selectedBlocks); 17 for (let pb of parsed) { 18 if (pb.type === "block") result.push(await renderBlock(pb.block, tx)); 19 else 20 result.push( 21 `<ul>${( 22 await Promise.all( 23 pb.children.map(async (c) => await renderList(c, tx)), 24 ) 25 ).join("\n")} 26 </ul>`, 27 ); 28 } 29 return result; 30 }); 31 return data; 32} 33 34async function renderList(l: List, tx: ReadTransaction): Promise<string> { 35 let children = ( 36 await Promise.all(l.children.map(async (c) => await renderList(c, tx))) 37 ).join("\n"); 38 let [checked] = await scanIndex(tx).eav(l.block.value, "block/check-list"); 39 return `<li ${checked ? `data-checked=${checked.data.value}` : ""}>${await renderBlock(l.block, tx)} ${ 40 l.children.length > 0 41 ? ` 42 <ul>${children}</ul> 43 ` 44 : "" 45 }</li>`; 46} 47 48async function getAllFacts( 49 tx: ReadTransaction, 50 entity: string, 51): Promise<Array<Fact<any>>> { 52 let facts = await scanIndex(tx).eav(entity, ""); 53 let childFacts = ( 54 await Promise.all( 55 facts.map((f) => { 56 if ( 57 f.data.type === "reference" || 58 f.data.type === "ordered-reference" || 59 f.data.type === "spatial-reference" 60 ) { 61 return getAllFacts(tx, f.data.value); 62 } 63 return []; 64 }), 65 ) 66 ).flat(); 67 return [...facts, ...childFacts]; 68} 69 70const BlockTypeToHTML: { 71 [K in Fact<"block/type">["data"]["value"]]: ( 72 b: Block, 73 tx: ReadTransaction, 74 alignment?: Fact<"block/text-alignment">["data"]["value"], 75 ) => Promise<React.ReactNode>; 76} = { 77 datetime: async () => null, 78 rsvp: async () => null, 79 mailbox: async () => null, 80 poll: async () => null, 81 embed: async () => null, 82 "bluesky-post": async () => null, 83 math: async (b, tx, a) => { 84 let [math] = await scanIndex(tx).eav(b.value, "block/math"); 85 const html = Katex.renderToString(math?.data.value || "", { 86 displayMode: true, 87 throwOnError: false, 88 macros: { 89 "\\f": "#1f(#2)", 90 }, 91 }); 92 return renderToStaticMarkup( 93 <div 94 data-type="math" 95 data-tex={math?.data.value} 96 data-alignment={a} 97 dangerouslySetInnerHTML={{ __html: html }} 98 />, 99 ); 100 }, 101 "horizontal-rule": async () => <hr />, 102 image: async (b, tx, a) => { 103 let [src] = await scanIndex(tx).eav(b.value, "block/image"); 104 if (!src) return ""; 105 return <img src={src.data.src} data-alignment={a} />; 106 }, 107 code: async (b, tx, a) => { 108 let [code] = await scanIndex(tx).eav(b.value, "block/code"); 109 let [lang] = await scanIndex(tx).eav(b.value, "block/code-language"); 110 return <pre data-lang={lang?.data.value}>{code?.data.value || ""}</pre>; 111 }, 112 button: async (b, tx, a) => { 113 let [text] = await scanIndex(tx).eav(b.value, "button/text"); 114 let [url] = await scanIndex(tx).eav(b.value, "button/url"); 115 if (!text || !url) return ""; 116 return ( 117 <a href={url.data.value} data-type="button" data-alignment={a}> 118 {text.data.value} 119 </a> 120 ); 121 }, 122 blockquote: async (b, tx, a) => { 123 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 124 return ( 125 <RenderYJSFragment 126 value={value?.data.value} 127 attrs={{ 128 "data-alignment": a, 129 }} 130 wrapper={"blockquote"} 131 /> 132 ); 133 }, 134 heading: async (b, tx, a) => { 135 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 136 let [headingLevel] = await scanIndex(tx).eav( 137 b.value, 138 "block/heading-level", 139 ); 140 let wrapper = ("h" + (headingLevel?.data.value || 1)) as "h1" | "h2" | "h3"; 141 return ( 142 <RenderYJSFragment 143 value={value?.data.value} 144 attrs={{ 145 "data-alignment": a, 146 }} 147 wrapper={wrapper} 148 /> 149 ); 150 }, 151 link: async (b, tx, a) => { 152 let [url] = await scanIndex(tx).eav(b.value, "link/url"); 153 let [title] = await scanIndex(tx).eav(b.value, "link/title"); 154 if (!url) return ""; 155 return ( 156 <a href={url.data.value} target="_blank"> 157 {title.data.value} 158 </a> 159 ); 160 }, 161 card: async (b, tx, a) => { 162 let [card] = await scanIndex(tx).eav(b.value, "block/card"); 163 let facts = await getAllFacts(tx, card.data.value); 164 return ( 165 <div 166 data-type="card" 167 data-facts={btoa(JSON.stringify(facts))} 168 data-entityid={card.data.value} 169 /> 170 ); 171 }, 172 text: async (b, tx, a) => { 173 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 174 return ( 175 <RenderYJSFragment 176 value={value?.data.value} 177 attrs={{ 178 "data-alignment": a, 179 }} 180 wrapper="p" 181 /> 182 ); 183 }, 184}; 185 186async function renderBlock(b: Block, tx: ReadTransaction) { 187 let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment"); 188 let toHtml = BlockTypeToHTML[b.type]; 189 let element = await toHtml(b, tx, alignment?.data.value); 190 return renderToStaticMarkup(element); 191}