a tool for shared writing and social publishing
1import { Doc, applyUpdate, XmlElement, XmlHook, XmlText } from "yjs";
2import { nodes, marks } from "prosemirror-schema-basic";
3import { CSSProperties, Fragment } from "react";
4import { theme } from "tailwind.config";
5import * as base64 from "base64-js";
6
7type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p";
8export function RenderYJSFragment({
9 value,
10 wrapper,
11 attrs,
12}: {
13 value: string;
14 wrapper: BlockElements;
15 attrs?: { [k: string]: any };
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();
23 if (node.constructor === XmlElement) {
24 switch (node.nodeName as keyof typeof nodes) {
25 case "paragraph": {
26 let children = node.toArray();
27 return (
28 <BlockWrapper wrapper={wrapper} attrs={attrs}>
29 {children.length === 0 ? (
30 <br />
31 ) : (
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 key={index} />;
36 return (
37 <Fragment key={index}>
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 </Fragment>
60 );
61 }
62
63 if (node.constructor === XmlElement && node.nodeName === "hard_break") {
64 return <br key={index} />;
65 }
66
67 return null;
68 })
69 )}
70 </BlockWrapper>
71 );
72 }
73 case "hard_break":
74 return <div />;
75 default:
76 return null;
77 }
78 }
79 return <br />;
80}
81
82const BlockWrapper = (props: {
83 wrapper: BlockElements;
84 children?: React.ReactNode;
85 attrs?: { [k: string]: any };
86}) => {
87 if (props.wrapper === null && props.children === null) return <br />;
88 if (props.wrapper === null) return <>{props.children}</>;
89 switch (props.wrapper) {
90 case "p":
91 return <p {...props.attrs}>{props.children}</p>;
92 case "blockquote":
93 return <blockquote {...props.attrs}>{props.children}</blockquote>;
94
95 case "h1":
96 return <h1 {...props.attrs}>{props.children}</h1>;
97 case "h2":
98 return <h2 {...props.attrs}>{props.children}</h2>;
99 case "h3":
100 return <h3 {...props.attrs}>{props.children}</h3>;
101 }
102};
103
104export type Delta = {
105 insert: string;
106 attributes?: {
107 strong?: {};
108 code?: {};
109 em?: {};
110 underline?: {};
111 strikethrough?: {};
112 highlight?: { color: string };
113 link?: { href: string };
114 };
115};
116
117function attributesToStyle(d: Delta) {
118 let props = {
119 style: {},
120 className: "",
121 } as { style: CSSProperties; className: string } & {
122 [s: `data-${string}`]: any;
123 };
124
125 if (d.attributes?.code) props.className += " inline-code";
126 if (d.attributes?.strong) props.style.fontWeight = "700";
127 if (d.attributes?.em) props.style.fontStyle = "italic";
128 if (d.attributes?.underline) props.style.textDecoration = "underline";
129 if (d.attributes?.strikethrough) {
130 (props.style.textDecoration = "line-through"),
131 (props.style.textDecorationColor = theme.colors.tertiary);
132 }
133 if (d.attributes?.highlight) {
134 props.className += " highlight";
135 props["data-color"] = d.attributes.highlight.color;
136 props.style.backgroundColor =
137 d.attributes?.highlight.color === "1"
138 ? theme.colors["highlight-1"]
139 : d.attributes.highlight.color === "2"
140 ? theme.colors["highlight-2"]
141 : theme.colors["highlight-3"];
142 }
143
144 return props;
145}
146
147export function YJSFragmentToString(
148 node: XmlElement | XmlText | XmlHook,
149): string {
150 if (node.constructor === XmlElement) {
151 // Handle hard_break nodes specially
152 if (node.nodeName === "hard_break") {
153 return "\n";
154 }
155 return node
156 .toArray()
157 .map((f) => YJSFragmentToString(f))
158 .join("");
159 }
160 if (node.constructor === XmlText) {
161 return (node.toDelta() as Delta[])
162 .map((d) => {
163 return d.insert;
164 })
165 .join("");
166 }
167 return "";
168}