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