Serializer
import escapeHTML from "escape-html";
import React, { Fragment } from "react";
import { MdCheckBox, MdCheckBoxOutlineBlank } from "react-icons/md";
import { Text } from "slate";
import { twMerge } from "tailwind-merge";
import Media from "./Media";
//This copy-and-pasted from somewhere in lexical here: <https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts>
// DOM
export const DOM_ELEMENT_TYPE = 1;
export const DOM_TEXT_TYPE = 3;
// Reconciling
export const NO_DIRTY_NODES = 0;
export const HAS_DIRTY_NODES = 1;
export const FULL_RECONCILE = 2;
// Text node modes
export const IS_NORMAL = 0;
export const IS_TOKEN = 1;
export const IS_SEGMENTED = 2;
// IS_INERT = 3
// Text node formatting
export const IS_BOLD = 1;
export const IS_ITALIC = 1 << 1;
export const IS_STRIKETHROUGH = 1 << 2;
export const IS_UNDERLINE = 1 << 3;
export const IS_CODE = 1 << 4;
export const IS_SUBSCRIPT = 1 << 5;
export const IS_SUPERSCRIPT = 1 << 6;
export const IS_HIGHLIGHT = 1 << 7;
export const IS_ALL_FORMATTING =
IS_BOLD |
IS_ITALIC |
IS_STRIKETHROUGH |
IS_UNDERLINE |
IS_CODE |
IS_SUBSCRIPT |
IS_SUPERSCRIPT |
IS_HIGHLIGHT;
export const IS_DIRECTIONLESS = 1;
export const IS_UNMERGEABLE = 1 << 1;
// Element node formatting
export const IS_ALIGN_LEFT = 1;
export const IS_ALIGN_CENTER = 2;
export const IS_ALIGN_RIGHT = 3;
export const IS_ALIGN_JUSTIFY = 4;
export const IS_ALIGN_START = 5;
export const IS_ALIGN_END = 6;
export const TEXT_TYPE_TO_FORMAT = {
bold: IS_BOLD,
code: IS_CODE,
italic: IS_ITALIC,
strikethrough: IS_STRIKETHROUGH,
subscript: IS_SUBSCRIPT,
superscript: IS_SUPERSCRIPT,
underline: IS_UNDERLINE,
};
function generateTextAlign(node) {
if (node.format === "right") return "text-right";
if (node.format === "center") return "text-center";
else return "";
}
export default function serializeLexicalRichText({
children,
customClassNames,
parentNode = {},
}) {
return children
.map((node, i) => {
const classNames = {
h1: twMerge("h1 mt-6", customClassNames?.h1),
h2: twMerge("h2 mt-5", customClassNames?.h2),
h3: twMerge("h3 mt-4", customClassNames?.h3),
h4: twMerge("h4 mt-3", customClassNames?.h4),
h5: twMerge("h5 mt-2", customClassNames?.h5),
h6: twMerge("h6", customClassNames?.h6),
p: twMerge("p-small", customClassNames?.p),
ul: twMerge("ul", customClassNames?.ul),
ol: twMerge("ol list-decimal", customClassNames?.ol),
li: twMerge("li", customClassNames?.li),
blockquote: twMerge(
"blockquote font-bold text-lg text-gray-600",
customClassNames?.blockquote
),
a: twMerge("a", customClassNames?.a),
upload: twMerge("media", customClassNames?.img),
};
if (Text.isText(node)) {
let text = node.text ? (
<span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
) : (
<span className="opacity-0"> </span>
);
if (node["primaryColor"]) {
text = <span className="text-menzBlue-500">{text}</span>;
}
if (node.format === IS_BOLD || node.bold) {
text = <strong key={i}>{text}</strong>;
}
if (node.code) {
text = <code key={i}>{text}</code>;
}
if (node.italic) {
text = <em key={i}>{text}</em>;
}
if (node.underline) {
text = (
<span className="underline" key={i}>
{text}
</span>
);
}
if (node.strikethrough) {
text = (
<span className="line-through" key={i}>
{text}
</span>
);
}
return <Fragment key={i}>{text}</Fragment>;
}
if (!node) {
return null;
}
if (node.type === "heading") {
return (
<node.tag
className={`${classNames[node.tag]} ${generateTextAlign(node)}`}
key={i}
>
{serializeLexicalRichText({ children: node.children })}
</node.tag>
);
}
if (node.type === "list") {
if (node.listType === "bullet") {
return (
<ul className={`${classNames.ul}`} key={i}>
{serializeLexicalRichText({
children: node.children,
parentNode: node,
})}
</ul>
);
} else if (node.listType === "check") {
return (
<ul className={`${classNames.ul} list-none`} key={i}>
{serializeLexicalRichText({
children: node.children,
parentNode: node,
})}
</ul>
);
} else if (node.listType === "number") {
return (
<ol className={`${classNames.ol}`} key={i}>
{serializeLexicalRichText({
children: node.children,
parentNode: node,
})}
</ol>
);
}
}
if (node.type === "listitem" && node.children?.[0]?.type === "list") {
return (
<p className={`${classNames.p} ${generateTextAlign(node)}`} key={i}>
{serializeLexicalRichText({ children: node.children })}
</p>
);
}
if (node.type === "listitem" && node.checked) {
return (
<li className={`${classNames.li} flex gap-1`} key={i}>
<div>
<MdCheckBox className="w-4 h-4 text-green-500" />
</div>
<div className="line-through">
{serializeLexicalRichText({ children: node.children })}
</div>
</li>
);
} else if (node.type === "listitem" && parentNode.listType === "check") {
return (
<li className={`${classNames.li} flex gap-1`} key={i}>
<div>
<MdCheckBoxOutlineBlank className="w-4 h-4 text-green-500" />
</div>
<div className="">
{serializeLexicalRichText({ children: node.children })}
</div>
</li>
);
} else if (node.type === "listitem") {
return (
<li className={`${classNames.li}`} key={i}>
{serializeLexicalRichText({ children: node.children })}
</li>
);
}
if (["link", "autolink"].includes(node.type)) {
return (
<a
className={`${classNames.a}`}
href={escapeHTML(
node.fields?.linkType === "custom" ? node?.fields?.url : ""
)}
target={node.fields?.newTab ? "_blank" : "_self"}
key={i}
>
{serializeLexicalRichText({ children: node.children })}
</a>
);
}
switch (node.type) {
case "quote":
return (
<blockquote className={`${classNames.blockquote}`} key={i}>
{serializeLexicalRichText({ children: node.children })}
</blockquote>
);
case "upload":
return (
<div className="">
<Media className="" data={node.value} key={i} />
</div>
);
case "hr":
return <hr className="hr" key={i} />;
default:
return (
<p className={`${classNames.p} ${generateTextAlign(node)}`} key={i}>
{serializeLexicalRichText({ children: node.children })}
</p>
);
}
})
.filter((node) => node !== null);
}
Media
import Image from 'next/image';
import React from 'react';
const Media = ({
data,
className,
fill,
priority = false,
autoPlay = false,
muted = false,
loop = false,
controls = false,
}) => {
const { mimeType, url, alt } = data || {};
if (mimeType?.includes('video')) {
return (
<video
className={className}
autoPlay={autoPlay}
muted={muted}
loop={loop}
controls={controls}
>
<source src={url} />
</video>
);
}
return (
<Image
priority={priority}
className={className}
src={url}
alt={alt || 'Media'}
fill={fill}
/>
);
};
export default Media;