doc-repo 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.ja.md +162 -0
- package/README.md +162 -0
- package/dist/cli/exitCode.d.ts +2 -0
- package/dist/cli/exitCode.js +3 -0
- package/dist/cli/formatResultMessage.d.ts +2 -0
- package/dist/cli/formatResultMessage.js +18 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +43 -0
- package/dist/core/parser/convertMarkdown.d.ts +4 -0
- package/dist/core/parser/convertMarkdown.js +127 -0
- package/dist/core/scanner/detectRoot.d.ts +2 -0
- package/dist/core/scanner/detectRoot.js +22 -0
- package/dist/core/scanner/scanMarkdown.d.ts +2 -0
- package/dist/core/scanner/scanMarkdown.js +22 -0
- package/dist/core/site/atomicPublish.d.ts +1 -0
- package/dist/core/site/atomicPublish.js +21 -0
- package/dist/core/site/buildSiteBundle.d.ts +2 -0
- package/dist/core/site/buildSiteBundle.js +79 -0
- package/dist/core/site/copyAssets.d.ts +1 -0
- package/dist/core/site/copyAssets.js +8 -0
- package/dist/core/site/generateSite.d.ts +2 -0
- package/dist/core/site/generateSite.js +101 -0
- package/dist/core/site/renderPages.d.ts +2 -0
- package/dist/core/site/renderPages.js +82 -0
- package/dist/shared/errors.d.ts +9 -0
- package/dist/shared/errors.js +28 -0
- package/dist/shared/logger.d.ts +7 -0
- package/dist/shared/logger.js +16 -0
- package/dist/shared/sitePaths.d.ts +5 -0
- package/dist/shared/sitePaths.js +28 -0
- package/dist/shared/types.d.ts +45 -0
- package/dist/shared/types.js +1 -0
- package/package.json +64 -0
- package/templates/app.js +236 -0
- package/templates/page.html +23 -0
- package/templates/styles.css +157 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryuta Otsuka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# doc-repo
|
|
2
|
+
|
|
3
|
+
リポジトリ内の Markdown を、フォルダツリーでたどれる静的ドキュメントサイトに変換する CLI です。
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> `doc-repo` は現時点で alpha 想定です。CLI の引数仕様や生成ファイル構成は将来変更される可能性があります。
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## なぜ使うのか
|
|
11
|
+
|
|
12
|
+
リポジトリ内に仕様書・設計メモ・運用ドキュメントが増えると、次の課題が起きやすくなります。
|
|
13
|
+
|
|
14
|
+
- Markdown ファイルが複数階層に散らばり、全体を追いづらい
|
|
15
|
+
- エディタを開かないと文書の中身を横断閲覧しにくい
|
|
16
|
+
- 非開発者へ「まずどのファイルを見ればよいか」を共有しづらい
|
|
17
|
+
|
|
18
|
+
`doc-repo` は、これらを「左ツリー / 右本文」の 2 ペイン閲覧にまとめることで、閲覧導線をシンプルにします。
|
|
19
|
+
|
|
20
|
+
## 主な特徴
|
|
21
|
+
|
|
22
|
+
- リポジトリ内の `.md` を再帰的に自動収集
|
|
23
|
+
- ディレクトリ構造を保ったツリーナビゲーション
|
|
24
|
+
- ローカルサーバー不要(`index.html` を直接開ける)
|
|
25
|
+
- 対象ディレクトリ指定(`scopePath`)に対応
|
|
26
|
+
- `--open` で生成後にブラウザを自動起動
|
|
27
|
+
|
|
28
|
+
## クイックスタート
|
|
29
|
+
|
|
30
|
+
前提:
|
|
31
|
+
|
|
32
|
+
- Node.js 20 以上
|
|
33
|
+
|
|
34
|
+
リポジトリ内で実行:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx doc-repo
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
alpha タグで配布している期間は、次を利用してください。
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx doc-repo@alpha
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
生成後にブラウザで開く:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
open .doc-repo/index.html
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
特定ディレクトリのみ生成する場合:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx doc-repo specs
|
|
56
|
+
npx doc-repo docs/project
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CLI
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
doc-repo [scopePath] [--open]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| 引数 / オプション | 説明 | デフォルト |
|
|
66
|
+
| ----------------- | ---------------------------------------------------- | -------------- |
|
|
67
|
+
| `scopePath` | 生成対象ディレクトリ(Git ルート基準の相対パス) | Git ルート全体 |
|
|
68
|
+
| `--open` | 生成後に `.doc-repo/index.html` を既定ブラウザで開く | `false` |
|
|
69
|
+
|
|
70
|
+
### 対象ルートと収集対象の違い
|
|
71
|
+
|
|
72
|
+
- 対象ルート: Git ルート(見つからない場合はカレント)
|
|
73
|
+
- 収集対象: 対象ルート内で `scopePath` が指すディレクトリ配下
|
|
74
|
+
|
|
75
|
+
## 生成結果
|
|
76
|
+
|
|
77
|
+
`.doc-repo` 配下に、Markdown のツリーをミラーしたマルチページ静的サイトを生成します。
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
.doc-repo/
|
|
81
|
+
├── index.html # ホーム(README があれば README)へリダイレクト
|
|
82
|
+
├── styles.css
|
|
83
|
+
├── README.html
|
|
84
|
+
└── docs/
|
|
85
|
+
└── guide/
|
|
86
|
+
└── page.html
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
各 Markdown は単独の `.html` として出力され、文書間リンクは素の相対リンクになります。
|
|
90
|
+
そのため `file://` で直接開いても遷移できます。
|
|
91
|
+
|
|
92
|
+
信頼性に関する挙動:
|
|
93
|
+
|
|
94
|
+
- 生成成功時のみ `.doc-repo` を置換(再実行で最新化)
|
|
95
|
+
- 一時ディレクトリで生成してから切り替え
|
|
96
|
+
- 失敗時は既存 `.doc-repo` を保持
|
|
97
|
+
- Markdown 0 件時は空サイトを生成し、警告付き成功(exit code 0)
|
|
98
|
+
|
|
99
|
+
### 終了コード
|
|
100
|
+
|
|
101
|
+
| コード | 意味 |
|
|
102
|
+
| ------ | -------------------------- |
|
|
103
|
+
| `0` | 成功(警告付き成功を含む) |
|
|
104
|
+
| `1` | 失敗 |
|
|
105
|
+
|
|
106
|
+
## 現在の制限事項
|
|
107
|
+
|
|
108
|
+
- ローカルサーバー起動(`serve`)は未対応
|
|
109
|
+
- ファイル監視(`watch`)は未対応
|
|
110
|
+
- ブラウザ上での Markdown 編集は未対応
|
|
111
|
+
- include/exclude の詳細設定は未対応
|
|
112
|
+
|
|
113
|
+
## Markdown 対応方針(現状)
|
|
114
|
+
|
|
115
|
+
- 変換ライブラリ: `markdown-it`
|
|
116
|
+
- `html: false`(Markdown 内の生 HTML は無効)
|
|
117
|
+
- `linkify: true`, `typographer: true`
|
|
118
|
+
- GFM の一部拡張(例: task list、Mermaid、コードハイライト)は未対応
|
|
119
|
+
- 相対画像・相対リンクは現状「入力ファイル配置を維持した再配置」をしていないため、構成によっては期待通りに表示されない場合あり
|
|
120
|
+
|
|
121
|
+
## セキュリティ注意
|
|
122
|
+
|
|
123
|
+
- 生 HTML は無効化していますが、生成対象は基本的に信頼できるリポジトリを想定してください
|
|
124
|
+
- 未知のリポジトリに対して実行する場合は、生成結果を確認してから共有してください
|
|
125
|
+
|
|
126
|
+
## `.doc-repo` の Git 管理方針
|
|
127
|
+
|
|
128
|
+
用途に応じて選択してください。
|
|
129
|
+
|
|
130
|
+
- 一時生成物として扱う場合: `.gitignore` に `.doc-repo/` を追加
|
|
131
|
+
- 成果物配布・公開に使う場合: 生成物コミット運用も可(毎回再生成で置換される前提)
|
|
132
|
+
|
|
133
|
+
## 開発者向け
|
|
134
|
+
|
|
135
|
+
開発者向け手順:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm install
|
|
139
|
+
npm run dev
|
|
140
|
+
npm run dev -- specs
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
ビルド:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run build
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
ビルド済み CLI 実行:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
node dist/cli/index.js
|
|
153
|
+
node dist/cli/index.js specs
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Issues / Feedback
|
|
157
|
+
|
|
158
|
+
不具合報告や要望は GitHub Issues を利用してください。
|
|
159
|
+
|
|
160
|
+
## ライセンス
|
|
161
|
+
|
|
162
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# doc-repo
|
|
2
|
+
|
|
3
|
+
doc-repo is a CLI that converts Markdown files in your repository into a browsable static documentation site with a directory tree.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> `doc-repo` is currently in alpha. CLI arguments and generated file structure may change in future releases.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Why doc-repo?
|
|
11
|
+
|
|
12
|
+
As repositories grow, Markdown documents (specs, design notes, operational docs) tend to become harder to navigate.
|
|
13
|
+
|
|
14
|
+
- Markdown files are scattered across multiple directories.
|
|
15
|
+
- It is hard to browse content across files without opening an editor.
|
|
16
|
+
- It is difficult to share a clear reading path with non-developers.
|
|
17
|
+
|
|
18
|
+
doc-repo addresses this by generating a two-pane viewer (left tree / right content) that makes repository documents easier to explore.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- Recursively discovers `.md` files in your repository
|
|
23
|
+
- Preserves directory structure in tree navigation
|
|
24
|
+
- Works without a local server (`index.html` can be opened directly)
|
|
25
|
+
- Supports directory-scoped generation (`scopePath`)
|
|
26
|
+
- Supports `--open` to launch the generated page automatically
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Prerequisite:
|
|
31
|
+
|
|
32
|
+
- Node.js 20+
|
|
33
|
+
|
|
34
|
+
Run inside a repository:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx doc-repo
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If the package is published under an alpha tag, use:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx doc-repo@alpha
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Open the generated site:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
open .doc-repo/index.html
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Generate only a specific directory:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx doc-repo specs
|
|
56
|
+
npx doc-repo docs/project
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CLI
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
doc-repo [scopePath] [--open]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Argument / Option | Description | Default |
|
|
66
|
+
| ----------------- | ---------------------------------------------------------------------- | -------------- |
|
|
67
|
+
| `scopePath` | Directory to generate from (path relative to Git root) | Whole Git root |
|
|
68
|
+
| `--open` | Open `.doc-repo/index.html` with your default browser after generation | `false` |
|
|
69
|
+
|
|
70
|
+
### Target Root vs Collection Scope
|
|
71
|
+
|
|
72
|
+
- Target root: Git root (or current directory when Git root is not found)
|
|
73
|
+
- Collection scope: Directory under target root resolved from `scopePath`
|
|
74
|
+
|
|
75
|
+
## Output
|
|
76
|
+
|
|
77
|
+
doc-repo generates a multi-page static site under `.doc-repo`, mirroring your Markdown tree:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
.doc-repo/
|
|
81
|
+
├── index.html # redirects to the home page (README if present)
|
|
82
|
+
├── styles.css
|
|
83
|
+
├── README.html
|
|
84
|
+
└── docs/
|
|
85
|
+
└── guide/
|
|
86
|
+
└── page.html
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each Markdown file becomes a standalone `.html`, and links between documents are
|
|
90
|
+
plain relative links, so it works even when opened directly via `file://`.
|
|
91
|
+
|
|
92
|
+
Reliability behavior:
|
|
93
|
+
|
|
94
|
+
- Replaces `.doc-repo` only after a successful generation
|
|
95
|
+
- Generates in a staging directory before publishing
|
|
96
|
+
- Keeps existing `.doc-repo` when generation fails
|
|
97
|
+
- Generates an empty site and exits successfully with warning when no Markdown files are found
|
|
98
|
+
|
|
99
|
+
### Exit Codes
|
|
100
|
+
|
|
101
|
+
| Code | Meaning |
|
|
102
|
+
| ---- | ----------------------------------------- |
|
|
103
|
+
| `0` | Success (including success with warnings) |
|
|
104
|
+
| `1` | Failure |
|
|
105
|
+
|
|
106
|
+
## Current Limitations
|
|
107
|
+
|
|
108
|
+
- Local server mode (`serve`) is not supported yet
|
|
109
|
+
- File watching (`watch`) is not supported yet
|
|
110
|
+
- Browser-based Markdown editing is not supported yet
|
|
111
|
+
- Detailed include/exclude rules are not supported yet
|
|
112
|
+
|
|
113
|
+
## Markdown Support (Current)
|
|
114
|
+
|
|
115
|
+
- Converter: `markdown-it`
|
|
116
|
+
- `html: false` (raw HTML in Markdown is disabled)
|
|
117
|
+
- `linkify: true`, `typographer: true`
|
|
118
|
+
- Some GFM extensions (task lists, Mermaid, code highlighting) are not yet supported
|
|
119
|
+
- Relative images/links may not render as expected in some repository layouts because assets are not currently rewritten/rebased
|
|
120
|
+
|
|
121
|
+
## Security Notes
|
|
122
|
+
|
|
123
|
+
- Raw HTML is disabled, but generation is still intended for trusted repositories
|
|
124
|
+
- If you run doc-repo on unknown repositories, review output before sharing
|
|
125
|
+
|
|
126
|
+
## Git Policy for `.doc-repo`
|
|
127
|
+
|
|
128
|
+
Choose a policy based on your use case:
|
|
129
|
+
|
|
130
|
+
- Treat as temporary artifact: add `.doc-repo/` to `.gitignore`
|
|
131
|
+
- Treat as distributable artifact: committing generated files is also possible (output is replaced on each successful run)
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
For contributors:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm install
|
|
139
|
+
npm run dev
|
|
140
|
+
npm run dev -- specs
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Build:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run build
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Run compiled CLI:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
node dist/cli/index.js
|
|
153
|
+
node dist/cli/index.js specs
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Issues / Feedback
|
|
157
|
+
|
|
158
|
+
Please use GitHub Issues for bug reports and feature requests.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const formatResultMessage = (result) => {
|
|
2
|
+
const lines = [result.message, `生成対象: ${result.targetPath}`, `出力先: ${result.outputDir}`];
|
|
3
|
+
if (result.warnings.length > 0) {
|
|
4
|
+
lines.push("警告:");
|
|
5
|
+
for (const warning of result.warnings) {
|
|
6
|
+
lines.push(`- ${warning}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
if (result.status === "failure") {
|
|
10
|
+
if (result.errorReason) {
|
|
11
|
+
lines.push(`理由: ${result.errorReason}`);
|
|
12
|
+
}
|
|
13
|
+
if (result.hint) {
|
|
14
|
+
lines.push(`対処: ${result.hint}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return lines.join("\n");
|
|
18
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { generateSite } from "../core/site/generateSite.js";
|
|
6
|
+
import { formatResultMessage } from "./formatResultMessage.js";
|
|
7
|
+
import { resolveExitCode } from "./exitCode.js";
|
|
8
|
+
const openFile = (filePath) => {
|
|
9
|
+
if (process.platform === "win32") {
|
|
10
|
+
const child = spawn("cmd", ["/c", "start", "", filePath], { detached: true, stdio: "ignore" });
|
|
11
|
+
child.unref();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
15
|
+
const child = spawn(cmd, [filePath], { detached: true, stdio: "ignore" });
|
|
16
|
+
child.unref();
|
|
17
|
+
};
|
|
18
|
+
export const run = async (argv = process.argv, cwd = process.cwd()) => {
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name("doc-repo")
|
|
22
|
+
.description("Generate a static documentation site from Markdown files.")
|
|
23
|
+
.argument("[scopePath]", "Repository-relative directory to generate")
|
|
24
|
+
.option("--open", "生成後にブラウザで index.html を開く")
|
|
25
|
+
.action(async (scopePath, options) => {
|
|
26
|
+
const result = await generateSite({ cwd, scopePath });
|
|
27
|
+
const message = formatResultMessage(result);
|
|
28
|
+
if (result.status === "failure") {
|
|
29
|
+
console.error(message);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(message);
|
|
33
|
+
if (options?.open) {
|
|
34
|
+
openFile(path.join(result.outputDir, "index.html"));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
process.exitCode = resolveExitCode(result);
|
|
38
|
+
});
|
|
39
|
+
await program.parseAsync(argv);
|
|
40
|
+
};
|
|
41
|
+
if (process.env.VITEST !== "true") {
|
|
42
|
+
void run();
|
|
43
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import MarkdownIt from "markdown-it";
|
|
3
|
+
import { assetHref, docHref } from "../../shared/sitePaths.js";
|
|
4
|
+
const md = new MarkdownIt({
|
|
5
|
+
html: false,
|
|
6
|
+
linkify: true,
|
|
7
|
+
typographer: true,
|
|
8
|
+
});
|
|
9
|
+
const EXTERNAL_OR_HASH = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
|
|
10
|
+
// markdown-it は href を内部でパーセントエンコードするため、解決前に生パスへ戻す。
|
|
11
|
+
// 不正なエスケープはデコードせず元の値を返す(安全側)。
|
|
12
|
+
const decodePathSafe = (value) => {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(value);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const splitUrlAndSuffix = (url) => {
|
|
21
|
+
const hashIndex = url.indexOf("#");
|
|
22
|
+
const queryIndex = url.indexOf("?");
|
|
23
|
+
const firstSuffixIndex = hashIndex === -1 ? queryIndex : queryIndex === -1 ? hashIndex : Math.min(hashIndex, queryIndex);
|
|
24
|
+
if (firstSuffixIndex === -1) {
|
|
25
|
+
return { rawPath: decodePathSafe(url), suffix: "" };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
rawPath: decodePathSafe(url.slice(0, firstSuffixIndex)),
|
|
29
|
+
suffix: url.slice(firstSuffixIndex),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const rebaseRepoRelativeUrl = (url, relativePath) => {
|
|
33
|
+
if (!url || EXTERNAL_OR_HASH.test(url)) {
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
const { rawPath, suffix } = splitUrlAndSuffix(url);
|
|
37
|
+
const pathFromRoot = resolvePathFromRoot(rawPath, relativePath);
|
|
38
|
+
if (!pathFromRoot) {
|
|
39
|
+
return url;
|
|
40
|
+
}
|
|
41
|
+
return assetHref(relativePath, pathFromRoot, suffix);
|
|
42
|
+
};
|
|
43
|
+
// リポジトリルートからの正規化パスを求める。ルート外(../ で抜ける)や空は null。
|
|
44
|
+
const resolvePathFromRoot = (rawPath, relativePath) => {
|
|
45
|
+
if (!rawPath) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const normalizedRelativePath = relativePath.split(path.sep).join(path.posix.sep);
|
|
49
|
+
const pathFromRoot = rawPath.startsWith("/")
|
|
50
|
+
? rawPath.slice(1)
|
|
51
|
+
: path.posix.normalize(path.posix.join(path.posix.dirname(normalizedRelativePath), rawPath));
|
|
52
|
+
if (!pathFromRoot || pathFromRoot.startsWith("../")) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return pathFromRoot;
|
|
56
|
+
};
|
|
57
|
+
// 既知のページID集合に対してリンク先を解決し、一致したIDだけを返す。
|
|
58
|
+
// 推測ではなく実在ページとの突き合わせで判定するため、遷移可否が安定する。
|
|
59
|
+
// 厳密解決に失敗した .md リンクは、同名ページが一意な場合のみベース名で救済する。
|
|
60
|
+
const resolveDocId = (href, relativePath, knownIds, uniqueBasenames) => {
|
|
61
|
+
if (!href || EXTERNAL_OR_HASH.test(href)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const { rawPath } = splitUrlAndSuffix(href);
|
|
65
|
+
const pathFromRoot = resolvePathFromRoot(rawPath, relativePath);
|
|
66
|
+
if (/\.md$/i.test(rawPath)) {
|
|
67
|
+
if (pathFromRoot) {
|
|
68
|
+
const id = pathFromRoot.replace(/\.md$/i, "");
|
|
69
|
+
if (knownIds.has(id)) {
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// フォールバック: 相対パスがズレていても、同名ページが 1 件だけなら救済する。
|
|
74
|
+
const basename = path.posix.basename(rawPath).replace(/\.md$/i, "");
|
|
75
|
+
return uniqueBasenames.get(basename) ?? null;
|
|
76
|
+
}
|
|
77
|
+
if (!pathFromRoot) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const basename = path.posix.basename(rawPath);
|
|
81
|
+
const isDirectoryLike = rawPath.endsWith("/") || !basename.includes(".");
|
|
82
|
+
if (isDirectoryLike) {
|
|
83
|
+
const indexId = path.posix.join(pathFromRoot, "index");
|
|
84
|
+
if (knownIds.has(indexId)) {
|
|
85
|
+
return indexId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return knownIds.has(pathFromRoot) ? pathFromRoot : null;
|
|
89
|
+
};
|
|
90
|
+
const imageRule = md.renderer.rules.image;
|
|
91
|
+
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
|
92
|
+
const src = tokens[idx]?.attrGet("src");
|
|
93
|
+
const { relativePath } = (env ?? {});
|
|
94
|
+
if (src && relativePath) {
|
|
95
|
+
tokens[idx]?.attrSet("src", rebaseRepoRelativeUrl(src, relativePath));
|
|
96
|
+
}
|
|
97
|
+
return imageRule ? imageRule(tokens, idx, options, env, self) : self.renderToken(tokens, idx, options);
|
|
98
|
+
};
|
|
99
|
+
const linkOpenRule = md.renderer.rules.link_open;
|
|
100
|
+
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
|
101
|
+
const href = tokens[idx]?.attrGet("href");
|
|
102
|
+
const { relativePath, knownIds, uniqueBasenames } = (env ?? {});
|
|
103
|
+
if (href && relativePath) {
|
|
104
|
+
const docId = knownIds ? resolveDocId(href, relativePath, knownIds, uniqueBasenames ?? new Map()) : null;
|
|
105
|
+
if (docId) {
|
|
106
|
+
// 実在ページに一致したリンクは、現在ページからの相対 .html リンクへ。
|
|
107
|
+
tokens[idx]?.attrSet("href", docHref(relativePath, docId));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// それ以外(画像/添付/外部/未解決)は資産として相対パスへリベースする。
|
|
111
|
+
tokens[idx]?.attrSet("href", rebaseRepoRelativeUrl(href, relativePath));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return linkOpenRule ? linkOpenRule(tokens, idx, options, env, self) : self.renderToken(tokens, idx, options);
|
|
115
|
+
};
|
|
116
|
+
const findTitle = (source, relativePath) => {
|
|
117
|
+
const heading = source.match(/^#\s+(.+)$/m);
|
|
118
|
+
if (heading?.[1]) {
|
|
119
|
+
return heading[1].trim();
|
|
120
|
+
}
|
|
121
|
+
return path.basename(relativePath, ".md");
|
|
122
|
+
};
|
|
123
|
+
export const convertMarkdown = (source, relativePath, knownIds = new Set(), uniqueBasenames = new Map()) => {
|
|
124
|
+
const title = findTitle(source, relativePath);
|
|
125
|
+
const html = md.render(source, { relativePath, knownIds, uniqueBasenames });
|
|
126
|
+
return { title, html };
|
|
127
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
export const detectRoot = async (cwd) => {
|
|
4
|
+
let current = path.resolve(cwd);
|
|
5
|
+
while (true) {
|
|
6
|
+
const gitPath = path.join(current, ".git");
|
|
7
|
+
if (await fs.pathExists(gitPath)) {
|
|
8
|
+
return {
|
|
9
|
+
detectedRoot: current,
|
|
10
|
+
usedFallback: false,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const parent = path.dirname(current);
|
|
14
|
+
if (parent === current) {
|
|
15
|
+
return {
|
|
16
|
+
detectedRoot: path.resolve(cwd),
|
|
17
|
+
usedFallback: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
export const scanMarkdown = async (rootDir, scanDir = rootDir) => {
|
|
4
|
+
const relativePrefix = path.relative(rootDir, scanDir);
|
|
5
|
+
const matches = await fg("**/*.md", {
|
|
6
|
+
cwd: scanDir,
|
|
7
|
+
onlyFiles: true,
|
|
8
|
+
dot: false,
|
|
9
|
+
ignore: ["node_modules/**", ".git/**", ".doc-repo/**", "dist/**"],
|
|
10
|
+
});
|
|
11
|
+
return matches
|
|
12
|
+
.sort((a, b) => a.localeCompare(b))
|
|
13
|
+
.map((matchedPath) => {
|
|
14
|
+
const relativePath = relativePrefix
|
|
15
|
+
? path.posix.join(relativePrefix.split(path.sep).join(path.posix.sep), matchedPath)
|
|
16
|
+
: matchedPath;
|
|
17
|
+
return {
|
|
18
|
+
relativePath,
|
|
19
|
+
absolutePath: path.join(rootDir, relativePath),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const atomicPublish: (stagingDir: string, outputDir: string) => Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
export const atomicPublish = async (stagingDir, outputDir) => {
|
|
3
|
+
const backupDir = `${outputDir}.__backup__`;
|
|
4
|
+
await fs.remove(backupDir);
|
|
5
|
+
try {
|
|
6
|
+
if (await fs.pathExists(outputDir)) {
|
|
7
|
+
await fs.move(outputDir, backupDir, { overwrite: true });
|
|
8
|
+
}
|
|
9
|
+
await fs.move(stagingDir, outputDir, { overwrite: true });
|
|
10
|
+
await fs.remove(backupDir);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (!(await fs.pathExists(outputDir)) && (await fs.pathExists(backupDir))) {
|
|
14
|
+
await fs.move(backupDir, outputDir, { overwrite: true });
|
|
15
|
+
}
|
|
16
|
+
if (await fs.pathExists(stagingDir)) {
|
|
17
|
+
await fs.remove(stagingDir);
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
};
|