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";
6import { didToBlueskyUrl } from "src/utils/mentionUtils";
7import { AtMentionLink } from "components/AtMentionLink";
8import { Delta } from "src/utils/yjsFragmentToString";
9import { ProfilePopover } from "components/ProfilePopover";
10
11type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p" | "small";
12export function RenderYJSFragment({
13 value,
14 wrapper,
15 attrs,
16}: {
17 value: string;
18 wrapper: BlockElements;
19 attrs?: { [k: string]: any };
20}) {
21 if (!value)
22 return <BlockWrapper wrapper={wrapper} attrs={attrs}></BlockWrapper>;
23 let doc = new Doc();
24 const update = base64.toByteArray(value);
25 applyUpdate(doc, update);
26 let [node] = doc.getXmlElement("prosemirror").toArray();
27 if (node.constructor === XmlElement) {
28 switch (node.nodeName as keyof typeof nodes) {
29 case "paragraph": {
30 let children = node.toArray();
31 return (
32 <BlockWrapper wrapper={wrapper} attrs={attrs}>
33 {children.length === 0 ? (
34 <br />
35 ) : (
36 node.toArray().map((node, index) => {
37 if (node.constructor === XmlText) {
38 let deltas = node.toDelta() as Delta[];
39 if (deltas.length === 0) return <br key={index} />;
40 return (
41 <Fragment key={index}>
42 {deltas.map((d, index) => {
43 if (d.attributes?.link)
44 return (
45 <a
46 href={d.attributes.link.href}
47 key={index}
48 {...attributesToStyle(d)}
49 >
50 {d.insert}
51 </a>
52 );
53 return (
54 <span
55 key={index}
56 {...attributesToStyle(d)}
57 {...attrs}
58 >
59 {d.insert}
60 </span>
61 );
62 })}
63 </Fragment>
64 );
65 }
66
67 if (
68 node.constructor === XmlElement &&
69 node.nodeName === "hard_break"
70 ) {
71 return <br key={index} />;
72 }
73
74 // Handle didMention inline nodes
75 if (
76 node.constructor === XmlElement &&
77 node.nodeName === "didMention"
78 ) {
79 const did = node.getAttribute("did") || "";
80 const text = node.getAttribute("text") || "";
81 return (
82 <a
83 href={didToBlueskyUrl(did)}
84 target="_blank"
85 rel="noopener noreferrer"
86 key={index}
87 className="mention"
88 >
89 {text}
90 </a>
91 );
92 }
93
94 // Handle atMention inline nodes
95 if (
96 node.constructor === XmlElement &&
97 node.nodeName === "atMention"
98 ) {
99 const atURI = node.getAttribute("atURI") || "";
100 const text = node.getAttribute("text") || "";
101 return (
102 <AtMentionLink key={index} atURI={atURI}>
103 {text}
104 </AtMentionLink>
105 );
106 }
107
108 return null;
109 })
110 )}
111 </BlockWrapper>
112 );
113 }
114 case "hard_break":
115 return <div />;
116 default:
117 return null;
118 }
119 }
120 return <br />;
121}
122
123const BlockWrapper = (props: {
124 wrapper: BlockElements;
125 children?: React.ReactNode;
126 attrs?: { [k: string]: any };
127}) => {
128 if (props.wrapper === null && props.children === null) return <br />;
129 if (props.wrapper === null) return <>{props.children}</>;
130 switch (props.wrapper) {
131 case "p":
132 return <p {...props.attrs}>{props.children}</p>;
133 case "blockquote":
134 return <blockquote {...props.attrs}>{props.children}</blockquote>;
135
136 case "h1":
137 return <h1 {...props.attrs}>{props.children}</h1>;
138 case "h2":
139 return <h2 {...props.attrs}>{props.children}</h2>;
140 case "h3":
141 return <h3 {...props.attrs}>{props.children}</h3>;
142 }
143};
144
145function attributesToStyle(d: Delta) {
146 let props = {
147 style: {},
148 className: "",
149 } as { style: CSSProperties; className: string } & {
150 [s: `data-${string}`]: any;
151 };
152
153 if (d.attributes?.code) props.className += " inline-code";
154 if (d.attributes?.strong) props.style.fontWeight = "700";
155 if (d.attributes?.em) props.style.fontStyle = "italic";
156 if (d.attributes?.underline) props.style.textDecoration = "underline";
157 if (d.attributes?.strikethrough) {
158 (props.style.textDecoration = "line-through"),
159 (props.style.textDecorationColor = theme.colors.tertiary);
160 }
161 if (d.attributes?.highlight) {
162 props.className += " highlight";
163 props["data-color"] = d.attributes.highlight.color;
164 props.style.backgroundColor =
165 d.attributes?.highlight.color === "1"
166 ? theme.colors["highlight-1"]
167 : d.attributes.highlight.color === "2"
168 ? theme.colors["highlight-2"]
169 : theme.colors["highlight-3"];
170 }
171
172 return props;
173}