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 date (newest first)
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
- │ │ └── browse.ts Filesystem browser API
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">&#9790;</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
+ &#9729; 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">&#9729; 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
+ &#10005; 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, '&#39;');
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');