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