Skip to content

Commit bc12fd2

Browse files
committed
fix(markdown-preview): improve mermaid rendering and anchor scrolling
1 parent 98c882b commit bc12fd2

File tree

3 files changed

+261
-151
lines changed

3 files changed

+261
-151
lines changed

src/pages/markdownPreview/index.js

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import "katex/dist/katex.min.css";
2-
import "markdown-it-texmath/css/texmath.css";
31
import "./style.scss";
42

53
import fsOperation from "fileSystem";
@@ -10,6 +8,7 @@ import openFile from "lib/openFile";
108
import { highlightCodeBlock, initHighlighting } from "utils/codeHighlight";
119
import {
1210
getMarkdownBaseUri,
11+
hasMathContent,
1312
isExternalLink,
1413
isMarkdownPath,
1514
renderMarkdown,
@@ -19,6 +18,7 @@ import {
1918
let previewController = null;
2019
let mermaidModulePromise = null;
2120
let mermaidThemeSignature = "";
21+
let mathStylesPromise = null;
2222

2323
function getThemeColor(name, fallback) {
2424
const value = getComputedStyle(document.documentElement)
@@ -62,6 +62,18 @@ function getTargetElement(container, targetId) {
6262
);
6363
}
6464

65+
function getOffsetTopWithinContainer(target, container) {
66+
let top = 0;
67+
let element = target;
68+
69+
while (element && element !== container) {
70+
top += element.offsetTop || 0;
71+
element = element.offsetParent;
72+
}
73+
74+
return top;
75+
}
76+
6577
async function getMermaid() {
6678
if (!mermaidModulePromise) {
6779
mermaidModulePromise = import("mermaid")
@@ -75,22 +87,43 @@ async function getMermaid() {
7587
return mermaidModulePromise;
7688
}
7789

90+
async function ensureMathStyles() {
91+
if (!mathStylesPromise) {
92+
mathStylesPromise = Promise.all([
93+
import("katex/dist/katex.min.css"),
94+
import("markdown-it-texmath/css/texmath.css"),
95+
]).catch((error) => {
96+
mathStylesPromise = null;
97+
throw error;
98+
});
99+
}
100+
101+
return mathStylesPromise;
102+
}
103+
78104
function getMermaidThemeConfig() {
79105
const backgroundColor = getThemeColor("--background-color", "#1e1e1e");
80106
const panelColor = getThemeColor("--popup-background-color", "#2a2f3a");
81107
const borderColor = getThemeColor("--border-color", "#4a4f5a");
82108
const primaryTextColor = getThemeColor("--primary-text-color", "#f5f5f5");
83109
const accentColor = getThemeColor("--link-text-color", "#4ba3ff");
110+
const activeColor = getThemeColor("--active-color", accentColor);
84111

85112
return {
86113
startOnLoad: false,
87114
securityLevel: "strict",
115+
htmlLabels: false,
88116
theme: "base",
117+
flowchart: {
118+
htmlLabels: false,
119+
},
89120
themeVariables: {
90121
darkMode: isDarkColor(backgroundColor),
91122
background: backgroundColor,
92123
mainBkg: panelColor,
93124
primaryColor: panelColor,
125+
mainContrastColor: primaryTextColor,
126+
textColor: primaryTextColor,
94127
primaryTextColor,
95128
primaryBorderColor: borderColor,
96129
lineColor: primaryTextColor,
@@ -103,7 +136,29 @@ function getMermaidThemeConfig() {
103136
clusterBkg: panelColor,
104137
clusterBorder: borderColor,
105138
nodeBorder: borderColor,
139+
nodeTextColor: primaryTextColor,
140+
titleColor: primaryTextColor,
141+
defaultLinkColor: activeColor,
142+
actorTextColor: primaryTextColor,
143+
labelTextColor: primaryTextColor,
144+
loopTextColor: primaryTextColor,
145+
noteTextColor: primaryTextColor,
146+
sectionBkgColor: panelColor,
147+
sectionBkgColor2: backgroundColor,
148+
sectionTitleColor: primaryTextColor,
149+
sequenceNumberColor: primaryTextColor,
150+
signalTextColor: primaryTextColor,
151+
taskTextColor: primaryTextColor,
152+
taskTextDarkColor: primaryTextColor,
153+
taskTextOutsideColor: primaryTextColor,
106154
edgeLabelBackground: backgroundColor,
155+
pieTitleTextColor: primaryTextColor,
156+
pieLegendTextColor: primaryTextColor,
157+
pieSectionTextColor: primaryTextColor,
158+
git0: panelColor,
159+
git1: backgroundColor,
160+
git2: accentColor,
161+
git3: activeColor,
107162
},
108163
};
109164
}
@@ -186,15 +241,6 @@ async function resolveRenderedImages(container, file) {
186241
}),
187242
);
188243

189-
container.querySelectorAll("a[href]").forEach((link) => {
190-
const href = link.getAttribute("href");
191-
if (!href || href.startsWith("#") || isExternalLink(href)) return;
192-
link.setAttribute(
193-
"data-resolved-href",
194-
resolveMarkdownTarget(href, baseUri),
195-
);
196-
});
197-
198244
return objectUrls;
199245
}
200246

@@ -259,7 +305,11 @@ function createMarkdownPreview(file) {
259305

260306
const originalHref = link.getAttribute("href") || "";
261307
const resolvedHref =
262-
link.getAttribute("data-resolved-href") || originalHref;
308+
link.getAttribute("data-resolved-href") ||
309+
resolveMarkdownTarget(
310+
originalHref,
311+
getMarkdownBaseUri(previewState.file),
312+
);
263313
event.preventDefault();
264314
event.stopPropagation();
265315

@@ -302,11 +352,9 @@ function createMarkdownPreview(file) {
302352
const target = getTargetElement(previewState.content, targetId);
303353
if (!target) return;
304354

355+
const topOffset = 12;
305356
const top =
306-
target.getBoundingClientRect().top -
307-
previewState.content.getBoundingClientRect().top +
308-
previewState.content.scrollTop -
309-
12;
357+
getOffsetTopWithinContainer(target, previewState.content) - topOffset;
310358

311359
previewState.content.scrollTo({
312360
top: Math.max(0, top),
@@ -338,7 +386,10 @@ function createMarkdownPreview(file) {
338386
}
339387

340388
if (highlighted && highlighted !== originalCode) {
341-
codeElement.innerHTML = highlighted;
389+
codeElement.innerHTML = DOMPurify.sanitize(highlighted, {
390+
ALLOWED_TAGS: ["span"],
391+
ALLOWED_ATTR: ["class"],
392+
});
342393
}
343394
}),
344395
);
@@ -404,6 +455,7 @@ function createMarkdownPreview(file) {
404455

405456
const sanitizedSvg = DOMPurify.sanitize(svg, {
406457
USE_PROFILES: { svg: true, svgFilters: true },
458+
ADD_TAGS: ["style"],
407459
});
408460
block.innerHTML = sanitizedSvg;
409461
bindFunctions?.(block);
@@ -426,7 +478,13 @@ function createMarkdownPreview(file) {
426478
previewState.objectUrls = [];
427479

428480
const markdownText = previewState.file.session?.doc?.toString?.() || "";
429-
const { html } = await renderMarkdown(markdownText, previewState.file);
481+
const pendingRenderTasks = [
482+
renderMarkdown(markdownText, previewState.file),
483+
];
484+
if (hasMathContent(markdownText)) {
485+
pendingRenderTasks.push(ensureMathStyles());
486+
}
487+
const [{ html }] = await Promise.all(pendingRenderTasks);
430488

431489
if (previewState.disposed || version !== previewState.renderVersion) {
432490
return;

src/pages/markdownPreview/renderer.js

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,13 @@ import Url from "utils/Url";
99
const EXTERNAL_LINK_PATTERN = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
1010
const IMAGE_PLACEHOLDER =
1111
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
12+
const BLOCK_MATH_PATTERN = /(^|[^\\])\$\$[\s\S]+?\$\$/m;
13+
const INLINE_MATH_PATTERN = /(^|[^\\])\$(?!\s)(?:\\.|[^$\\\n])+\$(?!\w)/m;
14+
const BEGIN_END_MATH_PATTERN =
15+
/\\begin\{(?:equation|align|gather|multline|eqnarray)\*?\}[\s\S]*?\\end\{(?:equation|align|gather|multline|eqnarray)\*?\}/m;
1216

13-
let katexModulePromise = null;
14-
let mdItTexmathModulePromise = null;
15-
16-
async function getKatexAndTexmathModule() {
17-
if (!katexModulePromise) {
18-
katexModulePromise = import("katex")
19-
.then(({ default: katex }) => katex)
20-
.catch((error) => {
21-
katexModulePromise = null;
22-
throw error;
23-
});
24-
}
25-
26-
if (!mdItTexmathModulePromise) {
27-
mdItTexmathModulePromise = import("markdown-it-texmath")
28-
.then(({ default: markdownItTexmath }) => markdownItTexmath)
29-
.catch((error) => {
30-
mdItTexmathModulePromise = null;
31-
throw error;
32-
});
33-
}
34-
35-
return { katexModulePromise, mdItTexmathModulePromise };
36-
}
17+
let mathModulesPromise = null;
18+
let mathMarkdownItPromise = null;
3719

3820
function slugify(text) {
3921
return text
@@ -135,12 +117,35 @@ function collectTokens(tokens, callback) {
135117
}
136118
}
137119

138-
function createMarkdownIt() {
139-
const {
140-
katexModulePromise: katex,
141-
mdItTexmathModulePromise: markdownItTexmath,
142-
} = getKatexAndTexmathModule().then((m) => m);
120+
export function hasMathContent(text = "") {
121+
return (
122+
BLOCK_MATH_PATTERN.test(text) ||
123+
INLINE_MATH_PATTERN.test(text) ||
124+
BEGIN_END_MATH_PATTERN.test(text)
125+
);
126+
}
127+
128+
async function getKatexAndTexmathModules() {
129+
if (!mathModulesPromise) {
130+
mathModulesPromise = Promise.all([
131+
import("katex").then(({ default: katex }) => katex),
132+
import("markdown-it-texmath").then(
133+
({ default: markdownItTexmath }) => markdownItTexmath,
134+
),
135+
]).then(([katex, markdownItTexmath]) => ({
136+
katex,
137+
markdownItTexmath,
138+
}));
139+
mathModulesPromise = mathModulesPromise.catch((error) => {
140+
mathModulesPromise = null;
141+
throw error;
142+
});
143+
}
143144

145+
return mathModulesPromise;
146+
}
147+
148+
function createMarkdownIt({ katex = null, markdownItTexmath = null } = {}) {
144149
const md = markdownIt({
145150
html: true,
146151
linkify: true,
@@ -149,16 +154,20 @@ function createMarkdownIt() {
149154
md.use(MarkdownItGitHubAlerts)
150155
.use(anchor, { slugify })
151156
.use(markdownItTaskLists)
152-
.use(markdownItFootnote)
153-
.use(markdownItTexmath, {
157+
.use(markdownItFootnote);
158+
159+
if (katex && markdownItTexmath) {
160+
md.use(markdownItTexmath, {
154161
engine: katex,
155162
delimiters: ["dollars", "beg_end"],
156163
katexOptions: {
157164
throwOnError: false,
158165
strict: "ignore",
159166
},
160-
})
161-
.use(markdownItEmoji);
167+
});
168+
}
169+
170+
md.use(markdownItEmoji);
162171

163172
md.renderer.rules.image = (tokens, idx, options, env, self) => {
164173
const token = tokens[idx];
@@ -198,12 +207,33 @@ function createMarkdownIt() {
198207
return md;
199208
}
200209

201-
const md = createMarkdownIt();
210+
const baseMarkdownIt = createMarkdownIt();
211+
212+
async function getMarkdownIt(text = "") {
213+
if (!hasMathContent(text)) {
214+
return baseMarkdownIt;
215+
}
216+
217+
if (!mathMarkdownItPromise) {
218+
mathMarkdownItPromise = getKatexAndTexmathModules()
219+
.then(({ katex, markdownItTexmath }) =>
220+
createMarkdownIt({ katex, markdownItTexmath }),
221+
)
222+
.catch((error) => {
223+
mathMarkdownItPromise = null;
224+
throw error;
225+
});
226+
}
227+
228+
return mathMarkdownItPromise;
229+
}
202230

203231
export async function renderMarkdown(text, file) {
232+
const markdownText = text || "";
233+
const md = await getMarkdownIt(markdownText);
204234
const env = {};
205235
env.markdownBaseUri = getMarkdownBaseUri(file);
206-
const tokens = md.parse(text || "", env);
236+
const tokens = md.parse(markdownText, env);
207237

208238
collectTokens(tokens, (token) => {
209239
if (token.type === "link_open") {

0 commit comments

Comments
 (0)