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 <div />
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 return null;
64 })
65 )}
66 </BlockWrapper>
67 );
68 }
69 case "hard_break":
70 return <div />;
71 default:
72 return null;
73 }
74 }
75 return <br />;
76}
77
78const BlockWrapper = (props: {
79 wrapper: BlockElements;
80 children?: React.ReactNode;
81 attrs?: { [k: string]: any };
82}) => {
83 if (props.wrapper === null && props.children === null) return <br />;
84 if (props.wrapper === null) return <>{props.children}</>;
85 switch (props.wrapper) {
86 case "p":
87 return <p {...props.attrs}>{props.children}</p>;
88 case "blockquote":
89 return <blockquote {...props.attrs}>{props.children}</blockquote>;
90
91 case "h1":
92 return <h1 {...props.attrs}>{props.children}</h1>;
93 case "h2":
94 return <h2 {...props.attrs}>{props.children}</h2>;
95 case "h3":
96 return <h3 {...props.attrs}>{props.children}</h3>;
97 }
98};
99
100export type Delta = {
101 insert: string;
102 attributes?: {
103 strong?: {};
104 code?: {};
105 em?: {};
106 underline?: {};
107 strikethrough?: {};
108 highlight?: { color: string };
109 link?: { href: string };
110 };
111};
112
113function attributesToStyle(d: Delta) {
114 let props = {
115 style: {},
116 className: "",
117 } as { style: CSSProperties; className: string } & {
118 [s: `data-${string}`]: any;
119 };
120
121 if (d.attributes?.code) props.className += " inline-code";
122 if (d.attributes?.strong) props.style.fontWeight = "700";
123 if (d.attributes?.em) props.style.fontStyle = "italic";
124 if (d.attributes?.underline) props.style.textDecoration = "underline";
125 if (d.attributes?.strikethrough) {
126 (props.style.textDecoration = "line-through"),
127 (props.style.textDecorationColor = theme.colors.tertiary);
128 }
129 if (d.attributes?.highlight) {
130 props.className += " highlight";
131 props["data-color"] = d.attributes.highlight.color;
132 props.style.backgroundColor =
133 d.attributes?.highlight.color === "1"
134 ? theme.colors["highlight-1"]
135 : d.attributes.highlight.color === "2"
136 ? theme.colors["highlight-2"]
137 : theme.colors["highlight-3"];
138 }
139
140 return props;
141}
142
143export function YJSFragmentToString(
144 node: XmlElement | XmlText | XmlHook,
145): string {
146 if (node.constructor === XmlElement) {
147 return node
148 .toArray()
149 .map((f) => YJSFragmentToString(f))
150 .join("");
151 }
152 if (node.constructor === XmlText) {
153 return (node.toDelta() as Delta[])
154 .map((d) => {
155 return d.insert;
156 })
157 .join("");
158 }
159 return "";
160}