a tool for shared writing and social publishing
at feature/fonts 173 lines 5.7 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 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}