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}