@limcpf/everything-is-a-markdown 0.6.5 → 0.6.7
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 +47 -0
- package/README.md +462 -131
- package/package.json +1 -1
- package/src/build.ts +28 -2
- package/src/markdown.ts +69 -0
- package/src/runtime/app.css +84 -2
- package/src/runtime/app.js +204 -1
package/README.ko.md
CHANGED
|
@@ -180,6 +180,25 @@ title: Work In Progress
|
|
|
180
180
|
---
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
+
## 위키링크 지원
|
|
184
|
+
|
|
185
|
+
위키링크는 아래 순서로 해석됩니다.
|
|
186
|
+
|
|
187
|
+
1. 볼트 기준 상대 경로(확장자 `.md` 제외)
|
|
188
|
+
2. `prefix`
|
|
189
|
+
3. `title` 정확히 일치
|
|
190
|
+
4. 파일 stem(유일할 때만)
|
|
191
|
+
|
|
192
|
+
예시:
|
|
193
|
+
|
|
194
|
+
- `[[posts/2024/setup-guide]]`
|
|
195
|
+
- `[[BC-VO-02]]`
|
|
196
|
+
- `[[Building a File-System Blog]]`
|
|
197
|
+
- `[[setup-guide]]`
|
|
198
|
+
- `[[Building a File-System Blog|먼저 읽기]]`
|
|
199
|
+
|
|
200
|
+
대상을 찾지 못하거나 같은 `title`을 가진 게시 문서가 여러 개면 링크로 바꾸지 않고 빌드 warning을 출력합니다.
|
|
201
|
+
|
|
183
202
|
## Mermaid 다이어그램 지원
|
|
184
203
|
|
|
185
204
|
코드 블록의 언어를 `mermaid`로 작성하면 브라우저에서 Mermaid 다이어그램으로 렌더링합니다.
|
|
@@ -197,6 +216,34 @@ Mermaid fence는 일반 코드 블록 UI와 분리된 전용 컨테이너(`.merm
|
|
|
197
216
|
본문 이미지도 동일한 폭 정책을 적용해 글 읽기 흐름을 유지합니다.
|
|
198
217
|
설정에서 Mermaid를 비활성화하거나 CDN 로드가 실패하면, 같은 컨테이너 안에서 소스 코드 텍스트를 유지하고 하단에 경고 메시지를 표시합니다.
|
|
199
218
|
|
|
219
|
+
## 본문 이미지 레이아웃
|
|
220
|
+
|
|
221
|
+
본문 이미지는 이제 방향에 따라 표시 폭을 다르게 조정합니다.
|
|
222
|
+
|
|
223
|
+
- 가로 이미지는 기본 읽기 폭을 유지합니다.
|
|
224
|
+
- 세로 이미지는 더 좁은 최대 폭을 적용해 본문을 과하게 압도하지 않게 합니다.
|
|
225
|
+
- 정사각형에 가까운 이미지는 중간 폭을 사용합니다.
|
|
226
|
+
|
|
227
|
+
`markdown.images: "keep"`로 로컬 Markdown 이미지를 허용하면, 이미지만 있는 문단은 자동으로 `figure.content-image` 래퍼로 승격됩니다.
|
|
228
|
+
고정 비율이나 자르기 방식을 직접 지정하고 싶을 때는 raw HTML figure 유틸리티를 사용할 수 있습니다.
|
|
229
|
+
|
|
230
|
+
예시:
|
|
231
|
+
|
|
232
|
+
```html
|
|
233
|
+
<figure class="image-frame ratio-4x3 fit-cover">
|
|
234
|
+
<img src="/assets/hero.jpg" alt="Cover-framed image" />
|
|
235
|
+
</figure>
|
|
236
|
+
|
|
237
|
+
<figure class="image-frame ratio-4x5 fit-contain">
|
|
238
|
+
<img src="/assets/poster.jpg" alt="Contain-framed image" />
|
|
239
|
+
</figure>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
지원 유틸리티:
|
|
243
|
+
|
|
244
|
+
- 비율: `ratio-16x9`, `ratio-4x3`, `ratio-3x2`, `ratio-4x5`
|
|
245
|
+
- 맞춤 방식: `fit-cover`, `fit-contain`
|
|
246
|
+
|
|
200
247
|
`blog.config.ts`에서 설정:
|
|
201
248
|
|
|
202
249
|
```ts
|
package/README.md
CHANGED
|
@@ -2,226 +2,557 @@
|
|
|
2
2
|
|
|
3
3
|
Language: **English** | [한국어](README.ko.md)
|
|
4
4
|
|
|
5
|
-
Everything-Is-A-Markdown is a CLI
|
|
5
|
+
Everything-Is-A-Markdown is a Bun-based CLI that turns a local Markdown vault into a static site with a file-explorer UI. It is designed for workflows where most notes stay private and only explicitly published documents are exposed as a browsable website.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The generated site keeps a two-panel experience:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- Keep private notes and public content separate
|
|
9
|
+
- Left: folder tree, virtual folders, branch filters
|
|
10
|
+
- Right: rendered document viewer with metadata, backlinks, and previous/next navigation
|
|
12
11
|
|
|
13
|
-
##
|
|
12
|
+
## What It Does
|
|
14
13
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
- Builds a static site from a local Markdown vault
|
|
15
|
+
- Publishes only notes with `publish: true`
|
|
16
|
+
- Requires a `prefix` field for every published note and uses it as the public route
|
|
17
|
+
- Supports Obsidian-style wikilinks such as `[[note]]` and `[[note|label]]`
|
|
18
|
+
- Renders code blocks with Shiki
|
|
19
|
+
- Renders Mermaid blocks in the browser with runtime fallback handling
|
|
20
|
+
- Generates per-route HTML pages for direct access without SPA-only routing
|
|
21
|
+
- Produces a manifest used by the runtime tree/navigation UI
|
|
22
|
+
- Supports a `Recent` virtual folder and an optional pinned virtual folder
|
|
23
|
+
- Supports branch-based filtering in the sidebar
|
|
24
|
+
- Can generate sitemap/robots/canonical metadata when SEO config is provided
|
|
25
|
+
|
|
26
|
+
## Important Behavior
|
|
27
|
+
|
|
28
|
+
This project currently uses `prefix`-based public routes, not vault-relative path routes.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
|
|
32
|
+
- Source file: `posts/2024/setup-guide.md`
|
|
33
|
+
- Frontmatter: `prefix: BC-VO-02`
|
|
34
|
+
- Public route: `/BC-VO-02/`
|
|
35
|
+
|
|
36
|
+
If two notes normalize to the same public route, the builder keeps both and automatically appends a suffix to later collisions.
|
|
37
|
+
|
|
38
|
+
## Who This Fits
|
|
39
|
+
|
|
40
|
+
- Obsidian users who want selective publishing
|
|
41
|
+
- Personal docs/blog workflows backed by a local vault
|
|
42
|
+
- Static hosting targets such as Cloudflare Pages, GitHub Pages, or any plain file server
|
|
43
|
+
- Projects that want a lightweight runtime instead of a full app framework
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- `bun` installed
|
|
48
|
+
- A Markdown vault directory
|
|
49
|
+
|
|
50
|
+
This repository is authored around Bun. The CLI entry point is `src/cli.ts`, and package scripts assume Bun is available.
|
|
17
51
|
|
|
18
52
|
## Install
|
|
19
53
|
|
|
54
|
+
For local development in this repository:
|
|
55
|
+
|
|
20
56
|
```bash
|
|
21
57
|
bun install
|
|
22
58
|
```
|
|
23
59
|
|
|
24
|
-
|
|
60
|
+
To run the published package without cloning:
|
|
25
61
|
|
|
26
62
|
```bash
|
|
27
|
-
|
|
63
|
+
bunx @limcpf/everything-is-a-markdown build --vault ./vault --out ./dist
|
|
28
64
|
```
|
|
29
65
|
|
|
30
|
-
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
1. Prepare a vault with Markdown files.
|
|
69
|
+
2. Add frontmatter to notes you want to publish.
|
|
70
|
+
3. Run a build or start the dev server.
|
|
31
71
|
|
|
32
|
-
|
|
33
|
-
- `bun run dev`: Run local dev server (default `http://localhost:3000`)
|
|
34
|
-
- `bun run clean`: Remove `dist` and `.cache`
|
|
72
|
+
Example:
|
|
35
73
|
|
|
36
|
-
|
|
74
|
+
```bash
|
|
75
|
+
bun run dev -- --vault ./test-vault --out ./dist
|
|
76
|
+
```
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
- `--out <path>`: Output directory (default `dist`)
|
|
40
|
-
- `--exclude <glob>`: Add exclude pattern (repeatable)
|
|
41
|
-
- `--new-within-days <n>`: NEW badge threshold days (integer `>= 0`, default `7`)
|
|
42
|
-
- `--recent-limit <n>`: Recent folder item limit (integer `>= 1`, default `5`)
|
|
43
|
-
- `--port <n>`: Dev server port (default `3000`)
|
|
78
|
+
Build once:
|
|
44
79
|
|
|
45
|
-
|
|
80
|
+
```bash
|
|
81
|
+
bun run build -- --vault ./test-vault --out ./dist
|
|
82
|
+
```
|
|
46
83
|
|
|
47
|
-
|
|
48
|
-
Internally, this command uses the `markdownlint` Node API.
|
|
84
|
+
Clean generated artifacts:
|
|
49
85
|
|
|
50
86
|
```bash
|
|
51
|
-
bun run
|
|
87
|
+
bun run clean -- --out ./dist
|
|
52
88
|
```
|
|
53
89
|
|
|
54
|
-
|
|
90
|
+
## CLI
|
|
55
91
|
|
|
56
92
|
```bash
|
|
57
|
-
bun run
|
|
93
|
+
bun run src/cli.ts [build|dev|clean] [options]
|
|
58
94
|
```
|
|
59
95
|
|
|
96
|
+
Package script aliases:
|
|
97
|
+
|
|
98
|
+
- `bun run build`
|
|
99
|
+
- `bun run dev`
|
|
100
|
+
- `bun run clean`
|
|
101
|
+
- `bun run blog`
|
|
102
|
+
|
|
60
103
|
Options:
|
|
61
104
|
|
|
62
|
-
- `--
|
|
63
|
-
- `--
|
|
64
|
-
- `--
|
|
65
|
-
- `--
|
|
105
|
+
- `--vault <path>`: vault root directory, default `.`.
|
|
106
|
+
- `--out <path>`: output directory, default `dist`.
|
|
107
|
+
- `--exclude <glob>`: exclude glob pattern, repeatable. `.obsidian/**` is excluded by default.
|
|
108
|
+
- `--new-within-days <n>`: NEW badge threshold, integer `>= 0`, default `7`.
|
|
109
|
+
- `--recent-limit <n>`: number of items in the `Recent` virtual folder, integer `>= 1`, default `5`.
|
|
110
|
+
- `--menu-config <path>`: JSON file that overrides `pinnedMenu`.
|
|
111
|
+
- `--port <n>`: dev server port, default `3000`.
|
|
112
|
+
- `-h`, `--help`: show help.
|
|
113
|
+
|
|
114
|
+
Notes:
|
|
115
|
+
|
|
116
|
+
- Unknown CLI options fail fast.
|
|
117
|
+
- Invalid numeric options fail fast.
|
|
118
|
+
- `clean` removes both the output directory and `.cache`.
|
|
119
|
+
|
|
120
|
+
## Frontmatter
|
|
121
|
+
|
|
122
|
+
Only documents with `publish: true` are considered for output.
|
|
66
123
|
|
|
67
|
-
|
|
124
|
+
### Required for published docs
|
|
68
125
|
|
|
69
|
-
|
|
126
|
+
- `publish: true`
|
|
127
|
+
- `prefix: "BC-VO-02"`
|
|
128
|
+
|
|
129
|
+
If `publish: true` is set but `prefix` is missing, the note is skipped and a build warning is emitted.
|
|
130
|
+
|
|
131
|
+
### Supported fields
|
|
132
|
+
|
|
133
|
+
- `title`: display title. Falls back to a title derived from the file name.
|
|
134
|
+
- `description`: summary used in UI and SEO metadata.
|
|
135
|
+
- `tags`: string array.
|
|
136
|
+
- `date` or `createdDate`: publish/created date.
|
|
137
|
+
- `updatedDate`, `modifiedDate`, or `lastModified`: update date.
|
|
138
|
+
- `branch`: branch label used by the runtime filter UI.
|
|
139
|
+
- `draft: true`: excludes the note even if `publish: true`.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
|
|
143
|
+
```md
|
|
144
|
+
---
|
|
145
|
+
publish: true
|
|
146
|
+
prefix: BC-VO-02
|
|
147
|
+
branch: dev
|
|
148
|
+
title: Setup Guide
|
|
149
|
+
date: "2024-09-15"
|
|
150
|
+
updatedDate: "2024-09-20T09:30:00"
|
|
151
|
+
description: How to set up your development environment
|
|
152
|
+
tags: ["tutorial", "setup"]
|
|
153
|
+
---
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Routing Model
|
|
157
|
+
|
|
158
|
+
Public routes are derived from `prefix`, not from the file path.
|
|
159
|
+
|
|
160
|
+
Normalization rules:
|
|
161
|
+
|
|
162
|
+
- trims whitespace
|
|
163
|
+
- normalizes Unicode
|
|
164
|
+
- converts spaces and `_` to `-`
|
|
165
|
+
- converts `/` to `-`
|
|
166
|
+
- removes unsupported punctuation
|
|
167
|
+
- preserves letter case from the original prefix
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
|
|
171
|
+
- `prefix: BC-VO-02` -> `/BC-VO-02/`
|
|
172
|
+
- `prefix: Docs / Intro` -> `/Docs-Intro/`
|
|
173
|
+
|
|
174
|
+
The root `index.html` opens a default home document. If a document route is `/index/`, that route is preferred as home; otherwise the most recent document in the default branch is used.
|
|
175
|
+
|
|
176
|
+
## Output Structure
|
|
177
|
+
|
|
178
|
+
The build writes a static site into `dist/` by default.
|
|
179
|
+
|
|
180
|
+
Typical output:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
dist/
|
|
184
|
+
404.html
|
|
185
|
+
index.html
|
|
186
|
+
manifest.json
|
|
187
|
+
robots.txt # only when seo.siteUrl is configured
|
|
188
|
+
sitemap.xml # only when seo.siteUrl is configured
|
|
189
|
+
_app/index.html
|
|
190
|
+
assets/
|
|
191
|
+
app.<hash>.css
|
|
192
|
+
app.<hash>.js
|
|
193
|
+
content/
|
|
194
|
+
<sha1-of-doc-id>.html
|
|
195
|
+
BC-VO-00/
|
|
196
|
+
index.html
|
|
197
|
+
BC-VO-01/
|
|
198
|
+
index.html
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Key points:
|
|
202
|
+
|
|
203
|
+
- Every published route gets its own `index.html` for direct access.
|
|
204
|
+
- Rendered article bodies are stored separately under `dist/content/`.
|
|
205
|
+
- Runtime assets are content-hashed.
|
|
206
|
+
- Static files declared in config are copied into the same relative paths under `dist/`.
|
|
207
|
+
- Build cache is stored under `.cache/build-index.json`.
|
|
208
|
+
|
|
209
|
+
## Config File
|
|
210
|
+
|
|
211
|
+
The builder automatically loads one of these files from the current working directory:
|
|
212
|
+
|
|
213
|
+
- `blog.config.ts`
|
|
214
|
+
- `blog.config.js`
|
|
215
|
+
- `blog.config.mjs`
|
|
216
|
+
- `blog.config.cjs`
|
|
217
|
+
|
|
218
|
+
Example:
|
|
70
219
|
|
|
71
220
|
```ts
|
|
72
|
-
|
|
221
|
+
export default {
|
|
73
222
|
vaultDir: "./vault",
|
|
74
223
|
outDir: "./dist",
|
|
224
|
+
exclude: [".obsidian/**", "private/**"],
|
|
75
225
|
staticPaths: ["assets", "public/favicon.ico"],
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
226
|
+
pinnedMenu: {
|
|
227
|
+
label: "NOTICE",
|
|
228
|
+
sourceDir: "announcements",
|
|
229
|
+
},
|
|
230
|
+
ui: {
|
|
231
|
+
newWithinDays: 7,
|
|
232
|
+
recentLimit: 5,
|
|
80
233
|
},
|
|
81
234
|
markdown: {
|
|
235
|
+
wikilinks: true,
|
|
236
|
+
images: "omit-local",
|
|
237
|
+
gfm: true,
|
|
238
|
+
highlight: {
|
|
239
|
+
engine: "shiki",
|
|
240
|
+
theme: "github-dark",
|
|
241
|
+
},
|
|
82
242
|
mermaid: {
|
|
83
243
|
enabled: true,
|
|
84
244
|
cdnUrl: "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js",
|
|
85
245
|
theme: "default",
|
|
86
246
|
},
|
|
87
247
|
},
|
|
248
|
+
seo: {
|
|
249
|
+
siteUrl: "https://example.com",
|
|
250
|
+
pathBase: "/blog",
|
|
251
|
+
siteName: "My Vault",
|
|
252
|
+
defaultTitle: "My Vault",
|
|
253
|
+
defaultDescription: "Published notes from my vault",
|
|
254
|
+
locale: "en_US",
|
|
255
|
+
twitterCard: "summary_large_image",
|
|
256
|
+
twitterSite: "@example",
|
|
257
|
+
twitterCreator: "@example",
|
|
258
|
+
defaultSocialImage: "/assets/social.png",
|
|
259
|
+
defaultOgImage: "/assets/og.png",
|
|
260
|
+
defaultTwitterImage: "/assets/twitter.png",
|
|
261
|
+
},
|
|
88
262
|
};
|
|
89
|
-
|
|
90
|
-
export default config;
|
|
91
263
|
```
|
|
92
264
|
|
|
93
|
-
|
|
265
|
+
### Config fields
|
|
94
266
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
267
|
+
- `vaultDir`: default vault root.
|
|
268
|
+
- `outDir`: default output directory.
|
|
269
|
+
- `exclude`: extra exclude globs.
|
|
270
|
+
- `staticPaths`: vault-relative files or directories copied into output.
|
|
271
|
+
- `pinnedMenu`: optional virtual folder shown above `Recent`.
|
|
272
|
+
- `ui.newWithinDays`: threshold for NEW badge.
|
|
273
|
+
- `ui.recentLimit`: number of items in `Recent`.
|
|
274
|
+
- `markdown.wikilinks`: enable or disable wikilink resolution.
|
|
275
|
+
- `markdown.images`: `"keep"` or `"omit-local"`.
|
|
276
|
+
- `markdown.gfm`: enable or disable GFM table/strikethrough support.
|
|
277
|
+
- `markdown.highlight.theme`: Shiki theme.
|
|
278
|
+
- `markdown.mermaid.*`: Mermaid runtime settings.
|
|
279
|
+
- `seo.*`: canonical URL, social metadata, sitemap, robots, and path-base behavior.
|
|
99
280
|
|
|
100
|
-
`
|
|
281
|
+
### `staticPaths`
|
|
101
282
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
283
|
+
- Must be vault-relative
|
|
284
|
+
- Can point to either a file or a directory
|
|
285
|
+
- Are copied as-is into the output directory
|
|
286
|
+
- Invalid or missing paths are skipped with a warning
|
|
105
287
|
|
|
106
|
-
|
|
288
|
+
### `pinnedMenu`
|
|
289
|
+
|
|
290
|
+
`pinnedMenu` creates a virtual folder at the top of the sidebar by collecting published docs whose vault-relative path starts with the configured `sourceDir`.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
pinnedMenu: {
|
|
296
|
+
label: "NOTICE",
|
|
297
|
+
sourceDir: "announcements",
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
CLI override example:
|
|
107
302
|
|
|
108
303
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
304
|
+
bun run build -- --menu-config ./menu.config.json
|
|
305
|
+
```
|
|
111
306
|
|
|
112
|
-
|
|
113
|
-
|
|
307
|
+
JSON shape:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"pinnedMenu": {
|
|
312
|
+
"label": "NOTICE",
|
|
313
|
+
"sourceDir": "announcements"
|
|
314
|
+
}
|
|
315
|
+
}
|
|
114
316
|
```
|
|
115
317
|
|
|
116
|
-
## Markdown
|
|
318
|
+
## Markdown Features
|
|
117
319
|
|
|
118
|
-
|
|
320
|
+
### Supported
|
|
119
321
|
|
|
120
|
-
|
|
322
|
+
- Common Markdown via `markdown-it`
|
|
323
|
+
- GFM tables and strikethrough when enabled
|
|
324
|
+
- Raw HTML inside Markdown
|
|
325
|
+
- Syntax-highlighted fenced code blocks with Shiki
|
|
326
|
+
- External links opened with `target="_blank"` and `rel="noopener noreferrer"`
|
|
327
|
+
- Obsidian-style wikilinks for document links
|
|
121
328
|
|
|
122
|
-
|
|
123
|
-
Only documents with this value are included in build output.
|
|
124
|
-
- `prefix: "A-01"`
|
|
125
|
-
Public identifier for the document route (`/A-01/`).
|
|
126
|
-
If `publish: true` but `prefix` is missing, the document is skipped with a build warning.
|
|
329
|
+
### Wikilinks
|
|
127
330
|
|
|
128
|
-
|
|
331
|
+
Resolution order:
|
|
129
332
|
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
- `branch: dev`
|
|
135
|
-
Branch filter label.
|
|
136
|
-
- `description: "..."`
|
|
137
|
-
Short summary.
|
|
138
|
-
- `tags: ["tag1", "tag2"]`
|
|
139
|
-
String array.
|
|
140
|
-
- `date: "YYYY-MM-DD"` or `createdDate: "..."`
|
|
141
|
-
Created date.
|
|
142
|
-
- `updatedDate: "..."` or `modifiedDate: "..."` or `lastModified: "..."`
|
|
143
|
-
Updated date.
|
|
333
|
+
1. Vault-relative path without `.md`
|
|
334
|
+
2. `prefix`
|
|
335
|
+
3. `title` (exact match)
|
|
336
|
+
4. File stem if unique
|
|
144
337
|
|
|
145
|
-
|
|
338
|
+
Supported forms:
|
|
146
339
|
|
|
147
|
-
|
|
340
|
+
- `[[posts/2024/setup-guide]]`
|
|
341
|
+
- `[[BC-VO-02]]`
|
|
342
|
+
- `[[Building a File-System Blog]]`
|
|
343
|
+
- `[[setup-guide]]`
|
|
344
|
+
- `[[Building a File-System Blog|Read this first]]`
|
|
345
|
+
- `[[setup-guide|Read this first]]`
|
|
148
346
|
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
publish: true
|
|
152
|
-
prefix: "DEV-01"
|
|
153
|
-
branch: dev
|
|
154
|
-
title: Setup Guide
|
|
155
|
-
date: "2024-09-15"
|
|
156
|
-
updatedDate: "2024-09-20T09:30:00"
|
|
157
|
-
description: How to set up your development environment
|
|
158
|
-
tags: ["tutorial", "setup"]
|
|
159
|
-
---
|
|
347
|
+
If a target cannot be resolved, or a `title` matches multiple published docs, the Markdown is emitted as plain text and the build prints a warning.
|
|
160
348
|
|
|
161
|
-
|
|
162
|
-
```
|
|
349
|
+
### Images
|
|
163
350
|
|
|
164
|
-
|
|
351
|
+
Image behavior is controlled by `markdown.images`.
|
|
165
352
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
publish: false
|
|
169
|
-
title: Internal Notes
|
|
170
|
-
---
|
|
171
|
-
```
|
|
353
|
+
- `"keep"`: keeps Markdown image output as-is.
|
|
354
|
+
- `"omit-local"`: replaces local images with an italic placeholder and emits a warning.
|
|
172
355
|
|
|
173
|
-
|
|
356
|
+
Remote URLs are kept even when `"omit-local"` is used.
|
|
174
357
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
```
|
|
358
|
+
This rule also applies to Obsidian-style image embeds such as `![[image.png]]`.
|
|
359
|
+
|
|
360
|
+
### Code Blocks
|
|
361
|
+
|
|
362
|
+
Regular fenced code blocks are rendered with:
|
|
182
363
|
|
|
183
|
-
|
|
364
|
+
- Shiki highlighting
|
|
365
|
+
- a desktop-style code header
|
|
366
|
+
- a copy button
|
|
367
|
+
- optional filename text when fence info contains extra tokens
|
|
184
368
|
|
|
185
|
-
|
|
369
|
+
Example:
|
|
186
370
|
|
|
371
|
+
````md
|
|
372
|
+
```ts blog.config.ts
|
|
373
|
+
export default {};
|
|
374
|
+
```
|
|
187
375
|
````
|
|
376
|
+
|
|
377
|
+
### Mermaid
|
|
378
|
+
|
|
379
|
+
Mermaid fences are rendered in the browser:
|
|
380
|
+
|
|
381
|
+
````md
|
|
188
382
|
```mermaid
|
|
189
383
|
flowchart LR
|
|
190
384
|
A --> B
|
|
191
385
|
```
|
|
192
386
|
````
|
|
193
387
|
|
|
194
|
-
|
|
195
|
-
Mermaid fences are rendered inside a dedicated diagram container (`.mermaid-block`) instead of the regular code block UI, so they do not show code headers, filenames, or copy buttons.
|
|
196
|
-
Rendered SVG output is centered and automatically constrained to `min(100%, 720px)` on both desktop and mobile.
|
|
197
|
-
Regular content images inside the viewer follow the same width policy to keep reading flow stable.
|
|
198
|
-
If Mermaid is disabled in config or CDN loading fails, the source block is shown as-is in the same container and a warning message appears below it.
|
|
388
|
+
Behavior:
|
|
199
389
|
|
|
200
|
-
|
|
390
|
+
- rendered on first load and on document navigation
|
|
391
|
+
- centered and width-constrained in the viewer
|
|
392
|
+
- if rendering fails, source remains visible and an error message is shown
|
|
393
|
+
- invalid Mermaid CDN URLs or invalid theme values are normalized back to safe defaults
|
|
201
394
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
395
|
+
## Body Image Layout
|
|
396
|
+
|
|
397
|
+
Body images now use orientation-aware sizing inside the viewer:
|
|
398
|
+
|
|
399
|
+
- Landscape images keep the standard reading width.
|
|
400
|
+
- Portrait images are automatically constrained to a narrower max width so they do not dominate the article.
|
|
401
|
+
- Near-square images get an intermediate width.
|
|
402
|
+
|
|
403
|
+
When local Markdown images are enabled with `markdown.images: "keep"`, standalone image paragraphs are promoted into a dedicated `figure.content-image` wrapper automatically.
|
|
404
|
+
Raw HTML remains available for manual framing when you want a fixed ratio or a specific crop mode.
|
|
405
|
+
|
|
406
|
+
Example frame utilities:
|
|
407
|
+
|
|
408
|
+
```html
|
|
409
|
+
<figure class="image-frame ratio-4x3 fit-cover">
|
|
410
|
+
<img src="/assets/hero.jpg" alt="Cover-framed image" />
|
|
411
|
+
</figure>
|
|
412
|
+
|
|
413
|
+
<figure class="image-frame ratio-4x5 fit-contain">
|
|
414
|
+
<img src="/assets/poster.jpg" alt="Contain-framed image" />
|
|
415
|
+
</figure>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Supported frame utilities:
|
|
419
|
+
|
|
420
|
+
- Ratios: `ratio-16x9`, `ratio-4x3`, `ratio-3x2`, `ratio-4x5`
|
|
421
|
+
- Fit modes: `fit-cover`, `fit-contain`
|
|
422
|
+
|
|
423
|
+
## UI and Runtime Behavior
|
|
424
|
+
|
|
425
|
+
The generated site includes a client-side runtime that powers navigation without requiring a framework.
|
|
426
|
+
|
|
427
|
+
Main behaviors:
|
|
428
|
+
|
|
429
|
+
- folder tree with expand/collapse state in `localStorage`
|
|
430
|
+
- `Recent` virtual folder
|
|
431
|
+
- optional pinned virtual folder
|
|
432
|
+
- branch pills in the sidebar
|
|
433
|
+
- active document syncing with browser history
|
|
434
|
+
- direct-link loading from route HTML
|
|
435
|
+
- previous/next document navigation
|
|
436
|
+
- backlink list for notes referenced by other notes
|
|
437
|
+
- mobile sidebar with accessibility handling and focus trap behavior
|
|
438
|
+
- theme mode persistence: `light`, `dark`, `system`
|
|
439
|
+
- sidebar width persistence on desktop
|
|
440
|
+
|
|
441
|
+
Branch behavior:
|
|
442
|
+
|
|
443
|
+
- the default branch is `dev`
|
|
444
|
+
- notes without `branch` belong to the default branch view
|
|
445
|
+
- notes with `branch: main` or another value appear only in that branch
|
|
446
|
+
|
|
447
|
+
## SEO Support
|
|
448
|
+
|
|
449
|
+
SEO artifacts are generated only when `seo.siteUrl` is configured.
|
|
450
|
+
|
|
451
|
+
With SEO enabled, the build writes:
|
|
452
|
+
|
|
453
|
+
- canonical URLs
|
|
454
|
+
- Open Graph metadata
|
|
455
|
+
- Twitter metadata
|
|
456
|
+
- JSON-LD structured data
|
|
457
|
+
- `robots.txt`
|
|
458
|
+
- `sitemap.xml`
|
|
459
|
+
|
|
460
|
+
`seo.pathBase` is supported for subpath deployments such as `/blog`.
|
|
461
|
+
|
|
462
|
+
Example:
|
|
463
|
+
|
|
464
|
+
- public site root: `https://example.com`
|
|
465
|
+
- path base: `/blog`
|
|
466
|
+
- route: `/BC-VO-02/`
|
|
467
|
+
- canonical URL: `https://example.com/blog/BC-VO-02/`
|
|
468
|
+
|
|
469
|
+
## Incremental Build and Caching
|
|
470
|
+
|
|
471
|
+
The build caches source metadata and output hashes in `.cache/build-index.json`.
|
|
472
|
+
|
|
473
|
+
This allows it to:
|
|
474
|
+
|
|
475
|
+
- skip unchanged rendered content
|
|
476
|
+
- restore missing generated content files on a later build
|
|
477
|
+
- restore missing hashed runtime assets on a later build
|
|
478
|
+
- remove stale route pages and content files when documents are removed or routes change
|
|
479
|
+
|
|
480
|
+
## Markdown Lint for Published Docs
|
|
481
|
+
|
|
482
|
+
This repository includes a publish-only Markdown lint command:
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
bun run lint:md:publish -- --out-dir ./reports
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Strict mode:
|
|
489
|
+
|
|
490
|
+
```bash
|
|
491
|
+
bun run lint:md:publish -- --out-dir ./reports --strict
|
|
210
492
|
```
|
|
211
493
|
|
|
212
|
-
|
|
494
|
+
Options:
|
|
213
495
|
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
496
|
+
- `--out-dir <path>`: required report directory
|
|
497
|
+
- `--strict`: exits with status `1` when issues exist
|
|
498
|
+
- `--vault <path>`: override vault root
|
|
499
|
+
- `--exclude <glob>`: extra exclude patterns
|
|
217
500
|
|
|
218
|
-
|
|
501
|
+
What it checks:
|
|
502
|
+
|
|
503
|
+
- only notes with `publish: true`
|
|
504
|
+
- skips docs missing `prefix`
|
|
505
|
+
- runs `markdownlint`
|
|
506
|
+
- adds a custom rule that forbids H1 in the Markdown body after frontmatter
|
|
507
|
+
- writes a JSON report file
|
|
508
|
+
|
|
509
|
+
## Example Vault
|
|
510
|
+
|
|
511
|
+
This repository includes `test-vault/` as a working sample.
|
|
512
|
+
|
|
513
|
+
Example files:
|
|
514
|
+
|
|
515
|
+
- `test-vault/about.md`
|
|
516
|
+
- `test-vault/posts/2024/setup-guide.md`
|
|
517
|
+
- `test-vault/posts/2024/file-system-blog.md`
|
|
518
|
+
- `test-vault/posts/2024/mermaid-example.md`
|
|
519
|
+
|
|
520
|
+
Try it with:
|
|
219
521
|
|
|
220
522
|
```bash
|
|
221
|
-
|
|
222
|
-
|
|
523
|
+
bun run dev -- --vault ./test-vault --out ./dist
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Development
|
|
527
|
+
|
|
528
|
+
Scripts from `package.json`:
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
bun install
|
|
532
|
+
bun run build -- --vault ./test-vault --out ./dist
|
|
533
|
+
bun run dev -- --vault ./test-vault --out ./dist
|
|
534
|
+
bun run test:e2e
|
|
223
535
|
```
|
|
224
536
|
|
|
537
|
+
E2E coverage in `tests/e2e/` includes:
|
|
538
|
+
|
|
539
|
+
- build regression around incremental outputs
|
|
540
|
+
- subpath routing with `seo.pathBase`
|
|
541
|
+
- prefix routing, backlinks, and branch switching
|
|
542
|
+
- Mermaid runtime behavior
|
|
543
|
+
- mobile sidebar accessibility and focus trap behavior
|
|
544
|
+
- runtime XSS/path-base guardrails
|
|
545
|
+
|
|
546
|
+
## Known Limitations
|
|
547
|
+
|
|
548
|
+
- Bun is required; this is not a generic Node-only CLI.
|
|
549
|
+
- Public routing depends on `prefix`, not on file path.
|
|
550
|
+
- Published docs without `prefix` are skipped.
|
|
551
|
+
- Local images may be omitted depending on config.
|
|
552
|
+
- Wikilinks resolve only to published docs.
|
|
553
|
+
- Mermaid rendering depends on runtime script loading in the browser.
|
|
554
|
+
- SEO files are not generated unless `seo.siteUrl` is configured.
|
|
555
|
+
|
|
225
556
|
## License
|
|
226
557
|
|
|
227
|
-
MIT. See
|
|
558
|
+
MIT. See [LICENSE](/home/lim/code/Everything-Is-A-Markdown/LICENSE).
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -44,6 +44,7 @@ interface RuntimeAssets {
|
|
|
44
44
|
interface WikiLookup {
|
|
45
45
|
byPath: Map<string, DocRecord>;
|
|
46
46
|
byPrefix: Map<string, DocRecord[]>;
|
|
47
|
+
byTitle: Map<string, DocRecord[]>;
|
|
47
48
|
byStem: Map<string, DocRecord[]>;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -573,6 +574,7 @@ async function readPublishedDocs(options: BuildOptions, previousSources: BuildCa
|
|
|
573
574
|
function createWikiLookup(docs: DocRecord[]): WikiLookup {
|
|
574
575
|
const byPath = new Map<string, DocRecord>();
|
|
575
576
|
const byPrefix = new Map<string, DocRecord[]>();
|
|
577
|
+
const byTitle = new Map<string, DocRecord[]>();
|
|
576
578
|
const byStem = new Map<string, DocRecord[]>();
|
|
577
579
|
|
|
578
580
|
for (const doc of docs) {
|
|
@@ -583,13 +585,19 @@ function createWikiLookup(docs: DocRecord[]): WikiLookup {
|
|
|
583
585
|
prefixBucket.push(doc);
|
|
584
586
|
byPrefix.set(prefixKey, prefixBucket);
|
|
585
587
|
}
|
|
588
|
+
const titleKey = normalizeWikiTarget(doc.title);
|
|
589
|
+
if (titleKey) {
|
|
590
|
+
const titleBucket = byTitle.get(titleKey) ?? [];
|
|
591
|
+
titleBucket.push(doc);
|
|
592
|
+
byTitle.set(titleKey, titleBucket);
|
|
593
|
+
}
|
|
586
594
|
const stem = path.basename(doc.relNoExt).toLowerCase();
|
|
587
595
|
const bucket = byStem.get(stem) ?? [];
|
|
588
596
|
bucket.push(doc);
|
|
589
597
|
byStem.set(stem, bucket);
|
|
590
598
|
}
|
|
591
599
|
|
|
592
|
-
return { byPath, byPrefix, byStem };
|
|
600
|
+
return { byPath, byPrefix, byTitle, byStem };
|
|
593
601
|
}
|
|
594
602
|
|
|
595
603
|
function resolveWikiTargetDoc(
|
|
@@ -622,6 +630,20 @@ function resolveWikiTargetDoc(
|
|
|
622
630
|
return null;
|
|
623
631
|
}
|
|
624
632
|
|
|
633
|
+
const titleMatches = lookup.byTitle.get(normalized) ?? [];
|
|
634
|
+
if (titleMatches.length === 1) {
|
|
635
|
+
return titleMatches[0];
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (warnOnDuplicate && titleMatches.length > 1) {
|
|
639
|
+
console.warn(
|
|
640
|
+
`[wikilink] Duplicate title target "${input}" in ${currentDoc.relPath}. Candidates: ${titleMatches
|
|
641
|
+
.map((item) => item.relPath)
|
|
642
|
+
.join(", ")}`,
|
|
643
|
+
);
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
625
647
|
if (normalized.includes("/")) {
|
|
626
648
|
return null;
|
|
627
649
|
}
|
|
@@ -737,7 +759,11 @@ function pickHomeDoc(docs: DocRecord[]): DocRecord | null {
|
|
|
737
759
|
const inDefaultBranch = docs.filter((doc) => doc.branch == null || doc.branch === DEFAULT_BRANCH);
|
|
738
760
|
const candidates = inDefaultBranch.length > 0 ? inDefaultBranch : docs;
|
|
739
761
|
const byRoute = candidates.find((doc) => doc.route === "/index/");
|
|
740
|
-
|
|
762
|
+
if (byRoute) {
|
|
763
|
+
return byRoute;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return [...candidates].sort(compareByRecentDateThenPath)[0] ?? null;
|
|
741
767
|
}
|
|
742
768
|
|
|
743
769
|
function buildPinnedMenuFolder(docs: DocRecord[], options: BuildOptions): FolderNode | null {
|
package/src/markdown.ts
CHANGED
|
@@ -24,6 +24,8 @@ type RuleOptions = RenderRuleArgs[2];
|
|
|
24
24
|
type RuleEnv = RenderRuleArgs[3];
|
|
25
25
|
type RuleSelf = RenderRuleArgs[4];
|
|
26
26
|
type LinkOpenRule = NonNullable<MarkdownIt["renderer"]["rules"]["link_open"]>;
|
|
27
|
+
type ImageRule = NonNullable<MarkdownIt["renderer"]["rules"]["image"]>;
|
|
28
|
+
type ParagraphRule = NonNullable<MarkdownIt["renderer"]["rules"]["paragraph_open"]>;
|
|
27
29
|
|
|
28
30
|
function escapeMarkdownLabel(input: string): string {
|
|
29
31
|
return input.replace(/[\[\]]/g, "");
|
|
@@ -141,6 +143,22 @@ function escapeHtmlAttr(input: string): string {
|
|
|
141
143
|
return input.replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
function isStandaloneImageParagraph(tokens: RuleTokens, idx: number): boolean {
|
|
147
|
+
const open = tokens[idx];
|
|
148
|
+
const inline = tokens[idx + 1];
|
|
149
|
+
const close = tokens[idx + 2];
|
|
150
|
+
|
|
151
|
+
if (open?.type !== "paragraph_open" || inline?.type !== "inline" || close?.type !== "paragraph_close") {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const children = (inline.children ?? []).filter((child) => {
|
|
156
|
+
return !(child.type === "text" && !child.content.trim());
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return children.length === 1 && children[0]?.type === "image";
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
function createMarkdownIt<L extends string, T extends string>(
|
|
145
163
|
highlighter: HighlighterGeneric<L, T>,
|
|
146
164
|
theme: string,
|
|
@@ -201,6 +219,57 @@ function createMarkdownIt<L extends string, T extends string>(
|
|
|
201
219
|
};
|
|
202
220
|
md.renderer.rules.fence = fenceRule;
|
|
203
221
|
|
|
222
|
+
const defaultImage = md.renderer.rules.image as ImageRule | undefined;
|
|
223
|
+
const imageRule: ImageRule = (
|
|
224
|
+
tokens: RuleTokens,
|
|
225
|
+
idx: number,
|
|
226
|
+
options: RuleOptions,
|
|
227
|
+
env: RuleEnv,
|
|
228
|
+
self: RuleSelf,
|
|
229
|
+
) => {
|
|
230
|
+
tokens[idx].attrJoin("class", "content-media");
|
|
231
|
+
if (defaultImage) {
|
|
232
|
+
return defaultImage(tokens, idx, options, env, self);
|
|
233
|
+
}
|
|
234
|
+
return self.renderToken(tokens, idx, options);
|
|
235
|
+
};
|
|
236
|
+
md.renderer.rules.image = imageRule;
|
|
237
|
+
|
|
238
|
+
const defaultParagraphOpen = md.renderer.rules.paragraph_open as ParagraphRule | undefined;
|
|
239
|
+
const defaultParagraphClose = md.renderer.rules.paragraph_close as ParagraphRule | undefined;
|
|
240
|
+
const paragraphOpenRule: ParagraphRule = (
|
|
241
|
+
tokens: RuleTokens,
|
|
242
|
+
idx: number,
|
|
243
|
+
options: RuleOptions,
|
|
244
|
+
env: RuleEnv,
|
|
245
|
+
self: RuleSelf,
|
|
246
|
+
) => {
|
|
247
|
+
if (isStandaloneImageParagraph(tokens, idx)) {
|
|
248
|
+
return `<figure class="content-image">`;
|
|
249
|
+
}
|
|
250
|
+
if (defaultParagraphOpen) {
|
|
251
|
+
return defaultParagraphOpen(tokens, idx, options, env, self);
|
|
252
|
+
}
|
|
253
|
+
return self.renderToken(tokens, idx, options);
|
|
254
|
+
};
|
|
255
|
+
const paragraphCloseRule: ParagraphRule = (
|
|
256
|
+
tokens: RuleTokens,
|
|
257
|
+
idx: number,
|
|
258
|
+
options: RuleOptions,
|
|
259
|
+
env: RuleEnv,
|
|
260
|
+
self: RuleSelf,
|
|
261
|
+
) => {
|
|
262
|
+
if (isStandaloneImageParagraph(tokens, idx - 2)) {
|
|
263
|
+
return `</figure>`;
|
|
264
|
+
}
|
|
265
|
+
if (defaultParagraphClose) {
|
|
266
|
+
return defaultParagraphClose(tokens, idx, options, env, self);
|
|
267
|
+
}
|
|
268
|
+
return self.renderToken(tokens, idx, options);
|
|
269
|
+
};
|
|
270
|
+
md.renderer.rules.paragraph_open = paragraphOpenRule;
|
|
271
|
+
md.renderer.rules.paragraph_close = paragraphCloseRule;
|
|
272
|
+
|
|
204
273
|
const defaultLinkOpen = md.renderer.rules.link_open as LinkOpenRule | undefined;
|
|
205
274
|
const linkOpenRule: LinkOpenRule = (
|
|
206
275
|
tokens: RuleTokens,
|
package/src/runtime/app.css
CHANGED
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
--content-heading-accent-soft: rgba(136, 57, 239, 0.32);
|
|
68
68
|
--content-heading-subtle: var(--latte-subtext1);
|
|
69
69
|
--content-visual-max-width: 720px;
|
|
70
|
+
--content-image-landscape-max-width: 720px;
|
|
71
|
+
--content-image-square-max-width: 640px;
|
|
72
|
+
--content-image-portrait-max-width: 560px;
|
|
73
|
+
--content-image-radius: 8px;
|
|
74
|
+
--content-image-frame-bg: linear-gradient(180deg, var(--latte-base), var(--latte-mantle));
|
|
70
75
|
--mermaid-wide-max-width: 640px;
|
|
71
76
|
--mermaid-tall-max-height: 560px;
|
|
72
77
|
--desktop-sidebar-default: 420px;
|
|
@@ -1212,14 +1217,91 @@ body.mobile-toggle-left .mobile-menu-toggle {
|
|
|
1212
1217
|
}
|
|
1213
1218
|
|
|
1214
1219
|
/* Images */
|
|
1220
|
+
.viewer-content .content-image,
|
|
1221
|
+
.viewer-content .image-frame {
|
|
1222
|
+
margin: 1.5rem auto;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.viewer-content .content-image {
|
|
1226
|
+
width: fit-content;
|
|
1227
|
+
max-width: min(100%, var(--content-image-landscape-max-width));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.viewer-content .content-image.is-square {
|
|
1231
|
+
max-width: min(100%, var(--content-image-square-max-width));
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
.viewer-content .content-image.is-portrait {
|
|
1235
|
+
max-width: min(100%, var(--content-image-portrait-max-width));
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1215
1238
|
.viewer-content img {
|
|
1216
1239
|
display: block;
|
|
1217
1240
|
width: auto;
|
|
1218
|
-
max-width: min(100%, var(--content-
|
|
1241
|
+
max-width: min(100%, var(--content-image-landscape-max-width));
|
|
1219
1242
|
height: auto;
|
|
1220
1243
|
margin: 0 auto;
|
|
1221
|
-
border-radius:
|
|
1244
|
+
border-radius: var(--content-image-radius);
|
|
1245
|
+
border: 1px solid var(--latte-surface0);
|
|
1246
|
+
background: var(--content-image-frame-bg);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.viewer-content img.is-square {
|
|
1250
|
+
max-width: min(100%, var(--content-image-square-max-width));
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
.viewer-content img.is-portrait {
|
|
1254
|
+
max-width: min(100%, var(--content-image-portrait-max-width));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.viewer-content .image-frame {
|
|
1258
|
+
width: min(100%, var(--content-image-landscape-max-width));
|
|
1259
|
+
overflow: hidden;
|
|
1260
|
+
border-radius: 12px;
|
|
1222
1261
|
border: 1px solid var(--latte-surface0);
|
|
1262
|
+
background: var(--content-image-frame-bg);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.viewer-content .image-frame.is-square {
|
|
1266
|
+
width: min(100%, var(--content-image-square-max-width));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.viewer-content .image-frame.is-portrait {
|
|
1270
|
+
width: min(100%, var(--content-image-portrait-max-width));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.viewer-content .image-frame > img {
|
|
1274
|
+
width: 100%;
|
|
1275
|
+
max-width: none;
|
|
1276
|
+
height: 100%;
|
|
1277
|
+
margin: 0;
|
|
1278
|
+
border: 0;
|
|
1279
|
+
border-radius: 0;
|
|
1280
|
+
background: transparent;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
.viewer-content .image-frame.fit-cover > img {
|
|
1284
|
+
object-fit: cover;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.viewer-content .image-frame.fit-contain > img {
|
|
1288
|
+
object-fit: contain;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.viewer-content .image-frame.ratio-16x9 {
|
|
1292
|
+
aspect-ratio: 16 / 9;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.viewer-content .image-frame.ratio-4x3 {
|
|
1296
|
+
aspect-ratio: 4 / 3;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.viewer-content .image-frame.ratio-3x2 {
|
|
1300
|
+
aspect-ratio: 3 / 2;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.viewer-content .image-frame.ratio-4x5 {
|
|
1304
|
+
aspect-ratio: 4 / 5;
|
|
1223
1305
|
}
|
|
1224
1306
|
|
|
1225
1307
|
/* Horizontal rule */
|
package/src/runtime/app.js
CHANGED
|
@@ -26,6 +26,12 @@ const MERMAID_WIDE_RATIO = 2.4;
|
|
|
26
26
|
const MERMAID_TALL_RATIO = 0.85;
|
|
27
27
|
const MERMAID_BLOCK_WIDE_CLASS = "is-wide";
|
|
28
28
|
const MERMAID_BLOCK_TALL_CLASS = "is-tall";
|
|
29
|
+
const CONTENT_IMAGE_LANDSCAPE_CLASS = "is-landscape";
|
|
30
|
+
const CONTENT_IMAGE_PORTRAIT_CLASS = "is-portrait";
|
|
31
|
+
const CONTENT_IMAGE_SQUARE_CLASS = "is-square";
|
|
32
|
+
const CONTENT_IMAGE_LANDSCAPE_THRESHOLD = 1.1;
|
|
33
|
+
const CONTENT_IMAGE_PORTRAIT_THRESHOLD = 0.9;
|
|
34
|
+
const contentImageDimensionCache = new Map();
|
|
29
35
|
const mermaidRuntime = {
|
|
30
36
|
initialized: false,
|
|
31
37
|
loadingPromise: null,
|
|
@@ -156,6 +162,170 @@ function normalizeRenderedMermaidSvg(block) {
|
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
function clearContentImageClasses(target) {
|
|
166
|
+
if (!(target instanceof Element)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
target.classList.remove(
|
|
171
|
+
CONTENT_IMAGE_LANDSCAPE_CLASS,
|
|
172
|
+
CONTENT_IMAGE_PORTRAIT_CLASS,
|
|
173
|
+
CONTENT_IMAGE_SQUARE_CLASS,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function syncContentImageClasses(image, className) {
|
|
178
|
+
if (!(image instanceof HTMLImageElement)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
clearContentImageClasses(image);
|
|
183
|
+
image.classList.add(className);
|
|
184
|
+
|
|
185
|
+
const figure = image.closest("figure");
|
|
186
|
+
if (
|
|
187
|
+
figure instanceof HTMLElement &&
|
|
188
|
+
(figure.classList.contains("content-image") || figure.classList.contains("image-frame"))
|
|
189
|
+
) {
|
|
190
|
+
clearContentImageClasses(figure);
|
|
191
|
+
figure.classList.add(className);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readIntrinsicImageDimensions(imageLike) {
|
|
196
|
+
if (!(imageLike instanceof HTMLImageElement)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const width =
|
|
201
|
+
imageLike.naturalWidth > 0
|
|
202
|
+
? imageLike.naturalWidth
|
|
203
|
+
: Number.parseFloat(imageLike.getAttribute("width") ?? "");
|
|
204
|
+
const height =
|
|
205
|
+
imageLike.naturalHeight > 0
|
|
206
|
+
? imageLike.naturalHeight
|
|
207
|
+
: Number.parseFloat(imageLike.getAttribute("height") ?? "");
|
|
208
|
+
|
|
209
|
+
if (!(width > 0) || !(height > 0)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { width, height };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function classifyContentImageByDimensions(image, dimensions) {
|
|
217
|
+
if (!(image instanceof HTMLImageElement) || !dimensions) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const aspectRatio = dimensions.width / dimensions.height;
|
|
222
|
+
if (aspectRatio >= CONTENT_IMAGE_LANDSCAPE_THRESHOLD) {
|
|
223
|
+
syncContentImageClasses(image, CONTENT_IMAGE_LANDSCAPE_CLASS);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (aspectRatio <= CONTENT_IMAGE_PORTRAIT_THRESHOLD) {
|
|
228
|
+
syncContentImageClasses(image, CONTENT_IMAGE_PORTRAIT_CLASS);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
syncContentImageClasses(image, CONTENT_IMAGE_SQUARE_CLASS);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveContentImageDimensions(image) {
|
|
236
|
+
const immediate = readIntrinsicImageDimensions(image);
|
|
237
|
+
if (immediate) {
|
|
238
|
+
return Promise.resolve(immediate);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const source = image.currentSrc || image.getAttribute("src") || "";
|
|
242
|
+
if (!source) {
|
|
243
|
+
return Promise.resolve(null);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const cached = contentImageDimensionCache.get(source);
|
|
247
|
+
if (cached) {
|
|
248
|
+
return cached;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const pending = new Promise((resolve) => {
|
|
252
|
+
const probe = new Image();
|
|
253
|
+
const finalize = () => {
|
|
254
|
+
resolve(readIntrinsicImageDimensions(probe));
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
probe.addEventListener("load", finalize, { once: true });
|
|
258
|
+
probe.addEventListener("error", () => resolve(null), { once: true });
|
|
259
|
+
probe.src = source;
|
|
260
|
+
|
|
261
|
+
if (probe.complete) {
|
|
262
|
+
if (typeof probe.decode === "function") {
|
|
263
|
+
probe.decode().then(finalize).catch(finalize);
|
|
264
|
+
} else {
|
|
265
|
+
finalize();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
contentImageDimensionCache.set(source, pending);
|
|
271
|
+
return pending;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function prepareContentImage(image) {
|
|
275
|
+
if (!(image instanceof HTMLImageElement) || image.closest(".mermaid-block")) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const figure = image.closest("figure");
|
|
280
|
+
if (
|
|
281
|
+
figure instanceof HTMLElement &&
|
|
282
|
+
(figure.classList.contains("content-image") || figure.classList.contains("image-frame"))
|
|
283
|
+
) {
|
|
284
|
+
clearContentImageClasses(figure);
|
|
285
|
+
}
|
|
286
|
+
clearContentImageClasses(image);
|
|
287
|
+
|
|
288
|
+
const handleLoad = () => {
|
|
289
|
+
resolveContentImageDimensions(image)
|
|
290
|
+
.then((dimensions) => {
|
|
291
|
+
if (!dimensions) {
|
|
292
|
+
handleError();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
classifyContentImageByDimensions(image, dimensions);
|
|
296
|
+
})
|
|
297
|
+
.catch(() => {
|
|
298
|
+
handleError();
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
const handleError = () => {
|
|
302
|
+
clearContentImageClasses(image);
|
|
303
|
+
if (
|
|
304
|
+
figure instanceof HTMLElement &&
|
|
305
|
+
(figure.classList.contains("content-image") || figure.classList.contains("image-frame"))
|
|
306
|
+
) {
|
|
307
|
+
clearContentImageClasses(figure);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
image.addEventListener("load", handleLoad, { once: true });
|
|
312
|
+
image.addEventListener("error", handleError, { once: true });
|
|
313
|
+
|
|
314
|
+
if (image.complete) {
|
|
315
|
+
handleLoad();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function enhanceContentImages(root) {
|
|
320
|
+
if (!(root instanceof HTMLElement)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const image of root.querySelectorAll("img")) {
|
|
325
|
+
prepareContentImage(image);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
159
329
|
function parseMermaidNodes() {
|
|
160
330
|
const contentEl = document.getElementById("viewer-content");
|
|
161
331
|
if (!(contentEl instanceof HTMLElement)) {
|
|
@@ -508,6 +678,37 @@ function isDocVisibleInBranch(doc, branch, defaultBranch) {
|
|
|
508
678
|
return docBranch === branch;
|
|
509
679
|
}
|
|
510
680
|
|
|
681
|
+
function parseDateToEpochMs(value) {
|
|
682
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const parsed = Date.parse(value);
|
|
687
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function getRecentSortEpochMs(doc) {
|
|
691
|
+
return parseDateToEpochMs(doc.updatedDate) ?? parseDateToEpochMs(doc.date);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function compareDocsByRecentDateThenRoute(left, right) {
|
|
695
|
+
const leftEpoch = getRecentSortEpochMs(left);
|
|
696
|
+
const rightEpoch = getRecentSortEpochMs(right);
|
|
697
|
+
|
|
698
|
+
if (leftEpoch != null && rightEpoch != null) {
|
|
699
|
+
const byDate = rightEpoch - leftEpoch;
|
|
700
|
+
if (byDate !== 0) {
|
|
701
|
+
return byDate;
|
|
702
|
+
}
|
|
703
|
+
} else if (leftEpoch != null && rightEpoch == null) {
|
|
704
|
+
return -1;
|
|
705
|
+
} else if (leftEpoch == null && rightEpoch != null) {
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return left.route.localeCompare(right.route, "ko-KR");
|
|
710
|
+
}
|
|
711
|
+
|
|
511
712
|
function cloneFilteredTree(nodes, visibleDocIds) {
|
|
512
713
|
const filteredNodes = [];
|
|
513
714
|
|
|
@@ -559,7 +760,7 @@ function pickHomeRoute(view) {
|
|
|
559
760
|
if (view.routeMap["/index/"]) {
|
|
560
761
|
return "/index/";
|
|
561
762
|
}
|
|
562
|
-
return view.docs[0]?.route || "/";
|
|
763
|
+
return [...view.docs].sort(compareDocsByRecentDateThenRoute)[0]?.route || "/";
|
|
563
764
|
}
|
|
564
765
|
|
|
565
766
|
function loadExpandedSet() {
|
|
@@ -1738,6 +1939,7 @@ async function start() {
|
|
|
1738
1939
|
metaEl.innerHTML = renderMeta(doc);
|
|
1739
1940
|
updateBacklinks(doc);
|
|
1740
1941
|
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id, pathBase);
|
|
1942
|
+
enhanceContentImages(contentEl);
|
|
1741
1943
|
await renderMermaidBlocks(mermaidConfig);
|
|
1742
1944
|
document.title = composeDocumentTitle(doc.title, siteTitle);
|
|
1743
1945
|
if (viewerEl instanceof HTMLElement) {
|
|
@@ -1764,6 +1966,7 @@ async function start() {
|
|
|
1764
1966
|
|
|
1765
1967
|
updateBacklinks(doc);
|
|
1766
1968
|
navEl.innerHTML = renderNav(view.docs, view.docIndexById, id, pathBase);
|
|
1969
|
+
enhanceContentImages(contentEl);
|
|
1767
1970
|
await renderMermaidBlocks(mermaidConfig);
|
|
1768
1971
|
|
|
1769
1972
|
document.title = composeDocumentTitle(doc.title, siteTitle);
|