living-documentation 1.2.0 → 2.0.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/README.md +33 -12
- package/dist/src/frontend/admin.html +60 -27
- package/dist/src/frontend/index.html +160 -13
- package/dist/src/lib/parser.d.ts +1 -1
- package/dist/src/lib/parser.d.ts.map +1 -1
- package/dist/src/lib/parser.js +63 -29
- package/dist/src/lib/parser.js.map +1 -1
- 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 +46 -10
- package/dist/src/routes/documents.js.map +1 -1
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +42 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +5 -1
- package/dist/src/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,11 +11,14 @@ No cloud, no database, no build step — just point it at a folder of `.md` file
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **Sidebar** grouped by category, sorted by date (newest first)
|
|
14
|
+
- **General section** — always first, always expanded; holds uncategorized docs and extra files
|
|
15
|
+
- **Extra files** — include Markdown files from outside the docs folder (e.g. `README.md`, `CLAUDE.md`)
|
|
14
16
|
- **Full-text search** — instant filter + server-side content search
|
|
15
17
|
- **Dark mode** — follows system preference, manually toggleable
|
|
18
|
+
- **Syntax highlighting** — always dark, high-contrast code blocks
|
|
16
19
|
- **Export to PDF** — print-friendly layout via `window.print()`
|
|
17
20
|
- **Deep links** — share a direct URL to any document (`?doc=…`)
|
|
18
|
-
- **Admin panel** — configure title, theme, filename pattern in the browser
|
|
21
|
+
- **Admin panel** — configure title, theme, filename pattern, and extra files in the browser
|
|
19
22
|
- **Zero frontend build** — Tailwind and highlight.js loaded from CDN
|
|
20
23
|
|
|
21
24
|
---
|
|
@@ -51,7 +54,7 @@ living-documentation ./docs
|
|
|
51
54
|
git clone <repo>
|
|
52
55
|
cd living-documentation
|
|
53
56
|
npm install
|
|
54
|
-
npm run dev -- ./docs #
|
|
57
|
+
npm run dev -- ./docs # nodemon + ts-node, auto-restarts on changes
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
---
|
|
@@ -103,7 +106,7 @@ YYYY_MM_DD_[Category]_title_words.md
|
|
|
103
106
|
2023_11_03_[Backend]_api_versioning_strategy.md
|
|
104
107
|
```
|
|
105
108
|
|
|
106
|
-
Files that don't match the pattern are still shown — they appear under **
|
|
109
|
+
Files that don't match the pattern are still shown — they appear under **General** with the filename as the title.
|
|
107
110
|
|
|
108
111
|
---
|
|
109
112
|
|
|
@@ -117,12 +120,28 @@ A `.living-doc.json` file is created automatically in your docs folder on first
|
|
|
117
120
|
"filenamePattern": "YYYY_MM_DD_[Category]_title",
|
|
118
121
|
"title": "Living Documentation",
|
|
119
122
|
"theme": "system",
|
|
120
|
-
"port": 4321
|
|
123
|
+
"port": 4321,
|
|
124
|
+
"extraFiles": []
|
|
121
125
|
}
|
|
122
126
|
```
|
|
123
127
|
|
|
124
128
|
You can edit it manually or use the **Admin panel** at [http://localhost:4321/admin](http://localhost:4321/admin).
|
|
125
129
|
|
|
130
|
+
### Extra files
|
|
131
|
+
|
|
132
|
+
The `extraFiles` field accepts an ordered list of absolute paths to `.md` files located **outside** the docs folder. These files always appear in the **General** section, before regular General documents, in the order defined.
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"extraFiles": [
|
|
137
|
+
"/path/to/project/README.md",
|
|
138
|
+
"/path/to/project/CLAUDE.md"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use the Admin panel's **General — Extra Files** section to browse the filesystem and manage this list without editing the config manually.
|
|
144
|
+
|
|
126
145
|
---
|
|
127
146
|
|
|
128
147
|
## Project structure
|
|
@@ -135,7 +154,8 @@ living-documentation/
|
|
|
135
154
|
│ ├── server.ts Express app
|
|
136
155
|
│ ├── routes/
|
|
137
156
|
│ │ ├── documents.ts Documents API
|
|
138
|
-
│ │
|
|
157
|
+
│ │ ├── config.ts Config API
|
|
158
|
+
│ │ └── browse.ts Filesystem browser API
|
|
139
159
|
│ ├── lib/
|
|
140
160
|
│ │ ├── parser.ts Filename parser
|
|
141
161
|
│ │ └── config.ts Config management
|
|
@@ -152,13 +172,14 @@ living-documentation/
|
|
|
152
172
|
|
|
153
173
|
## API reference
|
|
154
174
|
|
|
155
|
-
| Method | Endpoint | Description
|
|
156
|
-
| ------ | -------------------------- |
|
|
157
|
-
| `GET` | `/api/documents` | List all documents with metadata
|
|
158
|
-
| `GET` | `/api/documents/:id` | Get document content + rendered HTML
|
|
159
|
-
| `GET` | `/api/documents/search?q=` | Full-text search
|
|
160
|
-
| `GET` | `/api/config` | Read config
|
|
161
|
-
| `PUT` | `/api/config` | Update config (`title`, `theme`, `filenamePattern`)
|
|
175
|
+
| Method | Endpoint | Description |
|
|
176
|
+
| ------ | -------------------------- | ------------------------------------------------------------------ |
|
|
177
|
+
| `GET` | `/api/documents` | List all documents with metadata (includes extra files) |
|
|
178
|
+
| `GET` | `/api/documents/:id` | Get document content + rendered HTML |
|
|
179
|
+
| `GET` | `/api/documents/search?q=` | Full-text search |
|
|
180
|
+
| `GET` | `/api/config` | Read config |
|
|
181
|
+
| `PUT` | `/api/config` | Update config (`title`, `theme`, `filenamePattern`, `extraFiles`) |
|
|
182
|
+
| `GET` | `/api/browse?path=` | List directories and `.md` files at a given filesystem path |
|
|
162
183
|
|
|
163
184
|
---
|
|
164
185
|
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
placeholder="YYYY_MM_DD_[Category]_title" />
|
|
154
154
|
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
155
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>
|
|
156
|
+
<span id="pattern-desc-example" 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
157
|
</p>
|
|
158
158
|
</div>
|
|
159
159
|
</div>
|
|
@@ -220,12 +220,22 @@
|
|
|
220
220
|
|
|
221
221
|
async function saveConfig(e) {
|
|
222
222
|
e.preventDefault();
|
|
223
|
+
const filenamePattern = document.getElementById("field-pattern").value.trim();
|
|
224
|
+
if (filenamePattern) {
|
|
225
|
+
const catCount = (filenamePattern.match(/\[Category\]/gi) || []).length;
|
|
226
|
+
if (catCount === 0) {
|
|
227
|
+
showMsg("Filename pattern must contain [Category]", "error");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (catCount > 1) {
|
|
231
|
+
showMsg("Filename pattern must contain [Category] exactly once", "error");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
223
235
|
const payload = {
|
|
224
236
|
title: document.getElementById("field-title").value.trim(),
|
|
225
237
|
theme: document.getElementById("field-theme").value,
|
|
226
|
-
filenamePattern
|
|
227
|
-
.getElementById("field-pattern")
|
|
228
|
-
.value.trim(),
|
|
238
|
+
filenamePattern,
|
|
229
239
|
};
|
|
230
240
|
|
|
231
241
|
try {
|
|
@@ -263,29 +273,48 @@
|
|
|
263
273
|
"readme.md",
|
|
264
274
|
];
|
|
265
275
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
function buildPatternsFromFormat(patternStr) {
|
|
277
|
+
if (!patternStr) patternStr = "YYYY_MM_DD_[Category]_title";
|
|
278
|
+
const hasDate = /YYYY.*MM.*DD/.test(patternStr);
|
|
279
|
+
const hasCategory = /\[Category\]/i.test(patternStr);
|
|
280
|
+
const dateGroup = "(\\d{4}_\\d{2}_\\d{2})";
|
|
281
|
+
const catGroup = "\\[([^\\]]+)\\]";
|
|
282
|
+
const catBeforeDate =
|
|
283
|
+
hasDate && hasCategory &&
|
|
284
|
+
patternStr.search(/\[Category\]/i) < patternStr.search(/YYYY/i);
|
|
285
|
+
let full = null, dateOnly = null;
|
|
286
|
+
if (hasDate && hasCategory) {
|
|
287
|
+
const ordered = catBeforeDate
|
|
288
|
+
? catGroup + "_" + dateGroup
|
|
289
|
+
: dateGroup + "_" + catGroup;
|
|
290
|
+
full = new RegExp("^" + ordered + "_(.+)\\.md$", "i");
|
|
291
|
+
dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
|
|
292
|
+
} else if (hasDate) {
|
|
293
|
+
dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
|
|
294
|
+
} else if (hasCategory) {
|
|
295
|
+
full = new RegExp("^" + catGroup + "_(.+)\\.md$", "i");
|
|
296
|
+
}
|
|
297
|
+
return { full, dateOnly, hasDate, hasCategory, catBeforeDate };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parsePreview(filename, patterns) {
|
|
301
|
+
if (patterns.full) {
|
|
302
|
+
const m = filename.match(patterns.full);
|
|
303
|
+
if (m) {
|
|
304
|
+
if (patterns.hasDate && patterns.hasCategory) {
|
|
305
|
+
const dateStr = patterns.catBeforeDate ? m[2] : m[1];
|
|
306
|
+
const category = patterns.catBeforeDate ? m[1] : m[2];
|
|
307
|
+
return { date: dateStr.replace(/_/g, "-"), category, title: titleCase(m[3]), match: true };
|
|
308
|
+
} else if (patterns.hasCategory) {
|
|
309
|
+
return { date: null, category: m[1], title: titleCase(m[2]), match: true };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
280
312
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
date:
|
|
285
|
-
|
|
286
|
-
title: titleCase(d[2]),
|
|
287
|
-
match: true,
|
|
288
|
-
};
|
|
313
|
+
if (patterns.dateOnly) {
|
|
314
|
+
const m = filename.match(patterns.dateOnly);
|
|
315
|
+
if (m) {
|
|
316
|
+
return { date: m[1].replace(/_/g, "-"), category: "General", title: titleCase(m[2]), match: true };
|
|
317
|
+
}
|
|
289
318
|
}
|
|
290
319
|
return {
|
|
291
320
|
date: null,
|
|
@@ -303,9 +332,13 @@
|
|
|
303
332
|
}
|
|
304
333
|
|
|
305
334
|
function updatePreview() {
|
|
335
|
+
const patternVal = document.getElementById("field-pattern").value.trim();
|
|
336
|
+
const descSpan = document.getElementById("pattern-desc-example");
|
|
337
|
+
descSpan.textContent = (patternVal || "YYYY_MM_DD_[Category]_title") + ".md";
|
|
338
|
+
const patterns = buildPatternsFromFormat(patternVal);
|
|
306
339
|
const rows = document.getElementById("preview-rows");
|
|
307
340
|
rows.innerHTML = EXAMPLES.map((f) => {
|
|
308
|
-
const p = parsePreview(f);
|
|
341
|
+
const p = parsePreview(f, patterns);
|
|
309
342
|
return `
|
|
310
343
|
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
|
|
311
344
|
<p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
<p class="text-sm text-gray-500 dark:text-gray-500">
|
|
152
152
|
Choose a document from the sidebar to start reading.
|
|
153
153
|
</p>
|
|
154
|
-
<p class="mt-4 text-xs text-gray-400 dark:text-gray-600 font-mono bg-gray-100 dark:bg-gray-800 rounded-lg px-3 py-2 inline-block">
|
|
154
|
+
<p id="welcome-pattern" class="mt-4 text-xs text-gray-400 dark:text-gray-600 font-mono bg-gray-100 dark:bg-gray-800 rounded-lg px-3 py-2 inline-block">
|
|
155
155
|
YYYY_MM_DD_[Category]_title.md
|
|
156
156
|
</p>
|
|
157
157
|
<p class="mt-2 text-xs text-gray-400">Expected filename pattern</p>
|
|
@@ -175,16 +175,37 @@
|
|
|
175
175
|
</div>
|
|
176
176
|
<!-- Actions -->
|
|
177
177
|
<div class="flex items-center gap-2 shrink-0">
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
178
|
+
<!-- View mode actions -->
|
|
179
|
+
<div id="view-actions" class="flex items-center gap-2">
|
|
180
|
+
<button onclick="exportPDF()" title="Export as PDF"
|
|
181
|
+
class="no-print text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700
|
|
182
|
+
text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
183
|
+
📄 Export PDF
|
|
184
|
+
</button>
|
|
185
|
+
<button onclick="copyLink()" id="copy-link-btn" title="Copy link"
|
|
186
|
+
class="no-print text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700
|
|
187
|
+
text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
188
|
+
🔗 Copy link
|
|
189
|
+
</button>
|
|
190
|
+
<button onclick="enterEditMode()" title="Edit document"
|
|
191
|
+
class="no-print text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700
|
|
192
|
+
text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
193
|
+
✎ Edit
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
<!-- Edit mode actions -->
|
|
197
|
+
<div id="edit-actions" class="hidden flex items-center gap-2">
|
|
198
|
+
<span id="edit-save-msg" class="text-xs"></span>
|
|
199
|
+
<button onclick="exitEditMode()" title="Cancel editing"
|
|
200
|
+
class="text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700
|
|
201
|
+
text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
202
|
+
Cancel
|
|
203
|
+
</button>
|
|
204
|
+
<button onclick="saveDocument()" title="Save document"
|
|
205
|
+
class="text-sm px-4 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors">
|
|
206
|
+
Save
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
188
209
|
</div>
|
|
189
210
|
</div>
|
|
190
211
|
</header>
|
|
@@ -201,6 +222,14 @@
|
|
|
201
222
|
prose-code:before:content-none prose-code:after:content-none
|
|
202
223
|
prose-pre:bg-[#0d1117] prose-pre:border prose-pre:border-gray-700">
|
|
203
224
|
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Markdown editor -->
|
|
227
|
+
<textarea id="doc-editor"
|
|
228
|
+
class="hidden w-full min-h-[70vh] px-4 py-3 text-sm font-mono leading-relaxed
|
|
229
|
+
rounded-lg border border-gray-300 dark:border-gray-600
|
|
230
|
+
bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100
|
|
231
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y"
|
|
232
|
+
spellcheck="false"></textarea>
|
|
204
233
|
</article>
|
|
205
234
|
|
|
206
235
|
</main>
|
|
@@ -211,6 +240,7 @@
|
|
|
211
240
|
// ── State ──────────────────────────────────────────────────
|
|
212
241
|
let allDocs = [];
|
|
213
242
|
let currentDocId = null;
|
|
243
|
+
let currentDocContent = '';
|
|
214
244
|
let searchQuery = '';
|
|
215
245
|
let expandedCategories = new Set();
|
|
216
246
|
|
|
@@ -222,10 +252,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
222
252
|
await loadConfig();
|
|
223
253
|
await loadDocuments();
|
|
224
254
|
|
|
225
|
-
// Deep-link via ?doc=id
|
|
255
|
+
// Deep-link via ?doc=id, otherwise open first General doc
|
|
226
256
|
const params = new URLSearchParams(location.search);
|
|
227
257
|
const docId = params.get('doc');
|
|
228
|
-
if (docId)
|
|
258
|
+
if (docId) {
|
|
259
|
+
openDocument(docId, true);
|
|
260
|
+
} else {
|
|
261
|
+
const first = allDocs.find(d => d.category === 'General') ?? allDocs[0];
|
|
262
|
+
if (first) openDocument(first.id, true);
|
|
263
|
+
}
|
|
229
264
|
});
|
|
230
265
|
|
|
231
266
|
// ── Dark mode ──────────────────────────────────────────────
|
|
@@ -254,6 +289,9 @@ async function loadConfig() {
|
|
|
254
289
|
const cfg = await fetch('/api/config').then(r => r.json());
|
|
255
290
|
if (cfg.title) document.title = cfg.title;
|
|
256
291
|
document.getElementById('app-title').textContent = cfg.title || 'Living Documentation';
|
|
292
|
+
if (cfg.filenamePattern) {
|
|
293
|
+
document.getElementById('welcome-pattern').textContent = cfg.filenamePattern + '.md';
|
|
294
|
+
}
|
|
257
295
|
} catch { /* non-fatal */ }
|
|
258
296
|
}
|
|
259
297
|
|
|
@@ -376,6 +414,9 @@ async function openDocument(id, skipHistory = false) {
|
|
|
376
414
|
return r.json();
|
|
377
415
|
});
|
|
378
416
|
|
|
417
|
+
currentDocContent = doc.content;
|
|
418
|
+
exitEditMode();
|
|
419
|
+
|
|
379
420
|
document.getElementById('doc-title').textContent = doc.title;
|
|
380
421
|
document.getElementById('doc-category').textContent = doc.category;
|
|
381
422
|
document.getElementById('doc-date').textContent = doc.formattedDate || '';
|
|
@@ -457,6 +498,112 @@ async function doSearch(q) {
|
|
|
457
498
|
} catch { /* ignore */ }
|
|
458
499
|
}
|
|
459
500
|
|
|
501
|
+
// ── Edit mode ──────────────────────────────────────────────
|
|
502
|
+
function enterEditMode() {
|
|
503
|
+
const editor = document.getElementById('doc-editor');
|
|
504
|
+
editor.value = currentDocContent;
|
|
505
|
+
document.getElementById('doc-content').classList.add('hidden');
|
|
506
|
+
editor.classList.remove('hidden');
|
|
507
|
+
document.getElementById('view-actions').classList.add('hidden');
|
|
508
|
+
document.getElementById('edit-actions').classList.remove('hidden');
|
|
509
|
+
editor.focus();
|
|
510
|
+
editor.addEventListener('paste', handleEditorPaste);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function exitEditMode() {
|
|
514
|
+
const editor = document.getElementById('doc-editor');
|
|
515
|
+
editor.removeEventListener('paste', handleEditorPaste);
|
|
516
|
+
editor.classList.add('hidden');
|
|
517
|
+
document.getElementById('doc-content').classList.remove('hidden');
|
|
518
|
+
document.getElementById('edit-actions').classList.add('hidden');
|
|
519
|
+
document.getElementById('view-actions').classList.remove('hidden');
|
|
520
|
+
document.getElementById('edit-save-msg').textContent = '';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function handleEditorPaste(e) {
|
|
524
|
+
const items = Array.from(e.clipboardData?.items ?? []);
|
|
525
|
+
const imageItem = items.find(item => item.type.startsWith('image/'));
|
|
526
|
+
if (!imageItem) return;
|
|
527
|
+
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
|
|
530
|
+
const ext = imageItem.type.split('/')[1].replace('jpeg', 'jpg') || 'png';
|
|
531
|
+
const blob = imageItem.getAsFile();
|
|
532
|
+
if (!blob) return;
|
|
533
|
+
|
|
534
|
+
const editor = document.getElementById('doc-editor');
|
|
535
|
+
const msgEl = document.getElementById('edit-save-msg');
|
|
536
|
+
|
|
537
|
+
// Insert placeholder at cursor while uploading
|
|
538
|
+
const placeholder = ``;
|
|
539
|
+
const start = editor.selectionStart;
|
|
540
|
+
const before = editor.value.slice(0, start);
|
|
541
|
+
const after = editor.value.slice(editor.selectionEnd);
|
|
542
|
+
editor.value = before + placeholder + after;
|
|
543
|
+
editor.selectionStart = editor.selectionEnd = start + placeholder.length;
|
|
544
|
+
|
|
545
|
+
msgEl.textContent = 'Uploading image…';
|
|
546
|
+
msgEl.className = 'text-xs text-gray-400';
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const base64 = await new Promise((resolve, reject) => {
|
|
550
|
+
const reader = new FileReader();
|
|
551
|
+
reader.onload = () => resolve(reader.result);
|
|
552
|
+
reader.onerror = reject;
|
|
553
|
+
reader.readAsDataURL(blob);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const res = await fetch('/api/images/upload', {
|
|
557
|
+
method: 'POST',
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: JSON.stringify({ data: base64, ext }),
|
|
560
|
+
});
|
|
561
|
+
if (!res.ok) throw new Error(await res.text());
|
|
562
|
+
|
|
563
|
+
const { filename } = await res.json();
|
|
564
|
+
const mdImage = ``;
|
|
565
|
+
|
|
566
|
+
// Replace placeholder with final markdown
|
|
567
|
+
editor.value = editor.value.replace(placeholder, mdImage);
|
|
568
|
+
msgEl.textContent = '';
|
|
569
|
+
} catch (err) {
|
|
570
|
+
// Remove placeholder on failure
|
|
571
|
+
editor.value = editor.value.replace(placeholder, '');
|
|
572
|
+
msgEl.textContent = 'Image upload failed: ' + err.message;
|
|
573
|
+
msgEl.className = 'text-xs text-red-500 dark:text-red-400';
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function saveDocument() {
|
|
578
|
+
if (!currentDocId) return;
|
|
579
|
+
const content = document.getElementById('doc-editor').value;
|
|
580
|
+
const msgEl = document.getElementById('edit-save-msg');
|
|
581
|
+
msgEl.textContent = 'Saving…';
|
|
582
|
+
msgEl.className = 'text-xs text-gray-400';
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const res = await fetch('/api/documents/' + currentDocId, {
|
|
586
|
+
method: 'PUT',
|
|
587
|
+
headers: { 'Content-Type': 'application/json' },
|
|
588
|
+
body: JSON.stringify({ content }),
|
|
589
|
+
});
|
|
590
|
+
if (!res.ok) throw new Error(await res.text());
|
|
591
|
+
|
|
592
|
+
currentDocContent = content;
|
|
593
|
+
|
|
594
|
+
// Re-fetch rendered HTML and update view
|
|
595
|
+
const doc = await fetch('/api/documents/' + currentDocId).then(r => r.json());
|
|
596
|
+
const contentEl = document.getElementById('doc-content');
|
|
597
|
+
contentEl.innerHTML = doc.html;
|
|
598
|
+
contentEl.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
|
599
|
+
|
|
600
|
+
exitEditMode();
|
|
601
|
+
} catch (err) {
|
|
602
|
+
msgEl.textContent = 'Save failed: ' + err.message;
|
|
603
|
+
msgEl.className = 'text-xs text-red-500 dark:text-red-400';
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
460
607
|
// ── Helpers ────────────────────────────────────────────────
|
|
461
608
|
function exportPDF() { window.print(); }
|
|
462
609
|
|
package/dist/src/lib/parser.d.ts
CHANGED
|
@@ -6,5 +6,5 @@ export interface DocMetadata {
|
|
|
6
6
|
date: string | null;
|
|
7
7
|
formattedDate: string | null;
|
|
8
8
|
}
|
|
9
|
-
export declare function parseFilename(filename: string): DocMetadata;
|
|
9
|
+
export declare function parseFilename(filename: string, filenamePattern?: string): DocMetadata;
|
|
10
10
|
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/lib/parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/lib/parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAkED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAwCrF"}
|
package/dist/src/lib/parser.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseFilename = parseFilename;
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
4
|
+
function buildPatternsFromFormat(patternStr) {
|
|
5
|
+
const hasDate = /YYYY.*MM.*DD/.test(patternStr);
|
|
6
|
+
const hasCategory = /\[Category\]/i.test(patternStr);
|
|
7
|
+
const dateGroup = '(\\d{4}_\\d{2}_\\d{2})';
|
|
8
|
+
const catGroup = '\\[([^\\]]+)\\]';
|
|
9
|
+
const catBeforeDate = hasDate && hasCategory &&
|
|
10
|
+
patternStr.search(/\[Category\]/i) < patternStr.search(/YYYY/i);
|
|
11
|
+
if (hasDate && hasCategory) {
|
|
12
|
+
const ordered = catBeforeDate
|
|
13
|
+
? `${catGroup}_${dateGroup}`
|
|
14
|
+
: `${dateGroup}_${catGroup}`;
|
|
15
|
+
return {
|
|
16
|
+
full: new RegExp(`^${ordered}_(.+)\\.md$`, 'i'),
|
|
17
|
+
dateOnly: new RegExp(`^${dateGroup}_(.+)\\.md$`, 'i'),
|
|
18
|
+
hasDate,
|
|
19
|
+
hasCategory,
|
|
20
|
+
catBeforeDate,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
else if (hasDate) {
|
|
24
|
+
return {
|
|
25
|
+
full: null,
|
|
26
|
+
dateOnly: new RegExp(`^${dateGroup}_(.+)\\.md$`, 'i'),
|
|
27
|
+
hasDate,
|
|
28
|
+
hasCategory,
|
|
29
|
+
catBeforeDate: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
else if (hasCategory) {
|
|
33
|
+
return {
|
|
34
|
+
full: new RegExp(`^${catGroup}_(.+)\\.md$`, 'i'),
|
|
35
|
+
dateOnly: null,
|
|
36
|
+
hasDate,
|
|
37
|
+
hasCategory,
|
|
38
|
+
catBeforeDate: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { full: null, dateOnly: null, hasDate, hasCategory, catBeforeDate: false };
|
|
42
|
+
}
|
|
8
43
|
function formatDate(iso) {
|
|
9
44
|
const [year, month, day] = iso.split('-').map(Number);
|
|
10
45
|
const d = new Date(Date.UTC(year, month - 1, day));
|
|
@@ -21,33 +56,32 @@ function titleCase(raw) {
|
|
|
21
56
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
22
57
|
.join(' ');
|
|
23
58
|
}
|
|
24
|
-
function parseFilename(filename) {
|
|
59
|
+
function parseFilename(filename, filenamePattern) {
|
|
25
60
|
const id = encodeURIComponent(filename.slice(0, -3));
|
|
26
|
-
const full =
|
|
27
|
-
if (
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
const { full: FULL_PAT, dateOnly: DATE_ONLY_PAT, hasDate, hasCategory, catBeforeDate } = buildPatternsFromFormat(filenamePattern ?? 'YYYY_MM_DD_[Category]_title');
|
|
62
|
+
if (FULL_PAT) {
|
|
63
|
+
const full = filename.match(FULL_PAT);
|
|
64
|
+
if (full) {
|
|
65
|
+
if (hasDate && hasCategory) {
|
|
66
|
+
const dateStr = catBeforeDate ? full[2] : full[1];
|
|
67
|
+
const category = catBeforeDate ? full[1] : full[2];
|
|
68
|
+
const titlePart = full[3];
|
|
69
|
+
const date = dateStr.replace(/_/g, '-');
|
|
70
|
+
return { id, filename, title: titleCase(titlePart), category, date, formattedDate: formatDate(date) };
|
|
71
|
+
}
|
|
72
|
+
else if (hasCategory) {
|
|
73
|
+
const [, category, titlePart] = full;
|
|
74
|
+
return { id, filename, title: titleCase(titlePart), category, date: null, formattedDate: null };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
38
77
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
id,
|
|
45
|
-
|
|
46
|
-
title: titleCase(titlePart),
|
|
47
|
-
category: 'General',
|
|
48
|
-
date,
|
|
49
|
-
formattedDate: formatDate(date),
|
|
50
|
-
};
|
|
78
|
+
if (DATE_ONLY_PAT) {
|
|
79
|
+
const dateOnly = filename.match(DATE_ONLY_PAT);
|
|
80
|
+
if (dateOnly) {
|
|
81
|
+
const [, dateStr, titlePart] = dateOnly;
|
|
82
|
+
const date = dateStr.replace(/_/g, '-');
|
|
83
|
+
return { id, filename, title: titleCase(titlePart), category: 'General', date, formattedDate: formatDate(date) };
|
|
84
|
+
}
|
|
51
85
|
}
|
|
52
86
|
// Fallback — no date, no category
|
|
53
87
|
const rawTitle = filename.slice(0, -3).replace(/[_-]+/g, ' ');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/lib/parser.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/lib/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;AAIpD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,
|
|
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,CAuDrD"}
|
|
@@ -34,6 +34,16 @@ function configRouter(docsPath) {
|
|
|
34
34
|
safe[key] = patch[key];
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
// filenamePattern: must contain exactly one [Category]
|
|
38
|
+
if ('filenamePattern' in safe && typeof safe.filenamePattern === 'string') {
|
|
39
|
+
const matches = safe.filenamePattern.match(/\[Category\]/gi);
|
|
40
|
+
if (!matches || matches.length === 0) {
|
|
41
|
+
return res.status(400).json({ error: 'filenamePattern must contain [Category]' });
|
|
42
|
+
}
|
|
43
|
+
if (matches.length > 1) {
|
|
44
|
+
return res.status(400).json({ error: 'filenamePattern must contain [Category] exactly once' });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
37
47
|
// extraFiles: only absolute .md paths
|
|
38
48
|
if ('extraFiles' in patch && Array.isArray(patch.extraFiles)) {
|
|
39
49
|
safe.extraFiles = patch.extraFiles.filter((f) => typeof f === 'string' &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/routes/config.ts"],"names":[],"mappings":";;;;;AAIA,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/routes/config.ts"],"names":[],"mappings":";;;;;AAIA,oCAuDC;AA3DD,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,uDAAuD;YACvD,IAAI,iBAAiB,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,eAAe,KAAK,QAAQ,EAAE,CAAC;gBAC1E,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBAC7D,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACrC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;gBACpF,CAAC;gBACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACvB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sDAAsD,EAAE,CAAC,CAAC;gBACjG,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;AAwDpD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,
|
|
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,CA8IxD"}
|
|
@@ -10,14 +10,14 @@ const path_1 = __importDefault(require("path"));
|
|
|
10
10
|
const marked_1 = require("marked");
|
|
11
11
|
const parser_1 = require("../lib/parser");
|
|
12
12
|
const config_1 = require("../lib/config");
|
|
13
|
-
function listDocs(docsPath, extraFiles = []) {
|
|
13
|
+
function listDocs(docsPath, extraFiles = [], filenamePattern) {
|
|
14
14
|
// Extra files first, in config order, always General
|
|
15
15
|
const extraDocs = [];
|
|
16
16
|
for (const filePath of extraFiles) {
|
|
17
17
|
if (!filePath.endsWith('.md') || !fs_1.default.existsSync(filePath))
|
|
18
18
|
continue;
|
|
19
19
|
const filename = path_1.default.basename(filePath);
|
|
20
|
-
const meta = (0, parser_1.parseFilename)(filename);
|
|
20
|
+
const meta = (0, parser_1.parseFilename)(filename, filenamePattern);
|
|
21
21
|
extraDocs.push({
|
|
22
22
|
...meta,
|
|
23
23
|
id: encodeURIComponent(filePath.slice(0, -3)),
|
|
@@ -28,7 +28,7 @@ function listDocs(docsPath, extraFiles = []) {
|
|
|
28
28
|
const regularDocs = fs_1.default
|
|
29
29
|
.readdirSync(docsPath)
|
|
30
30
|
.filter((f) => f.toLowerCase().endsWith('.md'))
|
|
31
|
-
.map((filename) => (0, parser_1.parseFilename)(filename));
|
|
31
|
+
.map((filename) => (0, parser_1.parseFilename)(filename, filenamePattern));
|
|
32
32
|
regularDocs.sort((a, b) => {
|
|
33
33
|
if (a.date && b.date)
|
|
34
34
|
return b.date.localeCompare(a.date);
|
|
@@ -59,8 +59,8 @@ function documentsRouter(docsPath) {
|
|
|
59
59
|
// GET /api/documents — list all docs with metadata
|
|
60
60
|
router.get('/', (_req, res) => {
|
|
61
61
|
try {
|
|
62
|
-
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
63
|
-
res.json(listDocs(docsPath, extraFiles));
|
|
62
|
+
const { extraFiles = [], filenamePattern } = (0, config_1.readConfig)(docsPath);
|
|
63
|
+
res.json(listDocs(docsPath, extraFiles, filenamePattern));
|
|
64
64
|
}
|
|
65
65
|
catch {
|
|
66
66
|
res.status(500).json({ error: 'Failed to list documents' });
|
|
@@ -72,8 +72,8 @@ function documentsRouter(docsPath) {
|
|
|
72
72
|
if (!query)
|
|
73
73
|
return res.json([]);
|
|
74
74
|
try {
|
|
75
|
-
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
76
|
-
const docs = listDocs(docsPath, extraFiles);
|
|
75
|
+
const { extraFiles = [], filenamePattern } = (0, config_1.readConfig)(docsPath);
|
|
76
|
+
const docs = listDocs(docsPath, extraFiles, filenamePattern);
|
|
77
77
|
const results = [];
|
|
78
78
|
for (const doc of docs) {
|
|
79
79
|
const filePath = resolveDocPath(docsPath, doc, extraFiles);
|
|
@@ -106,7 +106,7 @@ function documentsRouter(docsPath) {
|
|
|
106
106
|
// GET /api/documents/:id — get a single document (content + rendered HTML)
|
|
107
107
|
router.get('/:id', async (req, res) => {
|
|
108
108
|
const id = decodeURIComponent(req.params.id);
|
|
109
|
-
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
109
|
+
const { extraFiles = [], filenamePattern } = (0, config_1.readConfig)(docsPath);
|
|
110
110
|
// Extra file: id is an absolute path without .md
|
|
111
111
|
if (path_1.default.isAbsolute(id)) {
|
|
112
112
|
const filePath = id + '.md';
|
|
@@ -118,7 +118,7 @@ function documentsRouter(docsPath) {
|
|
|
118
118
|
}
|
|
119
119
|
try {
|
|
120
120
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
121
|
-
const meta = (0, parser_1.parseFilename)(path_1.default.basename(filePath));
|
|
121
|
+
const meta = (0, parser_1.parseFilename)(path_1.default.basename(filePath), filenamePattern);
|
|
122
122
|
const html = marked_1.marked.parse(content);
|
|
123
123
|
res.json({ ...meta, id: req.params.id, category: 'General', content, html });
|
|
124
124
|
}
|
|
@@ -138,7 +138,7 @@ function documentsRouter(docsPath) {
|
|
|
138
138
|
}
|
|
139
139
|
try {
|
|
140
140
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
141
|
-
const metadata = (0, parser_1.parseFilename)(filename);
|
|
141
|
+
const metadata = (0, parser_1.parseFilename)(filename, filenamePattern);
|
|
142
142
|
const html = marked_1.marked.parse(content);
|
|
143
143
|
res.json({ ...metadata, content, html });
|
|
144
144
|
}
|
|
@@ -146,6 +146,42 @@ function documentsRouter(docsPath) {
|
|
|
146
146
|
res.status(500).json({ error: 'Failed to read document' });
|
|
147
147
|
}
|
|
148
148
|
});
|
|
149
|
+
// PUT /api/documents/:id — update document content
|
|
150
|
+
router.put('/:id', (req, res) => {
|
|
151
|
+
const id = decodeURIComponent(req.params.id);
|
|
152
|
+
const { content } = req.body;
|
|
153
|
+
if (typeof content !== 'string') {
|
|
154
|
+
return res.status(400).json({ error: 'content is required' });
|
|
155
|
+
}
|
|
156
|
+
const { extraFiles = [] } = (0, config_1.readConfig)(docsPath);
|
|
157
|
+
// Extra file: id is an absolute path without .md
|
|
158
|
+
if (path_1.default.isAbsolute(id)) {
|
|
159
|
+
const filePath = id + '.md';
|
|
160
|
+
if (!extraFiles.includes(filePath)) {
|
|
161
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
fs_1.default.writeFileSync(filePath, content, 'utf-8');
|
|
165
|
+
return res.json({ ok: true });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return res.status(500).json({ error: 'Failed to write document' });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Normal file inside docsPath
|
|
172
|
+
const filename = id + '.md';
|
|
173
|
+
const filePath = safeFilePath(docsPath, filename);
|
|
174
|
+
if (!filePath) {
|
|
175
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
fs_1.default.writeFileSync(filePath, content, 'utf-8');
|
|
179
|
+
return res.json({ ok: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return res.status(500).json({ error: 'Failed to write document' });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
149
185
|
return router;
|
|
150
186
|
}
|
|
151
187
|
//# sourceMappingURL=documents.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../src/routes/documents.ts"],"names":[],"mappings":";;;;;AAwDA,
|
|
1
|
+
{"version":3,"file":"documents.js","sourceRoot":"","sources":["../../../src/routes/documents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../../src/routes/images.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAIpD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAuCrD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
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.imagesRouter = imagesRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
function imagesRouter(docsPath) {
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
// POST /api/images/upload — save a base64-encoded image to DOCS_FOLDER/images/
|
|
13
|
+
router.post('/upload', (req, res) => {
|
|
14
|
+
const { data, ext } = req.body;
|
|
15
|
+
if (typeof data !== 'string' || !data) {
|
|
16
|
+
return res.status(400).json({ error: 'data is required' });
|
|
17
|
+
}
|
|
18
|
+
const safeExt = (typeof ext === 'string' ? ext.replace(/[^a-z0-9]/gi, '') : 'png').toLowerCase() || 'png';
|
|
19
|
+
// Strip base64 data URL prefix if present
|
|
20
|
+
const base64 = data.replace(/^data:image\/[^;]+;base64,/, '');
|
|
21
|
+
const imagesDir = path_1.default.join(docsPath, 'images');
|
|
22
|
+
if (!fs_1.default.existsSync(imagesDir)) {
|
|
23
|
+
fs_1.default.mkdirSync(imagesDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
27
|
+
const timestamp = `${now.getFullYear()}_${pad(now.getMonth() + 1)}_${pad(now.getDate())}` +
|
|
28
|
+
`_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
29
|
+
const random = Math.random().toString(36).slice(2, 6);
|
|
30
|
+
const filename = `image_${timestamp}_${random}.${safeExt}`;
|
|
31
|
+
const filePath = path_1.default.join(imagesDir, filename);
|
|
32
|
+
try {
|
|
33
|
+
fs_1.default.writeFileSync(filePath, Buffer.from(base64, 'base64'));
|
|
34
|
+
res.json({ filename });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
res.status(500).json({ error: 'Failed to save image' });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return router;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=images.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.js","sourceRoot":"","sources":["../../../src/routes/images.ts"],"names":[],"mappings":";;;;;AAIA,oCAuCC;AA3CD,qCAAoD;AACpD,4CAAoB;AACpB,gDAAwB;AAExB,SAAgB,YAAY,CAAC,QAAgB;IAC3C,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,+EAA+E;IAC/E,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QACrD,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,IAAuC,CAAC;QAElE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YACtC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC7D,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC;QAE1G,0CAA0C;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;QAE9D,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,YAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACtD,MAAM,SAAS,GACb,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE;YACvE,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;QAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACtD,MAAM,QAAQ,GAAG,SAAS,SAAS,IAAI,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3D,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEhD,IAAI,CAAC;YACH,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
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":"AASA,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,CAgD/B"}
|
package/dist/src/server.js
CHANGED
|
@@ -10,19 +10,23 @@ const child_process_1 = require("child_process");
|
|
|
10
10
|
const documents_1 = require("./routes/documents");
|
|
11
11
|
const config_1 = require("./routes/config");
|
|
12
12
|
const browse_1 = require("./routes/browse");
|
|
13
|
+
const images_1 = require("./routes/images");
|
|
13
14
|
const config_2 = require("./lib/config");
|
|
14
15
|
async function startServer({ docsPath, port, openBrowser = false, }) {
|
|
15
16
|
const app = (0, express_1.default)();
|
|
16
|
-
app.use(express_1.default.json());
|
|
17
|
+
app.use(express_1.default.json({ limit: '20mb' }));
|
|
17
18
|
// Persist initial state to .living-doc.json
|
|
18
19
|
(0, config_2.writeConfig)(docsPath, { docsFolder: docsPath, port });
|
|
19
20
|
// API
|
|
20
21
|
app.use('/api/documents', (0, documents_1.documentsRouter)(docsPath));
|
|
21
22
|
app.use('/api/config', (0, config_1.configRouter)(docsPath));
|
|
22
23
|
app.use('/api/browse', (0, browse_1.browseRouter)());
|
|
24
|
+
app.use('/api/images', (0, images_1.imagesRouter)(docsPath));
|
|
23
25
|
// Static frontend assets
|
|
24
26
|
const frontendPath = path_1.default.join(__dirname, 'frontend');
|
|
25
27
|
app.use(express_1.default.static(frontendPath));
|
|
28
|
+
// Static assets from docs folder (images, etc.)
|
|
29
|
+
app.use('/images', express_1.default.static(path_1.default.join(docsPath, 'images')));
|
|
26
30
|
app.get('/', (_req, res) => res.sendFile(path_1.default.join(frontendPath, 'index.html')));
|
|
27
31
|
app.get('/admin', (_req, res) => res.sendFile(path_1.default.join(frontendPath, 'admin.html')));
|
|
28
32
|
return new Promise((resolve) => {
|
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":"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"}
|