@limcpf/everything-is-a-markdown 0.6.7 → 0.6.8
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 +16 -1
- package/README.md +19 -7
- package/package.json +1 -1
- package/src/build.ts +57 -6
- package/src/config.ts +30 -10
- package/src/types.ts +7 -2
package/README.ko.md
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
언어: [English](README.md) | **한국어**
|
|
4
4
|
|
|
5
5
|
Everything-Is-A-Markdown은 로컬 Markdown 볼트를 정적 웹사이트로 빌드해, 폴더/파일 탐색 구조를 유지한 채 공개할 수 있게 해주는 CLI 도구입니다.
|
|
6
|
+
공개 URL은 `prefix` 기준으로 생성되고, 사이드바 폴더 구조는 실제 파일 경로가 아니라 `category_path` 기준으로 만들어집니다.
|
|
6
7
|
|
|
7
8
|
## 이 앱은 무엇을 하나요
|
|
8
9
|
|
|
9
10
|
- Markdown 볼트에서 정적 문서/블로그 사이트를 생성
|
|
10
11
|
- `publish: true` 문서만 선택적으로 공개
|
|
12
|
+
- 공개 문서는 `prefix`와 `category_path`를 필수로 사용
|
|
11
13
|
- 비공개 노트와 공개 콘텐츠 분리
|
|
12
14
|
|
|
13
15
|
## Obsidian 사용자에게 특히 잘 맞습니다
|
|
@@ -73,6 +75,10 @@ const config = {
|
|
|
73
75
|
vaultDir: "./vault",
|
|
74
76
|
outDir: "./dist",
|
|
75
77
|
staticPaths: ["assets", "public/favicon.ico"],
|
|
78
|
+
pinnedMenu: {
|
|
79
|
+
label: "NOTICE",
|
|
80
|
+
categoryPath: "announcements",
|
|
81
|
+
},
|
|
76
82
|
seo: {
|
|
77
83
|
siteUrl: "https://example.com",
|
|
78
84
|
pathBase: "/blog",
|
|
@@ -97,6 +103,12 @@ export default config;
|
|
|
97
103
|
- 지정한 경로의 파일들을 `dist`에 같은 상대 경로로 복사
|
|
98
104
|
- 예: 볼트 `assets/og.png` -> `dist/assets/og.png`
|
|
99
105
|
|
|
106
|
+
`pinnedMenu`:
|
|
107
|
+
|
|
108
|
+
- `categoryPath`: 문서 frontmatter `category_path` prefix 기준으로 가상 폴더를 구성
|
|
109
|
+
- `sourceDir`: 기존 파일 경로 prefix 기준
|
|
110
|
+
- 둘 다 있으면 `categoryPath`가 우선
|
|
111
|
+
|
|
100
112
|
`seo.pathBase`:
|
|
101
113
|
|
|
102
114
|
- 서브패스 배포(예: `/blog`)를 정식 지원합니다.
|
|
@@ -123,7 +135,9 @@ bun run build -- --vault ./test-vault --out ./dist
|
|
|
123
135
|
이 값이 `true`인 문서만 빌드 결과에 포함됩니다.
|
|
124
136
|
- `prefix: "A-01"`
|
|
125
137
|
문서의 공개 식별자이자 라우트(`/A-01/`) 기준입니다.
|
|
126
|
-
|
|
138
|
+
- `category_path: "engineering/blog/frontend"`
|
|
139
|
+
사이드바 폴더 구조 기준입니다.
|
|
140
|
+
`publish: true`인데 `prefix`나 `category_path`가 없으면 빌드 경고를 출력하고 문서를 제외합니다.
|
|
127
141
|
|
|
128
142
|
선택:
|
|
129
143
|
|
|
@@ -150,6 +164,7 @@ bun run build -- --vault ./test-vault --out ./dist
|
|
|
150
164
|
---
|
|
151
165
|
publish: true
|
|
152
166
|
prefix: "DEV-01"
|
|
167
|
+
category_path: "guides/setup"
|
|
153
168
|
branch: dev
|
|
154
169
|
title: Setup Guide
|
|
155
170
|
date: "2024-09-15"
|
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ The generated site keeps a two-panel experience:
|
|
|
13
13
|
|
|
14
14
|
- Builds a static site from a local Markdown vault
|
|
15
15
|
- Publishes only notes with `publish: true`
|
|
16
|
-
- Requires
|
|
16
|
+
- Requires `prefix` and `category_path` for every published note
|
|
17
|
+
- Uses `prefix` as the public route and `category_path` as the sidebar folder path
|
|
17
18
|
- Supports Obsidian-style wikilinks such as `[[note]]` and `[[note|label]]`
|
|
18
19
|
- Renders code blocks with Shiki
|
|
19
20
|
- Renders Mermaid blocks in the browser with runtime fallback handling
|
|
@@ -25,7 +26,7 @@ The generated site keeps a two-panel experience:
|
|
|
25
26
|
|
|
26
27
|
## Important Behavior
|
|
27
28
|
|
|
28
|
-
This project currently uses `prefix`-based public routes, not vault-relative path routes.
|
|
29
|
+
This project currently uses `prefix`-based public routes, not vault-relative path routes. Sidebar folders are built from `category_path`, not from the actual file location.
|
|
29
30
|
|
|
30
31
|
Example:
|
|
31
32
|
|
|
@@ -125,8 +126,9 @@ Only documents with `publish: true` are considered for output.
|
|
|
125
126
|
|
|
126
127
|
- `publish: true`
|
|
127
128
|
- `prefix: "BC-VO-02"`
|
|
129
|
+
- `category_path: "engineering/blog/frontend"`
|
|
128
130
|
|
|
129
|
-
If `publish: true` is set but `prefix` is missing, the note is skipped and a build warning is emitted.
|
|
131
|
+
If `publish: true` is set but `prefix` or `category_path` is missing, the note is skipped and a build warning is emitted.
|
|
130
132
|
|
|
131
133
|
### Supported fields
|
|
132
134
|
|
|
@@ -144,6 +146,7 @@ Example:
|
|
|
144
146
|
---
|
|
145
147
|
publish: true
|
|
146
148
|
prefix: BC-VO-02
|
|
149
|
+
category_path: engineering/blog/frontend
|
|
147
150
|
branch: dev
|
|
148
151
|
title: Setup Guide
|
|
149
152
|
date: "2024-09-15"
|
|
@@ -157,6 +160,8 @@ tags: ["tutorial", "setup"]
|
|
|
157
160
|
|
|
158
161
|
Public routes are derived from `prefix`, not from the file path.
|
|
159
162
|
|
|
163
|
+
Sidebar folders are derived from `category_path`, not from the file path.
|
|
164
|
+
|
|
160
165
|
Normalization rules:
|
|
161
166
|
|
|
162
167
|
- trims whitespace
|
|
@@ -225,7 +230,7 @@ export default {
|
|
|
225
230
|
staticPaths: ["assets", "public/favicon.ico"],
|
|
226
231
|
pinnedMenu: {
|
|
227
232
|
label: "NOTICE",
|
|
228
|
-
|
|
233
|
+
categoryPath: "announcements",
|
|
229
234
|
},
|
|
230
235
|
ui: {
|
|
231
236
|
newWithinDays: 7,
|
|
@@ -269,6 +274,8 @@ export default {
|
|
|
269
274
|
- `exclude`: extra exclude globs.
|
|
270
275
|
- `staticPaths`: vault-relative files or directories copied into output.
|
|
271
276
|
- `pinnedMenu`: optional virtual folder shown above `Recent`.
|
|
277
|
+
- `pinnedMenu.sourceDir`: optional legacy file-path prefix matcher.
|
|
278
|
+
- `pinnedMenu.categoryPath`: optional category-path prefix matcher for the sidebar virtual folder.
|
|
272
279
|
- `ui.newWithinDays`: threshold for NEW badge.
|
|
273
280
|
- `ui.recentLimit`: number of items in `Recent`.
|
|
274
281
|
- `markdown.wikilinks`: enable or disable wikilink resolution.
|
|
@@ -287,14 +294,19 @@ export default {
|
|
|
287
294
|
|
|
288
295
|
### `pinnedMenu`
|
|
289
296
|
|
|
290
|
-
`pinnedMenu` creates a virtual folder at the top of the sidebar by collecting published docs
|
|
297
|
+
`pinnedMenu` creates a virtual folder at the top of the sidebar by collecting published docs that match either:
|
|
298
|
+
|
|
299
|
+
- `categoryPath`: the document `category_path` equals or starts with the configured prefix
|
|
300
|
+
- `sourceDir`: the vault-relative file path starts with the configured prefix
|
|
301
|
+
|
|
302
|
+
If both are present, `categoryPath` wins.
|
|
291
303
|
|
|
292
304
|
Example:
|
|
293
305
|
|
|
294
306
|
```ts
|
|
295
307
|
pinnedMenu: {
|
|
296
308
|
label: "NOTICE",
|
|
297
|
-
|
|
309
|
+
categoryPath: "announcements",
|
|
298
310
|
}
|
|
299
311
|
```
|
|
300
312
|
|
|
@@ -310,7 +322,7 @@ JSON shape:
|
|
|
310
322
|
{
|
|
311
323
|
"pinnedMenu": {
|
|
312
324
|
"label": "NOTICE",
|
|
313
|
-
"
|
|
325
|
+
"categoryPath": "announcements"
|
|
314
326
|
}
|
|
315
327
|
}
|
|
316
328
|
```
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
toDocId,
|
|
22
22
|
} from "./utils";
|
|
23
23
|
|
|
24
|
-
const CACHE_VERSION =
|
|
24
|
+
const CACHE_VERSION = 4;
|
|
25
25
|
const CACHE_DIR_NAME = ".cache";
|
|
26
26
|
const CACHE_FILE_NAME = "build-index.json";
|
|
27
27
|
const DEFAULT_BRANCH = "dev";
|
|
@@ -125,6 +125,7 @@ function normalizeCachedSourceEntry(value: unknown): CachedSourceEntry | null {
|
|
|
125
125
|
const draft = value.draft === true;
|
|
126
126
|
const title = typeof value.title === "string" && value.title.trim().length > 0 ? value.title.trim() : undefined;
|
|
127
127
|
const prefix = typeof value.prefix === "string" && value.prefix.trim().length > 0 ? value.prefix.trim() : undefined;
|
|
128
|
+
const categoryPath = normalizeCategoryPath(value.categoryPath);
|
|
128
129
|
const date = typeof value.date === "string" && value.date.trim().length > 0 ? value.date.trim() : undefined;
|
|
129
130
|
const updatedDate =
|
|
130
131
|
typeof value.updatedDate === "string" && value.updatedDate.trim().length > 0 ? value.updatedDate.trim() : undefined;
|
|
@@ -147,6 +148,7 @@ function normalizeCachedSourceEntry(value: unknown): CachedSourceEntry | null {
|
|
|
147
148
|
draft,
|
|
148
149
|
title,
|
|
149
150
|
prefix,
|
|
151
|
+
categoryPath,
|
|
150
152
|
date,
|
|
151
153
|
updatedDate,
|
|
152
154
|
description,
|
|
@@ -276,6 +278,24 @@ function normalizeFrontmatterDate(value: unknown): string | null {
|
|
|
276
278
|
return null;
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
function normalizeCategoryPath(value: unknown): string | undefined {
|
|
282
|
+
if (typeof value !== "string") {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const normalized = value
|
|
287
|
+
.trim()
|
|
288
|
+
.replace(/\\/g, "/")
|
|
289
|
+
.replace(/^\/+/, "")
|
|
290
|
+
.replace(/\/+$/, "")
|
|
291
|
+
.split("/")
|
|
292
|
+
.map((segment) => segment.trim())
|
|
293
|
+
.filter(Boolean)
|
|
294
|
+
.join("/");
|
|
295
|
+
|
|
296
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
279
299
|
function extractFrontmatterScalar(raw: string, key: string): string | null {
|
|
280
300
|
const frontmatterMatch = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
281
301
|
if (!frontmatterMatch) {
|
|
@@ -353,6 +373,15 @@ function pickDocPrefix(frontmatter: Record<string, unknown>, raw: string): strin
|
|
|
353
373
|
return undefined;
|
|
354
374
|
}
|
|
355
375
|
|
|
376
|
+
function pickDocCategoryPath(frontmatter: Record<string, unknown>, raw: string): string | undefined {
|
|
377
|
+
const literal = normalizeCategoryPath(extractFrontmatterScalar(raw, "category_path"));
|
|
378
|
+
if (literal) {
|
|
379
|
+
return literal;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return normalizeCategoryPath(frontmatter.category_path);
|
|
383
|
+
}
|
|
384
|
+
|
|
356
385
|
function appendRouteSuffix(route: string, suffix: string): string {
|
|
357
386
|
const clean = route.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
358
387
|
if (!clean) {
|
|
@@ -470,6 +499,7 @@ function toCachedSourceEntry(raw: string, parsed: matter.GrayMatterFile<string>)
|
|
|
470
499
|
draft: parsed.data.draft === true,
|
|
471
500
|
title: typeof parsed.data.title === "string" && parsed.data.title.trim().length > 0 ? parsed.data.title.trim() : undefined,
|
|
472
501
|
prefix: pickDocPrefix(parsed.data as Record<string, unknown>, raw),
|
|
502
|
+
categoryPath: pickDocCategoryPath(parsed.data as Record<string, unknown>, raw),
|
|
473
503
|
date: pickDocDate(parsed.data as Record<string, unknown>, raw),
|
|
474
504
|
updatedDate: pickDocUpdatedDate(parsed.data as Record<string, unknown>, raw),
|
|
475
505
|
description: typeof parsed.data.description === "string" ? parsed.data.description.trim() || undefined : undefined,
|
|
@@ -500,6 +530,7 @@ function toDocRecord(
|
|
|
500
530
|
fileName,
|
|
501
531
|
title: entry.title ?? makeTitleFromFileName(fileName),
|
|
502
532
|
prefix: entry.prefix,
|
|
533
|
+
categoryPath: entry.categoryPath ?? "",
|
|
503
534
|
date: entry.date,
|
|
504
535
|
updatedDate: entry.updatedDate,
|
|
505
536
|
description: entry.description,
|
|
@@ -564,6 +595,12 @@ async function readPublishedDocs(options: BuildOptions, previousSources: BuildCa
|
|
|
564
595
|
console.warn(`[publish] Skipped published doc without prefix: ${relPath}`);
|
|
565
596
|
continue;
|
|
566
597
|
}
|
|
598
|
+
|
|
599
|
+
if (!completeEntry.categoryPath) {
|
|
600
|
+
console.warn(`[publish] Skipped published doc without category_path: ${relPath}`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
567
604
|
docs.push(toDocRecord(sourcePath, relPath, completeEntry, newThreshold));
|
|
568
605
|
}
|
|
569
606
|
|
|
@@ -755,6 +792,10 @@ function compareByRecentDateThenPath(left: DocRecord, right: DocRecord): number
|
|
|
755
792
|
return left.relNoExt.localeCompare(right.relNoExt, "ko-KR");
|
|
756
793
|
}
|
|
757
794
|
|
|
795
|
+
function matchesPathPrefix(value: string, prefix: string): boolean {
|
|
796
|
+
return value === prefix || value.startsWith(`${prefix}/`);
|
|
797
|
+
}
|
|
798
|
+
|
|
758
799
|
function pickHomeDoc(docs: DocRecord[]): DocRecord | null {
|
|
759
800
|
const inDefaultBranch = docs.filter((doc) => doc.branch == null || doc.branch === DEFAULT_BRANCH);
|
|
760
801
|
const candidates = inDefaultBranch.length > 0 ? inDefaultBranch : docs;
|
|
@@ -771,17 +812,27 @@ function buildPinnedMenuFolder(docs: DocRecord[], options: BuildOptions): Folder
|
|
|
771
812
|
return null;
|
|
772
813
|
}
|
|
773
814
|
|
|
815
|
+
const categoryPath = options.pinnedMenu.categoryPath;
|
|
774
816
|
const sourceDir = options.pinnedMenu.sourceDir;
|
|
775
|
-
const sourcePrefix = `${sourceDir}/`;
|
|
776
817
|
const children = docs
|
|
777
|
-
.filter((doc) =>
|
|
818
|
+
.filter((doc) => {
|
|
819
|
+
if (categoryPath) {
|
|
820
|
+
return matchesPathPrefix(doc.categoryPath, categoryPath);
|
|
821
|
+
}
|
|
822
|
+
if (sourceDir) {
|
|
823
|
+
return matchesPathPrefix(doc.relNoExt, sourceDir);
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
})
|
|
778
827
|
.sort((left, right) => left.relNoExt.localeCompare(right.relNoExt, "ko-KR"))
|
|
779
828
|
.map((doc) => fileNodeFromDoc(doc));
|
|
780
829
|
|
|
830
|
+
const pathKey = categoryPath ? `category/${categoryPath}` : `source/${sourceDir ?? "unknown"}`;
|
|
831
|
+
|
|
781
832
|
return {
|
|
782
833
|
type: "folder",
|
|
783
834
|
name: options.pinnedMenu.label,
|
|
784
|
-
path: `__virtual__/pinned/${
|
|
835
|
+
path: `__virtual__/pinned/${pathKey}`,
|
|
785
836
|
virtual: true,
|
|
786
837
|
children,
|
|
787
838
|
};
|
|
@@ -799,8 +850,7 @@ function buildTree(docs: DocRecord[], options: BuildOptions): TreeNode[] {
|
|
|
799
850
|
folderIndex.set("", root);
|
|
800
851
|
|
|
801
852
|
for (const doc of docs) {
|
|
802
|
-
const
|
|
803
|
-
const folders = segments.slice(0, -1);
|
|
853
|
+
const folders = doc.categoryPath.split("/");
|
|
804
854
|
|
|
805
855
|
let currentPath = "";
|
|
806
856
|
let parent = root;
|
|
@@ -861,6 +911,7 @@ function buildManifest(docs: DocRecord[], tree: TreeNode[], options: BuildOption
|
|
|
861
911
|
route: doc.route,
|
|
862
912
|
title: doc.title,
|
|
863
913
|
prefix: doc.prefix,
|
|
914
|
+
categoryPath: doc.categoryPath,
|
|
864
915
|
date: doc.date,
|
|
865
916
|
updatedDate: doc.updatedDate,
|
|
866
917
|
tags: doc.tags,
|
package/src/config.ts
CHANGED
|
@@ -151,19 +151,38 @@ function normalizePinnedMenu(raw: unknown, errorPrefix = "[config]"): PinnedMenu
|
|
|
151
151
|
|
|
152
152
|
const menu = raw as Record<string, unknown>;
|
|
153
153
|
const sourceDirRaw = menu.sourceDir;
|
|
154
|
+
const categoryPathRaw = menu.categoryPath;
|
|
154
155
|
const labelRaw = menu.label;
|
|
156
|
+
const normalizeMenuPath = (value: unknown, fieldName: "sourceDir" | "categoryPath"): string | undefined => {
|
|
157
|
+
if (value == null) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
161
|
+
throw new Error(`${errorPrefix} "pinnedMenu.${fieldName}" must be a non-empty string`);
|
|
162
|
+
}
|
|
155
163
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
const normalized = value
|
|
165
|
+
.trim()
|
|
166
|
+
.replace(/\\/g, "/")
|
|
167
|
+
.replace(/^\/+/, "")
|
|
168
|
+
.replace(/\/+$/, "")
|
|
169
|
+
.split("/")
|
|
170
|
+
.map((segment) => segment.trim())
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join("/");
|
|
173
|
+
|
|
174
|
+
if (!normalized) {
|
|
175
|
+
throw new Error(`${errorPrefix} "pinnedMenu.${fieldName}" must not be root`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return normalized;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const normalizedSourceDir = normalizeMenuPath(sourceDirRaw, "sourceDir");
|
|
182
|
+
const normalizedCategoryPath = normalizeMenuPath(categoryPathRaw, "categoryPath");
|
|
159
183
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.replace(/\\/g, "/")
|
|
163
|
-
.replace(/^\/+/, "")
|
|
164
|
-
.replace(/\/+$/, "");
|
|
165
|
-
if (!normalizedSourceDir) {
|
|
166
|
-
throw new Error(`${errorPrefix} "pinnedMenu.sourceDir" must not be root`);
|
|
184
|
+
if (!normalizedSourceDir && !normalizedCategoryPath) {
|
|
185
|
+
throw new Error(`${errorPrefix} "pinnedMenu" must include "sourceDir" or "categoryPath"`);
|
|
167
186
|
}
|
|
168
187
|
|
|
169
188
|
const label =
|
|
@@ -174,6 +193,7 @@ function normalizePinnedMenu(raw: unknown, errorPrefix = "[config]"): PinnedMenu
|
|
|
174
193
|
return {
|
|
175
194
|
label,
|
|
176
195
|
sourceDir: normalizedSourceDir,
|
|
196
|
+
categoryPath: normalizedCategoryPath,
|
|
177
197
|
};
|
|
178
198
|
}
|
|
179
199
|
|
package/src/types.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface BuildSeoOptions {
|
|
|
32
32
|
|
|
33
33
|
export interface PinnedMenuOption {
|
|
34
34
|
label: string;
|
|
35
|
-
sourceDir
|
|
35
|
+
sourceDir?: string;
|
|
36
|
+
categoryPath?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface UserConfig {
|
|
@@ -42,7 +43,8 @@ export interface UserConfig {
|
|
|
42
43
|
staticPaths?: string[];
|
|
43
44
|
pinnedMenu?: {
|
|
44
45
|
label?: string;
|
|
45
|
-
sourceDir
|
|
46
|
+
sourceDir?: string;
|
|
47
|
+
categoryPath?: string;
|
|
46
48
|
};
|
|
47
49
|
ui?: {
|
|
48
50
|
newWithinDays?: number;
|
|
@@ -96,6 +98,7 @@ export interface DocRecord {
|
|
|
96
98
|
fileName: string;
|
|
97
99
|
title: string;
|
|
98
100
|
prefix?: string;
|
|
101
|
+
categoryPath: string;
|
|
99
102
|
date?: string;
|
|
100
103
|
updatedDate?: string;
|
|
101
104
|
description?: string;
|
|
@@ -156,6 +159,7 @@ export interface Manifest {
|
|
|
156
159
|
route: string;
|
|
157
160
|
title: string;
|
|
158
161
|
prefix?: string;
|
|
162
|
+
categoryPath: string;
|
|
159
163
|
contentUrl: string;
|
|
160
164
|
date?: string;
|
|
161
165
|
updatedDate?: string;
|
|
@@ -185,6 +189,7 @@ export interface BuildCache {
|
|
|
185
189
|
draft: boolean;
|
|
186
190
|
title?: string;
|
|
187
191
|
prefix?: string;
|
|
192
|
+
categoryPath?: string;
|
|
188
193
|
date?: string;
|
|
189
194
|
updatedDate?: string;
|
|
190
195
|
description?: string;
|