flowchart LR
A[md/mmd/stdin] --> B[extractBlocks]
B --> C[主题合成 preset + theme-js + config + css]
C --> D[Renderer 页面池 mermaid.render]
D --> E[SVG 重算 viewBox]
D --> F[PNG 墨迹二次裁切]
E --> G[svgo 压缩]
G --> H[落盘 + --json]
F --> H
块提取是纯正则,FENCE_RE 扫所有围栏语言,认识的才收(src/cli.ts:161):
1
2
3
4
5
6
7
8
// any fenced block with a recognized language (mermaid / table / list)
constFENCE_RE=/^[ \t]*```([A-Za-z0-9_-]+)[^\n]*\n([\s\S]*?)^[ \t]*```\s*$/gm;functionextractBlocks(input: string,raw: string):Block[]{// ... kind = fenceKind(m[1]); kind === 'unknown' 就跳过
// 记录 line(块在原文的行号,报错时 agent 能定位回源文件)
blocks.push({input,base,index: blocks.length+1,line,code,kind});}
// src/render.ts:154 — ELK 懒加载的关键
if(this.opts.layout==='elk'&&this.assets.elkJs){// stash the payload; ELK is parsed lazily on the first flowchart render
// (its 7MB bundle costs ~0.8s of parse per page and only flowchart/
// graph diagrams ever use it)
awaitpage.evaluate((b)=>{(windowasany).__elkB64=b;},b64);}
// getCTM maps into VIEWPORT space, which includes the (stale)
// root viewBox scale; divide it out so all leaves land in the
// root's own user space, the space the viewBox is written in.
constm=root.inverse().multiply(mRaw);
// geometric bounds miss marker arrowheads and glyph overshoot; a
// second capture clipped to the actual ink gives pixel-exact,
// symmetric file margins
constink=inkBBox(first);if(!ink)returnfirst;constmx=Math.max(1,padX*sc-2);constclip={x: Math.max(0,(ink.x0-mx)/sc),// ... y / width / height 同理
};return(awaitpage.screenshot({clip,omitBackground: transparent}))asBuffer;
themeVariables:{// ---- core derivation chain: every diagram inherits from these.
// primaryColor in particular must be set — its default (#fff4dd cream)
// is the "native mermaid" look leaking through every unset corner.
primaryColor:'#E8F3FF',primaryTextColor:'#1D2129',primaryBorderColor:'#4098FC',// ...
// ---- pie: AntV G2 default categorical palette
pie1:'#5B8FF9',pie2:'#5AD8A6',/* ... pie12 */// ---- xyChart: plotColorPalette (defaults start with mermaid purple #ECECFF)
xyChart:{plotColorPalette:'#5B8FF9,#F6BD16,#5AD8A6,...',/* ... */},},
/* shape-coding: mermaid variables only offer a single primaryColor for node
shapes, so the rect=process / diamond=decision / circle=state / store=purple
colour roles are painted here */.noderect{fill:#E8F3FF;stroke:#4098FC;}.nodepolygon{fill:#FFF7E6;stroke:#FF9A2E;}.nodecircle,.nodeellipse{fill:#E8FFEA;stroke:#23C343;}.nodepath{fill:#F5E8FF;stroke:#722ED1;}
for(leti=0;i<data.length;i+=4){if(data[i+3]<250)continue;// AA fringe
constkey='#'+[data[i],data[i+1],data[i+2]].map((v)=>v.toString(16).padStart(2,'0').toUpperCase()).join('');counts.set(key,(counts.get(key)??0)+1);}
问题来了:抗锯齿和半透明填充会产生调色板色的合成产物——比如 35% 透明度的雷达曲线叠在白底上,逐像素值不在调色板里,但它是合法的。硬把抗锯齿当坏值,工具会误报到没法用。所以有个合成容差判定 isDerived:某颜色 c 合法,当且仅当存在调色板色 P 和 alpha a,使 P*a + 255*(1-a) 在每通道 ±3 内还原出 c(theme-census.ts:48):
最后一层是刺眼默认色黑名单:有些 mermaid 默认色面积很小(journey 的 actor 圆点、单条错误的连线),按面积阈值会漏掉,但它们恰恰是最扎眼的。黑名单里的颜色任何覆盖率都报(theme-census.ts:61):
1
2
3
4
5
6
7
// known garish mermaid defaults — flag at ANY coverage (tiny elements
// like journey actor dots fall below the area threshold)
constloud=['#7CFC00','#00FFFF','#8FBC8F','#ECECFF','#9370DB','#191970','#8B008B','#FF0000','#00BFFF','#FF8888'];constoff=[...counts.entries()].filter(([c,n])=>!allowed.has(c)&&!isDerived(c)&&(n/total>0.0015||(loud.includes(c)&&n>total*0.0001)));
// a wedged renderer (bad diagram, crashed tab) must fail loud, never hang
returnawaitPromise.race([fn(page),newPromise<never>((_,rej)=>{timer=setTimeout(()=>rej(newError('render timed out after 60s')),60_000);}),]);