Skip to content

Commit 7d80f71

Browse files
feat: mermaid render support; experts session starts
1 parent fa96d4b commit 7d80f71

15 files changed

Lines changed: 1555 additions & 47 deletions

site/.vitepress/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineConfig } from 'vitepress'
22
import { navZh } from './nav'
33
import { buildSidebar } from './sidebar'
44
import { cppTemplateEscapePlugin } from '../plugins/escape-cpp-templates'
5+
import { mermaidPlugin } from '../plugins/mermaid-plugin'
56
import { viteCppEscape } from '../plugins/vite-escape-cpp'
67

78
export default defineConfig({
@@ -41,6 +42,7 @@ export default defineConfig({
4142
},
4243
config(md) {
4344
cppTemplateEscapePlugin(md)
45+
md.use(mermaidPlugin)
4446
},
4547
},
4648

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { PluginSimple } from 'markdown-it'
2+
import type MarkdownIt from 'markdown-it'
3+
4+
function escapeHtml(s: string): string {
5+
return s
6+
.replaceAll('&', '&')
7+
.replaceAll('<', '&lt;')
8+
.replaceAll('>', '&gt;')
9+
.replaceAll('"', '&quot;')
10+
.replaceAll("'", '&#39;')
11+
}
12+
13+
/**
14+
* 把 ```mermaid 围栏改写成客户端能识别的占位 div。
15+
*
16+
* 为什么要抢在 Shiki 之前:VitePress 通过覆盖 md.renderer.rules.fence 让 Shiki
17+
* 接管所有 fence 渲染。这里在 core ruler 阶段(tokenize 之后、render 之前)把
18+
* mermaid 围栏的 token.type 改成自定义类型,渲染时分发键就不再是 'fence',
19+
* Shiki 永远匹配不到它——否则图表源码会被当成普通代码块高亮。
20+
*/
21+
export const mermaidPlugin: PluginSimple = (md: MarkdownIt) => {
22+
md.core.ruler.push('mermaid_block', (state) => {
23+
for (let i = 0; i < state.tokens.length; i++) {
24+
const token = state.tokens[i]
25+
if (token.type === 'fence' && token.info.trim() === 'mermaid') {
26+
token.type = 'mermaid_diagram'
27+
token.tag = ''
28+
token.nesting = 0
29+
}
30+
}
31+
return true
32+
})
33+
34+
md.renderer.rules.mermaid_diagram = (tokens, idx) => {
35+
const raw = tokens[idx].content.trim()
36+
const encoded = encodeURIComponent(raw)
37+
// data-mermaid 供客户端解码后渲染成 SVG。
38+
// visually-hidden 的 span 让本地搜索(minisearch)能索引到图表里的词
39+
// (节点标签、类名、时序消息等);否则占位 div 文本为空,图表内的词搜不到。
40+
return (
41+
`<div class="mermaid-diagram" data-mermaid="${encoded}" data-rendered="false">` +
42+
`<span class="mermaid-search-text" aria-hidden="true">${escapeHtml(raw)}</span>` +
43+
`</div>`
44+
)
45+
}
46+
}

site/.vitepress/theme/custom.css

Lines changed: 195 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@
44
font-size: 0.82rem;
55
}
66

7-
.vp-doc h1 { line-height: 1.4; }
8-
.vp-doc h2 { line-height: 1.45; }
9-
.vp-doc h3 { line-height: 1.5; }
10-
.vp-doc h4 { line-height: 1.55; }
7+
.vp-doc h1 {
8+
line-height: 1.4;
9+
}
10+
11+
.vp-doc h2 {
12+
line-height: 1.45;
13+
}
14+
15+
.vp-doc h3 {
16+
line-height: 1.5;
17+
}
18+
19+
.vp-doc h4 {
20+
line-height: 1.55;
21+
}
1122

1223
.vp-doc p {
1324
margin-bottom: 1.2em;
@@ -29,7 +40,7 @@
2940
font-size: 0.75rem;
3041
}
3142

32-
.vp-doc :not(pre) > code {
43+
.vp-doc :not(pre)>code {
3344
font-size: 0.78em;
3445
padding: 0.15em 0.35em;
3546
border-radius: 4px;
@@ -106,23 +117,19 @@
106117
position: absolute;
107118
inset: 0;
108119
z-index: -1;
109-
background: linear-gradient(
110-
135deg,
111-
var(--vp-c-brand-soft) 0%,
112-
transparent 50%,
113-
var(--vp-c-indigo-soft) 100%
114-
);
120+
background: linear-gradient(135deg,
121+
var(--vp-c-brand-soft) 0%,
122+
transparent 50%,
123+
var(--vp-c-indigo-soft) 100%);
115124
opacity: 0.5;
116125
pointer-events: none;
117126
}
118127

119128
.VPHero .name.clip {
120-
background: linear-gradient(
121-
135deg,
122-
var(--vp-c-brand-1) 0%,
123-
var(--vp-c-indigo-1) 50%,
124-
var(--vp-c-purple-1) 100%
125-
);
129+
background: linear-gradient(135deg,
130+
var(--vp-c-brand-1) 0%,
131+
var(--vp-c-indigo-1) 50%,
132+
var(--vp-c-purple-1) 100%);
126133
-webkit-background-clip: text;
127134
background-clip: text;
128135
-webkit-text-fill-color: transparent;
@@ -140,25 +147,64 @@
140147
opacity: 0;
141148
transform: translateY(20px);
142149
}
150+
143151
to {
144152
opacity: 1;
145153
transform: translateY(0);
146154
}
147155
}
148156

149-
.VPFeatures .item { animation: feature-fade-up 0.65s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; }
150-
.VPFeatures .item:nth-child(1) { animation-delay: 0ms; }
151-
.VPFeatures .item:nth-child(2) { animation-delay: 78ms; }
152-
.VPFeatures .item:nth-child(3) { animation-delay: 156ms; }
153-
.VPFeatures .item:nth-child(4) { animation-delay: 234ms; }
154-
.VPFeatures .item:nth-child(5) { animation-delay: 312ms; }
155-
.VPFeatures .item:nth-child(6) { animation-delay: 390ms; }
156-
.VPFeatures .item:nth-child(7) { animation-delay: 468ms; }
157-
.VPFeatures .item:nth-child(8) { animation-delay: 546ms; }
158-
.VPFeatures .item:nth-child(9) { animation-delay: 624ms; }
159-
.VPFeatures .item:nth-child(10) { animation-delay: 702ms; }
160-
.VPFeatures .item:nth-child(11) { animation-delay: 780ms; }
161-
.VPFeatures .item:nth-child(12) { animation-delay: 858ms; }
157+
.VPFeatures .item {
158+
animation: feature-fade-up 0.65s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
159+
}
160+
161+
.VPFeatures .item:nth-child(1) {
162+
animation-delay: 0ms;
163+
}
164+
165+
.VPFeatures .item:nth-child(2) {
166+
animation-delay: 78ms;
167+
}
168+
169+
.VPFeatures .item:nth-child(3) {
170+
animation-delay: 156ms;
171+
}
172+
173+
.VPFeatures .item:nth-child(4) {
174+
animation-delay: 234ms;
175+
}
176+
177+
.VPFeatures .item:nth-child(5) {
178+
animation-delay: 312ms;
179+
}
180+
181+
.VPFeatures .item:nth-child(6) {
182+
animation-delay: 390ms;
183+
}
184+
185+
.VPFeatures .item:nth-child(7) {
186+
animation-delay: 468ms;
187+
}
188+
189+
.VPFeatures .item:nth-child(8) {
190+
animation-delay: 546ms;
191+
}
192+
193+
.VPFeatures .item:nth-child(9) {
194+
animation-delay: 624ms;
195+
}
196+
197+
.VPFeatures .item:nth-child(10) {
198+
animation-delay: 702ms;
199+
}
200+
201+
.VPFeatures .item:nth-child(11) {
202+
animation-delay: 780ms;
203+
}
204+
205+
.VPFeatures .item:nth-child(12) {
206+
animation-delay: 858ms;
207+
}
162208

163209
@media (prefers-reduced-motion: reduce) {
164210
.VPFeatures .item {
@@ -180,16 +226,16 @@
180226
border-radius: 14px !important;
181227
background-color: var(--vp-c-bg) !important;
182228
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04),
183-
0 1px 2px rgba(0, 0, 0, 0.06);
229+
0 1px 2px rgba(0, 0, 0, 0.06);
184230
transition: border-color 0.39s ease,
185-
box-shadow 0.39s ease,
186-
transform 0.39s ease;
231+
box-shadow 0.39s ease,
232+
transform 0.39s ease;
187233
}
188234

189235
.VPFeature.link:hover {
190236
border-color: var(--vp-c-brand-1) !important;
191237
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1),
192-
0 4px 8px rgba(0, 0, 0, 0.06);
238+
0 4px 8px rgba(0, 0, 0, 0.06);
193239
transform: translateY(-4px);
194240
}
195241

@@ -203,16 +249,14 @@
203249
width: 48px !important;
204250
height: 48px !important;
205251
border-radius: 12px !important;
206-
background: linear-gradient(
207-
135deg,
208-
var(--vp-c-brand-soft) 0%,
209-
var(--vp-c-indigo-soft) 100%
210-
) !important;
252+
background: linear-gradient(135deg,
253+
var(--vp-c-brand-soft) 0%,
254+
var(--vp-c-indigo-soft) 100%) !important;
211255
color: var(--vp-c-brand-1) !important;
212256
font-size: 22px !important;
213257
transition: background 0.39s ease,
214-
color 0.39s ease,
215-
transform 0.39s ease;
258+
color 0.39s ease,
259+
transform 0.39s ease;
216260
}
217261

218262
.VPFeature.link:hover .icon {
@@ -310,12 +354,12 @@
310354
background-color: var(--vp-c-bg-elv) !important;
311355
border-color: var(--vp-c-border) !important;
312356
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2),
313-
0 1px 2px rgba(0, 0, 0, 0.15);
357+
0 1px 2px rgba(0, 0, 0, 0.15);
314358
}
315359

316360
.dark .VPFeature.link:hover {
317361
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3),
318-
0 4px 8px rgba(0, 0, 0, 0.2);
362+
0 4px 8px rgba(0, 0, 0, 0.2);
319363
}
320364

321365
/* ── Responsive ──────────────────────────────────────────────── */
@@ -483,3 +527,111 @@
483527
.dark .vp-doc img {
484528
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
485529
}
530+
531+
/* ================================================================
532+
Mermaid Diagrams
533+
================================================================ */
534+
535+
.vp-doc .mermaid-diagram {
536+
margin: 1.5em 0;
537+
text-align: center;
538+
/* 小图居中;与 svg 的 max-width:100% 配合实现响应式 */
539+
overflow-x: hidden;
540+
/* 图已按比例缩放进容器宽度,永远不出现横向滚动条 */
541+
/* 关键:在容器层压低行高,隔离 .vp-doc 全局的 line-height:1.85(见文首)。
542+
mermaid 在 dagre 布局阶段按默认行高(约 1.2)预算每个节点的 foreignObject 高度,
543+
但渲染时标签的行高会从 .vp-doc 继承到 1.85,把三行文本撑得比节点框高,
544+
第三行(如 fetch_add / fetch_sub)就被 foreignObject 裁掉。
545+
这里在容器层降到 1.2,行高会级联进 mermaid 的 inline-block 包裹 div,
546+
渲染高度才能与 mermaid 的预算对齐。(只在 .nodeLabel 上设无效:行高由包裹 div 决定。)*/
547+
line-height: 1.2;
548+
}
549+
550+
.vp-doc .mermaid-diagram svg {
551+
display: inline-block;
552+
/* 配合容器 text-align:center 实现居中 */
553+
max-width: 100%;
554+
/* 响应式:超宽图按比例缩小到容器宽度,不再出现横向滚动条 */
555+
height: auto;
556+
/* 与 max-width 联动保持比例,连带高度同缩,绝不裁剪 */
557+
/* mermaid 渲染的 SVG 内部默认 overflow:hidden,多行/中文标签算宽不准时会被节点框裁掉;
558+
强制 visible,保证节点与边标签的文字永不遮挡 */
559+
overflow: visible !important;
560+
}
561+
562+
/*
563+
* 节点 / 边标签。
564+
* 行高真正生效的是上面 .mermaid-diagram 容器层那条 line-height:1.2(会级联进 mermaid
565+
* 的 inline-block 包裹 div);这里再设一次是双保险,并显式覆盖 mermaid 自身注入的样式。
566+
* white-space:nowrap:mermaid 的包裹 div 本就带 nowrap,这里显式钉死,保证行内 token
567+
* (如 fetch_add / fetch_sub)不会被节点框宽度误差折行。
568+
*/
569+
.vp-doc .mermaid-diagram .nodeLabel,
570+
.vp-doc .mermaid-diagram .edgeLabel {
571+
overflow: visible !important;
572+
white-space: nowrap;
573+
line-height: 1.1;
574+
}
575+
576+
/* 加载中:骨架占位,避免空白跳动 */
577+
.vp-doc .mermaid-diagram[data-rendered='false'] {
578+
min-height: 80px;
579+
align-items: center;
580+
background: linear-gradient(90deg,
581+
var(--vp-c-bg-soft) 25%,
582+
var(--vp-c-divider) 37%,
583+
var(--vp-c-bg-soft) 63%);
584+
background-size: 400% 100%;
585+
border-radius: 8px;
586+
animation: mermaid-shimmer 1.4s ease infinite;
587+
}
588+
589+
@keyframes mermaid-shimmer {
590+
0% {
591+
background-position: 100% 0;
592+
}
593+
594+
100% {
595+
background-position: 0 0;
596+
}
597+
}
598+
599+
@media (prefers-reduced-motion: reduce) {
600+
.vp-doc .mermaid-diagram[data-rendered='false'] {
601+
animation: none;
602+
}
603+
}
604+
605+
/* 渲染失败:把源码以代码块形式兜底显示 */
606+
.vp-doc .mermaid-diagram[data-rendered='error'] {
607+
display: block;
608+
}
609+
610+
.vp-doc .mermaid-error {
611+
margin: 0;
612+
padding: 12px 16px;
613+
text-align: left;
614+
font-family: var(--vp-font-family-mono);
615+
font-size: 0.78rem;
616+
line-height: 1.6;
617+
color: var(--vp-c-text-2);
618+
background: var(--vp-c-bg-soft);
619+
border: 1px solid var(--vp-c-divider);
620+
border-radius: 8px;
621+
overflow-x: auto;
622+
white-space: pre-wrap;
623+
word-break: break-word;
624+
}
625+
626+
/* 让本地搜索能索引到图表源码,但视觉上不可见 */
627+
.vp-doc .mermaid-search-text {
628+
position: absolute;
629+
width: 1px;
630+
height: 1px;
631+
padding: 0;
632+
margin: -1px;
633+
overflow: hidden;
634+
clip: rect(0, 0, 0, 0);
635+
white-space: nowrap;
636+
border: 0;
637+
}

site/.vitepress/theme/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import HomeRoadmap from './components/HomeRoadmap.vue'
55
import HomeMeta from './components/HomeMeta.vue'
66
import CardGrid from './components/CardGrid.vue'
77
import CardLink from './components/CardLink.vue'
8+
import { setupMermaid } from './mermaid-client'
89
import './custom.css'
910

1011
export default {
@@ -14,6 +15,9 @@ export default {
1415
'home-features-after': () => h(HomeRoadmap),
1516
})
1617
},
18+
setup() {
19+
setupMermaid()
20+
},
1721
enhanceApp({ app }) {
1822
app.component('HomeMeta', HomeMeta)
1923
app.component('CardGrid', CardGrid)

0 commit comments

Comments
 (0)