a tool for shared writing and social publishing
at 10f35a1361dfb4672b03391b91090b368c5abbf9 212 lines 6.3 kB view raw
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 // Check if the first child is an ordered list 21 let isOrdered = pb.children[0]?.block.listData?.listStyle === "ordered"; 22 let tag = isOrdered ? "ol" : "ul"; 23 result.push( 24 `<${tag}>${( 25 await Promise.all( 26 pb.children.map(async (c) => await renderList(c, tx)), 27 ) 28 ).join("\n")} 29 </${tag}>`, 30 ); 31 } 32 } 33 return result; 34 }); 35 return data; 36} 37 38async function renderList(l: List, tx: ReadTransaction): Promise<string> { 39 let children = ( 40 await Promise.all(l.children.map(async (c) => await renderList(c, tx))) 41 ).join("\n"); 42 let [checked] = await scanIndex(tx).eav(l.block.value, "block/check-list"); 43 44 // Check if nested children are ordered or unordered 45 let isOrdered = l.children[0]?.block.listData?.listStyle === "ordered"; 46 let tag = isOrdered ? "ol" : "ul"; 47 48 return `<li ${checked ? `data-checked=${checked.data.value}` : ""}>${await renderBlock(l.block, tx)} ${ 49 l.children.length > 0 50 ? ` 51 <${tag}>${children}</${tag}> 52 ` 53 : "" 54 }</li>`; 55} 56 57async function getAllFacts( 58 tx: ReadTransaction, 59 entity: string, 60): Promise<Array<Fact<any>>> { 61 let facts = await scanIndex(tx).eav(entity, ""); 62 let childFacts = ( 63 await Promise.all( 64 facts.map((f) => { 65 if ( 66 f.data.type === "reference" || 67 f.data.type === "ordered-reference" || 68 f.data.type === "spatial-reference" 69 ) { 70 return getAllFacts(tx, f.data.value); 71 } 72 return []; 73 }), 74 ) 75 ).flat(); 76 return [...facts, ...childFacts]; 77} 78 79const BlockTypeToHTML: { 80 [K in Fact<"block/type">["data"]["value"]]: ( 81 b: Block, 82 tx: ReadTransaction, 83 alignment?: Fact<"block/text-alignment">["data"]["value"], 84 ) => Promise<React.ReactNode>; 85} = { 86 datetime: async () => null, 87 rsvp: async () => null, 88 mailbox: async () => null, 89 poll: async () => null, 90 embed: async () => null, 91 "bluesky-post": async (b, tx) => { 92 let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); 93 if (!post) return null; 94 return ( 95 <div 96 data-type="bluesky-post" 97 data-bluesky-post={JSON.stringify(post.data.value)} 98 /> 99 ); 100 }, 101 math: async (b, tx, a) => { 102 let [math] = await scanIndex(tx).eav(b.value, "block/math"); 103 const html = Katex.renderToString(math?.data.value || "", { 104 displayMode: true, 105 throwOnError: false, 106 macros: { 107 "\\f": "#1f(#2)", 108 }, 109 }); 110 return renderToStaticMarkup( 111 <div 112 data-type="math" 113 data-tex={math?.data.value} 114 data-alignment={a} 115 dangerouslySetInnerHTML={{ __html: html }} 116 />, 117 ); 118 }, 119 "horizontal-rule": async () => <hr />, 120 image: async (b, tx, a) => { 121 let [src] = await scanIndex(tx).eav(b.value, "block/image"); 122 if (!src) return ""; 123 return <img src={src.data.src} data-alignment={a} />; 124 }, 125 code: async (b, tx, a) => { 126 let [code] = await scanIndex(tx).eav(b.value, "block/code"); 127 let [lang] = await scanIndex(tx).eav(b.value, "block/code-language"); 128 return <pre data-lang={lang?.data.value}>{code?.data.value || ""}</pre>; 129 }, 130 button: async (b, tx, a) => { 131 let [text] = await scanIndex(tx).eav(b.value, "button/text"); 132 let [url] = await scanIndex(tx).eav(b.value, "button/url"); 133 if (!text || !url) return ""; 134 return ( 135 <a href={url.data.value} data-type="button" data-alignment={a}> 136 {text.data.value} 137 </a> 138 ); 139 }, 140 blockquote: async (b, tx, a) => { 141 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 142 return ( 143 <RenderYJSFragment 144 value={value?.data.value} 145 attrs={{ 146 "data-alignment": a, 147 }} 148 wrapper={"blockquote"} 149 /> 150 ); 151 }, 152 heading: async (b, tx, a) => { 153 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 154 let [headingLevel] = await scanIndex(tx).eav( 155 b.value, 156 "block/heading-level", 157 ); 158 let wrapper = ("h" + (headingLevel?.data.value || 1)) as "h1" | "h2" | "h3"; 159 return ( 160 <RenderYJSFragment 161 value={value?.data.value} 162 attrs={{ 163 "data-alignment": a, 164 }} 165 wrapper={wrapper} 166 /> 167 ); 168 }, 169 link: async (b, tx, a) => { 170 let [url] = await scanIndex(tx).eav(b.value, "link/url"); 171 let [title] = await scanIndex(tx).eav(b.value, "link/title"); 172 if (!url) return ""; 173 return ( 174 <a href={url.data.value} target="_blank"> 175 {title.data.value} 176 </a> 177 ); 178 }, 179 card: async (b, tx, a) => { 180 let [card] = await scanIndex(tx).eav(b.value, "block/card"); 181 let facts = await getAllFacts(tx, card.data.value); 182 return ( 183 <div 184 data-type="card" 185 data-facts={JSON.stringify(facts)} 186 data-entityid={card.data.value} 187 /> 188 ); 189 }, 190 text: async (b, tx, a) => { 191 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 192 let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size"); 193 194 return ( 195 <RenderYJSFragment 196 value={value?.data.value} 197 attrs={{ 198 "data-alignment": a, 199 "data-text-size": textSize?.data.value, 200 }} 201 wrapper="p" 202 /> 203 ); 204 }, 205}; 206 207async function renderBlock(b: Block, tx: ReadTransaction) { 208 let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment"); 209 let toHtml = BlockTypeToHTML[b.type]; 210 let element = await toHtml(b, tx, alignment?.data.value); 211 return renderToStaticMarkup(element); 212}