a tool for shared writing and social publishing
at 5c1da06c43b099a4836bef01e460585552edc851 203 lines 6.0 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 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 (b, tx) => { 83 let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); 84 if (!post) return null; 85 return ( 86 <div 87 data-type="bluesky-post" 88 data-bluesky-post={JSON.stringify(post.data.value)} 89 /> 90 ); 91 }, 92 math: async (b, tx, a) => { 93 let [math] = await scanIndex(tx).eav(b.value, "block/math"); 94 const html = Katex.renderToString(math?.data.value || "", { 95 displayMode: true, 96 throwOnError: false, 97 macros: { 98 "\\f": "#1f(#2)", 99 }, 100 }); 101 return renderToStaticMarkup( 102 <div 103 data-type="math" 104 data-tex={math?.data.value} 105 data-alignment={a} 106 dangerouslySetInnerHTML={{ __html: html }} 107 />, 108 ); 109 }, 110 "horizontal-rule": async () => <hr />, 111 image: async (b, tx, a) => { 112 let [src] = await scanIndex(tx).eav(b.value, "block/image"); 113 if (!src) return ""; 114 return <img src={src.data.src} data-alignment={a} />; 115 }, 116 code: async (b, tx, a) => { 117 let [code] = await scanIndex(tx).eav(b.value, "block/code"); 118 let [lang] = await scanIndex(tx).eav(b.value, "block/code-language"); 119 return <pre data-lang={lang?.data.value}>{code?.data.value || ""}</pre>; 120 }, 121 button: async (b, tx, a) => { 122 let [text] = await scanIndex(tx).eav(b.value, "button/text"); 123 let [url] = await scanIndex(tx).eav(b.value, "button/url"); 124 if (!text || !url) return ""; 125 return ( 126 <a href={url.data.value} data-type="button" data-alignment={a}> 127 {text.data.value} 128 </a> 129 ); 130 }, 131 blockquote: async (b, tx, a) => { 132 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 133 return ( 134 <RenderYJSFragment 135 value={value?.data.value} 136 attrs={{ 137 "data-alignment": a, 138 }} 139 wrapper={"blockquote"} 140 /> 141 ); 142 }, 143 heading: async (b, tx, a) => { 144 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 145 let [headingLevel] = await scanIndex(tx).eav( 146 b.value, 147 "block/heading-level", 148 ); 149 let wrapper = ("h" + (headingLevel?.data.value || 1)) as "h1" | "h2" | "h3"; 150 return ( 151 <RenderYJSFragment 152 value={value?.data.value} 153 attrs={{ 154 "data-alignment": a, 155 }} 156 wrapper={wrapper} 157 /> 158 ); 159 }, 160 link: async (b, tx, a) => { 161 let [url] = await scanIndex(tx).eav(b.value, "link/url"); 162 let [title] = await scanIndex(tx).eav(b.value, "link/title"); 163 if (!url) return ""; 164 return ( 165 <a href={url.data.value} target="_blank"> 166 {title.data.value} 167 </a> 168 ); 169 }, 170 card: async (b, tx, a) => { 171 let [card] = await scanIndex(tx).eav(b.value, "block/card"); 172 let facts = await getAllFacts(tx, card.data.value); 173 return ( 174 <div 175 data-type="card" 176 data-facts={JSON.stringify(facts)} 177 data-entityid={card.data.value} 178 /> 179 ); 180 }, 181 text: async (b, tx, a) => { 182 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 183 let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size"); 184 185 return ( 186 <RenderYJSFragment 187 value={value?.data.value} 188 attrs={{ 189 "data-alignment": a, 190 "data-text-size": textSize?.data.value, 191 }} 192 wrapper="p" 193 /> 194 ); 195 }, 196}; 197 198async function renderBlock(b: Block, tx: ReadTransaction) { 199 let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment"); 200 let toHtml = BlockTypeToHTML[b.type]; 201 let element = await toHtml(b, tx, alignment?.data.value); 202 return renderToStaticMarkup(element); 203}