@limcpf/everything-is-a-markdown 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -42,6 +42,28 @@ bun run blog [build|dev|clean] [options]
42
42
  - `--recent-limit <n>`: Recent 폴더 문서 수 제한 (정수 `>= 1`, 기본 `5`)
43
43
  - `--port <n>`: 개발 서버 포트 (기본 `3000`)
44
44
 
45
+ ## Markdown Lint (publish 전용)
46
+
47
+ `publish: true` 문서만 대상으로 Markdown lint를 실행하고, 결과를 JSON 파일로 저장할 수 있습니다.
48
+ 내부적으로 `markdownlint` Node API를 사용합니다.
49
+
50
+ ```bash
51
+ bun run lint:md:publish -- --out-dir ./reports
52
+ ```
53
+
54
+ 엄격 모드(`--strict`)를 추가하면 위반이 있을 때 종료 코드 `1`을 반환합니다.
55
+
56
+ ```bash
57
+ bun run lint:md:publish -- --out-dir ./reports --strict
58
+ ```
59
+
60
+ 옵션:
61
+
62
+ - `--out-dir <path>`: 리포트 출력 디렉터리 (필수)
63
+ - `--strict`: 위반이 있으면 종료 코드 `1`
64
+ - `--vault <path>`: Markdown 루트 디렉터리 재지정 (선택)
65
+ - `--exclude <glob>`: 제외 패턴 추가 (반복 가능)
66
+
45
67
  ## 설정 파일 (`blog.config.ts`)
46
68
 
47
69
  SEO/UI/정적 파일 설정은 config 파일에서 관리할 수 있습니다.
@@ -56,6 +78,13 @@ const config = {
56
78
  pathBase: "/blog",
57
79
  defaultOgImage: "/assets/og.png",
58
80
  },
81
+ markdown: {
82
+ mermaid: {
83
+ enabled: true,
84
+ cdnUrl: "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js",
85
+ theme: "default",
86
+ },
87
+ },
59
88
  };
60
89
 
61
90
  export default config;
@@ -151,6 +180,37 @@ title: Work In Progress
151
180
  ---
152
181
  ```
153
182
 
183
+ ## Mermaid 다이어그램 지원
184
+
185
+ 코드 블록의 언어를 `mermaid`로 작성하면 브라우저에서 Mermaid 다이어그램으로 렌더링합니다.
186
+
187
+ ````md
188
+ ```mermaid
189
+ flowchart LR
190
+ A --> B
191
+ ```
192
+ ````
193
+
194
+ 문서 전환 시에도 해당 블록을 다시 렌더링합니다. 설정에서 Mermaid를 비활성화하면 소스 코드 블록 그대로 표시됩니다.
195
+
196
+ `blog.config.ts`에서 설정:
197
+
198
+ ```ts
199
+ markdown: {
200
+ mermaid: {
201
+ enabled: true, // false면 코드 블록만 표시
202
+ cdnUrl: "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js", // http/https 또는 /, ./, ../ 경로
203
+ theme: "default", // mermaid.initialize({ theme }) 값
204
+ },
205
+ },
206
+ ```
207
+
208
+ 유효성 검증 및 런타임 가드레일:
209
+
210
+ - `markdown.mermaid.cdnUrl`에 잘못된 값(예: `javascript:`)이 들어오면 빌드 시 기본 CDN URL로 자동 폴백합니다.
211
+ - `markdown.mermaid.theme`이 유효한 식별자 형식이 아니면 빌드 시 `default`로 자동 폴백합니다.
212
+ - 런타임 로더는 실패 후 남은 Mermaid 스크립트를 정리하고, 중복 삽입을 피하며, 다음 렌더에서 재시도합니다.
213
+
154
214
  ## bunx 실행 (선택)
155
215
 
156
216
  ```bash
package/README.md CHANGED
@@ -42,6 +42,28 @@ Common options:
42
42
  - `--recent-limit <n>`: Recent folder item limit (integer `>= 1`, default `5`)
43
43
  - `--port <n>`: Dev server port (default `3000`)
44
44
 
45
+ ## Markdown Lint (publish only)
46
+
47
+ You can run Markdown lint only for `publish: true` documents and save results as a JSON report.
48
+ Internally, this command uses the `markdownlint` Node API.
49
+
50
+ ```bash
51
+ bun run lint:md:publish -- --out-dir ./reports
52
+ ```
53
+
54
+ Add strict mode (`--strict`) to return exit code `1` when violations exist.
55
+
56
+ ```bash
57
+ bun run lint:md:publish -- --out-dir ./reports --strict
58
+ ```
59
+
60
+ Options:
61
+
62
+ - `--out-dir <path>`: Report output directory (required)
63
+ - `--strict`: Return exit code `1` when violations exist
64
+ - `--vault <path>`: Override Markdown root directory (optional)
65
+ - `--exclude <glob>`: Add exclude pattern (repeatable)
66
+
45
67
  ## Config File (`blog.config.ts`)
46
68
 
47
69
  Use a config file for SEO/UI/static assets.
@@ -56,6 +78,13 @@ const config = {
56
78
  pathBase: "/blog",
57
79
  defaultOgImage: "/assets/og.png",
58
80
  },
81
+ markdown: {
82
+ mermaid: {
83
+ enabled: true,
84
+ cdnUrl: "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js",
85
+ theme: "default",
86
+ },
87
+ },
59
88
  };
60
89
 
61
90
  export default config;
@@ -151,6 +180,37 @@ title: Work In Progress
151
180
  ---
152
181
  ```
153
182
 
183
+ ## Mermaid Diagram Support
184
+
185
+ This project supports Mermaid diagrams written as fenced code blocks:
186
+
187
+ ````
188
+ ```mermaid
189
+ flowchart LR
190
+ A --> B
191
+ ```
192
+ ````
193
+
194
+ Rendering happens in the browser on first load and when navigating to another document. If Mermaid is disabled in config or CDN loading fails, the source block is shown as-is and a warning message appears.
195
+
196
+ Configuration options (`blog.config.ts`):
197
+
198
+ ```ts
199
+ markdown: {
200
+ mermaid: {
201
+ enabled: true, // false to keep only source blocks
202
+ cdnUrl: "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js", // CDN URL (http/https or /, ./, ../)
203
+ theme: "default", // passed to mermaid.initialize({ theme })
204
+ },
205
+ },
206
+ ```
207
+
208
+ Validation and runtime guardrails:
209
+
210
+ - Invalid `markdown.mermaid.cdnUrl` values (for example `javascript:`) automatically fall back to the default CDN URL at build time.
211
+ - Invalid `markdown.mermaid.theme` values automatically fall back to `default` at build time.
212
+ - Runtime loader removes stale Mermaid script tags after failures, avoids duplicate injection, and retries cleanly on the next render.
213
+
154
214
  ## bunx (Optional)
155
215
 
156
216
  ```bash
package/bun.lock CHANGED
@@ -16,6 +16,7 @@
16
16
  "@types/markdown-it": "^14.1.2",
17
17
  "@types/picomatch": "^4.0.2",
18
18
  "bun-types": "^1.3.8",
19
+ "markdownlint": "^0.40.0",
19
20
  },
20
21
  },
21
22
  },
@@ -36,8 +37,12 @@
36
37
 
37
38
  "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
38
39
 
40
+ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
41
+
39
42
  "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
40
43
 
44
+ "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
45
+
41
46
  "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
42
47
 
43
48
  "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
@@ -46,6 +51,8 @@
46
51
 
47
52
  "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
48
53
 
54
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
55
+
49
56
  "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="],
50
57
 
51
58
  "@types/picomatch": ["@types/picomatch@4.0.2", "", {}, "sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA=="],
@@ -54,20 +61,32 @@
54
61
 
55
62
  "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
56
63
 
64
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
65
+
57
66
  "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
58
67
 
59
68
  "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
60
69
 
61
70
  "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
62
71
 
72
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
73
+
63
74
  "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
64
75
 
65
76
  "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
66
77
 
78
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
79
+
67
80
  "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
68
81
 
69
82
  "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
70
83
 
84
+ "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
85
+
86
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
87
+
88
+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
89
+
71
90
  "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
72
91
 
73
92
  "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
@@ -80,6 +99,8 @@
80
99
 
81
100
  "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
82
101
 
102
+ "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
103
+
83
104
  "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
84
105
 
85
106
  "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
@@ -88,34 +109,90 @@
88
109
 
89
110
  "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
90
111
 
112
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
113
+
114
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
115
+
116
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
117
+
91
118
  "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
92
119
 
120
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
121
+
93
122
  "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
94
123
 
124
+ "katex": ["katex@0.16.28", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg=="],
125
+
95
126
  "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
96
127
 
97
128
  "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
98
129
 
99
130
  "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
100
131
 
132
+ "markdownlint": ["markdownlint@0.40.0", "", { "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2", "string-width": "8.1.0" } }, "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg=="],
133
+
101
134
  "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
102
135
 
103
136
  "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
104
137
 
138
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
139
+
140
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
141
+
142
+ "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="],
143
+
144
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
145
+
146
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
147
+
148
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
149
+
150
+ "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
151
+
152
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
153
+
154
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
155
+
156
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
157
+
158
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
159
+
160
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
161
+
105
162
  "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
106
163
 
164
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
165
+
166
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
167
+
168
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
169
+
170
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
171
+
107
172
  "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
108
173
 
174
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
175
+
176
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
177
+
178
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
179
+
109
180
  "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
110
181
 
182
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
183
+
111
184
  "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
112
185
 
113
186
  "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
114
187
 
188
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
189
+
115
190
  "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
116
191
 
117
192
  "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
118
193
 
194
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
195
+
119
196
  "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
120
197
 
121
198
  "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
@@ -142,8 +219,12 @@
142
219
 
143
220
  "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
144
221
 
222
+ "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
223
+
145
224
  "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
146
225
 
226
+ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
227
+
147
228
  "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
148
229
 
149
230
  "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@@ -169,5 +250,7 @@
169
250
  "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
170
251
 
171
252
  "js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
253
+
254
+ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
172
255
  }
173
256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limcpf/everything-is-a-markdown",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "build": "bun run src/cli.ts build",
22
22
  "dev": "bun run src/cli.ts dev",
23
23
  "clean": "bun run src/cli.ts clean",
24
+ "lint:md:publish": "bun run scripts/lint-published-markdown.ts",
24
25
  "test:e2e": "playwright test",
25
26
  "test:e2e:focus-trap": "playwright test tests/e2e/mobile-sidebar-focus-trap.spec.ts"
26
27
  },
@@ -35,6 +36,7 @@
35
36
  "@playwright/test": "^1.58.2",
36
37
  "@types/markdown-it": "^14.1.2",
37
38
  "@types/picomatch": "^4.0.2",
38
- "bun-types": "^1.3.8"
39
+ "bun-types": "^1.3.8",
40
+ "markdownlint": "^0.40.0"
39
41
  }
40
42
  }
package/src/build.ts CHANGED
@@ -868,6 +868,7 @@ function buildManifest(docs: DocRecord[], tree: TreeNode[], options: BuildOption
868
868
  siteTitle: resolveSiteTitle(options),
869
869
  pathBase: options.seo?.pathBase ?? "",
870
870
  defaultBranch: DEFAULT_BRANCH,
871
+ mermaid: options.mermaid,
871
872
  branches,
872
873
  ui: {
873
874
  newWithinDays: options.newWithinDays,
package/src/config.ts CHANGED
@@ -25,8 +25,53 @@ const DEFAULTS = {
25
25
  imagePolicy: "omit-local" as const,
26
26
  gfm: true,
27
27
  shikiTheme: "github-dark",
28
+ mermaid: {
29
+ enabled: true,
30
+ cdnUrl: "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js",
31
+ theme: "default",
32
+ },
28
33
  };
29
34
 
35
+ const MERMAID_URL_MAX_LENGTH = 1024;
36
+ const MERMAID_THEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9._-]*$/;
37
+ const MERMAID_CDN_URL_PATTERN = /^(https?:\/\/|\/|\.{1,2}\/)[^\s"'><]+$/;
38
+
39
+ function normalizeMermaidEnabled(value: unknown): boolean {
40
+ return typeof value === "boolean" ? value : DEFAULTS.mermaid.enabled;
41
+ }
42
+
43
+ function normalizeMermaidCdnUrl(value: unknown): string {
44
+ if (typeof value !== "string") {
45
+ return DEFAULTS.mermaid.cdnUrl;
46
+ }
47
+
48
+ const normalized = value.trim();
49
+ if (!normalized) {
50
+ return DEFAULTS.mermaid.cdnUrl;
51
+ }
52
+
53
+ if (normalized.length > MERMAID_URL_MAX_LENGTH || !MERMAID_CDN_URL_PATTERN.test(normalized)) {
54
+ console.warn(`[config] invalid mermaid.cdnUrl: ${JSON.stringify(value)}. fallback to default ${JSON.stringify(DEFAULTS.mermaid.cdnUrl)}.`);
55
+ return DEFAULTS.mermaid.cdnUrl;
56
+ }
57
+
58
+ return normalized;
59
+ }
60
+
61
+ function normalizeMermaidTheme(value: unknown): string {
62
+ if (typeof value !== "string") {
63
+ return DEFAULTS.mermaid.theme;
64
+ }
65
+
66
+ const normalized = value.trim();
67
+ if (!MERMAID_THEME_PATTERN.test(normalized)) {
68
+ console.warn(`[config] invalid mermaid.theme: ${JSON.stringify(value)}. fallback to default ${JSON.stringify(DEFAULTS.mermaid.theme)}.`);
69
+ return DEFAULTS.mermaid.theme;
70
+ }
71
+
72
+ return normalized;
73
+ }
74
+
30
75
  export function parseCliArgs(argv: string[]): CliArgs {
31
76
  const [first] = argv;
32
77
  const command = first === "build" || first === "dev" || first === "clean" ? first : "build";
@@ -250,6 +295,11 @@ export function resolveBuildOptions(
250
295
  imagePolicy: userConfig.markdown?.images ?? DEFAULTS.imagePolicy,
251
296
  gfm: userConfig.markdown?.gfm ?? DEFAULTS.gfm,
252
297
  shikiTheme: userConfig.markdown?.highlight?.theme ?? DEFAULTS.shikiTheme,
298
+ mermaid: {
299
+ enabled: normalizeMermaidEnabled(userConfig.markdown?.mermaid?.enabled),
300
+ cdnUrl: normalizeMermaidCdnUrl(userConfig.markdown?.mermaid?.cdnUrl),
301
+ theme: normalizeMermaidTheme(userConfig.markdown?.mermaid?.theme),
302
+ },
253
303
  seo,
254
304
  };
255
305
  }
package/src/markdown.ts CHANGED
@@ -16,6 +16,7 @@ export interface MarkdownRenderer {
16
16
  }
17
17
 
18
18
  const FENCE_LANG_RE = /^```([\w-+#.]+)/gm;
19
+ const MERMAID_LANG = "mermaid";
19
20
  type RenderRule = NonNullable<MarkdownIt["renderer"]["rules"]["fence"]>;
20
21
  type RenderRuleArgs = Parameters<RenderRule>;
21
22
  type RuleTokens = RenderRuleArgs[0];
@@ -28,6 +29,15 @@ function escapeMarkdownLabel(input: string): string {
28
29
  return input.replace(/[\[\]]/g, "");
29
30
  }
30
31
 
32
+ function escapeHtmlText(input: string): string {
33
+ return String(input)
34
+ .replace(/&/g, "&amp;")
35
+ .replace(/</g, "&lt;")
36
+ .replace(/>/g, "&gt;")
37
+ .replace(/"/g, "&quot;")
38
+ .replace(/'/g, "&#39;");
39
+ }
40
+
31
41
  function parseWikiInner(inner: string): { target: string; label?: string } {
32
42
  const [target, label] = inner.split("|").map((part) => part.trim());
33
43
  return { target, label: label || undefined };
@@ -107,6 +117,9 @@ async function loadFenceLanguages<L extends string, T extends string>(
107
117
  break;
108
118
  }
109
119
  if (match[1]) {
120
+ if (match[1].toLowerCase() === MERMAID_LANG) {
121
+ continue;
122
+ }
110
123
  langs.add(match[1].toLowerCase());
111
124
  }
112
125
  }
@@ -152,6 +165,25 @@ function createMarkdownIt<L extends string, T extends string>(
152
165
  const lang = parts[0]?.toLowerCase() || "text";
153
166
  const fileName = parts.slice(1).join(" ") || null;
154
167
 
168
+ if (lang === MERMAID_LANG) {
169
+ const source = escapeHtmlText(token.content);
170
+ const filename = fileName ? escapeHtmlAttr(fileName) : MERMAID_LANG;
171
+ return `<div class="code-block mermaid-block">
172
+ <div class="code-header">
173
+ <div class="code-dots">
174
+ <span class="dot dot-red"></span>
175
+ <span class="dot dot-yellow"></span>
176
+ <span class="dot dot-green"></span>
177
+ </div>
178
+ <span class="code-filename">${filename}</span>
179
+ <button class="code-copy" title="Copy code" data-code="${escapeHtmlAttr(token.content)}">
180
+ <span class="material-symbols-outlined">content_copy</span>
181
+ </button>
182
+ </div>
183
+ <pre class="mermaid">${source}</pre>
184
+ </div>`;
185
+ }
186
+
155
187
  let codeHtml: string;
156
188
  try {
157
189
  codeHtml = highlighter.codeToHtml(token.content, {
@@ -1098,6 +1098,35 @@ body.mobile-toggle-left .mobile-menu-toggle {
1098
1098
  tab-size: 2;
1099
1099
  }
1100
1100
 
1101
+ .viewer-content .code-block pre.mermaid {
1102
+ margin: 0;
1103
+ border: none;
1104
+ border-radius: 0;
1105
+ padding: 0;
1106
+ background: transparent !important;
1107
+ color: inherit;
1108
+ white-space: pre-wrap;
1109
+ word-break: break-word;
1110
+ }
1111
+
1112
+ .viewer-content .code-block pre.mermaid svg {
1113
+ width: 100%;
1114
+ height: auto;
1115
+ display: block;
1116
+ }
1117
+
1118
+ .viewer-content .mermaid-render-error {
1119
+ margin: 0;
1120
+ padding: 10px 16px;
1121
+ border: 1px solid rgba(244, 63, 94, 0.35);
1122
+ border-radius: 0 0 8px 8px;
1123
+ border-top: none;
1124
+ color: var(--latte-red);
1125
+ background: rgba(244, 63, 94, 0.08);
1126
+ font-size: 0.84rem;
1127
+ line-height: 1.45;
1128
+ }
1129
+
1101
1130
  .viewer-content pre {
1102
1131
  position: relative;
1103
1132
  margin: 1.5rem 0;
@@ -15,6 +15,19 @@ const DEFAULT_SITE_TITLE = "File-System Blog";
15
15
  const BRANCH_KEY = "fsblog.branch";
16
16
  const FOCUSABLE_SELECTOR =
17
17
  'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
18
+ const MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js";
19
+ const MERMAID_DEFAULT_THEME = "default";
20
+ const MERMAID_SELECTOR = "pre.mermaid";
21
+ const MERMAID_ERROR_CLASS = "mermaid-render-error";
22
+ const MERMAID_THEME_VALIDATION_RE = /^[a-zA-Z][a-zA-Z0-9._-]*$/;
23
+ const MERMAID_URL_VALIDATION_RE = /^(https?:\/\/|\/|\.{1,2}\/)[^\s"'<>]+$/;
24
+ const mermaidRuntime = {
25
+ initialized: false,
26
+ loadingPromise: null,
27
+ scriptElement: null,
28
+ lastCdnUrl: "",
29
+ lastTheme: "",
30
+ };
18
31
 
19
32
  function escapeHtmlAttr(input) {
20
33
  return String(input)
@@ -24,6 +37,212 @@ function escapeHtmlAttr(input) {
24
37
  .replace(/>/g, "&gt;");
25
38
  }
26
39
 
40
+ function resolveMermaidConfig(manifest) {
41
+ const mermaid = manifest?.mermaid;
42
+ return {
43
+ enabled: mermaid?.enabled !== false,
44
+ cdnUrl:
45
+ typeof mermaid?.cdnUrl === "string" && mermaid.cdnUrl.trim()
46
+ ? mermaid.cdnUrl.trim()
47
+ : MERMAID_CDN,
48
+ theme:
49
+ typeof mermaid?.theme === "string" &&
50
+ MERMAID_THEME_VALIDATION_RE.test(mermaid.theme.trim())
51
+ ? mermaid.theme.trim()
52
+ : MERMAID_DEFAULT_THEME,
53
+ };
54
+ }
55
+
56
+ function toAbsoluteUrl(value) {
57
+ try {
58
+ return new URL(value, window.location.href).href;
59
+ } catch {
60
+ return value;
61
+ }
62
+ }
63
+
64
+ function normalizeMermaidTheme(value) {
65
+ const normalized = typeof value === "string" ? value.trim() : "";
66
+ if (!normalized || !MERMAID_THEME_VALIDATION_RE.test(normalized)) {
67
+ return MERMAID_DEFAULT_THEME;
68
+ }
69
+ return normalized;
70
+ }
71
+
72
+ function normalizeMermaidUrl(value) {
73
+ const normalized = typeof value === "string" ? value.trim() : "";
74
+ if (!normalized || !MERMAID_URL_VALIDATION_RE.test(normalized)) {
75
+ return MERMAID_CDN;
76
+ }
77
+ return normalized;
78
+ }
79
+
80
+ function createMermaidLoadError(message) {
81
+ const paragraph = document.createElement("p");
82
+ paragraph.className = MERMAID_ERROR_CLASS;
83
+ paragraph.textContent = message;
84
+ return paragraph;
85
+ }
86
+
87
+ function removeMermaidErrorMessage(container) {
88
+ if (!(container instanceof HTMLElement)) {
89
+ return;
90
+ }
91
+
92
+ for (const message of container.querySelectorAll(`.${MERMAID_ERROR_CLASS}`)) {
93
+ message.remove();
94
+ }
95
+ }
96
+
97
+ function showMermaidError(preview, message) {
98
+ if (!(preview instanceof HTMLElement) || !(preview.parentElement instanceof HTMLElement)) {
99
+ return;
100
+ }
101
+
102
+ removeMermaidErrorMessage(preview.parentElement);
103
+ preview.parentElement.appendChild(createMermaidLoadError(message));
104
+ }
105
+
106
+ function parseMermaidNodes() {
107
+ const contentEl = document.getElementById("viewer-content");
108
+ if (!(contentEl instanceof HTMLElement)) {
109
+ return [];
110
+ }
111
+
112
+ return Array.from(contentEl.querySelectorAll(MERMAID_SELECTOR));
113
+ }
114
+
115
+ function resetMermaidNodes(nodes) {
116
+ for (const node of nodes) {
117
+ node.removeAttribute("data-mermaid-rendered");
118
+ if (node.parentElement instanceof HTMLElement) {
119
+ removeMermaidErrorMessage(node.parentElement);
120
+ }
121
+ }
122
+ }
123
+
124
+ async function loadMermaidLibrary(config) {
125
+ if (!config.enabled) {
126
+ return null;
127
+ }
128
+
129
+ const normalized = {
130
+ ...config,
131
+ theme: normalizeMermaidTheme(config.theme),
132
+ cdnUrl: normalizeMermaidUrl(config.cdnUrl),
133
+ };
134
+
135
+ if (window.mermaid) {
136
+ if (!mermaidRuntime.initialized || mermaidRuntime.lastTheme !== normalized.theme) {
137
+ window.mermaid.initialize({
138
+ startOnLoad: false,
139
+ theme: normalized.theme,
140
+ });
141
+ mermaidRuntime.initialized = true;
142
+ mermaidRuntime.lastTheme = normalized.theme;
143
+ }
144
+ return window.mermaid;
145
+ }
146
+
147
+ if (mermaidRuntime.loadingPromise) {
148
+ return mermaidRuntime.loadingPromise;
149
+ }
150
+
151
+ const expectedAbsoluteUrl = toAbsoluteUrl(normalized.cdnUrl);
152
+ const existingScript = document.getElementById("mermaid-runtime");
153
+ if (existingScript instanceof HTMLScriptElement) {
154
+ // 이전 로드가 비정상 종료된 스크립트 잔존을 막기 위해 재시도 전에 정리한다.
155
+ existingScript.remove();
156
+ mermaidRuntime.scriptElement = null;
157
+ mermaidRuntime.initialized = false;
158
+ mermaidRuntime.lastTheme = "";
159
+ mermaidRuntime.lastCdnUrl = "";
160
+ }
161
+
162
+ mermaidRuntime.loadingPromise = new Promise((resolve, reject) => {
163
+ let script = mermaidRuntime.scriptElement;
164
+ if (!(script instanceof HTMLScriptElement)) {
165
+ script = document.createElement("script");
166
+ script.id = "mermaid-runtime";
167
+ script.src = normalized.cdnUrl;
168
+ script.async = true;
169
+ script.crossOrigin = "anonymous";
170
+ mermaidRuntime.scriptElement = script;
171
+ mermaidRuntime.lastCdnUrl = expectedAbsoluteUrl;
172
+ }
173
+
174
+ const finalize = (error) => {
175
+ mermaidRuntime.loadingPromise = null;
176
+ if (error) {
177
+ if (mermaidRuntime.scriptElement instanceof HTMLScriptElement) {
178
+ mermaidRuntime.scriptElement.remove();
179
+ mermaidRuntime.scriptElement = null;
180
+ }
181
+ mermaidRuntime.initialized = false;
182
+ mermaidRuntime.lastTheme = "";
183
+ mermaidRuntime.lastCdnUrl = "";
184
+ reject(error);
185
+ return;
186
+ }
187
+ resolve(window.mermaid ?? null);
188
+ };
189
+
190
+ script.addEventListener("load", () => {
191
+ if (window.mermaid && (!mermaidRuntime.initialized || mermaidRuntime.lastTheme !== normalized.theme)) {
192
+ window.mermaid.initialize({
193
+ startOnLoad: false,
194
+ theme: normalized.theme,
195
+ });
196
+ mermaidRuntime.initialized = true;
197
+ mermaidRuntime.lastTheme = normalized.theme;
198
+ }
199
+ finalize();
200
+ });
201
+ script.addEventListener("error", () => {
202
+ finalize(new Error(`Mermaid 라이브러리 로드 실패: ${normalized.cdnUrl}`));
203
+ });
204
+
205
+ if (!script.isConnected) {
206
+ document.head.appendChild(script);
207
+ }
208
+ });
209
+
210
+ return mermaidRuntime.loadingPromise;
211
+ }
212
+
213
+ async function renderMermaidBlocks(config) {
214
+ const blocks = parseMermaidNodes();
215
+ if (blocks.length === 0) {
216
+ return;
217
+ }
218
+
219
+ resetMermaidNodes(blocks);
220
+
221
+ try {
222
+ const mermaid = await loadMermaidLibrary(config);
223
+ if (!mermaid) {
224
+ for (const block of blocks) {
225
+ showMermaidError(block, "Mermaid 렌더링이 비활성화되어 코드 블록을 그대로 표시합니다.");
226
+ }
227
+ return;
228
+ }
229
+ if (typeof mermaid.run === "function") {
230
+ await mermaid.run({ nodes: blocks });
231
+ return;
232
+ }
233
+ if (typeof mermaid.init === "function") {
234
+ await mermaid.init({ startOnLoad: false }, blocks);
235
+ return;
236
+ }
237
+ throw new Error("Mermaid 렌더러 API가 존재하지 않습니다.");
238
+ } catch (error) {
239
+ const message = `Mermaid 렌더링 실패: ${error instanceof Error ? error.message : String(error)}`;
240
+ for (const block of blocks) {
241
+ showMermaidError(block, message);
242
+ }
243
+ }
244
+ }
245
+
27
246
  function toSafeUrlPath(input) {
28
247
  const value = String(input);
29
248
  return value
@@ -1253,6 +1472,7 @@ async function start() {
1253
1472
  }
1254
1473
  manifest = await manifestRes.json();
1255
1474
  }
1475
+ const mermaidConfig = resolveMermaidConfig(manifest);
1256
1476
  const pathBase = normalizePathBase(manifest.pathBase);
1257
1477
  const siteTitle = resolveSiteTitle(manifest);
1258
1478
  const defaultBranch = normalizeBranch(manifest.defaultBranch) || DEFAULT_BRANCH;
@@ -1445,6 +1665,7 @@ async function start() {
1445
1665
  metaEl.innerHTML = renderMeta(doc);
1446
1666
  updateBacklinks(doc);
1447
1667
  navEl.innerHTML = renderNav(view.docs, view.docIndexById, id, pathBase);
1668
+ await renderMermaidBlocks(mermaidConfig);
1448
1669
  document.title = composeDocumentTitle(doc.title, siteTitle);
1449
1670
  if (viewerEl instanceof HTMLElement) {
1450
1671
  viewerEl.scrollTo(0, 0);
@@ -1470,6 +1691,7 @@ async function start() {
1470
1691
 
1471
1692
  updateBacklinks(doc);
1472
1693
  navEl.innerHTML = renderNav(view.docs, view.docIndexById, id, pathBase);
1694
+ await renderMermaidBlocks(mermaidConfig);
1473
1695
 
1474
1696
  document.title = composeDocumentTitle(doc.title, siteTitle);
1475
1697
  if (viewerEl instanceof HTMLElement) {
package/src/types.ts CHANGED
@@ -56,6 +56,11 @@ export interface UserConfig {
56
56
  engine?: "shiki";
57
57
  theme?: string;
58
58
  };
59
+ mermaid?: {
60
+ enabled?: boolean;
61
+ cdnUrl?: string;
62
+ theme?: string;
63
+ };
59
64
  };
60
65
  seo?: UserSeoConfig;
61
66
  }
@@ -73,6 +78,11 @@ export interface BuildOptions {
73
78
  imagePolicy: ImagePolicy;
74
79
  gfm: boolean;
75
80
  shikiTheme: string;
81
+ mermaid: {
82
+ enabled: boolean;
83
+ cdnUrl: string;
84
+ theme: string;
85
+ };
76
86
  seo: BuildSeoOptions | null;
77
87
  }
78
88
 
@@ -130,6 +140,11 @@ export interface Manifest {
130
140
  pathBase: string;
131
141
  defaultBranch: string;
132
142
  branches: string[];
143
+ mermaid: {
144
+ enabled: boolean;
145
+ cdnUrl: string;
146
+ theme: string;
147
+ };
133
148
  ui: {
134
149
  newWithinDays: number;
135
150
  recentLimit: number;