Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/spec/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ graph LR;
- `showLineNumbers` 顯示行號(預設隱藏):`​```python title="x.py" showLineNumbers {3}`。
- mermaid 於 **client 端動態載入渲染**(動態 import,只有含 mermaid 的頁面才載入;
build 時轉 SVG 為長期方向,內容大量撰寫前評估)。
- 圖表配色一律由 `src/scripts/content.ts` 依 tokens 套用,圖內不寫顏色。兩條約定:
邊標籤寫**捷運線名**(板南線、淡水信義線、松山新店線、中和新蘆線、文湖線、環狀線)該段自動上該線色;
`class 站名 dest` 把該站標成終點(深色實心)。

---

Expand Down
11 changes: 6 additions & 5 deletions src/content/articles/transportation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ category: life
tags: [行, 生活]
description: 從臺北車站到臺科怎麼走、YouBike 與共享運具怎麼用、機車停車位怎麼申請
order: 3
updated: 2026-07-27
updated: 2026-09-01
---

## 第一次來臺科
Expand All @@ -22,17 +22,18 @@ updated: 2026-07-27

::::tabs
:::tab[路線一:走西門]
**板南線** 臺北車站 → 西門, **松山新店線** → 公館
**板南線**(往頂埔方向)臺北車站 → 西門,站內轉 **松山新店線**(往新店方向)→ 公館
:::
:::tab[路線二:走中正紀念堂]
**淡水信義線** 臺北車站 → 中正紀念堂, **松山新店線** → 公館
**淡水信義線**(往象山方向)臺北車站 → 中正紀念堂,站內轉 **松山新店線**(往新店方向)→ 公館
:::
::::

```mermaid
graph LR;
臺北車站--板南線---西門--松山新店線---公館
臺北車站--淡水信義線---中正紀念堂--松山新店線---公館
臺北車站([臺北車站])--板南線---西門([西門])--松山新店線---公館([公館])
臺北車站--淡水信義線---中正紀念堂([中正紀念堂])--松山新店線---公館
class 公館 dest;
```

兩條路線時間差不多,看你在臺北車站站在哪個月台比較近就選哪條。
Expand Down
57 changes: 56 additions & 1 deletion src/scripts/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,68 @@ export function initYt() {
}
}

// Taipei metro line colors live in tokens.css; the edge label text is the line name
const MRT_LINES: Record<string, string> = {
板南線: '--mrt-bl',
淡水信義線: '--mrt-r',
松山新店線: '--mrt-g',
中和新蘆線: '--mrt-o',
文湖線: '--mrt-br',
環狀線: '--mrt-y',
};

// mermaid rules are scoped by svg id and outrank any stylesheet, so the palette rides in
// through themeCSS, which lands inside that scope; themeVariables would reject oklch() tokens
const MERMAID_CSS = `
.node rect, .node path, .node polygon, .node circle { fill: var(--paper); stroke: var(--line-strong); stroke-width: 2px; }
.node rect { rx: 8px; ry: 8px; }
.node .nodeLabel { color: var(--ink); font-weight: 650; }
.flowchart-link { stroke: var(--line-strong); }
.marker { fill: var(--line-strong); stroke: var(--line-strong); }
.edgeLabel, .edgeLabel p, .labelBkg { background: var(--surface); }
.edgeLabel p { font-size: var(--text-tiny); font-weight: 700; letter-spacing: 0.03em; }
.dest rect, .dest path { fill: var(--ink); stroke: var(--ink); }
.dest .nodeLabel { color: var(--paper); font-weight: 750; }
`;

export async function initMermaid() {
if (!document.querySelector('pre.mermaid')) return;
try {
const { default: mermaid } = await import('mermaid');
mermaid.initialize({ startOnLoad: false, theme: 'neutral', fontFamily: 'inherit' });
mermaid.initialize({
startOnLoad: false,
theme: 'neutral',
fontFamily: 'inherit',
themeCSS: MERMAID_CSS,
flowchart: { curve: 'basis', nodeSpacing: 34, rankSpacing: 92, padding: 12 },
});
await mermaid.run({ querySelector: 'pre.mermaid', suppressErrors: false });
paintMrtLines();
} catch (err) {
console.error('[mermaid] render failed:', err);
// the placeholder hides the source until mermaid swaps in the svg, so hand it back
// here: without this the block stays an empty box whenever rendering falls over
for (const pre of document.querySelectorAll('pre.mermaid:not([data-processed])')) {
pre.setAttribute('data-mermaid-failed', '');
}
}
}

function paintMrtLines() {
for (const svg of document.querySelectorAll('pre.mermaid svg')) {
// one label group per edge in edge order, empty ones included, so index pairs the two;
// a count mismatch means mermaid changed that and the pairing would color the wrong edge
const paths = svg.querySelectorAll<SVGPathElement>('.edgePaths > path');
const labels = svg.querySelectorAll<SVGGElement>('.edgeLabels > g');
if (paths.length !== labels.length) continue;
paths.forEach((path, i) => {
const varName = MRT_LINES[labels[i].textContent?.trim() ?? ''];
if (!varName) return;
const color = `var(${varName})`;
path.style.stroke = color; // inline: the color varies per edge, themeCSS above is static
path.style.strokeWidth = '5px';
const chip = labels[i].querySelector<HTMLElement>('p');
if (chip) chip.style.color = color;
});
}
}
19 changes: 17 additions & 2 deletions src/styles/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -818,14 +818,29 @@ html:not([data-dept='csie']) [data-dept-only='csie'] {
/* ---- Mermaid ---- */
pre.mermaid {
display: flex;
justify-content: center;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1.2rem;
padding: 1.5rem 1.2rem;
overflow-x: auto;
}

/* 窄螢幕寧可橫向捲動,等比縮到手機寬度會讓站名小到看不清;
置中用 auto margin,justify-content 會把溢出的左半邊裁掉且捲不回來 */
pre.mermaid svg {
min-width: 26rem;
margin-inline: auto;
}

/* 渲染前先佔位,不閃出原始碼。無 JS 不套用;渲染失敗時 content.ts 補上 data-mermaid-failed,
兩種情況都讓原始碼留著可讀,不會只剩一個空框 */
@media (scripting: enabled) {
pre.mermaid:not([data-processed]):not([data-mermaid-failed]) {
min-height: 10rem;
color: transparent;
}
}

/* ---- 註腳 ---- */
.md sup a {
text-decoration: none;
Expand Down
9 changes: 9 additions & 0 deletions src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@
--c-info: oklch(54.5% 0.125 55);
--c-misc: oklch(50% 0.055 60);

/* 北捷路線色(文內路線圖):四條與分類色同源,紅、黃另行校準到相同明度。
環狀線原色過亮,壓到 4.5:1 後偏橄欖金,可讀性優先於還原度 */
--mrt-bl: var(--c-life);
--mrt-r: oklch(52% 0.18 27);
--mrt-g: var(--c-course);
--mrt-o: var(--c-info);
--mrt-br: var(--c-misc);
--mrt-y: oklch(52.5% 0.11 108);

/* 語意色(callout) */
--clr-tip: var(--c-course);
--clr-info: var(--c-life);
Expand Down
Loading