living-documentation 2.0.0 → 3.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/README.md
CHANGED
|
@@ -10,7 +10,8 @@ No cloud, no database, no build step — just point it at a folder of `.md` file
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **Sidebar** grouped by category, sorted by
|
|
13
|
+
- **Sidebar** grouped by category, sorted alphabetically by full filename
|
|
14
|
+
- **Recursive folder scanning** — subdirectories are scanned automatically; subdirectory name becomes the category
|
|
14
15
|
- **General section** — always first, always expanded; holds uncategorized docs and extra files
|
|
15
16
|
- **Extra files** — include Markdown files from outside the docs folder (e.g. `README.md`, `CLAUDE.md`)
|
|
16
17
|
- **Full-text search** — instant filter + server-side content search
|
|
@@ -19,6 +20,8 @@ No cloud, no database, no build step — just point it at a folder of `.md` file
|
|
|
19
20
|
- **Export to PDF** — print-friendly layout via `window.print()`
|
|
20
21
|
- **Deep links** — share a direct URL to any document (`?doc=…`)
|
|
21
22
|
- **Admin panel** — configure title, theme, filename pattern, and extra files in the browser
|
|
23
|
+
- **Inline editing** — edit any document directly in the browser, saves to disk instantly
|
|
24
|
+
- **Image paste** — paste an image from clipboard in the editor; auto-uploaded and inserted as Markdown
|
|
22
25
|
- **Zero frontend build** — Tailwind and highlight.js loaded from CDN
|
|
23
26
|
|
|
24
27
|
---
|
|
@@ -108,6 +111,25 @@ YYYY_MM_DD_[Category]_title_words.md
|
|
|
108
111
|
|
|
109
112
|
Files that don't match the pattern are still shown — they appear under **General** with the filename as the title.
|
|
110
113
|
|
|
114
|
+
### Subdirectories
|
|
115
|
+
|
|
116
|
+
The docs folder is scanned **recursively**. Files in subdirectories are automatically discovered:
|
|
117
|
+
|
|
118
|
+
- If the filename matches the pattern (contains `[Category]`), the category from the filename is used.
|
|
119
|
+
- Otherwise, the **subdirectory name** becomes the category.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
docs/
|
|
123
|
+
├── 2024_01_15_[DevOps]_deploy.md → category: DevOps
|
|
124
|
+
├── adrs/
|
|
125
|
+
│ ├── my-decision.md → category: Adrs
|
|
126
|
+
│ └── 2024_03_01_[Architecture]_event_sourcing.md → category: Architecture
|
|
127
|
+
└── guides/
|
|
128
|
+
└── onboarding.md → category: Guides
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The pattern is **configurable** in the Admin panel. Token order is respected — `[Category]_YYYY_MM_DD_title` is valid. `[Category]` must appear exactly once.
|
|
132
|
+
|
|
111
133
|
---
|
|
112
134
|
|
|
113
135
|
## Config file
|
|
@@ -153,9 +175,10 @@ living-documentation/
|
|
|
153
175
|
├── src/
|
|
154
176
|
│ ├── server.ts Express app
|
|
155
177
|
│ ├── routes/
|
|
156
|
-
│ │ ├── documents.ts Documents API
|
|
178
|
+
│ │ ├── documents.ts Documents API (list, search, read, write)
|
|
157
179
|
│ │ ├── config.ts Config API
|
|
158
|
-
│ │
|
|
180
|
+
│ │ ├── browse.ts Filesystem browser API
|
|
181
|
+
│ │ └── images.ts Image upload API
|
|
159
182
|
│ ├── lib/
|
|
160
183
|
│ │ ├── parser.ts Filename parser
|
|
161
184
|
│ │ └── config.ts Config management
|
|
@@ -176,10 +199,12 @@ living-documentation/
|
|
|
176
199
|
| ------ | -------------------------- | ------------------------------------------------------------------ |
|
|
177
200
|
| `GET` | `/api/documents` | List all documents with metadata (includes extra files) |
|
|
178
201
|
| `GET` | `/api/documents/:id` | Get document content + rendered HTML |
|
|
202
|
+
| `PUT` | `/api/documents/:id` | Save document content to disk |
|
|
179
203
|
| `GET` | `/api/documents/search?q=` | Full-text search |
|
|
180
204
|
| `GET` | `/api/config` | Read config |
|
|
181
205
|
| `PUT` | `/api/config` | Update config (`title`, `theme`, `filenamePattern`, `extraFiles`) |
|
|
182
206
|
| `GET` | `/api/browse?path=` | List directories and `.md` files at a given filesystem path |
|
|
207
|
+
| `POST` | `/api/images/upload` | Upload a base64 image; saved to `DOCS_FOLDER/images/` |
|
|
183
208
|
|
|
184
209
|
---
|
|
185
210
|
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
14
14
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
15
15
|
|
|
16
|
+
<!-- WordCloud2.js — vendored from npm, no CDN dependency -->
|
|
17
|
+
<script src="/vendor/wordcloud2.js"></script>
|
|
18
|
+
|
|
16
19
|
<script>
|
|
17
20
|
tailwind.config = {
|
|
18
21
|
darkMode: 'class',
|
|
@@ -102,6 +105,12 @@
|
|
|
102
105
|
<span id="dark-icon" class="text-lg leading-none">☾</span>
|
|
103
106
|
</button>
|
|
104
107
|
|
|
108
|
+
<!-- Word Cloud -->
|
|
109
|
+
<button onclick="openWordCloud()"
|
|
110
|
+
class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium">
|
|
111
|
+
☁ Word Cloud
|
|
112
|
+
</button>
|
|
113
|
+
|
|
105
114
|
<!-- Admin link -->
|
|
106
115
|
<a href="/admin"
|
|
107
116
|
class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium">
|
|
@@ -236,6 +245,21 @@
|
|
|
236
245
|
</div><!-- end body row -->
|
|
237
246
|
</div><!-- end root -->
|
|
238
247
|
|
|
248
|
+
<!-- ── Word Cloud overlay ── -->
|
|
249
|
+
<div id="wc-overlay" class="hidden fixed inset-0 z-50 flex flex-col bg-white dark:bg-gray-950">
|
|
250
|
+
<div class="flex items-center justify-between px-6 h-14 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shrink-0">
|
|
251
|
+
<h2 class="font-semibold text-base">☁ Word Cloud</h2>
|
|
252
|
+
<button onclick="closeWordCloud()"
|
|
253
|
+
class="text-sm px-3 py-1.5 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
|
254
|
+
✕ Close
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
<div id="wc-body" class="flex-1 relative overflow-hidden flex items-center justify-center">
|
|
258
|
+
<p id="wc-status" class="text-gray-400 dark:text-gray-500 text-sm animate-pulse"></p>
|
|
259
|
+
<canvas id="wc-canvas" class="hidden absolute inset-0 w-full h-full"></canvas>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
239
263
|
<script>
|
|
240
264
|
// ── State ──────────────────────────────────────────────────
|
|
241
265
|
let allDocs = [];
|
|
@@ -638,6 +662,105 @@ function esc(str) {
|
|
|
638
662
|
.replace(/'/g, ''');
|
|
639
663
|
}
|
|
640
664
|
|
|
665
|
+
// ── Word Cloud ─────────────────────────────────────────────
|
|
666
|
+
const WC_STOP_WORDS = new Set([
|
|
667
|
+
// English
|
|
668
|
+
'the','and','for','are','but','not','you','all','this','that','with','have','from',
|
|
669
|
+
'they','will','one','been','can','has','was','more','also','when','there','their',
|
|
670
|
+
'what','about','which','would','into','than','then','each','just','over','after',
|
|
671
|
+
'such','here','its','your','our','some','were','very','only','out','had','she',
|
|
672
|
+
'his','her','him','who','how','any','other','these','those','being','may','use',
|
|
673
|
+
'used','using','should','could','would','shall','must','need','via','per','like',
|
|
674
|
+
// French
|
|
675
|
+
'les','des','une','pour','pas','sur','par','est','qui','que','dans','avec','sont',
|
|
676
|
+
'plus','tout','aux','mais','comme','vous','nous','leur','lui','elle','ils','elles',
|
|
677
|
+
'ces','ses','mon','ton','son','mes','tes','ainsi','donc','alors','car','peut','fait',
|
|
678
|
+
'encore','bien','aussi','très','même','entre','vers','dont','sans','sous',
|
|
679
|
+
]);
|
|
680
|
+
|
|
681
|
+
async function openWordCloud() {
|
|
682
|
+
const overlay = document.getElementById('wc-overlay');
|
|
683
|
+
const status = document.getElementById('wc-status');
|
|
684
|
+
const canvas = document.getElementById('wc-canvas');
|
|
685
|
+
overlay.classList.remove('hidden');
|
|
686
|
+
status.textContent = 'Loading documents…';
|
|
687
|
+
status.classList.remove('hidden');
|
|
688
|
+
canvas.classList.add('hidden');
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const docs = allDocs.length ? allDocs : await fetch('/api/documents').then(r => r.json());
|
|
692
|
+
status.textContent = `Analyzing ${docs.length} documents…`;
|
|
693
|
+
|
|
694
|
+
const results = await Promise.all(
|
|
695
|
+
docs.map(doc => fetch('/api/documents/' + doc.id).then(r => r.ok ? r.json() : null).catch(() => null))
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const freq = {};
|
|
699
|
+
for (const doc of results) {
|
|
700
|
+
if (!doc?.content) continue;
|
|
701
|
+
for (const w of extractWordsFromMarkdown(doc.content)) {
|
|
702
|
+
freq[w] = (freq[w] || 0) + 1;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const list = Object.entries(freq)
|
|
707
|
+
.filter(([, n]) => n >= 2)
|
|
708
|
+
.sort((a, b) => b[1] - a[1])
|
|
709
|
+
.slice(0, 150);
|
|
710
|
+
|
|
711
|
+
if (!list.length) { status.textContent = 'Not enough words found.'; return; }
|
|
712
|
+
|
|
713
|
+
renderWordCloud(list);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
status.textContent = 'Error: ' + err.message;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function extractWordsFromMarkdown(text) {
|
|
720
|
+
return text
|
|
721
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
722
|
+
.replace(/`[^`\n]+`/g, '')
|
|
723
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
724
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
725
|
+
.replace(/[#*_~>`|!\[\](){}=\-+]/g, ' ')
|
|
726
|
+
.toLowerCase()
|
|
727
|
+
.split(/[^a-zàâäéèêëïîôùûü']+/)
|
|
728
|
+
.map(w => w.replace(/^'+|'+$/g, ''))
|
|
729
|
+
.filter(w => w.length > 3 && !WC_STOP_WORDS.has(w));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function renderWordCloud(list) {
|
|
733
|
+
const canvas = document.getElementById('wc-canvas');
|
|
734
|
+
const body = document.getElementById('wc-body');
|
|
735
|
+
canvas.width = body.clientWidth;
|
|
736
|
+
canvas.height = body.clientHeight;
|
|
737
|
+
|
|
738
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
739
|
+
const colors = isDark
|
|
740
|
+
? ['#60a5fa','#34d399','#f9a8d4','#a78bfa','#fbbf24','#6ee7b7','#93c5fd','#fb923c']
|
|
741
|
+
: ['#1d4ed8','#047857','#7c3aed','#b45309','#be123c','#0369a1','#4338ca','#c2410c'];
|
|
742
|
+
|
|
743
|
+
const maxFreq = list[0][1];
|
|
744
|
+
const wordList = list.map(([w, n]) => [w, Math.max(10, Math.round(72 * n / maxFreq))]);
|
|
745
|
+
|
|
746
|
+
document.getElementById('wc-status').classList.add('hidden');
|
|
747
|
+
canvas.classList.remove('hidden');
|
|
748
|
+
|
|
749
|
+
WordCloud(canvas, {
|
|
750
|
+
list: wordList,
|
|
751
|
+
gridSize: Math.round(8 * canvas.width / 1024),
|
|
752
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
753
|
+
color: () => colors[Math.floor(Math.random() * colors.length)],
|
|
754
|
+
backgroundColor: isDark ? '#030712' : '#ffffff',
|
|
755
|
+
rotateRatio: 0.3,
|
|
756
|
+
minSize: 10,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function closeWordCloud() {
|
|
761
|
+
document.getElementById('wc-overlay').classList.add('hidden');
|
|
762
|
+
}
|
|
763
|
+
|
|
641
764
|
// Browser back/forward
|
|
642
765
|
window.addEventListener('popstate', e => {
|
|
643
766
|
const id = e.state?.docId || new URLSearchParams(location.search).get('doc');
|