Closed sithembiso closed 2 years ago
I just realised that there's a readOnly
setting here:
<LexicalComposer initialConfig={{readOnly: true}}>
<RichTextPlugin
contentEditable={
<ContentEditable />
}
/>
</LexicalComposer>
Still trying to find out more.
The resulting HTML can be used outside of React context (as an email template), please give a hint how to render the state to static HTML.
It should be able to read static HTML as well in the future? To continue editing? Right?
This sounds like it should be simple to accomplish, but I've been looking inside the codebase, no ideas yet. Any input will be highly appreciated.
Any news here? I also need.
I need to export HTML content because I will render this content on another page, as HTML.
While no helper functions exist at the moment, you could write your own lexical nodeMap to HTML serializer function in the meantime!
Here is a (quick) example on how one might make their own serializer from a nodeMap
.
const serializeToHtml = (editorState) => {
const nodeMap = editorState._nodeMap.reduce((map, [key, value]) => {
map.set(key, value);
return map;
}, new Map());
const renderText = (node) => {
switch (node.__format) {
case 1: // bold
return `<strong>${node.__text}</strong>`;
case 1 << 1: // italic
return `<em>${node.__text}</em>`;
case 1 << 2: // strikethrough
return `<s>${node.__text}</s>`;
case 1 << 3: // underline
return `<u>${node.__text}</u>`;
case 1 << 4: // code
return `<code>${node.__text}</code>`;
case 1 << 5: // subscript
return `<sub>${node.__text}</sub>`;
case 1 << 6: // superscript
return `<sup>${node.__text}</sup>`;
default:
console.log("unknown text format", node.__format);
return node.__text;
}
};
const renderStyle = (format) => {
switch (format) {
case 1: // left
return `text-align: left;`;
case 2: // center
return `text-align: center;`;
case 3: // right
return `text-align: right;`;
case 4: // justify
return `text-align: justify;`;
default: // justify
console.log("unknown text-align", format);
return ``;
}
};
const renderNode = (key) => {
const node = nodeMap.get(key);
switch (node.__type) {
case "root":
return node.__children.map((k) => renderNode(k)).join("");
case "heading":
case "list":
return `<${node.__tag}>${node.__children
.map((k) => renderNode(k))
.join("")}</${node.__tag}>`;
case "text":
return renderText(node);
case "quote":
return `<blockquote>${node.__children
.map((k) => renderNode(k))
.join("")}</blockquote>`;
case "paragraph":
return `<p${
node.__format ? ` style="${renderStyle(node.__format)}"` : ``
}>${node.__children.map((k) => renderNode(k)).join("")}</p>`;
case "listitem":
return `<li>${node.__children
.map((k) => renderNode(k))
.join("")}</li>`;
case "link":
return `<a href="${node.__url}">${node.__children
.map((k) => renderNode(k))
.join("")}</a>`;
case "hashtag":
return `<a href="${node.__text}">${node.__text}</a>`;
case "emoji":
return `<span class="${node.__className}">${node.__text}</span>`;
default:
console.log("unknown type", node.__type);
return "";
}
};
renderNode("root");
};
I'd definitely avoid using the private methods of the editor state directly. Instead you can do editorState.read(()=> …) and do the logic in there with the actual public APIs. Not only will it be faster, it will also be backward compatible.
This is meant to be with deserialized JSON of editor state. Does that advice still hold true?
This is meant to be with deserialized JSON of editor state. Does that advice still hold true?
I'd use the @lexical/headless
package in this case, then you get the best if both worlds
Here is a (quick) example on how one might make their own serializer from a
nodeMap
.const serializeToHtml = (editorState) => { const nodeMap = editorState._nodeMap.reduce((map, [key, value]) => { map.set(key, value); return map; }, new Map()); const renderText = (node) => { switch (node.__format) { case 1: // bold return `<strong>${node.__text}</strong>`; case 1 << 1: // italic return `<em>${node.__text}</em>`; case 1 << 2: // strikethrough return `<s>${node.__text}</s>`; case 1 << 3: // underline return `<u>${node.__text}</u>`; case 1 << 4: // code return `<code>${node.__text}</code>`; case 1 << 5: // subscript return `<sub>${node.__text}</sub>`; case 1 << 6: // superscript return `<sup>${node.__text}</sup>`; default: console.log("unknown text format", node.__format); return node.__text; } }; const renderStyle = (format) => { switch (format) { case 1: // left return `text-align: left;`; case 2: // center return `text-align: center;`; case 3: // right return `text-align: right;`; case 4: // justify return `text-align: justify;`; default: // justify console.log("unknown text-align", format); return ``; } }; const renderNode = (key) => { const node = nodeMap.get(key); switch (node.__type) { case "root": return node.__children.map((k) => renderNode(k)).join(""); case "heading": case "list": return `<${node.__tag}>${node.__children .map((k) => renderNode(k)) .join("")}</${node.__tag}>`; case "text": return renderText(node); case "quote": return `<blockquote>${node.__children .map((k) => renderNode(k)) .join("")}</blockquote>`; case "paragraph": return `<p${ node.__format ? ` style="${renderStyle(node.__format)}"` : `` }>${node.__children.map((k) => renderNode(k)).join("")}</p>`; case "listitem": return `<li>${node.__children .map((k) => renderNode(k)) .join("")}</li>`; case "link": return `<a href="${node.__url}">${node.__children .map((k) => renderNode(k)) .join("")}</a>`; case "hashtag": return `<a href="${node.__text}">${node.__text}</a>`; case "emoji": return `<span class="${node.__className}">${node.__text}</span>`; default: console.log("unknown type", node.__type); return ""; } }; renderNode("root"); };
This helped a lot. Thank you @nicksrandall.
D
Here is a (quick) example on how one might make their own serializer from a
nodeMap
.const serializeToHtml = (editorState) => { const nodeMap = editorState._nodeMap.reduce((map, [key, value]) => { map.set(key, value); return map; }, new Map()); const renderText = (node) => { switch (node.__format) { case 1: // bold return `<strong>${node.__text}</strong>`; case 1 << 1: // italic return `<em>${node.__text}</em>`; case 1 << 2: // strikethrough return `<s>${node.__text}</s>`; case 1 << 3: // underline return `<u>${node.__text}</u>`; case 1 << 4: // code return `<code>${node.__text}</code>`; case 1 << 5: // subscript return `<sub>${node.__text}</sub>`; case 1 << 6: // superscript return `<sup>${node.__text}</sup>`; default: console.log("unknown text format", node.__format); return node.__text; } }; const renderStyle = (format) => { switch (format) { case 1: // left return `text-align: left;`; case 2: // center return `text-align: center;`; case 3: // right return `text-align: right;`; case 4: // justify return `text-align: justify;`; default: // justify console.log("unknown text-align", format); return ``; } }; const renderNode = (key) => { const node = nodeMap.get(key); switch (node.__type) { case "root": return node.__children.map((k) => renderNode(k)).join(""); case "heading": case "list": return `<${node.__tag}>${node.__children .map((k) => renderNode(k)) .join("")}</${node.__tag}>`; case "text": return renderText(node); case "quote": return `<blockquote>${node.__children .map((k) => renderNode(k)) .join("")}</blockquote>`; case "paragraph": return `<p${ node.__format ? ` style="${renderStyle(node.__format)}"` : `` }>${node.__children.map((k) => renderNode(k)).join("")}</p>`; case "listitem": return `<li>${node.__children .map((k) => renderNode(k)) .join("")}</li>`; case "link": return `<a href="${node.__url}">${node.__children .map((k) => renderNode(k)) .join("")}</a>`; case "hashtag": return `<a href="${node.__text}">${node.__text}</a>`; case "emoji": return `<span class="${node.__className}">${node.__text}</span>`; default: console.log("unknown type", node.__type); return ""; } }; renderNode("root"); };
This helped a lot. Thank you @nicksrandall.
This not working anymore, keep getting _nodeMap.reduce
is not a function
@TestCK, the _nodeMap is already a Map now so just change
const nodeMap = editorState._nodeMap.reduce((map, [key, value]) => {
map.set(key, value);
return map;
}, new Map());
for
const nodeMap = editorState._nodeMap;
and you should be good!
Huge thanks @nicksrandall for the snippet!
Here's a currently working update :
const serializeToHtml = (editorState) => {
if(!editorState)
return "";
const nodeMap = editorState._nodeMap;
const renderText = (node) => {
switch (node.__format) {
case 1: // bold
return `<strong>${node.__text}</strong>`;
case 1 << 1: // italic
return `<em>${node.__text}</em>`;
case 1 << 2: // strikethrough
return `<s>${node.__text}</s>`;
case 1 << 3: // underline
return `<u>${node.__text}</u>`;
case 1 << 4: // code
return `<code>${node.__text}</code>`;
case 1 << 5: // subscript
return `<sub>${node.__text}</sub>`;
case 1 << 6: // superscript
return `<sup>${node.__text}</sup>`;
default:
console.log("unknown text format", node.__format);
return node.__text;
}
};
const renderStyle = (format) => {
switch (format) {
case 1: // left
return `text-align: left;`;
case 2: // center
return `text-align: center;`;
case 3: // right
return `text-align: right;`;
case 4: // justify
return `text-align: justify;`;
default: // justify
console.log("unknown text-align", format);
return ``;
}
};
const renderNode = (key) => {
const node = nodeMap.get(key);
switch (node.__type) {
case "root":
return node.__children.map((k) => renderNode(k)).join("");
case "heading":
case "list":
return `<${node.__tag}>${node.__children
.map((k) => renderNode(k))
.join("")}</${node.__tag}>`;
case "text":
return renderText(node);
case "quote":
return `<blockquote>${node.__children
.map((k) => renderNode(k))
.join("")}</blockquote>`;
case "paragraph":
return `<p${
node.__format ? ` style="${renderStyle(node.__format)}"` : ``
}>${node.__children.map((k) => renderNode(k)).join("")}</p>`;
case "listitem":
return `<li>${node.__children
.map((k) => renderNode(k))
.join("")}</li>`;
case "link":
return `<a href="${node.__url}">${node.__children
.map((k) => renderNode(k))
.join("")}</a>`;
case "hashtag":
return `<a href="${node.__text}">${node.__text}</a>`;
case "emoji":
return `<span class="${node.__className}">${node.__text}</span>`;
default:
console.log("unknown type", node.__type);
return "";
}
};
return renderNode("root");
};
Please don't use the private parts of EditorState, especially _nodeMap
, it might change in the future and is intended to be an implementation detail. It's far better to walk through your tree with editorState.read(() => … )
.
@trueadm Ok i'm getting the hang of it!
Here's a better snippet for the others
function serializeToHtml(editorState) {
const renderText = (node) => {
switch (node.getFormat()) {
case 1: // bold
return `<strong>${node.getTextContent()}</strong>`;
case 1 << 1: // italic
return `<em>${node.getTextContent()}</em>`;
case 1 << 2: // strikethrough
return `<s>${node.getTextContent()}</s>`;
case 1 << 3: // underline
return `<u>${node.getTextContent()}</u>`;
case 1 << 4: // code
return `<code>${node.getTextContent()}</code>`;
case 1 << 5: // subscript
return `<sub>${node.getTextContent()}</sub>`;
case 1 << 6: // superscript
return `<sup>${node.getTextContent()}</sup>`;
default:
return node.getTextContent();
}
};
const renderStyle = (format) => {
switch (format) {
case 1: // left
return `text-align: left;`;
case 2: // center
return `text-align: center;`;
case 3: // right
return `text-align: right;`;
case 4: // justify
return `text-align: justify;`;
default: // justify
console.log("unknown text-align", format);
return ``;
}
};
const renderNode = (node) => {
switch (node.getType()) {
case "root":
return (node as RootNode).getChildren().map((k) => renderNode(k)).join("");
case "heading":
const headingNode = (node as HeadingNode);
return `<${headingNode.getTag()}>${headingNode.getChildren()
.map((k) => renderNode(k))
.join("")}</${headingNode.getTag()}>`;
case "list":
const listNode = (node as ListNode)
return `<${listNode.getTag()}>${listNode.getChildren()
.map((k) => renderNode(k))
.join("")}</${listNode.getTag()}>`;
case "text":
return renderText(node);
case "quote":
const quoteNode = (node as QuoteNode);
return `<blockquote>${quoteNode.getChildren()
.map((k) => renderNode(k))
.join("")}</blockquote>`;
case "paragraph":
const paragraphNode = (node as ParagraphNode);
return `<p${
paragraphNode.getFormat() ? ` style="${renderStyle(paragraphNode.getFormat())}"` : ``
}>${paragraphNode.getChildren().map((k) => renderNode(k)).join("")}</p>`;
case "listitem":
const listItemNode = (node as ListItemNode)
return `<li>${listItemNode.getChildren()
.map((k) => renderNode(k))
.join("")}</li>`;
case "link":
const linkNode = (node as LinkNode)
return `<a href="${linkNode.getURL()}">${linkNode.getChildren()
.map((k) => renderNode(k))
.join("")}</a>`;
default:
console.log("unknown type", node.getType());
return "";
}
};
return new Promise(resolve => {
editorState.read(() => {
resolve(renderNode($getRoot()));
});
});
}
// usage
document.getElementById("output").innerHTML = await serializeToHtml(editorState)
I made a PR about this serialization from root to HTML but the feature actually exists with $generateHtmlFromNodes without a second argument which is the selection (thanks to @acywatson).
it is the same case with JSON to HTML but you have to parse it after modifying the JSON then call the read method from parsed JSON with $generateHtmlFromNodes in it.
editorState.read(() => {
const {root} = editorState.toJSON()
delete root.children[0]
const parsed = editor.parseEditorState(JSON.stringify({
root: {
...root,
children: root.children.filter((v: any) => v)
}
}))
parsed.read(() => {
console.log($generateHtmlFromNodes(editor))
})
})
Hope this help
@trueadm Ok i'm getting the hang of it!
Here's a better snippet for the others
function serializeToHtml(editorState) { const renderText = (node) => { switch (node.getFormat()) { case 1: // bold return `<strong>${node.getTextContent()}</strong>`; case 1 << 1: // italic return `<em>${node.getTextContent()}</em>`; case 1 << 2: // strikethrough return `<s>${node.getTextContent()}</s>`; case 1 << 3: // underline return `<u>${node.getTextContent()}</u>`; case 1 << 4: // code return `<code>${node.getTextContent()}</code>`; case 1 << 5: // subscript return `<sub>${node.getTextContent()}</sub>`; case 1 << 6: // superscript return `<sup>${node.getTextContent()}</sup>`; default: return node.getTextContent(); } }; const renderStyle = (format) => { switch (format) { case 1: // left return `text-align: left;`; case 2: // center return `text-align: center;`; case 3: // right return `text-align: right;`; case 4: // justify return `text-align: justify;`; default: // justify console.log("unknown text-align", format); return ``; } }; const renderNode = (node) => { switch (node.getType()) { case "root": return (node as RootNode).getChildren().map((k) => renderNode(k)).join(""); case "heading": const headingNode = (node as HeadingNode); return `<${headingNode.getTag()}>${headingNode.getChildren() .map((k) => renderNode(k)) .join("")}</${headingNode.getTag()}>`; case "list": const listNode = (node as ListNode) return `<${listNode.getTag()}>${listNode.getChildren() .map((k) => renderNode(k)) .join("")}</${listNode.getTag()}>`; case "text": return renderText(node); case "quote": const quoteNode = (node as QuoteNode); return `<blockquote>${quoteNode.getChildren() .map((k) => renderNode(k)) .join("")}</blockquote>`; case "paragraph": const paragraphNode = (node as ParagraphNode); return `<p${ paragraphNode.getFormat() ? ` style="${renderStyle(paragraphNode.getFormat())}"` : `` }>${paragraphNode.getChildren().map((k) => renderNode(k)).join("")}</p>`; case "listitem": const listItemNode = (node as ListItemNode) return `<li>${listItemNode.getChildren() .map((k) => renderNode(k)) .join("")}</li>`; case "link": const linkNode = (node as LinkNode) return `<a href="${linkNode.getURL()}">${linkNode.getChildren() .map((k) => renderNode(k)) .join("")}</a>`; default: console.log("unknown type", node.getType()); return ""; } }; return new Promise(resolve => { editorState.read(() => { resolve(renderNode($getRoot())); }); }); } // usage document.getElementById("output").innerHTML = await serializeToHtml(editorState)
So this is the final solution? I would like to find a method who allow myself to display my JSON created from Lexical saved in my DB, to generate my pages. Why is it an async method? Thanks
@sithembiso what did you do finally because we have the same problematic
@erbsutil too if you found a solution :)
hey, anyone found good solution?
This is meant to be with deserialized JSON of editor state. Does that advice still hold true?
I'd use the
@lexical/headless
package in this case, then you get the best if both worlds
Is that reasonable to use client side?
@trueadm Ok i'm getting the hang of it! Here's a better snippet for the others
function serializeToHtml(editorState) { const renderText = (node) => { switch (node.getFormat()) { case 1: // bold return `<strong>${node.getTextContent()}</strong>`; case 1 << 1: // italic return `<em>${node.getTextContent()}</em>`; case 1 << 2: // strikethrough return `<s>${node.getTextContent()}</s>`; case 1 << 3: // underline return `<u>${node.getTextContent()}</u>`; case 1 << 4: // code return `<code>${node.getTextContent()}</code>`; case 1 << 5: // subscript return `<sub>${node.getTextContent()}</sub>`; case 1 << 6: // superscript return `<sup>${node.getTextContent()}</sup>`; default: return node.getTextContent(); } }; const renderStyle = (format) => { switch (format) { case 1: // left return `text-align: left;`; case 2: // center return `text-align: center;`; case 3: // right return `text-align: right;`; case 4: // justify return `text-align: justify;`; default: // justify console.log("unknown text-align", format); return ``; } }; const renderNode = (node) => { switch (node.getType()) { case "root": return (node as RootNode).getChildren().map((k) => renderNode(k)).join(""); case "heading": const headingNode = (node as HeadingNode); return `<${headingNode.getTag()}>${headingNode.getChildren() .map((k) => renderNode(k)) .join("")}</${headingNode.getTag()}>`; case "list": const listNode = (node as ListNode) return `<${listNode.getTag()}>${listNode.getChildren() .map((k) => renderNode(k)) .join("")}</${listNode.getTag()}>`; case "text": return renderText(node); case "quote": const quoteNode = (node as QuoteNode); return `<blockquote>${quoteNode.getChildren() .map((k) => renderNode(k)) .join("")}</blockquote>`; case "paragraph": const paragraphNode = (node as ParagraphNode); return `<p${ paragraphNode.getFormat() ? ` style="${renderStyle(paragraphNode.getFormat())}"` : `` }>${paragraphNode.getChildren().map((k) => renderNode(k)).join("")}</p>`; case "listitem": const listItemNode = (node as ListItemNode) return `<li>${listItemNode.getChildren() .map((k) => renderNode(k)) .join("")}</li>`; case "link": const linkNode = (node as LinkNode) return `<a href="${linkNode.getURL()}">${linkNode.getChildren() .map((k) => renderNode(k)) .join("")}</a>`; default: console.log("unknown type", node.getType()); return ""; } }; return new Promise(resolve => { editorState.read(() => { resolve(renderNode($getRoot())); }); }); } // usage document.getElementById("output").innerHTML = await serializeToHtml(editorState)
So this is the final solution? I would like to find a method who allow myself to display my JSON created from Lexical saved in my DB, to generate my pages. Why is it an async method? Thanks
Cool but this gives editorState.read is not a function
is I pass in a stringifyed JSON. Or is the idea to pass in Nodes?
Here's how I got mine working
I'd like to start with what I had in mind and what I eventually did
Here it goes (what I initially had in mind before finding a solution):
Problems I faced (on the server side) and how I solved them
Solution: Created a headless editor
const initialConfig = {
editorState: undefined,
namespace: "Namespace name",
nodes: [...Nodes],
onError: (error: Error) => {
throw error;
},
theme: EditorTheme,
};
const editor = createHeadlessEditor(initialConfig);
Solution: Turn your project into a workspace so you can import from the clientside code or use Lerna
Solution: Take the logic for handling HTML email template string extract into another folder that will be handled by webpack, then make output a library via the output.library
option in webpack
const configurations = {
// Other webpack configurations ...
output: {
// Other `webpack.output` configurations ...
library: {
name: "Any Name",
type: "umd",
},
},
};
Solution: I used a loader: MiniCssExtractPlugin.loader which outputs CSS files and also it's associated plugin
const configurations = {
// Other webpack configurations ...
module: {
rules: [
// Other `module.rules` configurations ...
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
],
}
],
},
plugins: [
// Other `plugins` configurations ...
new MiniCssExtractPlugin()
]
};
Solution: Configure MiniCssExtractPlugin to output only one CSS file via the optimization.splitChunks
option
const configurations = {
// Other webpack configurations ...
plugins: [
// Output one file named main.css
new MiniCssExtractPlugin({filename: "main.css)
],
// Group all CSS files into a single file
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: "styles",
type: "css/mini-extract",
chunks: "all",
enforce: true,
},
},
},
}
};
Solution: Attach the htmlString generated from $generateHTMLFromNode to a JSDOM instance, read the CSS file using fs module in Nodejs, add the CSS content to a style tag in the document head and return document.documentElement.outerHTML
to get the stringified HTML document. All these are done utilizing JSDOM methods
import { JSDOM } from "jsdom";
import fs from "fs";
import { resolve } from "path";
const makeHTMLTemplate = (html: string) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>${html}</body>
</html>`;
const getCssStyles = async (document: Document) => {
try {
// File path relative to the package where this is to be called
const cssFilePath = resolve("..file/path");
const data = fs.readFileSync(cssFilePath, "utf8");
const styleElement = document.createElement("style");
styleElement.textContent = data;
document.head.appendChild(styleElement);
} catch (err) {
console.error("Error reading CSS file:", err);
}
};
const getHTMLString = async (
htmlString: string | undefined
): Promise<string | undefined> => {
let htmlStringWithStyle: string | undefined = undefined;
try {
if (htmlString) {
htmlString = makeHTMLTemplate(htmlString);
const dom = new JSDOM(htmlString);
const document = dom.window.document;
// Append styles to head
getCssStyles(document);
// Get the HTML document in stringified format
htmlStringWithStyle = document.documentElement.outerHTML;
return htmlStringWithStyle;
} else {
return htmlStringWithStyle;
}
} catch (err) {
return htmlStringWithStyle;
}
};
export const retrieveHTML = async (savedTemplateEditorState: string) => {
try {
const initialConfig = {
editorState: undefined,
namespace: "Namespace",
// @ts-ignore
nodes: [...Nodes],
onError: (error: Error) => {
throw error;
},
theme: EditorTheme,
};
const editor = createHeadlessEditor(initialConfig);
const editorState = editor.parseEditorState(savedTemplateEditorState);
editor.setEditorState(editorState);
let htmlString: string | undefined = undefined;
const dom = new JSDOM();
// @ts-ignore
global.window = dom.window;
global.document = dom.window.document;
global.DocumentFragment = dom.window.DocumentFragment;
editorState.read(() => {
htmlString = $generateHtmlFromNodes(editor, null);
});
const template = await getHTMLString(htmlString);
return template;
} catch (e) {
console.log(e);
throw e;
}
};
Solution: Somehow make the styles in the style tag to be parsed to inline css in the body of the HTML document
Solution: Use the npm package inline-css
to convert the styles in the style tag to inline CSS via this simple method const html = await inlineCss(htmlStringWithStyle, { url: "." })
const getHTMLString = async (
htmlString: string | undefined
): Promise<string | undefined> => {
let htmlStringWithStyle: string | undefined = undefined;
try {
if (htmlString) {
htmlString = makeHTMLTemplate(htmlString);
const dom = new JSDOM(htmlString);
const document = dom.window.document;
// Append styles to head
getCssStyles(document);
// Get the HTML document in stringified format
htmlStringWithStyle = document.documentElement.outerHTML;
// Newly added snippet for inlining CSS >> Start
// Inline the styles currently located in the style tag in the head to individual tags
const html = await inlineCss(htmlStringWithStyle, { url: "." });
// Return stringified HTML with inline styles
htmlStringWithStyle = html;
const docWithStyle = new JSDOM(htmlStringWithStyle).window.document;
// Get all <style> elements in the head
const styleElements = docWithStyle.head.querySelectorAll("style");
// Loop through and remove each style element
styleElements.forEach((element) => {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
});
htmlStringWithStyle = docWithStyle.documentElement.outerHTML;
// Newly added snippet for inlining CSS >> Stop
return htmlStringWithStyle;
} else {
return htmlStringWithStyle;
}
} catch (err) {
return htmlStringWithStyle;
}
};
Here's what my webpack config file looks like (A huge chunk of the configuration was copied from Reactjs's config. I just made the tweaks that were relevant to me)
"use strict";
const fs = require("fs");
const path = require("path");
const webpack = require("webpack");
const resolve = require("resolve");
const PnpWebpackPlugin = require("pnp-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent");
const ESLintPlugin = require("eslint-webpack-plugin");
const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin");
const ForkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpackPlugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const postcssNormalize = require("postcss-normalize");
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === "true";
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === "true";
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || "10000"
);
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find((extension) =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
const paths = {
appPath: path.resolve("."),
appBuild: path.resolve(buildPath),
appIndexJs: resolveModule(path.resolve, "./index"),
appSrc: path.resolve("."),
appTsConfig: path.resolve("./tsconfig.json"),
appNodeModules: path.resolve("../../node_modules"),
publicUrlOrPath: "your/public/path",
}
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") {
return false;
}
try {
require.resolve("react/jsx-runtime");
return true;
} catch (e) {
return false;
}
})();
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
MiniCssExtractPlugin.loader,
{
loader: require.resolve("css-loader"),
options: cssOptions,
},
{
loader: require.resolve("postcss-loader"),
options: {
ident: "postcss",
plugins: () => [
require("postcss-flexbugs-fixes"),
require("postcss-preset-env")({
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
}),
postcssNormalize(),
],
sourceMap: true,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve("resolve-url-loader"),
options: {
sourceMap: true,
root: paths.appSrc,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
/** @type {import('webpack').Configuration} */
const configurations = {
mode: "development",
node: {
global: true,
},
devtool: false,
target: "node",
entry: paths.appIndexJs,
externals: [
"lexical",
"next",
"react",
"jsdom",
/^@lexical\/.+$/,
/^next\/.+$/,
"fs",
"inline-css",
],
output: {
path: paths.appBuild,
pathinfo: true,
filename: "static/js/bundle.js",
globalObject: "this",
library: {
name: "Library Name",
type: "umd",
},
umdNamedDefine: true,
publicPath: paths.publicUrlOrPath,
devtoolModuleFilenameTemplate: (info) =>
path.resolve(info.absoluteResourcePath).replace(/\\/g, "/"),
},
infrastructureLogging: {
level: "verbose",
appendOnly: true,
},
resolve: {
symlinks: true,
modules: ["node_modules", paths.appNodeModules],
extensions: paths.moduleFileExtensions
.map((ext) => `.${ext}`)
.filter((ext) => useTypeScript || !ext.includes("ts")),
alias: {
"@": "../..",
},
plugins: [
PnpWebpackPlugin,
],
},
module: {
strictExportPresence: true,
rules: [
{ parser: { requireEnsure: false } },
{
oneOf: [
{
test: [/\.avif$/],
loader: require.resolve("url-loader"),
options: {
limit: imageInlineSizeLimit,
mimetype: "image/avif",
name: "static/media/[name].[hash:8].[ext]",
},
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve("url-loader"),
options: {
limit: imageInlineSizeLimit,
name: "static/media/[name].[hash:8].[ext]",
},
},
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
loader: require.resolve("babel-loader"),
options: {
customize: require.resolve(
"babel-preset-react-app/webpack-overrides"
),
presets: [
[
require.resolve("babel-preset-react-app"),
{
runtime: hasJsxRuntime ? "automatic" : "classic",
},
],
],
plugins: [
[
require.resolve("babel-plugin-named-asset-import"),
{
loaderMap: {
svg: {
ReactComponent:
"@svgr/webpack?-svgo,+titleProp,+ref![path]",
},
},
},
],
].filter(Boolean),
cacheDirectory: true,
cacheCompression: false,
},
},
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve("babel-loader"),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve("babel-preset-react-app/dependencies"),
{ helpers: true },
],
],
sourceType: "unambiguous",
cacheDirectory: true,
cacheCompression: false,
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: true,
}),
sideEffects: true,
},
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: true,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: true,
},
"sass-loader"
),
sideEffects: true,
},
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: true,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
"sass-loader"
),
},
{
test: /\b_interop\w*\.cjs\b/,
use: [
{
loader: require.resolve("babel-loader"),
options: {
sourceType: "module",
},
},
],
},
{
loader: require.resolve("file-loader"),
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: "static/media/[name].[hash:8].[ext]",
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({ filename: "main.css", ignoreOrder: true }),
new ModuleNotFoundPlugin(paths.appPath),
new webpack.ProvidePlugin({
/**
* Create these files and export window, document and navigator from them respectively
*
* For example in WINDOW.js:
* const window = new JSDOM().window
* module.exports = window
*
* In DOCUMENT.js
* const window = require("./WINDOW.js")
* module.exports = window.document
*
* In NAVIGATOR.js
* const window = require("./WINDOW.js")
* module.exports = window.navigator
*/
window: path.resolve(__dirname, "jsdom/WINDOW.js"),
document: path.resolve(__dirname, "jsdom/DOCUMENT.js"),
navigator: path.resolve(__dirname, "jsdom/NAVIGATOR.js"),
}),
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync("typescript", {
basedir: paths.appNodeModules,
}),
async: true,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [],
silent: true,
}),
!disableESLintPlugin &&
new ESLintPlugin({
extensions: ["js", "mjs", "jsx", "ts", "tsx"],
formatter: require.resolve("react-dev-utils/eslintFormatter"),
eslintPath: require.resolve("eslint"),
failOnError: !emitErrorsAsWarnings,
context: paths.appSrc,
cache: true,
cacheLocation: path.resolve(
paths.appNodeModules,
".cache/.eslintcache"
),
cwd: paths.appPath,
resolvePluginsRelativeTo: __dirname,
baseConfig: {
extends: [require.resolve("eslint-config-react-app/base")],
rules: {
...(!hasJsxRuntime && {
"react/react-in-jsx-scope": "error",
}),
},
},
}),
new CleanWebpackPlugin(),
].filter(Boolean),
performance: false,
cache: {
type: "filesystem",
},
optimization: {
minimizer: [new CssMinimizerPlugin()],
minimize: true,
splitChunks: {
cacheGroups: {
styles: {
name: "styles",
type: "css/mini-extract",
chunks: "all",
enforce: true,
},
},
},
},
};
module.exports = configurations;
Is it possible to save the editor state from
JSON.stringify(editorState.toJSON());
and rehydrate it later but as read-only HTML instead displaying an editor? For example, using Lexical as a blog editor and rendering the output as a static post.