Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
6.4 kB · 190 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191import { Doc, applyUpdate, XmlElement, XmlHook, XmlText } from "yjs";import { nodes, marks } from "prosemirror-schema-basic";import { CSSProperties, Fragment } from "react";import { theme } from "tailwind.config";import * as base64 from "base64-js";import { didToBlueskyUrl } from "src/utils/mentionUtils";import { AtMentionLink } from "components/AtMentionLink";import { Delta } from "src/utils/yjsFragmentToString";import { ProfilePopover } from "components/ProfilePopover";
type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p" | "small";export function RenderYJSFragment({ value, wrapper, attrs,}: { value: string; wrapper: BlockElements; attrs?: { [k: string]: any };}) { if (!value) return <BlockWrapper wrapper={wrapper} attrs={attrs}></BlockWrapper>; let doc = new Doc(); const update = base64.toByteArray(value); applyUpdate(doc, update); let [node] = doc.getXmlElement("prosemirror").toArray(); if (node.constructor === XmlElement) { switch (node.nodeName as keyof typeof nodes) { case "paragraph": { let children = node.toArray(); return ( <BlockWrapper wrapper={wrapper} attrs={attrs}> {children.length === 0 ? ( <br /> ) : ( node.toArray().map((node, index) => { if (node.constructor === XmlText) { let deltas = node.toDelta() as Delta[]; if (deltas.length === 0) return <br key={index} />; return ( <Fragment key={index}> {deltas.map((d, index) => { if (d.attributes?.link) return ( <a href={d.attributes.link.href} key={index} {...attributesToStyle(d)} > {d.insert} </a> ); return ( <span key={index} {...attributesToStyle(d)} {...attrs} > {d.insert} </span> ); })} </Fragment> ); }
if ( node.constructor === XmlElement && node.nodeName === "hard_break" ) { return <br key={index} />; }
// Handle didMention inline nodes if ( node.constructor === XmlElement && node.nodeName === "didMention" ) { const did = node.getAttribute("did") || ""; const text = node.getAttribute("text") || ""; return ( <a href={didToBlueskyUrl(did)} target="_blank" rel="noopener noreferrer" key={index} className="mention" > {text} </a> ); }
// Handle footnote inline nodes if ( node.constructor === XmlElement && node.nodeName === "footnote" ) { const id = node.getAttribute("footnoteEntityID") || ""; return ( <span key={index} className="footnote-ref" data-footnote-id={id} /> ); }
// Handle atMention inline nodes if ( node.constructor === XmlElement && node.nodeName === "atMention" ) { const atURI = node.getAttribute("atURI") || ""; const text = node.getAttribute("text") || ""; const href = node.getAttribute("href") || undefined; const icon = node.getAttribute("icon") || undefined; return ( <AtMentionLink key={index} atURI={atURI} href={href} icon={icon}> {text} </AtMentionLink> ); }
return null; }) )} </BlockWrapper> ); } case "hard_break": return <div />; default: return null; } } return <br />;}
const BlockWrapper = (props: { wrapper: BlockElements; children?: React.ReactNode; attrs?: { [k: string]: any };}) => { if (props.wrapper === null && props.children === null) return <br />; if (props.wrapper === null) return <>{props.children}</>; switch (props.wrapper) { case "p": return <p {...props.attrs}>{props.children}</p>; case "blockquote": return <blockquote {...props.attrs}>{props.children}</blockquote>;
case "h1": return <h1 {...props.attrs}>{props.children}</h1>; case "h2": return <h2 {...props.attrs}>{props.children}</h2>; case "h3": return <h3 {...props.attrs}>{props.children}</h3>; }};
function attributesToStyle(d: Delta) { let props = { style: {}, className: "", } as { style: CSSProperties; className: string } & { [s: `data-${string}`]: any; };
if (d.attributes?.code) props.className += " inline-code"; if (d.attributes?.strong) props.style.fontWeight = "700"; if (d.attributes?.em) props.style.fontStyle = "italic"; if (d.attributes?.underline) props.style.textDecoration = "underline"; if (d.attributes?.strikethrough) { (props.style.textDecoration = "line-through"), (props.style.textDecorationColor = theme.colors.tertiary); } if (d.attributes?.highlight) { props.className += " highlight"; props["data-color"] = d.attributes.highlight.color; props.style.backgroundColor = d.attributes?.highlight.color === "1" ? theme.colors["highlight-1"] : d.attributes.highlight.color === "2" ? theme.colors["highlight-2"] : theme.colors["highlight-3"]; }
return props;}