a tool for shared writing and social publishing
at feature/footnotes 188 lines 6.3 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"; 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 footnote inline nodes 95 if ( 96 node.constructor === XmlElement && 97 node.nodeName === "footnote" 98 ) { 99 const id = node.getAttribute("footnoteEntityID") || ""; 100 return ( 101 <span 102 key={index} 103 className="footnote-ref" 104 data-footnote-id={id} 105 /> 106 ); 107 } 108 109 // Handle atMention inline nodes 110 if ( 111 node.constructor === XmlElement && 112 node.nodeName === "atMention" 113 ) { 114 const atURI = node.getAttribute("atURI") || ""; 115 const text = node.getAttribute("text") || ""; 116 return ( 117 <AtMentionLink key={index} atURI={atURI}> 118 {text} 119 </AtMentionLink> 120 ); 121 } 122 123 return null; 124 }) 125 )} 126 </BlockWrapper> 127 ); 128 } 129 case "hard_break": 130 return <div />; 131 default: 132 return null; 133 } 134 } 135 return <br />; 136} 137 138const BlockWrapper = (props: { 139 wrapper: BlockElements; 140 children?: React.ReactNode; 141 attrs?: { [k: string]: any }; 142}) => { 143 if (props.wrapper === null && props.children === null) return <br />; 144 if (props.wrapper === null) return <>{props.children}</>; 145 switch (props.wrapper) { 146 case "p": 147 return <p {...props.attrs}>{props.children}</p>; 148 case "blockquote": 149 return <blockquote {...props.attrs}>{props.children}</blockquote>; 150 151 case "h1": 152 return <h1 {...props.attrs}>{props.children}</h1>; 153 case "h2": 154 return <h2 {...props.attrs}>{props.children}</h2>; 155 case "h3": 156 return <h3 {...props.attrs}>{props.children}</h3>; 157 } 158}; 159 160function attributesToStyle(d: Delta) { 161 let props = { 162 style: {}, 163 className: "", 164 } as { style: CSSProperties; className: string } & { 165 [s: `data-${string}`]: any; 166 }; 167 168 if (d.attributes?.code) props.className += " inline-code"; 169 if (d.attributes?.strong) props.style.fontWeight = "700"; 170 if (d.attributes?.em) props.style.fontStyle = "italic"; 171 if (d.attributes?.underline) props.style.textDecoration = "underline"; 172 if (d.attributes?.strikethrough) { 173 (props.style.textDecoration = "line-through"), 174 (props.style.textDecorationColor = theme.colors.tertiary); 175 } 176 if (d.attributes?.highlight) { 177 props.className += " highlight"; 178 props["data-color"] = d.attributes.highlight.color; 179 props.style.backgroundColor = 180 d.attributes?.highlight.color === "1" 181 ? theme.colors["highlight-1"] 182 : d.attributes.highlight.color === "2" 183 ? theme.colors["highlight-2"] 184 : theme.colors["highlight-3"]; 185 } 186 187 return props; 188}