Skip to content

Commit

Permalink
Apply incremental DOM to markdown preview (#397)
Browse files Browse the repository at this point in the history
* feat: Apply incremental dom update

* fix: build error

* fix: pin incremental-dom version to 0.6.0
  • Loading branch information
blurfx authored Nov 3, 2024
1 parent a77f30d commit 9e08612
Show file tree
Hide file tree
Showing 4 changed files with 2,888 additions and 234 deletions.
16 changes: 10 additions & 6 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"@tanstack/react-query": "^5.17.15",
"@uiw/codemirror-extensions-basic-setup": "^4.23.2",
"@uiw/codemirror-theme-xcode": "^4.21.21",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/react-markdown-preview": "^5.0.7",
"@vscode/markdown-it-katex": "^1.1.0",
"axios": "^1.6.5",
"browser-image-resizer": "^2.4.1",
"clipboardy": "^4.0.0",
Expand All @@ -49,9 +49,15 @@
"codemirror-toolbar": "^0.0.4",
"color": "^4.2.3",
"form-data": "^4.0.0",
"hast-util-to-html": "^9.0.3",
"incremental-dom": "^0.6.0",
"katex": "^0.16.9",
"lib0": "^0.2.88",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"markdown-it-incremental-dom": "^2.1.0",
"markdown-it-prism": "^2.3.0",
"markdown-it-sanitizer": "^0.4.3",
"match-sorter": "^6.3.3",
"moment": "^2.30.1",
"notistack": "^3.0.1",
Expand All @@ -70,20 +76,18 @@
"react-social-login-buttons": "^3.9.1",
"react-use": "^17.5.0",
"redux-persist": "^6.0.0",
"rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1",
"rehype-rewrite": "^4.0.2",
"rehype-sanitize": "^6.0.0",
"remark-math": "^6.0.0",
"refractor": "^4.8.1",
"validator": "^13.12.0",
"vite-plugin-package-version": "^1.1.0",
"yorkie-js-sdk": "0.5.4"
},
"devDependencies": {
"@sentry/vite-plugin": "^2.20.1",
"@types/color": "^3.0.6",
"@types/incremental-dom": "^0.5.0",
"@types/katex": "^0.16.7",
"@types/lodash": "^4.14.202",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.9",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.43",
Expand Down
133 changes: 57 additions & 76 deletions frontend/src/components/editor/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
import { CircularProgress, Stack } from "@mui/material";
import MarkdownPreview from "@uiw/react-markdown-preview";
import katex from "katex";
import "katex/dist/katex.min.css";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import rehypeExternalLinks from "rehype-external-links";
import rehypeKatex from "rehype-katex";
import { getCodeString } from "rehype-rewrite";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import remarkMath from "remark-math";
import { useCurrentTheme } from "../../hooks/useCurrentTheme";
import { selectEditor } from "../../store/editorSlice";
import { addSoftLineBreak } from "../../utils/document";
import MarkdownIt from "markdown-it";
import { toHtml } from "hast-util-to-html";
import markdownItKatex from "@vscode/markdown-it-katex";
import { refractor } from "refractor";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import markdownItIncrementalDOM from "markdown-it-incremental-dom";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import markdownItSanitizer from "markdown-it-sanitizer";
import * as IncrementalDOM from "incremental-dom";
import "./editor.css";
import "./preview.css";

function Preview() {
const md = new MarkdownIt({
html: true,
linkify: true,
breaks: true,
highlight: (code: string, lang: string): string => {
try {
return `<pre class="language-${lang}"><code>${toHtml(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
refractor.highlight(code, lang) as any
)}</code></pre>`;
} catch (error) {
console.error(`Error highlighting code with language '${lang}':`, error);
return `<pre class="language-"><code>${md.utils.escapeHtml(code)}</code></pre>`;
}
},
})
.use(markdownItIncrementalDOM, IncrementalDOM, {
incrementalizeDefaultRules: false,
})
.use(markdownItKatex)
.use(markdownItSanitizer);

const Preview = () => {
const currentTheme = useCurrentTheme();
const editorStore = useSelector(selectEditor);
const [content, setContent] = useState("");
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!editorStore.doc) return;
Expand All @@ -30,89 +58,42 @@ function Preview() {

updatePreviewContent();

const unsubsribe = editorStore.doc.subscribe("$.content", () => {
const unsubscribe = editorStore.doc.subscribe("$.content", () => {
updatePreviewContent();
});

return () => {
unsubsribe();
unsubscribe();
setContent("");
};
}, [editorStore.doc]);

if (!editorStore?.doc)
useEffect(() => {
if (containerRef.current == null) {
return;
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
IncrementalDOM.patch(containerRef.current, md.renderToIncrementalDOM(content));
}, [content]);

if (!editorStore?.doc) {
return (
<Stack direction="row" justifyContent="center">
<CircularProgress sx={{ mt: 2 }} />
</Stack>
);
}

return (
<MarkdownPreview
style={{
paddingBottom: "2rem",
}}
source={addSoftLineBreak(content)}
wrapperElement={{
"data-color-mode": currentTheme,
style: {
whiteSpace: "wrap !important",
WebkitUserModify: "read-only",
},
}}
remarkPlugins={[remarkMath]}
rehypePlugins={[
[
rehypeSanitize,
{
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: [["className", /^language-./, "math-inline", "math-display"]],
},
},
],
rehypeKatex,
[rehypeExternalLinks, { target: "_blank" }],
]}
components={{
code: ({ children = [], className, ...props }) => {
// https://www.npmjs.com/package/@uiw/react-markdown-preview#support-custom-katex-preview
if (typeof children === "string" && /^\$\$(.*)\$\$/.test(children)) {
const html = katex.renderToString(children.replace(/^\$\$(.*)\$\$/, "$1"), {
throwOnError: false,
});
return (
<code
dangerouslySetInnerHTML={{ __html: html }}
style={{ background: "transparent" }}
/>
);
}
const code =
props.node && props.node.children
? getCodeString(props.node.children)
: children;
if (
typeof code === "string" &&
typeof className === "string" &&
/^language-katex/.test(className.toLocaleLowerCase())
) {
const html = katex.renderToString(code, {
throwOnError: false,
});
return (
<code
style={{ fontSize: "150%" }}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
return <code className={String(className)}>{children}</code>;
},
}}
<div
ref={containerRef}
data-color-mode={currentTheme}
style={{ paddingBottom: "2rem" }}
className="markdown-preview"
/>
);
}
};

export default Preview;
Loading

0 comments on commit 9e08612

Please sign in to comment.