@limcpf/everything-is-a-markdown 0.6.0 → 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 +38 -0
- package/README.md +38 -0
- package/package.json +1 -1
- package/src/build.ts +1 -0
- package/src/config.ts +50 -0
- package/src/markdown.ts +32 -0
- package/src/runtime/app.css +29 -0
- package/src/runtime/app.js +222 -0
- package/src/types.ts +15 -0
package/README.ko.md
CHANGED
|
@@ -78,6 +78,13 @@ const config = {
|
|
|
78
78
|
pathBase: "/blog",
|
|
79
79
|
defaultOgImage: "/assets/og.png",
|
|
80
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
|
+
},
|
|
81
88
|
};
|
|
82
89
|
|
|
83
90
|
export default config;
|
|
@@ -173,6 +180,37 @@ title: Work In Progress
|
|
|
173
180
|
---
|
|
174
181
|
```
|
|
175
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
|
+
|
|
176
214
|
## bunx 실행 (선택)
|
|
177
215
|
|
|
178
216
|
```bash
|
package/README.md
CHANGED
|
@@ -78,6 +78,13 @@ const config = {
|
|
|
78
78
|
pathBase: "/blog",
|
|
79
79
|
defaultOgImage: "/assets/og.png",
|
|
80
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
|
+
},
|
|
81
88
|
};
|
|
82
89
|
|
|
83
90
|
export default config;
|
|
@@ -173,6 +180,37 @@ title: Work In Progress
|
|
|
173
180
|
---
|
|
174
181
|
```
|
|
175
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
|
+
|
|
176
214
|
## bunx (Optional)
|
|
177
215
|
|
|
178
216
|
```bash
|
package/package.json
CHANGED
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, "&")
|
|
35
|
+
.replace(/</g, "<")
|
|
36
|
+
.replace(/>/g, ">")
|
|
37
|
+
.replace(/"/g, """)
|
|
38
|
+
.replace(/'/g, "'");
|
|
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, {
|
package/src/runtime/app.css
CHANGED
|
@@ -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;
|
package/src/runtime/app.js
CHANGED
|
@@ -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, ">");
|
|
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;
|