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 // 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}