living-documentation 1.0.1 → 1.2.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/dist/src/frontend/admin.html +476 -243
- package/dist/src/frontend/index.html +5 -5
- package/dist/src/lib/config.d.ts +1 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +1 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/parser.js +2 -2
- package/dist/src/lib/parser.js.map +1 -1
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +36 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts.map +1 -1
- package/dist/src/routes/config.js +10 -0
- package/dist/src/routes/config.js.map +1 -1
- package/dist/src/routes/documents.d.ts.map +1 -1
- package/dist/src/routes/documents.js +56 -11
- package/dist/src/routes/documents.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +2 -0
- package/dist/src/server.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,248 +1,318 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en" class="h-full">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<!--
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<!-- ── Form ── -->
|
|
60
|
-
<form id="config-form" class="space-y-6" novalidate>
|
|
61
|
-
|
|
62
|
-
<!-- Read-only info card -->
|
|
63
|
-
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5">
|
|
64
|
-
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">Server Info</h3>
|
|
65
|
-
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin — Living Documentation</title>
|
|
7
|
+
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
tailwind.config = { darkMode: "class", theme: { extend: {} } };
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
</head>
|
|
15
|
+
|
|
16
|
+
<body
|
|
17
|
+
class="h-full bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100"
|
|
18
|
+
>
|
|
19
|
+
<!-- ── Header ── -->
|
|
20
|
+
<header
|
|
21
|
+
class="flex items-center justify-between px-4 h-14 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm"
|
|
22
|
+
>
|
|
23
|
+
<div class="flex items-center gap-3">
|
|
24
|
+
<span
|
|
25
|
+
class="text-blue-600 dark:text-blue-400 text-xl select-none"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
>📚</span
|
|
28
|
+
>
|
|
29
|
+
<h1 class="text-base font-semibold tracking-tight">Admin Panel</h1>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="flex items-center gap-3">
|
|
33
|
+
<button
|
|
34
|
+
id="dark-toggle"
|
|
35
|
+
title="Toggle dark mode"
|
|
36
|
+
class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
37
|
+
>
|
|
38
|
+
<span id="dark-icon" class="text-lg leading-none">☾</span>
|
|
39
|
+
</button>
|
|
40
|
+
<span class="text-gray-300 dark:text-gray-700">|</span>
|
|
41
|
+
<a
|
|
42
|
+
href="/"
|
|
43
|
+
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
|
|
44
|
+
>
|
|
45
|
+
Back to docs →
|
|
46
|
+
</a>
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<!-- ── Page ── -->
|
|
51
|
+
<main class="max-w-6xl mx-auto px-6 py-10">
|
|
52
|
+
|
|
53
|
+
<!-- ── Two-column layout ── -->
|
|
54
|
+
<form id="config-form" novalidate>
|
|
55
|
+
|
|
56
|
+
<!-- Title + Save on same row -->
|
|
57
|
+
<div class="flex items-center justify-between mb-8">
|
|
66
58
|
<div>
|
|
67
|
-
<
|
|
68
|
-
<
|
|
59
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-50">Configuration</h2>
|
|
60
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
61
|
+
Settings are saved to
|
|
62
|
+
<code class="text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">.living-doc.json</code>
|
|
63
|
+
in your docs folder.
|
|
64
|
+
</p>
|
|
69
65
|
</div>
|
|
70
|
-
<div>
|
|
71
|
-
<
|
|
72
|
-
<
|
|
66
|
+
<div class="flex items-center gap-4 shrink-0">
|
|
67
|
+
<div id="save-msg" class="text-sm"></div>
|
|
68
|
+
<button type="submit"
|
|
69
|
+
class="px-5 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-950">
|
|
70
|
+
Save changes
|
|
71
|
+
</button>
|
|
73
72
|
</div>
|
|
74
|
-
</
|
|
75
|
-
</div>
|
|
73
|
+
</div>
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 space-y-5">
|
|
79
|
-
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Appearance & Metadata</h3>
|
|
75
|
+
<div class="grid grid-cols-2 gap-8 items-start">
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<label class="field-label" for="field-title">Site Title</label>
|
|
84
|
-
<input id="field-title" name="title" type="text" class="field-input"
|
|
85
|
-
placeholder="Living Documentation" />
|
|
86
|
-
<p class="field-hint">Displayed in the browser tab and sidebar header.</p>
|
|
87
|
-
</div>
|
|
77
|
+
<!-- ── Left column ── -->
|
|
78
|
+
<div class="space-y-6">
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
80
|
+
<!-- Server Info -->
|
|
81
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5">
|
|
82
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">Server Info</h3>
|
|
83
|
+
<dl class="grid grid-cols-[1fr_auto] gap-x-6 gap-y-3 text-sm">
|
|
84
|
+
<div>
|
|
85
|
+
<dt class="text-xs text-gray-400 uppercase tracking-wide mb-0.5">Docs Folder</dt>
|
|
86
|
+
<dd id="info-folder" class="font-mono text-xs text-gray-700 dark:text-gray-300 break-all">—</dd>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<dt class="text-xs text-gray-400 uppercase tracking-wide mb-0.5">Port</dt>
|
|
90
|
+
<dd id="info-port" class="font-mono text-xs text-gray-700 dark:text-gray-300">—</dd>
|
|
91
|
+
</div>
|
|
92
|
+
</dl>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Extra Files -->
|
|
96
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 space-y-4">
|
|
97
|
+
<div>
|
|
98
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">General — Extra Files</h3>
|
|
99
|
+
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Add Markdown files from outside the docs folder to the General section.</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- File browser -->
|
|
103
|
+
<div class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
104
|
+
<div class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
|
105
|
+
<button id="browse-up" onclick="browseUp()"
|
|
106
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0">
|
|
107
|
+
↑ Up
|
|
108
|
+
</button>
|
|
109
|
+
<span id="browse-path" class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"></span>
|
|
110
|
+
</div>
|
|
111
|
+
<div id="browse-list" class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"></div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Added files -->
|
|
115
|
+
<div>
|
|
116
|
+
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Added files</p>
|
|
117
|
+
<div id="extra-files-list" class="space-y-1"></div>
|
|
118
|
+
<p id="extra-files-empty" class="text-xs text-gray-400 italic">No extra files added yet.</p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
</div>
|
|
99
123
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
<!-- ── Right column ── -->
|
|
125
|
+
<div class="space-y-6">
|
|
126
|
+
|
|
127
|
+
<!-- Appearance & Metadata -->
|
|
128
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-800">
|
|
129
|
+
<div class="px-5 py-4">
|
|
130
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Appearance & Metadata</h3>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
133
|
+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="field-title">Site Title</label>
|
|
134
|
+
<input id="field-title" name="title" type="text"
|
|
135
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
136
|
+
placeholder="Living Documentation" />
|
|
137
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Displayed in the browser tab and sidebar header.</p>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
140
|
+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="field-theme">Default Theme</label>
|
|
141
|
+
<select id="field-theme" name="theme"
|
|
142
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
143
|
+
<option value="system">System (follow OS preference)</option>
|
|
144
|
+
<option value="light">Light</option>
|
|
145
|
+
<option value="dark">Dark</option>
|
|
146
|
+
</select>
|
|
147
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">Users can always override with the toggle button.</p>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
150
|
+
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300" for="field-pattern">Filename Pattern</label>
|
|
151
|
+
<input id="field-pattern" name="filenamePattern" type="text"
|
|
152
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
153
|
+
placeholder="YYYY_MM_DD_[Category]_title" />
|
|
154
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
155
|
+
Parsed for date, category, and title.
|
|
156
|
+
<span class="font-mono bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-1 rounded">YYYY_MM_DD_[Category]_title.md</span>
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Pattern Preview -->
|
|
162
|
+
<div id="pattern-preview" class="rounded-xl border border-blue-100 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/30 p-5">
|
|
163
|
+
<h3 class="text-sm font-semibold text-blue-700 dark:text-blue-400 mb-3">Pattern Preview</h3>
|
|
164
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">How your pattern parses example filenames:</p>
|
|
165
|
+
<div id="preview-rows" class="space-y-2 text-sm"></div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
</div>
|
|
109
169
|
</div>
|
|
110
|
-
</
|
|
170
|
+
</form>
|
|
171
|
+
</main>
|
|
111
172
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
173
|
+
<script>
|
|
174
|
+
// ── Boot ───────────────────────────────────────────────────
|
|
175
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
176
|
+
applyDarkMode(loadDarkPref());
|
|
177
|
+
setupDarkToggle();
|
|
178
|
+
await loadConfig();
|
|
179
|
+
setupPatternPreview();
|
|
180
|
+
document
|
|
181
|
+
.getElementById("config-form")
|
|
182
|
+
.addEventListener("submit", saveConfig);
|
|
183
|
+
});
|
|
119
184
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
185
|
+
// ── Dark mode ──────────────────────────────────────────────
|
|
186
|
+
function loadDarkPref() {
|
|
187
|
+
const saved = localStorage.getItem("ld-dark");
|
|
188
|
+
if (saved !== null) return saved === "true";
|
|
189
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
190
|
+
}
|
|
191
|
+
function applyDarkMode(dark) {
|
|
192
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
193
|
+
document.getElementById("dark-icon").textContent = dark ? "☀" : "☾";
|
|
194
|
+
}
|
|
195
|
+
function setupDarkToggle() {
|
|
196
|
+
document.getElementById("dark-toggle").addEventListener("click", () => {
|
|
197
|
+
const isDark = document.documentElement.classList.toggle("dark");
|
|
198
|
+
localStorage.setItem("ld-dark", isDark);
|
|
199
|
+
document.getElementById("dark-icon").textContent = isDark ? "☀" : "☾";
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Config ─────────────────────────────────────────────────
|
|
204
|
+
async function loadConfig() {
|
|
205
|
+
try {
|
|
206
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
207
|
+
document.getElementById("info-folder").textContent =
|
|
208
|
+
cfg.docsFolder || "—";
|
|
209
|
+
document.getElementById("info-port").textContent = cfg.port || "—";
|
|
210
|
+
document.getElementById("field-title").value = cfg.title || "";
|
|
211
|
+
document.getElementById("field-theme").value = cfg.theme || "system";
|
|
212
|
+
document.getElementById("field-pattern").value =
|
|
213
|
+
cfg.filenamePattern || "";
|
|
214
|
+
updatePreview(cfg.filenamePattern);
|
|
215
|
+
initExtraFiles(cfg);
|
|
216
|
+
} catch {
|
|
217
|
+
showMsg("Failed to load config.", "error");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function saveConfig(e) {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
const payload = {
|
|
224
|
+
title: document.getElementById("field-title").value.trim(),
|
|
225
|
+
theme: document.getElementById("field-theme").value,
|
|
226
|
+
filenamePattern: document
|
|
227
|
+
.getElementById("field-pattern")
|
|
228
|
+
.value.trim(),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch("/api/config", {
|
|
233
|
+
method: "PUT",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify(payload),
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok) throw new Error(await res.text());
|
|
238
|
+
showMsg("Saved!", "ok");
|
|
239
|
+
} catch (err) {
|
|
240
|
+
showMsg("Save failed: " + err.message, "error");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function showMsg(text, type) {
|
|
245
|
+
const el = document.getElementById("save-msg");
|
|
246
|
+
el.textContent = text;
|
|
247
|
+
el.className =
|
|
248
|
+
"text-sm " +
|
|
249
|
+
(type === "ok"
|
|
250
|
+
? "text-green-600 dark:text-green-400"
|
|
251
|
+
: "text-red-600 dark:text-red-400");
|
|
252
|
+
if (type === "ok")
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
el.textContent = "";
|
|
255
|
+
}, 3000);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Pattern preview ────────────────────────────────────────
|
|
259
|
+
const EXAMPLES = [
|
|
260
|
+
"2024_01_15_[DevOps]_deploy_pipeline.md",
|
|
261
|
+
"2023_11_03_[Frontend]_react_hooks_guide.md",
|
|
262
|
+
"2025_06_20_meeting_notes.md",
|
|
263
|
+
"readme.md",
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
// Parse like server does
|
|
267
|
+
const FULL_PAT = /^(\d{4}_\d{2}_\d{2})_\[([^\]]+)\]_(.+)\.md$/i;
|
|
268
|
+
const DATE_ONLY = /^(\d{4}_\d{2}_\d{2})_(.+)\.md$/i;
|
|
269
|
+
|
|
270
|
+
function parsePreview(filename) {
|
|
271
|
+
const full = filename.match(FULL_PAT);
|
|
272
|
+
if (full) {
|
|
273
|
+
const [, d, cat, t] = full;
|
|
274
|
+
return {
|
|
275
|
+
date: d.replace(/_/g, "-"),
|
|
276
|
+
category: cat,
|
|
277
|
+
title: titleCase(t),
|
|
278
|
+
match: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const d = filename.match(DATE_ONLY);
|
|
282
|
+
if (d) {
|
|
283
|
+
return {
|
|
284
|
+
date: d[1].replace(/_/g, "-"),
|
|
285
|
+
category: "General",
|
|
286
|
+
title: titleCase(d[2]),
|
|
287
|
+
match: true,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
date: null,
|
|
292
|
+
category: "General",
|
|
293
|
+
title: filename.replace(".md", ""),
|
|
294
|
+
match: false,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function titleCase(s) {
|
|
299
|
+
return s
|
|
300
|
+
.split("_")
|
|
301
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
302
|
+
.join(" ");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function updatePreview() {
|
|
306
|
+
const rows = document.getElementById("preview-rows");
|
|
307
|
+
rows.innerHTML = EXAMPLES.map((f) => {
|
|
308
|
+
const p = parsePreview(f);
|
|
309
|
+
return `
|
|
240
310
|
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
|
|
241
311
|
<p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
|
|
242
312
|
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
243
313
|
<div>
|
|
244
314
|
<span class="text-gray-400 block mb-0.5">Date</span>
|
|
245
|
-
<span class="${p.date ?
|
|
315
|
+
<span class="${p.date ? "text-green-600 dark:text-green-400" : "text-gray-400"}">${p.date || "none"}</span>
|
|
246
316
|
</div>
|
|
247
317
|
<div>
|
|
248
318
|
<span class="text-gray-400 block mb-0.5">Category</span>
|
|
@@ -254,18 +324,181 @@ function updatePreview() {
|
|
|
254
324
|
</div>
|
|
255
325
|
</div>
|
|
256
326
|
</div>`;
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function setupPatternPreview() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
327
|
+
}).join("");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function setupPatternPreview() {
|
|
331
|
+
document
|
|
332
|
+
.getElementById("field-pattern")
|
|
333
|
+
.addEventListener("input", updatePreview);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function esc(s) {
|
|
337
|
+
return String(s)
|
|
338
|
+
.replace(/&/g, "&")
|
|
339
|
+
.replace(/</g, "<")
|
|
340
|
+
.replace(/>/g, ">")
|
|
341
|
+
.replace(/"/g, """)
|
|
342
|
+
.replace(/'/g, "'");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Extra Files / Browser ──────────────────────────────────
|
|
346
|
+
let extraFiles = [];
|
|
347
|
+
let browseParent = null;
|
|
348
|
+
let browseCurrent = "";
|
|
349
|
+
let docsFolder = "";
|
|
350
|
+
|
|
351
|
+
function initExtraFiles(cfg) {
|
|
352
|
+
extraFiles = cfg.extraFiles || [];
|
|
353
|
+
docsFolder = cfg.docsFolder || "";
|
|
354
|
+
renderExtraFiles();
|
|
355
|
+
loadBrowse(cfg.docsFolder || "/");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function relativeDisplay(filePath) {
|
|
359
|
+
if (!docsFolder) return filePath;
|
|
360
|
+
const fromParts = docsFolder.split("/").filter(Boolean);
|
|
361
|
+
const toParts = filePath.split("/").filter(Boolean);
|
|
362
|
+
let common = 0;
|
|
363
|
+
while (
|
|
364
|
+
common < fromParts.length &&
|
|
365
|
+
common < toParts.length &&
|
|
366
|
+
fromParts[common] === toParts[common]
|
|
367
|
+
)
|
|
368
|
+
common++;
|
|
369
|
+
const ups = fromParts.length - common;
|
|
370
|
+
const downs = toParts.slice(common).join("/");
|
|
371
|
+
const rel = "../".repeat(ups) + downs;
|
|
372
|
+
return "$DOCS_FOLDER/" + rel;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function loadBrowse(dirPath) {
|
|
376
|
+
const list = document.getElementById("browse-list");
|
|
377
|
+
list.innerHTML =
|
|
378
|
+
'<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
379
|
+
try {
|
|
380
|
+
const data = await fetch(
|
|
381
|
+
"/api/browse?path=" + encodeURIComponent(dirPath),
|
|
382
|
+
).then((r) => r.json());
|
|
383
|
+
browseCurrent = data.current;
|
|
384
|
+
browseParent = data.parent;
|
|
385
|
+
document.getElementById("browse-path").textContent = relativeDisplay(data.current);
|
|
386
|
+
document.getElementById("browse-up").disabled = !data.parent;
|
|
387
|
+
|
|
388
|
+
const rows = [];
|
|
389
|
+
|
|
390
|
+
for (const dir of data.dirs) {
|
|
391
|
+
rows.push(`
|
|
392
|
+
<button data-path="${esc(dir.path)}" onclick="loadBrowse(this.dataset.path)"
|
|
393
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left
|
|
394
|
+
hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
395
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
396
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
|
|
397
|
+
</button>`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const file of data.files) {
|
|
401
|
+
const added = extraFiles.includes(file.path);
|
|
402
|
+
rows.push(`
|
|
403
|
+
<div class="flex items-center justify-between gap-2 px-3 py-2 text-sm
|
|
404
|
+
hover:bg-gray-50 dark:hover:bg-gray-800">
|
|
405
|
+
<span class="flex items-center gap-2 text-gray-700 dark:text-gray-300 min-w-0">
|
|
406
|
+
<span class="text-gray-400 shrink-0">📄</span>
|
|
407
|
+
<span class="truncate">${esc(file.name)}</span>
|
|
408
|
+
</span>
|
|
409
|
+
${
|
|
410
|
+
added
|
|
411
|
+
? '<span class="text-xs text-green-600 dark:text-green-400 shrink-0">Added</span>'
|
|
412
|
+
: `<button data-path="${esc(file.path)}" onclick="addExtraFile(this.dataset.path)"
|
|
413
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">+ Add</button>`
|
|
414
|
+
}
|
|
415
|
+
</div>`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (rows.length === 0) {
|
|
419
|
+
list.innerHTML =
|
|
420
|
+
'<p class="px-3 py-4 text-xs text-gray-400 text-center">Empty directory</p>';
|
|
421
|
+
} else {
|
|
422
|
+
list.innerHTML = rows.join("");
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
list.innerHTML =
|
|
426
|
+
'<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function browseUp() {
|
|
431
|
+
if (browseParent) loadBrowse(browseParent);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function addExtraFile(filePath) {
|
|
435
|
+
if (extraFiles.includes(filePath)) return;
|
|
436
|
+
extraFiles = [...extraFiles, filePath];
|
|
437
|
+
await saveExtraFiles();
|
|
438
|
+
renderExtraFiles();
|
|
439
|
+
loadBrowse(browseCurrent);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function removeExtraFile(filePath) {
|
|
443
|
+
extraFiles = extraFiles.filter((f) => f !== filePath);
|
|
444
|
+
await saveExtraFiles();
|
|
445
|
+
renderExtraFiles();
|
|
446
|
+
loadBrowse(browseCurrent);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function saveExtraFiles() {
|
|
450
|
+
try {
|
|
451
|
+
const res = await fetch("/api/config", {
|
|
452
|
+
method: "PUT",
|
|
453
|
+
headers: { "Content-Type": "application/json" },
|
|
454
|
+
body: JSON.stringify({ extraFiles }),
|
|
455
|
+
});
|
|
456
|
+
if (!res.ok) throw new Error(await res.text());
|
|
457
|
+
} catch (err) {
|
|
458
|
+
showMsg("Failed to save: " + err.message, "error");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function moveExtraFile(index, direction) {
|
|
463
|
+
const target = index + direction;
|
|
464
|
+
if (target < 0 || target >= extraFiles.length) return;
|
|
465
|
+
const next = [...extraFiles];
|
|
466
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
467
|
+
extraFiles = next;
|
|
468
|
+
saveExtraFiles();
|
|
469
|
+
renderExtraFiles();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderExtraFiles() {
|
|
473
|
+
const list = document.getElementById("extra-files-list");
|
|
474
|
+
const empty = document.getElementById("extra-files-empty");
|
|
475
|
+
if (extraFiles.length === 0) {
|
|
476
|
+
list.innerHTML = "";
|
|
477
|
+
empty.style.display = "";
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
empty.style.display = "none";
|
|
481
|
+
list.innerHTML = extraFiles
|
|
482
|
+
.map(
|
|
483
|
+
(f, i) => `
|
|
484
|
+
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800">
|
|
485
|
+
<div class="flex flex-col shrink-0">
|
|
486
|
+
<button data-i="${i}" data-d="-1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
|
|
487
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
|
|
488
|
+
${i === 0 ? "disabled" : ""}>↑</button>
|
|
489
|
+
<button data-i="${i}" data-d="1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
|
|
490
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
|
|
491
|
+
${i === extraFiles.length - 1 ? "disabled" : ""}>↓</button>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="overflow-x-auto flex-1 min-w-0">
|
|
494
|
+
<span class="font-mono text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap" title="${esc(f)}">${esc(relativeDisplay(f))}</span>
|
|
495
|
+
</div>
|
|
496
|
+
<button data-path="${esc(f)}" onclick="removeExtraFile(this.dataset.path)"
|
|
497
|
+
class="text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 shrink-0">Remove</button>
|
|
498
|
+
</div>`,
|
|
499
|
+
)
|
|
500
|
+
.join("");
|
|
501
|
+
}
|
|
502
|
+
</script>
|
|
503
|
+
</body>
|
|
271
504
|
</html>
|
|
@@ -287,15 +287,15 @@ function renderSidebar(docs) {
|
|
|
287
287
|
groups[doc.category].push(doc);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// Sort categories:
|
|
290
|
+
// Sort categories: General first, then alphabetical
|
|
291
291
|
const cats = Object.keys(groups).sort((a, b) => {
|
|
292
|
-
if (a === '
|
|
293
|
-
if (b === '
|
|
292
|
+
if (a === 'General') return -1;
|
|
293
|
+
if (b === 'General') return 1;
|
|
294
294
|
return a.localeCompare(b);
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
// Auto-expand
|
|
298
|
-
if (expandedCategories.size === 0)
|
|
297
|
+
// Auto-expand only General initially
|
|
298
|
+
if (expandedCategories.size === 0) expandedCategories.add('General');
|
|
299
299
|
|
|
300
300
|
tree.innerHTML = cats.map(cat => {
|
|
301
301
|
const catDocs = groups[cat];
|
package/dist/src/lib/config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface LivingDocConfig {
|
|
|
4
4
|
title: string;
|
|
5
5
|
theme: 'light' | 'dark' | 'system';
|
|
6
6
|
port: number;
|
|
7
|
+
extraFiles: string[];
|
|
7
8
|
}
|
|
8
9
|
export declare function getConfigPath(docsPath: string): string;
|
|
9
10
|
export declare function readConfig(docsPath: string): LivingDocConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/lib/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/lib/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAaD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAY5D;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,GAC9B,eAAe,CAMjB"}
|
package/dist/src/lib/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/lib/config.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/lib/config.ts"],"names":[],"mappings":";;;;;AAuBA,sCAEC;AAED,gCAYC;AAED,kCASC;AAlDD,4CAAoB;AACpB,gDAAwB;AAWxB,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAE3C,MAAM,QAAQ,GAAoB;IAChC,UAAU,EAAE,GAAG;IACf,eAAe,EAAE,6BAA6B;IAC9C,KAAK,EAAE,sBAAsB;IAC7B,KAAK,EAAE,QAAQ;IACf,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,EAAE;CACf,CAAC;AAEF,SAAgB,aAAa,CAAC,QAAgB;IAC5C,OAAO,cAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AAC9C,CAAC;AAED,SAAgB,UAAU,CAAC,QAAgB;IACzC,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,IAAI,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA6B,CAAC;YAC3D,OAAO,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;QACpC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;IACD,OAAO,EAAE,GAAG,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAC/C,CAAC;AAED,SAAgB,WAAW,CACzB,QAAgB,EAChB,KAA+B;IAE/B,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,OAAO,GAAoB,EAAE,GAAG,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;IAC1D,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC3C,YAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACxE,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/src/lib/parser.js
CHANGED
|
@@ -44,7 +44,7 @@ function parseFilename(filename) {
|
|
|
44
44
|
id,
|
|
45
45
|
filename,
|
|
46
46
|
title: titleCase(titlePart),
|
|
47
|
-
category: '
|
|
47
|
+
category: 'General',
|
|
48
48
|
date,
|
|
49
49
|
formattedDate: formatDate(date),
|
|
50
50
|
};
|
|
@@ -55,7 +55,7 @@ function parseFilename(filename) {
|
|
|
55
55
|
id,
|
|
56
56
|
filename,
|
|
57
57
|
title: rawTitle.charAt(0).toUpperCase() + rawTitle.slice(1),
|
|
58
|
-
category: '
|
|
58
|
+
category: 'General',
|
|
59
59
|
date: null,
|
|
60
60
|
formattedDate: null,
|
|
61
61
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/lib/parser.ts"],"names":[],"mappings":";;AAiCA,sCAyCC;AAjED,gDAAgD;AAChD,MAAM,YAAY,GAAG,8CAA8C,CAAC;AAEpE,mDAAmD;AACnD,MAAM,iBAAiB,GAAG,iCAAiC,CAAC;AAE5D,SAAS,UAAU,CAAC,GAAW;IAC7B,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,CAAC,kBAAkB,CAAC,OAAO,EAAE;QACnC,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,MAAM;QACb,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;KAChB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SACzE,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,SAAgB,aAAa,CAAC,QAAgB;IAC5C,MAAM,EAAE,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC1C,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC;QAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACxC,OAAO;YACL,EAAE;YACF,QAAQ;YACR,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC;YAC3B,QAAQ;YACR,IAAI;YACJ,aAAa,EAAE,UAAU,CAAC,IAAI,CAAC;SAChC,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACnD,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACxC,OAAO;YACL,EAAE;YACF,QAAQ;YACR,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC;YAC3B,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/lib/parser.ts"],"names":[],"mappings":";;AAiCA,sCAyCC;AAjED,gDAAgD;AAChD,MAAM,YAAY,GAAG,8CAA8C,CAAC;AAEpE,mDAAmD;AACnD,MAAM,iBAAiB,GAAG,iCAAiC,CAAC;AAE5D,SAAS,UAAU,CAAC,GAAW;IAC7B,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,CAAC,kBAAkB,CAAC,OAAO,EAAE;QACnC,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,MAAM;QACb,GAAG,EAAE,SAAS;QACd,QAAQ,EAAE,KAAK;KAChB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SACzE,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,SAAgB,aAAa,CAAC,QAAgB;IAC5C,MAAM,EAAE,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC1C,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC;QAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACxC,OAAO;YACL,EAAE;YACF,QAAQ;YACR,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC;YAC3B,QAAQ;YACR,IAAI;YACJ,aAAa,EAAE,UAAU,CAAC,IAAI,CAAC;SAChC,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACnD,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACxC,OAAO;YACL,EAAE;YACF,QAAQ;YACR,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC;YAC3B,QAAQ,EAAE,SAAS;YACnB,IAAI;YACJ,aAAa,EAAE,UAAU,CAAC,IAAI,CAAC;SAChC,CAAC;IACJ,CAAC;IAED,kCAAkC;IAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO;QACL,EAAE;QACF,QAAQ;QACR,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3D,QAAQ,EAAE,SAAS;QACnB,IAAI,EAAE,IAAI;QACV,aAAa,EAAE,IAAI;KACpB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../../src/routes/browse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAIpD,wBAAgB,YAAY,IAAI,MAAM,CA+BrC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.browseRouter = browseRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
function browseRouter() {
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
// GET /api/browse?path=... — list dirs and .md files at a given path
|
|
13
|
+
router.get('/', (req, res) => {
|
|
14
|
+
const rawPath = req.query.path || '/';
|
|
15
|
+
const current = path_1.default.resolve(rawPath);
|
|
16
|
+
try {
|
|
17
|
+
const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
18
|
+
const dirs = entries
|
|
19
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
20
|
+
.map((e) => ({ name: e.name, path: path_1.default.join(current, e.name) }))
|
|
21
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
22
|
+
const files = entries
|
|
23
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
|
|
24
|
+
.map((e) => ({ name: e.name, path: path_1.default.join(current, e.name) }))
|
|
25
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
const parsed = path_1.default.parse(current);
|
|
27
|
+
const parent = current === parsed.root ? null : path_1.default.dirname(current);
|
|
28
|
+
res.json({ current, parent, dirs, files });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
res.status(400).json({ error: 'Cannot read directory' });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return router;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=browse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browse.js","sourceRoot":"","sources":["../../../src/routes/browse.ts"],"names":[],"mappings":";;;;;AAIA,oCA+BC;AAnCD,qCAAoD;AACpD,4CAAoB;AACpB,gDAAwB;AAExB,SAAgB,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,qEAAqE;IACrE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QAC9C,MAAM,OAAO,GAAI,GAAG,CAAC,KAAK,CAAC,IAAe,IAAI,GAAG,CAAC;QAClD,MAAM,OAAO,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAEtC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,YAAE,CAAC,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAEjE,MAAM,IAAI,GAAG,OAAO;iBACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;iBACzD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,cAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;iBAChE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAEhD,MAAM,KAAK,GAAG,OAAO;iBAClB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;iBACjE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,cAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;iBAChE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAEhD,MAAM,MAAM,GAAG,cAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACnC,MAAM,MAAM,GAAG,OAAO,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAEtE,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/routes/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/routes/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAIpD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6CrD"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.configRouter = configRouter;
|
|
4
7
|
const express_1 = require("express");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
5
9
|
const config_1 = require("../lib/config");
|
|
6
10
|
function configRouter(docsPath) {
|
|
7
11
|
const router = (0, express_1.Router)();
|
|
@@ -30,6 +34,12 @@ function configRouter(docsPath) {
|
|
|
30
34
|
safe[key] = patch[key];
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
// extraFiles: only absolute .md paths
|
|
38
|
+
if ('extraFiles' in patch && Array.isArray(patch.extraFiles)) {
|
|
39
|
+
safe.extraFiles = patch.extraFiles.filter((f) => typeof f === 'string' &&
|
|
40
|
+
path_1.default.isAbsolute(f) &&
|
|
41
|
+
f.toLowerCase().endsWith('.md'));
|
|
42
|
+
}
|
|
33
43
|
const updated = (0, config_1.writeConfig)(docsPath, safe);
|
|
34
44
|
res.json(updated);
|
|
35
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/routes/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/routes/config.ts"],"names":[],"mappings":";;;;;AAIA,oCA6CC;AAjDD,qCAAoD;AACpD,gDAAwB;AACxB,0CAAyE;AAEzE,SAAgB,YAAY,CAAC,QAAgB;IAC3C,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,kBAAkB;IAClB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QAC/C,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,kBAAkB;IAClB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QAC9C,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,GAAG,CAAC,IAAgC,CAAC;YACnD,6DAA6D;YAC7D,MAAM,OAAO,GAA8B;gBACzC,OAAO;gBACP,iBAAiB;gBACjB,OAAO;aACR,CAAC;YACF,MAAM,IAAI,GAA6B,EAAE,CAAC;YAC1C,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;gBAC1B,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;oBAChB,IAAgC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;gBACtD,CAAC;YACH,CAAC;YACD,sCAAsC;YACtC,IAAI,YAAY,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC7D,IAAI,CAAC,UAAU,GAAI,KAAK,CAAC,UAAwB,CAAC,MAAM,CACtD,CAAC,CAAC,EAAe,EAAE,CACjB,OAAO,CAAC,KAAK,QAAQ;oBACrB,cAAI,CAAC,UAAU,CAAC,CAAC,CAAC;oBAClB,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAClC,CAAC;YACJ,CAAC;YACD,MAAM,OAAO,GAAG,IAAA,oBAAW,EAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC5C,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../src/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../../src/routes/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAwDpD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAqGxD"}
|
|
@@ -9,13 +9,27 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const marked_1 = require("marked");
|
|
11
11
|
const parser_1 = require("../lib/parser");
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const config_1 = require("../lib/config");
|
|
13
|
+
function listDocs(docsPath, extraFiles = []) {
|
|
14
|
+
// Extra files first, in config order, always General
|
|
15
|
+
const extraDocs = [];
|
|
16
|
+
for (const filePath of extraFiles) {
|
|
17
|
+
if (!filePath.endsWith('.md') || !fs_1.default.existsSync(filePath))
|
|
18
|
+
continue;
|
|
19
|
+
const filename = path_1.default.basename(filePath);
|
|
20
|
+
const meta = (0, parser_1.parseFilename)(filename);
|
|
21
|
+
extraDocs.push({
|
|
22
|
+
...meta,
|
|
23
|
+
id: encodeURIComponent(filePath.slice(0, -3)),
|
|
24
|
+
category: 'General',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// Regular docs sorted: dated newest first, then undated alphabetically
|
|
28
|
+
const regularDocs = fs_1.default
|
|
14
29
|
.readdirSync(docsPath)
|
|
15
|
-
.filter((f) => f.toLowerCase().endsWith('.md'))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
docs.sort((a, b) => {
|
|
30
|
+
.filter((f) => f.toLowerCase().endsWith('.md'))
|
|
31
|
+
.map((filename) => (0, parser_1.parseFilename)(filename));
|
|
32
|
+
regularDocs.sort((a, b) => {
|
|
19
33
|
if (a.date && b.date)
|
|
20
34
|
return b.date.localeCompare(a.date);
|
|
21
35
|
if (a.date)
|
|
@@ -24,7 +38,7 @@ function listDocs(docsPath) {
|
|
|
24
38
|
return 1;
|
|
25
39
|
return a.title.localeCompare(b.title);
|
|
26
40
|
});
|
|
27
|
-
return
|
|
41
|
+
return [...extraDocs, ...regularDocs];
|
|
28
42
|
}
|
|
29
43
|
function safeFilePath(docsPath, filename) {
|
|
30
44
|
const resolved = path_1.default.resolve(docsPath, filename);
|
|
@@ -32,12 +46,21 @@ function safeFilePath(docsPath, filename) {
|
|
|
32
46
|
return null;
|
|
33
47
|
return resolved;
|
|
34
48
|
}
|
|
49
|
+
function resolveDocPath(docsPath, doc, extraFiles) {
|
|
50
|
+
const id = decodeURIComponent(doc.id);
|
|
51
|
+
if (path_1.default.isAbsolute(id)) {
|
|
52
|
+
const filePath = id + '.md';
|
|
53
|
+
return extraFiles.includes(filePath) ? filePath : null;
|
|
54
|
+
}
|
|
55
|
+
return safeFilePath(docsPath, doc.filename);
|
|
56
|
+
}
|
|
35
57
|
function documentsRouter(docsPath) {
|
|
36
58
|
const router = (0, express_1.Router)();
|
|
37
59
|
// GET /api/documents — list all docs with metadata
|
|
38
60
|
router.get('/', (_req, res) => {
|
|
39
61
|
try {
|
|
40
|
-
|
|
62
|
+
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
63
|
+
res.json(listDocs(docsPath, extraFiles));
|
|
41
64
|
}
|
|
42
65
|
catch {
|
|
43
66
|
res.status(500).json({ error: 'Failed to list documents' });
|
|
@@ -49,10 +72,11 @@ function documentsRouter(docsPath) {
|
|
|
49
72
|
if (!query)
|
|
50
73
|
return res.json([]);
|
|
51
74
|
try {
|
|
52
|
-
const
|
|
75
|
+
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
76
|
+
const docs = listDocs(docsPath, extraFiles);
|
|
53
77
|
const results = [];
|
|
54
78
|
for (const doc of docs) {
|
|
55
|
-
const filePath =
|
|
79
|
+
const filePath = resolveDocPath(docsPath, doc, extraFiles);
|
|
56
80
|
if (!filePath || !fs_1.default.existsSync(filePath))
|
|
57
81
|
continue;
|
|
58
82
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
@@ -60,7 +84,6 @@ function documentsRouter(docsPath) {
|
|
|
60
84
|
const inCategory = doc.category.toLowerCase().includes(query);
|
|
61
85
|
const inContent = content.toLowerCase().includes(query);
|
|
62
86
|
if (inTitle || inCategory || inContent) {
|
|
63
|
-
// Extract a snippet around the first match in content
|
|
64
87
|
let excerpt = '';
|
|
65
88
|
if (inContent) {
|
|
66
89
|
const idx = content.toLowerCase().indexOf(query);
|
|
@@ -83,6 +106,28 @@ function documentsRouter(docsPath) {
|
|
|
83
106
|
// GET /api/documents/:id — get a single document (content + rendered HTML)
|
|
84
107
|
router.get('/:id', async (req, res) => {
|
|
85
108
|
const id = decodeURIComponent(req.params.id);
|
|
109
|
+
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
110
|
+
// Extra file: id is an absolute path without .md
|
|
111
|
+
if (path_1.default.isAbsolute(id)) {
|
|
112
|
+
const filePath = id + '.md';
|
|
113
|
+
if (!extraFiles.includes(filePath)) {
|
|
114
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
115
|
+
}
|
|
116
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
117
|
+
return res.status(404).json({ error: 'Document not found' });
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
121
|
+
const meta = (0, parser_1.parseFilename)(path_1.default.basename(filePath));
|
|
122
|
+
const html = marked_1.marked.parse(content);
|
|
123
|
+
res.json({ ...meta, id: req.params.id, category: 'General', content, html });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
res.status(500).json({ error: 'Failed to read document' });
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Normal file inside docsPath
|
|
86
131
|
const filename = id + '.md';
|
|
87
132
|
const filePath = safeFilePath(docsPath, filename);
|
|
88
133
|
if (!filePath) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../src/routes/documents.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../src/routes/documents.ts"],"names":[],"mappings":"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"}
|
package/dist/src/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,WAAW,CAAC,EAChC,QAAQ,EACR,IAAI,EACJ,WAAmB,GACpB,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA4C/B"}
|
package/dist/src/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const documents_1 = require("./routes/documents");
|
|
11
11
|
const config_1 = require("./routes/config");
|
|
12
|
+
const browse_1 = require("./routes/browse");
|
|
12
13
|
const config_2 = require("./lib/config");
|
|
13
14
|
async function startServer({ docsPath, port, openBrowser = false, }) {
|
|
14
15
|
const app = (0, express_1.default)();
|
|
@@ -18,6 +19,7 @@ async function startServer({ docsPath, port, openBrowser = false, }) {
|
|
|
18
19
|
// API
|
|
19
20
|
app.use('/api/documents', (0, documents_1.documentsRouter)(docsPath));
|
|
20
21
|
app.use('/api/config', (0, config_1.configRouter)(docsPath));
|
|
22
|
+
app.use('/api/browse', (0, browse_1.browseRouter)());
|
|
21
23
|
// Static frontend assets
|
|
22
24
|
const frontendPath = path_1.default.join(__dirname, 'frontend');
|
|
23
25
|
app.use(express_1.default.static(frontendPath));
|
package/dist/src/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":";;;;;AAcA,kCAgDC;AA9DD,sDAA8B;AAC9B,gDAAwB;AACxB,iDAAqC;AACrC,kDAAqD;AACrD,4CAA+C;AAC/C,4CAA+C;AAC/C,yCAA2C;AAQpC,KAAK,UAAU,WAAW,CAAC,EAChC,QAAQ,EACR,IAAI,EACJ,WAAW,GAAG,KAAK,GACL;IACd,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAExB,4CAA4C;IAC5C,IAAA,oBAAW,EAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtD,MAAM;IACN,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAA,2BAAe,EAAC,QAAQ,CAAC,CAAC,CAAC;IACrD,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,IAAA,qBAAY,EAAC,QAAQ,CAAC,CAAC,CAAC;IAC/C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,IAAA,qBAAY,GAAE,CAAC,CAAC;IAEvC,yBAAyB;IACzB,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACtD,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;IAEtC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CACzB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CACpD,CAAC;IACF,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAC9B,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CACpD,CAAC;IAEF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;YACpB,MAAM,GAAG,GAAG,oBAAoB,IAAI,EAAE,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;YACnD,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,cAAc,GAAG,QAAQ,CAAC,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,cAAc,QAAQ,EAAE,CAAC,CAAC;YACtC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;YACvC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAEhB,IAAI,WAAW,EAAE,CAAC;gBAChB,OAAO,CAAC,GAAG,CAAC,CAAC;YACf,CAAC;YAED,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,IAAI,QAAQ,KAAK,QAAQ;QAAE,IAAA,oBAAI,EAAC,SAAS,GAAG,GAAG,CAAC,CAAC;SAC5C,IAAI,QAAQ,KAAK,OAAO;QAAE,IAAA,oBAAI,EAAC,aAAa,GAAG,GAAG,CAAC,CAAC;;QACpD,IAAA,oBAAI,EAAC,aAAa,GAAG,GAAG,CAAC,CAAC;AACjC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "living-documentation",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A CLI tool that serves a local Markdown documentation viewer",
|
|
5
5
|
"main": "dist/src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc && node scripts/copy-assets.js && chmod +x dist/bin/cli.js",
|
|
15
|
-
"dev": "ts-node bin/cli.ts",
|
|
15
|
+
"dev": "nodemon --watch src --watch bin --ext ts,html --exec 'ts-node bin/cli.ts'",
|
|
16
16
|
"start": "node dist/bin/cli.js",
|
|
17
17
|
"prepublishOnly": "npm run build"
|
|
18
18
|
},
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/express": "^4.17.21",
|
|
34
34
|
"@types/node": "^20.14.0",
|
|
35
|
+
"nodemon": "^3.1.14",
|
|
35
36
|
"ts-node": "^10.9.2",
|
|
36
37
|
"typescript": "^5.4.5"
|
|
37
38
|
},
|