tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
291
fork
atom
a tool for shared writing and social publishing
291
fork
atom
overview
issues
28
pulls
pipelines
simplify getBlocksAsHtml
awarm.space
6 months ago
03f236ed
5a873fcb
+133
-124
3 changed files
expand all
collapse all
unified
split
components
Blocks
TextBlock
RenderYJSFragment.tsx
index.tsx
src
utils
getBlocksAsHTML.tsx
+51
-36
components/Blocks/TextBlock/RenderYJSFragment.tsx
···
1
1
-
import { XmlElement, XmlHook, XmlText } from "yjs";
1
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
5
+
import * as base64 from "base64-js";
5
6
7
7
+
type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p";
6
8
export function RenderYJSFragment({
7
7
-
node,
9
9
+
value,
8
10
wrapper,
9
11
attrs,
10
12
}: {
11
11
-
node: XmlElement | XmlText | XmlHook;
12
12
-
wrapper?: "h1" | "h2" | "h3" | null | "blockquote";
13
13
+
value: string;
14
14
+
wrapper: BlockElements;
13
15
attrs?: { [k: string]: any };
14
16
}) {
17
17
+
if (!value)
18
18
+
return <BlockWrapper wrapper={wrapper} attrs={attrs}></BlockWrapper>;
19
19
+
let doc = new Doc();
20
20
+
const update = base64.toByteArray(value);
21
21
+
applyUpdate(doc, update);
22
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
24
-
node
25
25
-
.toArray()
26
26
-
.map((f, index) => <RenderYJSFragment node={f} key={index} />)
32
32
+
node.toArray().map((node, index) => {
33
33
+
if (node.constructor === XmlText) {
34
34
+
let deltas = node.toDelta() as Delta[];
35
35
+
if (deltas.length === 0) return <br />;
36
36
+
return (
37
37
+
<>
38
38
+
{deltas.map((d, index) => {
39
39
+
if (d.attributes?.link)
40
40
+
return (
41
41
+
<a
42
42
+
href={d.attributes.link.href}
43
43
+
key={index}
44
44
+
{...attributesToStyle(d)}
45
45
+
>
46
46
+
{d.insert}
47
47
+
</a>
48
48
+
);
49
49
+
return (
50
50
+
<span
51
51
+
key={index}
52
52
+
{...attributesToStyle(d)}
53
53
+
{...attrs}
54
54
+
>
55
55
+
{d.insert}
56
56
+
</span>
57
57
+
);
58
58
+
})}
59
59
+
</>
60
60
+
);
61
61
+
}
62
62
+
63
63
+
return null;
64
64
+
})
27
65
)}
28
66
</BlockWrapper>
29
67
);
···
34
72
return null;
35
73
}
36
74
}
37
37
-
if (node.constructor === XmlText) {
38
38
-
let deltas = node.toDelta() as Delta[];
39
39
-
if (deltas.length === 0) return <br />;
40
40
-
return (
41
41
-
<>
42
42
-
{deltas.map((d, index) => {
43
43
-
if (d.attributes?.link)
44
44
-
return (
45
45
-
<a
46
46
-
href={d.attributes.link.href}
47
47
-
key={index}
48
48
-
{...attributesToStyle(d)}
49
49
-
>
50
50
-
{d.insert}
51
51
-
</a>
52
52
-
);
53
53
-
return (
54
54
-
<span key={index} {...attributesToStyle(d)} {...attrs}>
55
55
-
{d.insert}
56
56
-
</span>
57
57
-
);
58
58
-
})}
59
59
-
</>
60
60
-
);
61
61
-
}
62
62
-
return null;
75
75
+
return <br />;
63
76
}
64
77
65
78
const BlockWrapper = (props: {
66
66
-
wrapper?: "h1" | "h2" | "h3" | null | "blockquote";
67
67
-
children: React.ReactNode;
79
79
+
wrapper: BlockElements;
80
80
+
children?: React.ReactNode;
68
81
attrs?: { [k: string]: any };
69
82
}) => {
83
83
+
if (props.wrapper === null && props.children === null) return <br />;
70
84
if (props.wrapper === null) return <>{props.children}</>;
71
71
-
if (!props.wrapper) return <p {...props.attrs}>{props.children}</p>;
72
85
switch (props.wrapper) {
86
86
+
case "p":
87
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
160
-
let doc = new Y.Doc();
161
161
-
const update = base64.toByteArray(initialFact.data.value);
162
162
-
Y.applyUpdate(doc, update);
163
163
-
let nodes = doc.getXmlElement("prosemirror").toArray();
164
164
-
content = (
165
165
-
<>
166
166
-
{nodes.length === 0 && <br />}
167
167
-
{nodes.map((node, index) => (
168
168
-
<RenderYJSFragment key={index} node={node} />
169
169
-
))}
170
170
-
</>
171
171
-
);
160
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
5
-
import * as Y from "yjs";
6
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
41
-
return `<li ${checked ? `data-checked=${checked.data.value}` : ""}>${await renderBlock(l.block, tx, true)} ${
39
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
72
-
async function renderBlock(
73
73
-
b: Block,
74
74
-
tx: ReadTransaction,
75
75
-
ignoreWrapper?: boolean,
76
76
-
) {
77
77
-
let wrapper: undefined | "h1" | "h2" | "h3" | "blockquote";
78
78
-
let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment");
79
79
-
if (b.type === "horizontal-rule") {
80
80
-
return "<hr />";
81
81
-
}
82
82
-
if (b.type === "code") {
83
83
-
let [code] = await scanIndex(tx).eav(b.value, "block/code");
84
84
-
let [lang] = await scanIndex(tx).eav(b.value, "block/code-language");
85
85
-
return renderToStaticMarkup(
86
86
-
<pre data-lang={lang?.data.value}>{code?.data.value || ""}</pre>,
87
87
-
);
88
88
-
}
89
89
-
if (b.type === "math") {
70
70
+
const BlockTypeToHTML: {
71
71
+
[K in Fact<"block/type">["data"]["value"]]: (
72
72
+
b: Block,
73
73
+
tx: ReadTransaction,
74
74
+
alignment?: Fact<"block/text-alignment">["data"]["value"],
75
75
+
) => Promise<React.ReactNode>;
76
76
+
} = {
77
77
+
datetime: async () => null,
78
78
+
rsvp: async () => null,
79
79
+
mailbox: async () => null,
80
80
+
poll: async () => null,
81
81
+
embed: async () => null,
82
82
+
"bluesky-post": async () => null,
83
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
102
-
data-alignment={alignment?.data.value}
96
96
+
data-alignment={a}
103
97
dangerouslySetInnerHTML={{ __html: html }}
104
98
/>,
105
99
);
106
106
-
}
107
107
-
if (b.type === "image") {
100
100
+
},
101
101
+
"horizontal-rule": async () => <hr />,
102
102
+
image: async (b, tx, a) => {
108
103
let [src] = await scanIndex(tx).eav(b.value, "block/image");
109
104
if (!src) return "";
110
110
-
return renderToStaticMarkup(
111
111
-
<img src={src.data.src} data-alignment={alignment?.data.value} />,
112
112
-
);
113
113
-
}
114
114
-
if (b.type === "button") {
105
105
+
return <img src={src.data.src} data-alignment={a} />;
106
106
+
},
107
107
+
code: async (b, tx, a) => {
108
108
+
let [code] = await scanIndex(tx).eav(b.value, "block/code");
109
109
+
let [lang] = await scanIndex(tx).eav(b.value, "block/code-language");
110
110
+
return <pre data-lang={lang?.data.value}>{code?.data.value || ""}</pre>;
111
111
+
},
112
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
118
-
return renderToStaticMarkup(
119
119
-
<a
120
120
-
href={url.data.value}
121
121
-
data-type="button"
122
122
-
data-alignment={alignment?.data.value}
123
123
-
>
116
116
+
return (
117
117
+
<a href={url.data.value} data-type="button" data-alignment={a}>
124
118
{text.data.value}
125
125
-
</a>,
119
119
+
</a>
120
120
+
);
121
121
+
},
122
122
+
blockquote: async (b, tx, a) => {
123
123
+
let [value] = await scanIndex(tx).eav(b.value, "block/text");
124
124
+
return (
125
125
+
<RenderYJSFragment
126
126
+
value={value?.data.value}
127
127
+
attrs={{
128
128
+
"data-alignment": a,
129
129
+
}}
130
130
+
wrapper={"blockquote"}
131
131
+
/>
132
132
+
);
133
133
+
},
134
134
+
heading: async (b, tx, a) => {
135
135
+
let [value] = await scanIndex(tx).eav(b.value, "block/text");
136
136
+
let [headingLevel] = await scanIndex(tx).eav(
137
137
+
b.value,
138
138
+
"block/heading-level",
126
139
);
127
127
-
}
128
128
-
if (b.type === "blockquote") {
129
129
-
wrapper = "blockquote";
130
130
-
}
131
131
-
if (b.type === "heading") {
132
132
-
let headingLevel =
133
133
-
(await scanIndex(tx).eav(b.value, "block/heading-level"))[0]?.data
134
134
-
.value || 1;
135
135
-
wrapper = "h" + headingLevel;
136
136
-
}
137
137
-
if (b.type === "link") {
140
140
+
let wrapper = ("h" + (headingLevel?.data.value || 1)) as "h1" | "h2" | "h3";
141
141
+
return (
142
142
+
<RenderYJSFragment
143
143
+
value={value?.data.value}
144
144
+
attrs={{
145
145
+
"data-alignment": a,
146
146
+
}}
147
147
+
wrapper={wrapper}
148
148
+
/>
149
149
+
);
150
150
+
},
151
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
146
-
}
147
147
-
if (b.type === "card") {
160
160
+
},
161
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
157
-
}
158
158
-
if (b.type === "mailbox") {
159
159
-
return renderToStaticMarkup(
160
160
-
<div>
161
161
-
<a href={window.location.href} target="_blank">
162
162
-
View {b.type}
163
163
-
</a>{" "}
164
164
-
in Leaflet!
165
165
-
</div>,
171
171
+
},
172
172
+
text: async (b, tx, a) => {
173
173
+
let [value] = await scanIndex(tx).eav(b.value, "block/text");
174
174
+
return (
175
175
+
<RenderYJSFragment
176
176
+
value={value?.data.value}
177
177
+
attrs={{
178
178
+
"data-alignment": a,
179
179
+
}}
180
180
+
wrapper="p"
181
181
+
/>
166
182
);
167
167
-
}
168
168
-
let value = (await scanIndex(tx).eav(b.value, "block/text"))[0];
169
169
-
console.log("getBlockasHTML", value);
170
170
-
if (!value)
171
171
-
return ignoreWrapper ? "" : `<${wrapper || "p"}></${wrapper || "p"}>`;
172
172
-
let doc = new Y.Doc();
173
173
-
const update = base64.toByteArray(value.data.value);
174
174
-
Y.applyUpdate(doc, update);
175
175
-
let nodes = doc.getXmlElement("prosemirror").toArray();
176
176
-
return renderToStaticMarkup(
177
177
-
<RenderYJSFragment
178
178
-
attrs={{
179
179
-
"data-alignment": alignment?.data.value,
180
180
-
}}
181
181
-
node={nodes[0]}
182
182
-
wrapper={ignoreWrapper ? null : wrapper}
183
183
-
/>,
184
184
-
);
183
183
+
},
184
184
+
};
185
185
+
186
186
+
async function renderBlock(b: Block, tx: ReadTransaction) {
187
187
+
let [alignment] = await scanIndex(tx).eav(b.value, "block/text-alignment");
188
188
+
let toHtml = BlockTypeToHTML[b.type];
189
189
+
return renderToStaticMarkup(await toHtml(b, tx, alignment?.data.value));
185
190
}