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