a tool for shared writing and social publishing

simplify getBlocksAsHtml

+133 -124
+51 -36
components/Blocks/TextBlock/RenderYJSFragment.tsx
··· 1 - import { XmlElement, XmlHook, XmlText } from "yjs"; 1 + import { Doc, applyUpdate, XmlElement, XmlHook, XmlText } from "yjs"; 2 2 import { nodes, marks } from "prosemirror-schema-basic"; 3 3 import { CSSProperties } from "react"; 4 4 import { theme } from "tailwind.config"; 5 + import * as base64 from "base64-js"; 5 6 7 + type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p"; 6 8 export function RenderYJSFragment({ 7 - node, 9 + value, 8 10 wrapper, 9 11 attrs, 10 12 }: { 11 - node: XmlElement | XmlText | XmlHook; 12 - wrapper?: "h1" | "h2" | "h3" | null | "blockquote"; 13 + value: string; 14 + wrapper: BlockElements; 13 15 attrs?: { [k: string]: any }; 14 16 }) { 17 + if (!value) 18 + return <BlockWrapper wrapper={wrapper} attrs={attrs}></BlockWrapper>; 19 + let doc = new Doc(); 20 + const update = base64.toByteArray(value); 21 + applyUpdate(doc, update); 22 + let [node] = doc.getXmlElement("prosemirror").toArray(); 15 23 if (node.constructor === XmlElement) { 16 24 switch (node.nodeName as keyof typeof nodes) { 17 25 case "paragraph": { ··· 21 29 {children.length === 0 ? ( 22 30 <div /> 23 31 ) : ( 24 - node 25 - .toArray() 26 - .map((f, index) => <RenderYJSFragment node={f} key={index} />) 32 + node.toArray().map((node, index) => { 33 + if (node.constructor === XmlText) { 34 + let deltas = node.toDelta() as Delta[]; 35 + if (deltas.length === 0) return <br />; 36 + return ( 37 + <> 38 + {deltas.map((d, index) => { 39 + if (d.attributes?.link) 40 + return ( 41 + <a 42 + href={d.attributes.link.href} 43 + key={index} 44 + {...attributesToStyle(d)} 45 + > 46 + {d.insert} 47 + </a> 48 + ); 49 + return ( 50 + <span 51 + key={index} 52 + {...attributesToStyle(d)} 53 + {...attrs} 54 + > 55 + {d.insert} 56 + </span> 57 + ); 58 + })} 59 + </> 60 + ); 61 + } 62 + 63 + return null; 64 + }) 27 65 )} 28 66 </BlockWrapper> 29 67 ); ··· 34 72 return null; 35 73 } 36 74 } 37 - if (node.constructor === XmlText) { 38 - let deltas = node.toDelta() as Delta[]; 39 - if (deltas.length === 0) return <br />; 40 - return ( 41 - <> 42 - {deltas.map((d, index) => { 43 - if (d.attributes?.link) 44 - return ( 45 - <a 46 - href={d.attributes.link.href} 47 - key={index} 48 - {...attributesToStyle(d)} 49 - > 50 - {d.insert} 51 - </a> 52 - ); 53 - return ( 54 - <span key={index} {...attributesToStyle(d)} {...attrs}> 55 - {d.insert} 56 - </span> 57 - ); 58 - })} 59 - </> 60 - ); 61 - } 62 - return null; 75 + return <br />; 63 76 } 64 77 65 78 const BlockWrapper = (props: { 66 - wrapper?: "h1" | "h2" | "h3" | null | "blockquote"; 67 - children: React.ReactNode; 79 + wrapper: BlockElements; 80 + children?: React.ReactNode; 68 81 attrs?: { [k: string]: any }; 69 82 }) => { 83 + if (props.wrapper === null && props.children === null) return <br />; 70 84 if (props.wrapper === null) return <>{props.children}</>; 71 - if (!props.wrapper) return <p {...props.attrs}>{props.children}</p>; 72 85 switch (props.wrapper) { 86 + case "p": 87 + return <p {...props.attrs}>{props.children}</p>; 73 88 case "blockquote": 74 89 return <blockquote {...props.attrs}>{props.children}</blockquote>; 75 90
+1 -12
components/Blocks/TextBlock/index.tsx
··· 157 157 </div> 158 158 ); 159 159 } else { 160 - let doc = new Y.Doc(); 161 - const update = base64.toByteArray(initialFact.data.value); 162 - Y.applyUpdate(doc, update); 163 - let nodes = doc.getXmlElement("prosemirror").toArray(); 164 - content = ( 165 - <> 166 - {nodes.length === 0 && <br />} 167 - {nodes.map((node, index) => ( 168 - <RenderYJSFragment key={index} node={node} /> 169 - ))} 170 - </> 171 - ); 160 + content = <RenderYJSFragment value={initialFact.data.value} wrapper="p" />; 172 161 } 173 162 return ( 174 163 <div
+81 -76
src/utils/getBlocksAsHTML.tsx
··· 2 2 import type { Fact, ReplicacheMutators } from "src/replicache"; 3 3 import { scanIndex } from "src/replicache/utils"; 4 4 import { renderToStaticMarkup } from "react-dom/server"; 5 - import * as Y from "yjs"; 6 - import * as base64 from "base64-js"; 7 5 import { RenderYJSFragment } from "components/Blocks/TextBlock/RenderYJSFragment"; 8 6 import { Block } from "components/Blocks/Block"; 9 7 import { List, parseBlocksToList } from "./parseBlocksToList"; ··· 38 36 await Promise.all(l.children.map(async (c) => await renderList(c, tx))) 39 37 ).join("\n"); 40 38 let [checked] = await scanIndex(tx).eav(l.block.value, "block/check-list"); 41 - return `<li ${checked ? `data-checked=${checked.data.value}` : ""}>${await renderBlock(l.block, tx, true)} ${ 39 + return `<li ${checked ? `data-checked=${checked.data.value}` : ""}>${await renderBlock(l.block, tx)} ${ 42 40 l.children.length > 0 43 41 ? ` 44 42 <ul>${children}</ul> ··· 69 67 return [...facts, ...childFacts]; 70 68 } 71 69 72 - async function renderBlock( 73 - b: Block, 74 - tx: ReadTransaction, 75 - ignoreWrapper?: boolean, 76 - ) { 77 - let wrapper: undefined | "h1" | "h2" | "h3" | "blockquote"; 78 - let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment"); 79 - if (b.type === "horizontal-rule") { 80 - return "<hr />"; 81 - } 82 - if (b.type === "code") { 83 - let [code] = await scanIndex(tx).eav(b.value, "block/code"); 84 - let [lang] = await scanIndex(tx).eav(b.value, "block/code-language"); 85 - return renderToStaticMarkup( 86 - <pre data-lang={lang?.data.value}>{code?.data.value || ""}</pre>, 87 - ); 88 - } 89 - if (b.type === "math") { 70 + const 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) => { 90 84 let [math] = await scanIndex(tx).eav(b.value, "block/math"); 91 85 const html = Katex.renderToString(math?.data.value || "", { 92 86 displayMode: true, ··· 99 93 <div 100 94 data-type="math" 101 95 data-tex={math?.data.value} 102 - data-alignment={alignment?.data.value} 96 + data-alignment={a} 103 97 dangerouslySetInnerHTML={{ __html: html }} 104 98 />, 105 99 ); 106 - } 107 - if (b.type === "image") { 100 + }, 101 + "horizontal-rule": async () => <hr />, 102 + image: async (b, tx, a) => { 108 103 let [src] = await scanIndex(tx).eav(b.value, "block/image"); 109 104 if (!src) return ""; 110 - return renderToStaticMarkup( 111 - <img src={src.data.src} data-alignment={alignment?.data.value} />, 112 - ); 113 - } 114 - if (b.type === "button") { 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) => { 115 113 let [text] = await scanIndex(tx).eav(b.value, "button/text"); 116 114 let [url] = await scanIndex(tx).eav(b.value, "button/url"); 117 115 if (!text || !url) return ""; 118 - return renderToStaticMarkup( 119 - <a 120 - href={url.data.value} 121 - data-type="button" 122 - data-alignment={alignment?.data.value} 123 - > 116 + return ( 117 + <a href={url.data.value} data-type="button" data-alignment={a}> 124 118 {text.data.value} 125 - </a>, 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", 126 139 ); 127 - } 128 - if (b.type === "blockquote") { 129 - wrapper = "blockquote"; 130 - } 131 - if (b.type === "heading") { 132 - let headingLevel = 133 - (await scanIndex(tx).eav(b.value, "block/heading-level"))[0]?.data 134 - .value || 1; 135 - wrapper = "h" + headingLevel; 136 - } 137 - if (b.type === "link") { 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) => { 138 152 let [url] = await scanIndex(tx).eav(b.value, "link/url"); 139 153 let [title] = await scanIndex(tx).eav(b.value, "link/title"); 140 154 if (!url) return ""; ··· 143 157 {title.data.value} 144 158 </a>, 145 159 ); 146 - } 147 - if (b.type === "card") { 160 + }, 161 + card: async (b, tx, a) => { 148 162 let [card] = await scanIndex(tx).eav(b.value, "block/card"); 149 163 let facts = await getAllFacts(tx, card.data.value); 150 164 return renderToStaticMarkup( ··· 154 168 data-entityid={card.data.value} 155 169 />, 156 170 ); 157 - } 158 - if (b.type === "mailbox") { 159 - return renderToStaticMarkup( 160 - <div> 161 - <a href={window.location.href} target="_blank"> 162 - View {b.type} 163 - </a>{" "} 164 - in Leaflet! 165 - </div>, 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 + /> 166 182 ); 167 - } 168 - let value = (await scanIndex(tx).eav(b.value, "block/text"))[0]; 169 - console.log("getBlockasHTML", value); 170 - if (!value) 171 - return ignoreWrapper ? "" : `<${wrapper || "p"}></${wrapper || "p"}>`; 172 - let doc = new Y.Doc(); 173 - const update = base64.toByteArray(value.data.value); 174 - Y.applyUpdate(doc, update); 175 - let nodes = doc.getXmlElement("prosemirror").toArray(); 176 - return renderToStaticMarkup( 177 - <RenderYJSFragment 178 - attrs={{ 179 - "data-alignment": alignment?.data.value, 180 - }} 181 - node={nodes[0]} 182 - wrapper={ignoreWrapper ? null : wrapper} 183 - />, 184 - ); 183 + }, 184 + }; 185 + 186 + async 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 + return renderToStaticMarkup(await toHtml(b, tx, alignment?.data.value)); 185 190 }