Skip to content

Commit 7ac3e8c

Browse files
zhonghuiCopilot
andcommitted
fix: inject inline color on foreignObject HTML to fix EPUB Mermaid text
Root cause: EPUB readers do NOT apply SVG <style> scoped rules to HTML elements inside <foreignObject>. Mermaid renders node/edge labels as <div>/<span>/<p> in foreignObject with NO inline color — so text color is inherited from the EPUB reader's CSS, which can be white/invisible. No amount of themeVariables or --theme flags can fix this because the problem is in how EPUB readers scope SVG CSS, not in mmdc's rendering. Fix: post-process the SVG in renderMermaidToSvg() to inject explicit style="color:THEME.textColor" on every <div>, <span>, and <p> inside foreignObject. Also injects fill:nodeBg on .label-container shapes so the node background is explicit rather than CSS-dependent. Verified: all 10 spans in test SVG now carry style="color:#2C2C2C". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent eb6cc1f commit 7ac3e8c

1 file changed

Lines changed: 30 additions & 1 deletion

File tree

scripts/build-epub.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,36 @@ function renderMermaidToSvg(src) {
124124

125125
const svgRaw = fs.readFileSync(outFile, 'utf8');
126126
const m = svgRaw.match(/<svg[\s\S]*<\/svg>/i);
127-
return m ? m[0] : null;
127+
if (!m) return null;
128+
129+
// EPUB readers do NOT apply SVG <style> rules to HTML inside <foreignObject>.
130+
// Node/edge labels are rendered as <div>/<span>/<p> inside foreignObject,
131+
// so their text color is inherited from the EPUB reader's own CSS (which
132+
// may be white in dark-mode readers). Fix: inject explicit color inline.
133+
const textColor = THEME.textColor;
134+
const nodeBg = '#C8E6FA';
135+
const svg = m[0]
136+
// Inject color into every <div inside foreignObject
137+
.replace(/(<div\b[^>]*\bstyle=")([^"]*")/g,
138+
(_, before, rest) => `${before}color:${textColor};${rest}`)
139+
// Inject color into every <span inside foreignObject
140+
.replace(/(<span\b[^>]*\bstyle=")([^"]*")/g,
141+
(_, before, rest) => `${before}color:${textColor};${rest}`)
142+
// Also cover <div> / <span> with no style attr at all
143+
.replace(/<div\b([^>]*?)(?=\s*>)/g, (match, attrs) =>
144+
attrs.includes('style=') ? match : `<div${attrs} style="color:${textColor};"`)
145+
.replace(/<span\b([^>]*?)(?=\s*>)/g, (match, attrs) =>
146+
attrs.includes('style=') ? match : `<span${attrs} style="color:${textColor};"`)
147+
// Also inject color on <p> elements (edge labels use <p> tags)
148+
.replace(/<p\b([^>]*?)(?=\s*>)/g, (match, attrs) =>
149+
attrs.includes('style=') ? match
150+
: `<p${attrs} style="color:${textColor};"`)
151+
// Ensure node rects have the explicit fill (in case scoped CSS is stripped)
152+
.replace(/(<(?:rect|polygon|ellipse|circle)\b[^>]*\bclass="[^"]*label-container[^"]*"[^>]*\bstyle=")([^"]*")/g,
153+
(_, before, rest) => `${before}fill:${nodeBg};${rest}`)
154+
.replace(/(<(?:rect|polygon|ellipse|circle)\b[^>]*\bclass="[^"]*label-container[^"]*"(?!\s+style=)[^>]*?)(\s*\/>|>)/g,
155+
(_, before, end) => `${before} style="fill:${nodeBg};"${end}`);
156+
return svg;
128157
} catch (e) {
129158
// Print first stderr line so build output gives a useful hint
130159
const hint = (e.stderr || '').toString().split('\n').find(l => l.trim()) || e.message || '';

0 commit comments

Comments
 (0)