a tool for shared writing and social publishing
at main 5.6 kB view raw
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