dochub 1.0.4 → 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.
@@ -0,0 +1,390 @@
1
+ function escapeHtml(str) {
2
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3
+ }
4
+
5
+ function extractTitle(contentHtml) {
6
+ const m = contentHtml.match(/<h1[^>]*>([\s\S]*?)<\/h1>/);
7
+ return m ? m[1].replace(/<[^>]+>/g, '') : '';
8
+ }
9
+
10
+ function generatePageHtml(options) {
11
+ const {
12
+ config = { title: 'DocHub', description: '' },
13
+ sidebarHtml = '',
14
+ contentHtml = '',
15
+ searchData = [],
16
+ currentPath = '/',
17
+ isDev = false,
18
+ } = options;
19
+
20
+ const siteTitle = escapeHtml(config.title || 'DocHub');
21
+ const rawPageTitle = extractTitle(contentHtml);
22
+ const pageTitle = rawPageTitle ? `${rawPageTitle} - ${siteTitle}` : siteTitle;
23
+ const searchJson = JSON.stringify(searchData).replace(/<\/script>/gi, '<\\/script>');
24
+
25
+ return `<!DOCTYPE html>
26
+ <html lang="en" data-theme="light">
27
+ <head>
28
+ <meta charset="UTF-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
+ <title>${pageTitle}</title>
31
+ <style>
32
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
33
+ :root{--bg:#fff;--bg-secondary:#f8fafc;--bg-tertiary:#f1f5f9;--surface:#fff;--text:#0f172a;--text-secondary:#475569;--text-tertiary:#94a3b8;--accent:#3b82f6;--accent-hover:#2563eb;--accent-light:#eff6ff;--border:#e2e8f0;--sidebar-bg:#fafbfc;--code-bg:#1e293b;--code-text:#e2e8f0;--header-height:56px;--sidebar-width:280px}
34
+ [data-theme=dark]{--bg:#0b1120;--bg-secondary:#0f172a;--bg-tertiary:#1e293b;--surface:#1e293b;--text:#f1f5f9;--text-secondary:#94a3b8;--text-tertiary:#64748b;--accent:#60a5fa;--accent-hover:#3b82f6;--accent-light:#1e3a5f;--border:#334155;--sidebar-bg:#0f172a;--code-bg:#020617;--code-text:#e2e8f0}
35
+ html{font-size:16px}
36
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--bg);color:var(--text);line-height:1.7;-webkit-font-smoothing:antialiased;overflow-x:hidden}
37
+ a{color:var(--accent);text-decoration:none}
38
+ a:hover{color:var(--accent-hover)}
39
+ img{max-width:100%;height:auto;border-radius:8px}
40
+ hr{border:none;border-top:1px solid var(--border);margin:2rem 0}
41
+
42
+ .header{position:fixed;top:0;left:0;right:0;height:var(--header-height);background:rgba(255,255,255,.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 16px;z-index:100;gap:12px}
43
+ [data-theme=dark] .header{background:rgba(11,17,32,.85)}
44
+ .header-inner{display:flex;align-items:center;gap:12px;width:100%;max-width:1440px;margin:0 auto}
45
+ .sidebar-toggle{display:none;background:none;border:none;color:var(--text);cursor:pointer;padding:6px;border-radius:6px;font-size:20px;line-height:1}
46
+ .sidebar-toggle:hover{background:var(--bg-tertiary)}
47
+ .site-title{font-size:18px;font-weight:700;color:var(--text);white-space:nowrap;letter-spacing:-.3px}
48
+ .site-title:hover{color:var(--accent)}
49
+ .search-wrapper{flex:1;max-width:480px;position:relative;margin:0 auto}
50
+ .search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-tertiary);pointer-events:none}
51
+ .search-input{width:100%;height:36px;padding:0 14px 0 32px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);color:var(--text);font-size:14px;outline:none;transition:border-color .2s,box-shadow .2s}
52
+ .search-input::placeholder{color:var(--text-tertiary)}
53
+ .search-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light)}
54
+ .search-results{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,.12);max-height:360px;overflow-y:auto;display:none;z-index:200}
55
+ .search-results.show{display:block}
56
+ .search-result-item{display:block;padding:10px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
57
+ .search-result-item:last-child{border-bottom:none}
58
+ .search-result-item:hover,.search-result-item.highlighted{background:var(--accent-light)}
59
+ .search-result-title{font-size:14px;font-weight:600;color:var(--text)}
60
+ .search-result-path{font-size:12px;color:var(--text-tertiary);margin-top:2px}
61
+ .search-result-excerpt{font-size:13px;color:var(--text-secondary);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
62
+ .no-results{padding:20px;text-align:center;color:var(--text-tertiary);font-size:14px}
63
+ .theme-toggle{background:none;border:none;color:var(--text);cursor:pointer;padding:6px;border-radius:6px;font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;width:32px;height:32px;transition:background .15s}
64
+ .theme-toggle:hover{background:var(--bg-tertiary)}
65
+ .theme-toggle .sun{display:none}
66
+ .theme-toggle .moon{display:block}
67
+ [data-theme=dark] .theme-toggle .sun{display:block}
68
+ [data-theme=dark] .theme-toggle .moon{display:none}
69
+
70
+ .layout{display:flex;padding-top:var(--header-height);min-height:100vh}
71
+ .overlay{display:none}
72
+
73
+ .sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);height:calc(100vh - var(--header-height));position:sticky;top:var(--header-height);overflow-y:auto;background:var(--sidebar-bg);border-right:1px solid var(--border);padding:16px 0}
74
+ .sidebar::-webkit-scrollbar{width:6px}
75
+ .sidebar::-webkit-scrollbar-track{background:transparent}
76
+ .sidebar::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
77
+ .sidebar-nav{padding:0 12px}
78
+ .sidebar-section{margin-bottom:4px}
79
+ .sidebar-section-header{display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:6px;font-size:13px;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.5px;cursor:pointer;user-select:none}
80
+ .sidebar-section-header:hover{color:var(--text)}
81
+ .sidebar-section-header svg.arrow{width:12px;height:12px;transition:transform .2s;flex-shrink:0;color:var(--text-tertiary)}
82
+ .sidebar-section-header svg.arrow.open{transform:rotate(90deg)}
83
+ .sidebar-link{display:flex;align-items:center;gap:8px;padding:6px 8px 6px 20px;border-radius:6px;font-size:14px;color:var(--text-secondary);transition:all .15s;text-decoration:none}
84
+ .sidebar-link:hover{background:var(--bg-tertiary);color:var(--text)}
85
+ .sidebar-link.active{background:var(--accent-light);color:var(--accent);font-weight:600}
86
+ .sidebar-link .icon{flex-shrink:0;color:var(--text-tertiary)}
87
+ .sidebar-link.active .icon{color:var(--accent)}
88
+
89
+ .main{flex:1;min-width:0;padding:40px 48px;max-width:896px}
90
+ .main-content{max-width:720px}
91
+ .main-content>*+*{margin-top:1.25em}
92
+ .main-content h1{margin-top:0;font-size:2.25rem;font-weight:800;letter-spacing:-.5px;line-height:1.3;color:var(--text)}
93
+ .main-content h2{margin-top:2rem;font-size:1.5rem;font-weight:700;letter-spacing:-.3px;line-height:1.4;color:var(--text);padding-bottom:.3rem;border-bottom:1px solid var(--border)}
94
+ .main-content h3{margin-top:1.75rem;font-size:1.2rem;font-weight:600;color:var(--text)}
95
+ .main-content h4{margin-top:1.5rem;font-size:1.05rem;font-weight:600;color:var(--text)}
96
+ .main-content h5{margin-top:1.25rem;font-size:.95rem;font-weight:600;color:var(--text-secondary)}
97
+ .main-content h6{margin-top:1.25rem;font-size:.875rem;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.5px}
98
+ .main-content p{color:var(--text-secondary);line-height:1.8}
99
+ .main-content strong{color:var(--text);font-weight:600}
100
+ .main-content ul,.main-content ol{padding-left:1.5rem;color:var(--text-secondary)}
101
+ .main-content li{margin:.4em 0}
102
+ .main-content li::marker{color:var(--text-tertiary)}
103
+ .main-content blockquote{margin:1.5rem 0;padding:.75rem 1.25rem;border-left:4px solid var(--accent);background:var(--accent-light);border-radius:0 8px 8px 0;color:var(--text-secondary)}
104
+ .main-content blockquote p{margin:0}
105
+ .main-content table{width:100%;border-collapse:collapse;margin:1.5rem 0;font-size:14px}
106
+ .main-content th,.main-content td{padding:10px 14px;border:1px solid var(--border);text-align:left}
107
+ .main-content th{background:var(--bg-secondary);font-weight:600;color:var(--text)}
108
+ .main-content td{color:var(--text-secondary)}
109
+ .main-content tr:nth-child(even) td{background:var(--bg-secondary)}
110
+ .main-content code{background:var(--bg-tertiary);color:var(--accent);padding:2px 6px;border-radius:4px;font-size:.875em;font-family:'JetBrains Mono','Fira Code','Consolas',monospace}
111
+ .main-content pre{margin:1.5rem 0;border-radius:10px;overflow:hidden}
112
+ .main-content pre code{background:transparent;color:var(--code-text);padding:0;border-radius:0;font-size:14px;line-height:1.6}
113
+ .main-content :not(pre)>.code-block{display:block;overflow-x:auto;padding:1rem}
114
+ .code-block{background:var(--code-bg);padding:1.25rem;overflow-x:auto;-webkit-overflow-scrolling:touch}
115
+ .code-block::-webkit-scrollbar{height:6px}
116
+ .code-block::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}
117
+ .main-content a code{color:var(--accent)}
118
+ .main-content img{margin:1.5rem 0}
119
+
120
+ .hljs-keyword{color:#8250df}
121
+ .hljs-string{color:#0a3069}
122
+ .hljs-comment{color:#6e7781;font-style:italic}
123
+ .hljs-function{color:#8250df}
124
+ .hljs-number{color:#0550ae}
125
+ .hljs-type{color:#116329}
126
+ .hljs-attr{color:#0550ae}
127
+ .hljs-attribute{color:#0550ae}
128
+ .hljs-title{color:#8250df}
129
+ .hljs-literal{color:#0550ae}
130
+ .hljs-built_in{color:#116329}
131
+ .hljs-property{color:#0550ae}
132
+ .hljs-selector-class{color:#0550ae}
133
+ .hljs-regexp{color:#0a3069}
134
+ .hljs-symbol{color:#0550ae}
135
+ .hljs-variable{color:#0a3069}
136
+ .hljs-template-variable{color:#0a3069}
137
+ .hljs-doctag{color:#6e7781}
138
+ .hljs-meta{color:#6e7781}
139
+ .hljs-meta .hljs-keyword{color:#6e7781}
140
+ .hljs-meta .hljs-string{color:#0a3069}
141
+ .hljs-section{color:#8250df;font-weight:700}
142
+ .hljs-deletion{color:#82071e;background:#ffebe9}
143
+ .hljs-addition{color:#116329;background:#dafbe1}
144
+ [data-theme=dark] .hljs-keyword{color:#ff7b72}
145
+ [data-theme=dark] .hljs-string{color:#a5d6ff}
146
+ [data-theme=dark] .hljs-comment{color:#8b949e;font-style:italic}
147
+ [data-theme=dark] .hljs-function{color:#d2a8ff}
148
+ [data-theme=dark] .hljs-number{color:#79c0ff}
149
+ [data-theme=dark] .hljs-type{color:#7ee787}
150
+ [data-theme=dark] .hljs-attr{color:#79c0ff}
151
+ [data-theme=dark] .hljs-attribute{color:#79c0ff}
152
+ [data-theme=dark] .hljs-title{color:#d2a8ff}
153
+ [data-theme=dark] .hljs-literal{color:#79c0ff}
154
+ [data-theme=dark] .hljs-built_in{color:#7ee787}
155
+ [data-theme=dark] .hljs-property{color:#79c0ff}
156
+ [data-theme=dark] .hljs-selector-class{color:#79c0ff}
157
+ [data-theme=dark] .hljs-regexp{color:#a5d6ff}
158
+ [data-theme=dark] .hljs-symbol{color:#79c0ff}
159
+ [data-theme=dark] .hljs-variable{color:#a5d6ff}
160
+ [data-theme=dark] .hljs-template-variable{color:#a5d6ff}
161
+ [data-theme=dark] .hljs-doctag{color:#8b949e}
162
+ [data-theme=dark] .hljs-meta{color:#8b949e}
163
+ [data-theme=dark] .hljs-meta .hljs-keyword{color:#8b949e}
164
+ [data-theme=dark] .hljs-meta .hljs-string{color:#a5d6ff}
165
+ [data-theme=dark] .hljs-section{color:#d2a8ff;font-weight:700}
166
+ [data-theme=dark] .hljs-deletion{background:#490202;color:#ffa198}
167
+ [data-theme=dark] .hljs-addition{background:#04260f;color:#7ee787}
168
+
169
+ @media(max-width:768px){
170
+ .sidebar-toggle{display:block}
171
+ .sidebar{position:fixed;top:var(--header-height);left:-280px;bottom:0;z-index:300;transition:left .25s ease;box-shadow:4px 0 20px rgba(0,0,0,.1)}
172
+ .sidebar.open{left:0}
173
+ .overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:299}
174
+ .overlay.show{display:block}
175
+ .main{padding:24px 20px}
176
+ .search-wrapper{max-width:none}
177
+ .search-input{font-size:16px}
178
+ }
179
+ @media(max-width:480px){
180
+ .site-title{font-size:15px}
181
+ .main{padding:16px}
182
+ .main-content h1{font-size:1.75rem}
183
+ .main-content h2{font-size:1.25rem}
184
+ }
185
+ </style>
186
+ </head>
187
+ <body>
188
+ <div class="header">
189
+ <div class="header-inner">
190
+ <button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg></button>
191
+ <a href="/" class="site-title">${siteTitle}</a>
192
+ <div class="search-wrapper">
193
+ <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
194
+ <input type="search" class="search-input" id="searchInput" placeholder="Search documentation..." autocomplete="off" spellcheck="false">
195
+ <div class="search-results" id="searchResults"></div>
196
+ </div>
197
+ <button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
198
+ <span class="sun"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
199
+ <span class="moon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
200
+ </button>
201
+ </div>
202
+ </div>
203
+
204
+ <div class="overlay" id="overlay"></div>
205
+
206
+ <div class="layout">
207
+ <aside class="sidebar" id="sidebar">
208
+ <nav class="sidebar-nav">
209
+ ${sidebarHtml}
210
+ </nav>
211
+ </aside>
212
+ <main class="main">
213
+ <div class="main-content">
214
+ ${contentHtml}
215
+ </div>
216
+ </main>
217
+ </div>
218
+
219
+ <script id="dochubSearchData" type="application/json">${searchJson}</script>
220
+
221
+ <script>
222
+ (function(){'use strict';
223
+
224
+ var searchData = [];
225
+ try { searchData = JSON.parse(document.getElementById('dochubSearchData').textContent); } catch(e) {}
226
+
227
+ var searchInput = document.getElementById('searchInput');
228
+ var searchResults = document.getElementById('searchResults');
229
+ var themeToggle = document.getElementById('themeToggle');
230
+ var sidebar = document.getElementById('sidebar');
231
+ var sidebarToggle = document.getElementById('sidebarToggle');
232
+ var overlay = document.getElementById('overlay');
233
+
234
+ var currentTheme = localStorage.getItem('dochub-theme') || 'light';
235
+ document.documentElement.setAttribute('data-theme', currentTheme);
236
+
237
+ themeToggle.addEventListener('click', function() {
238
+ var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
239
+ document.documentElement.setAttribute('data-theme', next);
240
+ localStorage.setItem('dochub-theme', next);
241
+ });
242
+
243
+ sidebarToggle.addEventListener('click', function() {
244
+ sidebar.classList.toggle('open');
245
+ overlay.classList.toggle('show');
246
+ });
247
+ overlay.addEventListener('click', function() {
248
+ sidebar.classList.remove('open');
249
+ overlay.classList.remove('show');
250
+ });
251
+
252
+ function escapeHtml(text) {
253
+ var d = document.createElement('div');
254
+ d.appendChild(document.createTextNode(text));
255
+ return d.innerHTML;
256
+ }
257
+
258
+ function normalize(text) {
259
+ return text.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/ {2,}/g, ' ').trim();
260
+ }
261
+
262
+ function score(result, query) {
263
+ var nq = normalize(query);
264
+ var title = normalize(result.title);
265
+ var excerpt = normalize(result.excerpt || '');
266
+ var s = 0;
267
+ if (title === nq) s += 100;
268
+ if (title.indexOf(nq) === 0) s += 50;
269
+ if (title.indexOf(nq) > 0) s += 25;
270
+ if (excerpt.indexOf(nq) >= 0) s += 10;
271
+ if (nq.length > 2) {
272
+ var words = nq.split(/\\s+/);
273
+ words.forEach(function(w) {
274
+ if (title.indexOf(w) >= 0) s += 5;
275
+ if (excerpt.indexOf(w) >= 0) s += 2;
276
+ });
277
+ }
278
+ return s;
279
+ }
280
+
281
+ searchInput.addEventListener('input', function() {
282
+ var query = this.value.trim();
283
+ if (!query) {
284
+ searchResults.classList.remove('show');
285
+ return;
286
+ }
287
+
288
+ var scored = searchData.map(function(item) {
289
+ return { item: item, score: score(item, query) };
290
+ }).filter(function(s) { return s.score > 0; });
291
+ scored.sort(function(a, b) { return b.score - a.score; });
292
+ var results = scored.slice(0, 10).map(function(s) { return s.item; });
293
+
294
+ if (results.length === 0) {
295
+ searchResults.innerHTML = '<div class="no-results">No results found</div>';
296
+ searchResults.classList.add('show');
297
+ return;
298
+ }
299
+
300
+ var html = '';
301
+ results.forEach(function(r) {
302
+ var excerpt = r.excerpt || '';
303
+ var nq = normalize(query);
304
+ var idx = normalize(excerpt).indexOf(nq);
305
+ var displayExcerpt = '';
306
+ if (idx >= 0) {
307
+ var start = Math.max(0, idx - 40);
308
+ var end = Math.min(excerpt.length, idx + 80);
309
+ displayExcerpt = (start > 0 ? '...' : '') + excerpt.substring(start, end) + (end < excerpt.length ? '...' : '');
310
+ } else {
311
+ displayExcerpt = excerpt.substring(0, 100) + (excerpt.length > 100 ? '...' : '');
312
+ }
313
+ html += '<a href="' + escapeHtml(r.path) + '" class="search-result-item">' +
314
+ '<div class="search-result-title">' + escapeHtml(r.title) + '</div>' +
315
+ '<div class="search-result-path">' + escapeHtml(r.path) + '</div>' +
316
+ (displayExcerpt ? '<div class="search-result-excerpt">' + escapeHtml(displayExcerpt) + '</div>' : '') +
317
+ '</a>';
318
+ });
319
+ searchResults.innerHTML = html;
320
+ searchResults.classList.add('show');
321
+ });
322
+
323
+ searchInput.addEventListener('blur', function() {
324
+ setTimeout(function() { searchResults.classList.remove('show'); }, 200);
325
+ });
326
+ searchInput.addEventListener('focus', function() {
327
+ if (this.value.trim()) {
328
+ this.dispatchEvent(new Event('input'));
329
+ }
330
+ });
331
+
332
+ var highlightedIndex = -1;
333
+ searchResults.addEventListener('mouseover', function(e) {
334
+ var item = e.target.closest('.search-result-item');
335
+ if (item) {
336
+ searchResults.querySelectorAll('.search-result-item').forEach(function(el) {
337
+ el.classList.remove('highlighted');
338
+ });
339
+ item.classList.add('highlighted');
340
+ }
341
+ });
342
+
343
+ searchInput.addEventListener('keydown', function(e) {
344
+ var items = searchResults.querySelectorAll('.search-result-item');
345
+ if (e.key === 'ArrowDown') {
346
+ e.preventDefault();
347
+ highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1);
348
+ } else if (e.key === 'ArrowUp') {
349
+ e.preventDefault();
350
+ highlightedIndex = Math.max(highlightedIndex - 1, 0);
351
+ } else if (e.key === 'Enter' && highlightedIndex >= 0 && items[highlightedIndex]) {
352
+ items[highlightedIndex].click();
353
+ return;
354
+ } else {
355
+ highlightedIndex = -1;
356
+ }
357
+ items.forEach(function(el, i) {
358
+ el.classList.toggle('highlighted', i === highlightedIndex);
359
+ });
360
+ if (highlightedIndex >= 0 && items[highlightedIndex]) {
361
+ items[highlightedIndex].scrollIntoView({ block: 'nearest' });
362
+ }
363
+ });
364
+
365
+ var activeLink = sidebar.querySelector('.sidebar-link.active');
366
+ if (activeLink) {
367
+ activeLink.scrollIntoView({ block: 'center', behavior: 'auto' });
368
+ }
369
+
370
+ var folderToggles = sidebar.querySelectorAll('.sidebar-section-header');
371
+ folderToggles.forEach(function(header) {
372
+ header.addEventListener('click', function() {
373
+ var arrow = this.querySelector('.arrow');
374
+ var list = this.nextElementSibling;
375
+ if (list) {
376
+ var isHidden = list.style.display === 'none';
377
+ list.style.display = isHidden ? '' : 'none';
378
+ if (arrow) arrow.classList.toggle('open', isHidden);
379
+ }
380
+ });
381
+ });
382
+
383
+ })();
384
+ </script>
385
+ ${isDev ? '<script>var ws=new WebSocket("ws://"+location.host);ws.onmessage=function(e){if(e.data==="refresh"){location.reload()}};ws.onclose=function(){setTimeout(function(){location.reload()},1000)};</script>' : ''}
386
+ </body>
387
+ </html>`;
388
+ }
389
+
390
+ module.exports = { generatePageHtml };
package/src/utils.js ADDED
@@ -0,0 +1,111 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function loadConfig(directory) {
5
+ const jsPath = path.join(directory, 'dochub.config.js');
6
+ const jsonPath = path.join(directory, 'dochub.config.json');
7
+ try {
8
+ if (fs.existsSync(jsPath)) return require(path.resolve(jsPath));
9
+ if (fs.existsSync(jsonPath)) return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
10
+ } catch {}
11
+ return { title: 'DocHub', description: '' };
12
+ }
13
+
14
+ function walkMarkdownFiles(dir, rootDir) {
15
+ const files = [];
16
+ let entries;
17
+ try {
18
+ entries = fs.readdirSync(dir, { withFileTypes: true });
19
+ } catch {
20
+ return files;
21
+ }
22
+ const sorted = entries.sort((a, b) => {
23
+ if (a.isDirectory() && !b.isDirectory()) return -1;
24
+ if (!a.isDirectory() && b.isDirectory()) return 1;
25
+ return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' });
26
+ });
27
+ for (const entry of sorted) {
28
+ const fullPath = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ files.push(...walkMarkdownFiles(fullPath, rootDir));
31
+ } else if (entry.name.endsWith('.md')) {
32
+ const relative = path.relative(rootDir, fullPath);
33
+ let urlPath = '/' + relative.replace(/\\/g, '/').replace(/\.md$/i, '').replace(/readme$/i, '');
34
+ if (!urlPath) urlPath = '/';
35
+ if (urlPath !== '/' && urlPath.endsWith('/')) urlPath = urlPath.slice(0, -1);
36
+ let title = entry.name.replace(/\.md$/i, '');
37
+ try {
38
+ let content = fs.readFileSync(fullPath, 'utf8');
39
+ const h1 = extractTitle(content);
40
+ if (h1) title = h1;
41
+ } catch {}
42
+ files.push({ path: fullPath, url: urlPath, name: entry.name, title });
43
+ }
44
+ }
45
+ return files;
46
+ }
47
+
48
+ function extractTitle(content) {
49
+ const m = content.match(/^#\s+(.+)$/m);
50
+ return m ? m[1].trim() : '';
51
+ }
52
+
53
+ function getSearchData(dir) {
54
+ const markdownDir = path.join(dir, 'markdown');
55
+ if (!fs.existsSync(markdownDir)) return [];
56
+ const files = walkMarkdownFiles(markdownDir, markdownDir);
57
+ return files.map(f => {
58
+ let content = '';
59
+ try { content = fs.readFileSync(f.path, 'utf8'); } catch {}
60
+ const excerpt = content.replace(/<[^>]+>/g, '').replace(/```[\s\S]*?```/g, '').replace(/#{1,6}\s/g, '').trim().substring(0, 300);
61
+ return { path: f.url, title: f.title, excerpt };
62
+ });
63
+ }
64
+
65
+ function generateSidebarHtml(dir, currentPath) {
66
+ const markdownDir = path.join(dir, 'markdown');
67
+ if (!fs.existsSync(markdownDir)) return '<div class="sidebar-link" style="color:var(--text-tertiary);font-size:13px;padding:12px 8px">No markdown files found</div>';
68
+ const files = walkMarkdownFiles(markdownDir, markdownDir);
69
+
70
+ const rootFiles = files.filter(f => path.dirname(f.path) === markdownDir);
71
+ const dirs = new Map();
72
+ for (const f of files) {
73
+ const dirPath = path.dirname(f.path);
74
+ if (dirPath === markdownDir) continue;
75
+ const relative = path.relative(markdownDir, dirPath);
76
+ if (!dirs.has(relative)) dirs.set(relative, []);
77
+ dirs.get(relative).push(f);
78
+ }
79
+
80
+ let html = '';
81
+
82
+ for (const f of rootFiles) {
83
+ const active = f.url === currentPath || (currentPath === '/' && f.url === '/');
84
+ const label = extractLabel(f);
85
+ html += `<a href="${f.url}" class="sidebar-link${active ? ' active' : ''}"><svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>${label}</a>`;
86
+ }
87
+
88
+ const sortedDirs = [...dirs.entries()].sort((a, b) => a[0].localeCompare(b[0], 'en', { sensitivity: 'base' }));
89
+ for (const [dirName, dirFiles] of sortedDirs) {
90
+ const hasActive = dirFiles.some(f => f.url === currentPath || (currentPath === '/' && f.url === '/'));
91
+ html += `<div class="sidebar-section">`;
92
+ html += `<div class="sidebar-section-header"><svg class="arrow${hasActive ? ' open' : ''}" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>${dirName}</div>`;
93
+ html += `<ul style="list-style:none;padding:0;margin:0;display:${hasActive ? 'block' : 'none'}">`;
94
+ for (const f of dirFiles) {
95
+ const active = f.url === currentPath || (currentPath === '/' && f.url === '/');
96
+ const label = extractLabel(f);
97
+ html += `<li><a href="${f.url}" class="sidebar-link${active ? ' active' : ''}" style="padding-left:28px"><svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>${label}</a></li>`;
98
+ }
99
+ html += `</ul></div>`;
100
+ }
101
+
102
+ return html;
103
+ }
104
+
105
+ function extractLabel(file) {
106
+ if (file.title) return file.title;
107
+ let label = file.name.replace(/\.md$/i, '');
108
+ return label.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
109
+ }
110
+
111
+ module.exports = { loadConfig, walkMarkdownFiles, extractTitle, getSearchData, generateSidebarHtml };
@@ -1,43 +1,69 @@
1
- # Welcome to your first documentation!
2
-
3
- This project dynamically generates and displays documentation from Markdown files converted to HTML. It utilizes Socket.IO for real-time updates, integrates with Tailwind CSS for styling, and uses Highlight.js for code syntax highlighting.
4
-
5
- ## Features
6
-
7
- - **Real-time Updates:** Automatically refreshes content when documentation files are modified.
8
- - **Search Functionality:** Includes a search bar to filter through documentation content.
9
- - **Syntax Highlighting:** Uses Highlight.js to format code blocks for better readability.
10
- - **Dark Mode:** Implements a dark mode theme for improved viewing experience.
11
-
12
- ## Getting Started
13
-
14
- To generate and view your documentation, follow these steps:
15
-
16
- 1. **Installation:**
17
- - Ensure Node.js is installed on your system.
18
-
19
- 2. **Generate Documentation:**
20
- - Run `docify serve [directory]` in your terminal.
21
- - Replace `[directory]` with the path to your documentation files.
22
-
23
- 3. **View Documentation:**
24
- - Open a web browser and navigate to `http://localhost:3000`.
25
- - The sidebar lists different sections of your documentation.
26
- - Use the search bar to find specific topics within the documentation.
27
-
28
- ## Project Structure
29
-
30
- The project structure includes:
31
-
32
- - **HTML and CSS:** Main structure and styling managed using HTML and Tailwind CSS.
33
- - **JavaScript (Client-side):** Handles dynamic content updates, event handling, and integration with external libraries like Socket.IO and Highlight.js.
34
- - **Socket.IO:** Facilitates real-time communication to update documentation on file changes.
35
- - **Markdown to HTML Conversion:** Converts Markdown content fetched from the server-side into HTML for rendering.
36
-
37
- ## Getting Help
38
-
39
- If you encounter any issues or have questions about using this documentation project, feel free to reach out via [GitHub Issues](https://github.com/tyler-Github/dochub/issues).
40
-
41
- ## License
42
-
43
- This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
1
+ # Welcome to DocHub
2
+
3
+ DocHub is a modern documentation generator that transforms Markdown files into a beautiful, searchable documentation site with live-reload during development and static HTML export for production.
4
+
5
+ ## Features
6
+
7
+ - **Live-reload dev server** -- changes to markdown files instantly update the browser
8
+ - **Static site generation** -- export a fully self-contained HTML site for deployment
9
+ - **Full-text search** -- client-side search across all pages with relevance scoring
10
+ - **Dark mode** -- automatic theme toggle with persistent preference
11
+ - **Syntax highlighting** -- code blocks rendered with Highlight.js across 190+ languages
12
+ - **Responsive design** -- works on desktop, tablet, and mobile
13
+ - **Zero external runtime dependencies** -- everything is self-contained in the generated HTML
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Install globally
19
+ npm install -g dochub
20
+
21
+ # Scaffold a project
22
+ dochub init my-docs
23
+
24
+ # Start the dev server
25
+ cd my-docs && dochub serve
26
+
27
+ # Build for production
28
+ dochub build
29
+ ```
30
+
31
+ ## Project Structure
32
+
33
+ ```
34
+ my-docs/
35
+ ├── markdown/ # Write your documentation here
36
+ │ ├── README.md # Home page
37
+ │ ├── guide.md # Additional pages
38
+ │ └── folder/ # Organized into folders
39
+ │ └── README.md
40
+ └── dochub.config.js # Site configuration
41
+ ```
42
+
43
+ ## Writing Documentation
44
+
45
+ DocHub supports standard Markdown with the following features:
46
+
47
+ - Headers (h1 through h6)
48
+ - Bold, italic, and inline code
49
+ - Ordered and unordered lists
50
+ - Links and images
51
+ - Code blocks with syntax highlighting
52
+ - Blockquotes
53
+ - Tables
54
+ - Horizontal rules
55
+
56
+ ## Customization
57
+
58
+ Edit `dochub.config.js` in your project root to change the site title and description:
59
+
60
+ ```js
61
+ module.exports = {
62
+ title: 'My Documentation',
63
+ description: 'A great documentation site',
64
+ };
65
+ ```
66
+
67
+ ## Deployment
68
+
69
+ Run `dochub build` to generate a static `dist/` folder. Upload the contents to any static file host -- Netlify, Vercel, GitHub Pages, or a simple HTTP server.
@@ -1,3 +1,27 @@
1
- # Readme
2
-
3
- This is a readme in a folder.
1
+ # Nested Documentation
2
+
3
+ Pages can be organized into folders to keep your documentation structured.
4
+
5
+ ## Why Use Folders?
6
+
7
+ - **Organization** -- Group related topics together
8
+ - **Clean URLs** -- Pages get nested URLs like `/folder/page`
9
+ - **Scalability** -- Keeps large documentation sets manageable
10
+
11
+ ## Example Structure
12
+
13
+ ```
14
+ markdown/
15
+ ├── README.md # /
16
+ ├── guide.md # /guide
17
+ ├── api/
18
+ │ ├── README.md # /api
19
+ │ ├── authentication.md # /api/authentication
20
+ │ └── endpoints.md # /api/endpoints
21
+ └── deployment/
22
+ └── README.md # /deployment
23
+ ```
24
+
25
+ ## Navigation
26
+
27
+ Folders appear as collapsible sections in the sidebar. Click the folder name to expand or collapse its contents. The current page is highlighted automatically.