mkdnsite 0.0.1 → 1.1.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.
@@ -9,6 +9,9 @@ export function CLIENT_SCRIPTS (client: ClientConfig): string {
9
9
 
10
10
  const scripts: string[] = []
11
11
 
12
+ scripts.push(NAV_TOGGLE_SCRIPT)
13
+ scripts.push(STICKY_TABLE_SCRIPT)
14
+
12
15
  if (client.themeToggle) {
13
16
  scripts.push(THEME_TOGGLE_SCRIPT)
14
17
  }
@@ -23,6 +26,11 @@ export function CLIENT_SCRIPTS (client: ClientConfig): string {
23
26
 
24
27
  if (client.search) {
25
28
  scripts.push(SEARCH_SCRIPT)
29
+ scripts.push(HIGHLIGHT_SCRIPT)
30
+ }
31
+
32
+ if (client.charts) {
33
+ scripts.push(CHART_SCRIPT)
26
34
  }
27
35
 
28
36
  if (scripts.length === 0) return ''
@@ -30,6 +38,31 @@ export function CLIENT_SCRIPTS (client: ClientConfig): string {
30
38
  return `<script>${scripts.join('\n')}</script>`
31
39
  }
32
40
 
41
+ const NAV_TOGGLE_SCRIPT = `
42
+ (function(){
43
+ var toggle = document.querySelector('.mkdn-nav-toggle');
44
+ var nav = document.querySelector('.mkdn-nav');
45
+ var backdrop = document.querySelector('.mkdn-nav-backdrop');
46
+ if (!toggle || !nav) return;
47
+ function open() {
48
+ nav.classList.add('mkdn-nav--open');
49
+ if (backdrop) backdrop.classList.add('mkdn-nav-backdrop--visible');
50
+ toggle.setAttribute('aria-expanded', 'true');
51
+ }
52
+ function close() {
53
+ nav.classList.remove('mkdn-nav--open');
54
+ if (backdrop) backdrop.classList.remove('mkdn-nav-backdrop--visible');
55
+ toggle.setAttribute('aria-expanded', 'false');
56
+ }
57
+ toggle.addEventListener('click', function() {
58
+ nav.classList.contains('mkdn-nav--open') ? close() : open();
59
+ });
60
+ if (backdrop) backdrop.addEventListener('click', close);
61
+ nav.addEventListener('click', function(e) {
62
+ if (e.target && e.target.closest && e.target.closest('a')) close();
63
+ });
64
+ })();`
65
+
33
66
  const THEME_TOGGLE_SCRIPT = `
34
67
  (function(){
35
68
  var btn = document.querySelector('.mkdn-theme-toggle');
@@ -100,7 +133,376 @@ const MERMAID_SCRIPT = `
100
133
  `.trim()
101
134
 
102
135
  const SEARCH_SCRIPT = `
103
- /* Search: placeholder for client-side search functionality.
104
- Will be implemented with a pre-built content index served at /search-index.json.
105
- For MVP, this is a no-op that can be activated once the index endpoint exists. */
136
+ (function(){
137
+ // ── Search modal ──────────────────────────────────────────────────────────
138
+ var overlay, input, resultsEl, activeIdx = -1, debounceTimer, lastQuery = '';
139
+
140
+ function escHtml(s) {
141
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
142
+ }
143
+
144
+ function boldTerms(excerpt, terms) {
145
+ var safe = escHtml(excerpt);
146
+ var sorted = terms.slice().sort(function(a,b){ return b.length - a.length; });
147
+ sorted.forEach(function(t){
148
+ if (t.length < 2) return;
149
+ var escaped = t.replace(new RegExp('[.*+?^' + '$' + '{}()|[\\]\\\\]','g'),'\\$&');
150
+ var re = new RegExp('(' + escaped + ')', 'gi');
151
+ safe = safe.replace(re, '<mark>$1</mark>');
152
+ });
153
+ return safe;
154
+ }
155
+
156
+ function buildModal() {
157
+ overlay = document.createElement('div');
158
+ overlay.className = 'mkdn-search-overlay';
159
+ overlay.setAttribute('role','dialog');
160
+ overlay.setAttribute('aria-modal','true');
161
+ overlay.setAttribute('aria-label','Search');
162
+ overlay.innerHTML =
163
+ '<div class="mkdn-search-modal">' +
164
+ '<div class="mkdn-search-input-wrap">' +
165
+ '<svg class="mkdn-search-icon" xmlns="http://www.w3.org/2000/svg" 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"/><path d="m21 21-4.3-4.3"/></svg>' +
166
+ '<input type="text" class="mkdn-search-input" placeholder="Search documentation..." autocomplete="off" spellcheck="false">' +
167
+ '<kbd class="mkdn-search-kbd">Esc</kbd>' +
168
+ '</div>' +
169
+ '<div class="mkdn-search-results"><p class="mkdn-search-hint">Type to search\u2026</p></div>' +
170
+ '</div>';
171
+ document.body.appendChild(overlay);
172
+ input = overlay.querySelector('.mkdn-search-input');
173
+ resultsEl = overlay.querySelector('.mkdn-search-results');
174
+
175
+ // Close on overlay click (outside modal)
176
+ overlay.addEventListener('click', function(e){ if (e.target === overlay) closeModal(); });
177
+ input.addEventListener('input', onInput);
178
+ input.addEventListener('keydown', onKeydown);
179
+ }
180
+
181
+ function openModal() {
182
+ if (!overlay) buildModal();
183
+ overlay.classList.add('mkdn-search-overlay--open');
184
+ input.value = '';
185
+ lastQuery = '';
186
+ activeIdx = -1;
187
+ resultsEl.innerHTML = '<p class="mkdn-search-hint">Type to search\u2026</p>';
188
+ requestAnimationFrame(function(){ input.focus(); });
189
+ }
190
+
191
+ function closeModal() {
192
+ if (overlay) overlay.classList.remove('mkdn-search-overlay--open');
193
+ clearTimeout(debounceTimer);
194
+ }
195
+
196
+ function onInput() {
197
+ clearTimeout(debounceTimer);
198
+ debounceTimer = setTimeout(doSearch, 250);
199
+ }
200
+
201
+ function doSearch() {
202
+ var q = input.value.trim();
203
+ if (q === lastQuery) return;
204
+ lastQuery = q;
205
+ activeIdx = -1;
206
+ if (!q) { resultsEl.innerHTML = '<p class="mkdn-search-hint">Type to search\u2026</p>'; return; }
207
+ resultsEl.innerHTML = '<p class="mkdn-search-hint mkdn-search-hint--loading">Searching\u2026</p>';
208
+ var url = '/api/search?q=' + encodeURIComponent(q) + '&limit=10';
209
+ fetch(url).then(function(r){ return r.json(); }).then(function(results){
210
+ renderResults(results, q);
211
+ }).catch(function(){
212
+ resultsEl.innerHTML = '<p class="mkdn-search-hint">Search unavailable</p>';
213
+ });
214
+ }
215
+
216
+ function renderResults(results, q) {
217
+ if (!results.length) {
218
+ resultsEl.innerHTML = '<p class="mkdn-search-hint">No results found</p>';
219
+ return;
220
+ }
221
+ var terms = q.toLowerCase().split(/\\s+/).filter(function(t){ return t.length >= 2; });
222
+ var html = results.map(function(r, i){
223
+ var href = r.slug + '?q=' + encodeURIComponent(q);
224
+ return '<a href="' + href + '" class="mkdn-search-result' + (i === 0 ? ' mkdn-search-result--active' : '') + '" data-idx="' + i + '">' +
225
+ '<div class="mkdn-search-result-title">' + escHtml(r.title || r.slug) + '</div>' +
226
+ '<div class="mkdn-search-result-excerpt">' + boldTerms(r.excerpt || '', terms) + '</div>' +
227
+ '<div class="mkdn-search-result-slug">' + escHtml(r.slug) + '</div>' +
228
+ '</a>';
229
+ }).join('');
230
+ resultsEl.innerHTML = html;
231
+ activeIdx = 0;
232
+ resultsEl.querySelectorAll('.mkdn-search-result').forEach(function(el){
233
+ el.addEventListener('mouseenter', function(){
234
+ setActive(parseInt(el.getAttribute('data-idx')));
235
+ });
236
+ });
237
+ }
238
+
239
+ function setActive(idx) {
240
+ var items = resultsEl.querySelectorAll('.mkdn-search-result');
241
+ if (!items.length) return;
242
+ idx = Math.max(0, Math.min(idx, items.length - 1));
243
+ items.forEach(function(el, i){
244
+ el.classList.toggle('mkdn-search-result--active', i === idx);
245
+ });
246
+ items[idx].scrollIntoView({ block: 'nearest' });
247
+ activeIdx = idx;
248
+ }
249
+
250
+ function onKeydown(e) {
251
+ var items = resultsEl.querySelectorAll('.mkdn-search-result');
252
+ if (e.key === 'Escape') { closeModal(); return; }
253
+ if (e.key === 'ArrowDown') { e.preventDefault(); setActive(activeIdx + 1); return; }
254
+ if (e.key === 'ArrowUp') { e.preventDefault(); setActive(activeIdx - 1); return; }
255
+ if (e.key === 'Enter' && items.length) {
256
+ e.preventDefault();
257
+ var target = items[Math.max(0, activeIdx)];
258
+ if (target) { closeModal(); window.location.href = target.getAttribute('href'); }
259
+ }
260
+ }
261
+
262
+ // Keyboard shortcut: Cmd+K / Ctrl+K
263
+ document.addEventListener('keydown', function(e){
264
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openModal(); }
265
+ if (e.key === 'Escape' && overlay && overlay.classList.contains('mkdn-search-overlay--open')) closeModal();
266
+ });
267
+
268
+ // Search trigger button
269
+ document.addEventListener('click', function(e){
270
+ if (e.target && e.target.closest && e.target.closest('.mkdn-search-trigger')) openModal();
271
+ });
272
+ })();
273
+ `.trim()
274
+
275
+ const HIGHLIGHT_SCRIPT = `
276
+ (function(){
277
+ var params = new URLSearchParams(window.location.search);
278
+ var q = params.get('q');
279
+ if (!q) return;
280
+
281
+ params.delete('q');
282
+ var newUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '') + window.location.hash;
283
+ history.replaceState(null, '', newUrl);
284
+
285
+ var terms = q.toLowerCase().split(/ +/).filter(function(t){ return t.length >= 2; });
286
+ terms.sort(function(a, b){ return b.length - a.length; });
287
+ if (!terms.length) return;
288
+
289
+ var prose = document.querySelector('.mkdn-prose');
290
+ if (!prose) return;
291
+
292
+ var marks = [];
293
+
294
+ var walker = document.createTreeWalker(prose, 4, function(node){
295
+ var p = node.parentNode;
296
+ if (!p) return 2;
297
+ var tag = p.nodeName.toUpperCase();
298
+ if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'CODE' || tag === 'PRE') return 2;
299
+ return 1;
300
+ });
301
+
302
+ var textNodes = [];
303
+ while (walker.nextNode()) textNodes.push(walker.currentNode);
304
+
305
+ textNodes.forEach(function(node){
306
+ var text = node.nodeValue || '';
307
+ var lower = text.toLowerCase();
308
+ var found = false;
309
+ terms.forEach(function(t){ if (lower.indexOf(t) !== -1) found = true; });
310
+ if (!found) return;
311
+
312
+ var frag = document.createDocumentFragment();
313
+ var remaining = text;
314
+ var lowerRemaining = lower;
315
+
316
+ while (remaining.length > 0) {
317
+ var bestIdx = -1, bestLen = 0;
318
+ terms.forEach(function(t){
319
+ var idx = lowerRemaining.indexOf(t);
320
+ if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) { bestIdx = idx; bestLen = t.length; }
321
+ });
322
+
323
+ if (bestIdx === -1) { frag.appendChild(document.createTextNode(remaining)); break; }
324
+ if (bestIdx > 0) frag.appendChild(document.createTextNode(remaining.slice(0, bestIdx)));
325
+
326
+ var mark = document.createElement('mark');
327
+ mark.className = 'mkdn-search-highlight';
328
+ mark.textContent = remaining.slice(bestIdx, bestIdx + bestLen);
329
+ frag.appendChild(mark);
330
+ marks.push(mark);
331
+
332
+ remaining = remaining.slice(bestIdx + bestLen);
333
+ lowerRemaining = lowerRemaining.slice(bestIdx + bestLen);
334
+ }
335
+
336
+ node.parentNode.replaceChild(frag, node);
337
+ });
338
+
339
+ if (marks.length > 0) marks[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
340
+
341
+ function fadeHighlights() {
342
+ marks.forEach(function(m){ m.classList.add('mkdn-search-highlight--fading'); });
343
+ setTimeout(function(){
344
+ marks.forEach(function(m){
345
+ if (m.parentNode) m.parentNode.replaceChild(document.createTextNode(m.textContent || ''), m);
346
+ });
347
+ marks = [];
348
+ }, 600);
349
+ }
350
+
351
+ var fadeTimer = setTimeout(fadeHighlights, 8000);
352
+ document.addEventListener('click', function(){ clearTimeout(fadeTimer); fadeHighlights(); }, { once: true });
353
+ })();
354
+ `.trim()
355
+
356
+ const STICKY_TABLE_SCRIPT = `
357
+ (function(){
358
+ var wrappers = document.querySelectorAll('.mkdn-table-wrapper');
359
+ if (!wrappers.length) return;
360
+
361
+ var clones = [];
362
+
363
+ wrappers.forEach(function(wrapper){
364
+ var table = wrapper.querySelector('table');
365
+ if (!table) return;
366
+ var thead = table.querySelector('thead');
367
+ if (!thead) return;
368
+
369
+ var clone = document.createElement('div');
370
+ clone.className = 'mkdn-thead-clone mkdn-prose';
371
+ clone.style.display = 'none';
372
+
373
+ var cloneTable = document.createElement('table');
374
+ cloneTable.appendChild(thead.cloneNode(true));
375
+ clone.appendChild(cloneTable);
376
+ document.body.appendChild(clone);
377
+
378
+ clones.push({ wrapper: wrapper, table: table, thead: thead, clone: clone, cloneTable: cloneTable });
379
+ });
380
+
381
+ if (!clones.length) return;
382
+
383
+ function syncWidths (entry) {
384
+ entry.cloneTable.style.width = entry.table.getBoundingClientRect().width + 'px';
385
+ var ths = entry.thead.querySelectorAll('th');
386
+ var cloneThs = entry.clone.querySelectorAll('th');
387
+ ths.forEach(function(th, i){
388
+ if (cloneThs[i]) {
389
+ var w = th.getBoundingClientRect().width + 'px';
390
+ cloneThs[i].style.width = w;
391
+ cloneThs[i].style.minWidth = w;
392
+ cloneThs[i].style.maxWidth = w;
393
+ }
394
+ });
395
+ var wrapperRect = entry.wrapper.getBoundingClientRect();
396
+ entry.clone.style.left = wrapperRect.left + 'px';
397
+ entry.clone.style.width = wrapperRect.width + 'px';
398
+ entry.cloneTable.style.marginLeft = (-entry.wrapper.scrollLeft) + 'px';
399
+ }
400
+
401
+ function onScroll () {
402
+ clones.forEach(function(entry){
403
+ var theadRect = entry.thead.getBoundingClientRect();
404
+ var tableBottom = entry.table.getBoundingClientRect().bottom;
405
+ var theadHeight = theadRect.height;
406
+ if (theadRect.top < 0 && tableBottom > theadHeight) {
407
+ entry.clone.style.display = 'block';
408
+ syncWidths(entry);
409
+ } else {
410
+ entry.clone.style.display = 'none';
411
+ }
412
+ });
413
+ }
414
+
415
+ clones.forEach(function(entry){
416
+ entry.wrapper.addEventListener('scroll', function(){
417
+ if (entry.clone.style.display !== 'none') {
418
+ entry.cloneTable.style.marginLeft = (-entry.wrapper.scrollLeft) + 'px';
419
+ }
420
+ });
421
+ });
422
+
423
+ window.addEventListener('scroll', onScroll, { passive: true });
424
+ window.addEventListener('resize', onScroll, { passive: true });
425
+ })();
426
+ `.trim()
427
+
428
+ const CHART_SCRIPT = `
429
+ (function(){
430
+ var chartBlocks = document.querySelectorAll('code.language-chart');
431
+ if (chartBlocks.length === 0) return;
432
+
433
+ var s = document.createElement('script');
434
+ s.src = 'https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js';
435
+ s.onload = function(){
436
+ var root = getComputedStyle(document.documentElement);
437
+ var textMuted = root.getPropertyValue('--mkdn-text-muted').trim() || '#6b7280';
438
+ var borderColor = root.getPropertyValue('--mkdn-border').trim() || '#e5e7eb';
439
+ var accent = root.getPropertyValue('--mkdn-accent').trim() || '#6366f1';
440
+ var fontBody = root.getPropertyValue('--mkdn-font-body').trim() || 'inherit';
441
+
442
+ var palette = [
443
+ accent,
444
+ '#06b6d4', '#f59e0b', '#10b981', '#ef4444',
445
+ '#8b5cf6', '#ec4899', '#14b8a6'
446
+ ];
447
+
448
+ Chart.defaults.color = textMuted;
449
+ Chart.defaults.borderColor = borderColor;
450
+ Chart.defaults.font.family = fontBody;
451
+
452
+ chartBlocks.forEach(function(block, idx){
453
+ var pre = block.parentElement;
454
+ var raw = block.textContent || '';
455
+ var config;
456
+ try {
457
+ config = JSON.parse(raw);
458
+ } catch(e) {
459
+ var errEl = document.createElement('div');
460
+ errEl.className = 'mkdn-chart-error';
461
+ errEl.textContent = 'Chart error: invalid JSON';
462
+ pre.parentElement.replaceChild(errEl, pre);
463
+ return;
464
+ }
465
+
466
+ if (config.data && config.data.datasets) {
467
+ config.data.datasets.forEach(function(ds, i){
468
+ var color = palette[i % palette.length];
469
+ if (!ds.backgroundColor) {
470
+ if (config.type === 'pie' || config.type === 'doughnut' || config.type === 'polarArea') {
471
+ ds.backgroundColor = palette.slice(0, (ds.data || []).length);
472
+ } else {
473
+ ds.backgroundColor = color + '33';
474
+ }
475
+ }
476
+ if (!ds.borderColor) ds.borderColor = color;
477
+ if (config.type === 'line' || config.type === 'radar') {
478
+ if (ds.tension === undefined) ds.tension = 0.3;
479
+ if (ds.fill === undefined) ds.fill = false;
480
+ }
481
+ });
482
+ }
483
+
484
+ if (!config.options) config.options = {};
485
+ config.options.responsive = true;
486
+ config.options.maintainAspectRatio = true;
487
+
488
+ var container = document.createElement('div');
489
+ container.className = 'mkdn-chart';
490
+ var canvas = document.createElement('canvas');
491
+ canvas.id = 'mkdn-chart-' + idx;
492
+ container.appendChild(canvas);
493
+ pre.parentElement.replaceChild(container, pre);
494
+
495
+ try {
496
+ new Chart(canvas, config);
497
+ } catch(e) {
498
+ container.innerHTML = '';
499
+ var err = document.createElement('div');
500
+ err.className = 'mkdn-chart-error';
501
+ err.textContent = 'Chart error: ' + (e.message || 'unknown error');
502
+ container.appendChild(err);
503
+ }
504
+ });
505
+ };
506
+ document.head.appendChild(s);
507
+ })();
106
508
  `.trim()
@@ -12,6 +12,11 @@ export const DEFAULT_CONFIG: MkdnSiteConfig = {
12
12
  },
13
13
  theme: {
14
14
  mode: 'prose',
15
+ builtinCss: true,
16
+ pageTitle: false,
17
+ pageDate: false,
18
+ prevNext: false,
19
+ readingTime: false,
15
20
  showNav: true,
16
21
  showToc: true,
17
22
  colorScheme: 'system',
@@ -36,24 +41,60 @@ export const DEFAULT_CONFIG: MkdnSiteConfig = {
36
41
  copyButton: true,
37
42
  themeToggle: true,
38
43
  math: true,
39
- search: true
44
+ search: true,
45
+ charts: true
40
46
  },
41
- renderer: 'portable'
47
+ renderer: 'portable',
48
+ mcp: {
49
+ enabled: true,
50
+ endpoint: '/mcp'
51
+ },
52
+ csp: {
53
+ enabled: true
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Preset theme overrides applied before user config.
59
+ * User values always win over preset values.
60
+ */
61
+ const PRESET_THEME: Record<string, Partial<MkdnSiteConfig['theme']>> = {
62
+ docs: {
63
+ showNav: true,
64
+ showToc: true,
65
+ pageTitle: false,
66
+ pageDate: false,
67
+ prevNext: true,
68
+ readingTime: false
69
+ },
70
+ blog: {
71
+ showNav: false,
72
+ showToc: false,
73
+ pageTitle: true,
74
+ pageDate: true,
75
+ prevNext: true,
76
+ readingTime: true
77
+ }
42
78
  }
43
79
 
44
80
  /**
45
- * Deep merge user config with defaults.
81
+ * Deep merge user config with defaults, applying preset if set.
82
+ * Merge order: DEFAULT_CONFIG → preset → userConfig
46
83
  * User values take precedence at every nesting level.
47
84
  */
48
85
  export function resolveConfig (
49
86
  userConfig: Partial<MkdnSiteConfig>
50
87
  ): MkdnSiteConfig {
88
+ const presetTheme = userConfig.preset != null
89
+ ? (PRESET_THEME[userConfig.preset] ?? {})
90
+ : {}
91
+
51
92
  return {
52
93
  ...DEFAULT_CONFIG,
53
94
  ...userConfig,
54
95
  site: { ...DEFAULT_CONFIG.site, ...userConfig.site },
55
96
  server: { ...DEFAULT_CONFIG.server, ...userConfig.server },
56
- theme: { ...DEFAULT_CONFIG.theme, ...userConfig.theme },
97
+ theme: { ...DEFAULT_CONFIG.theme, ...presetTheme, ...userConfig.theme },
57
98
  negotiation: {
58
99
  ...DEFAULT_CONFIG.negotiation,
59
100
  ...userConfig.negotiation,
@@ -63,6 +104,28 @@ export function resolveConfig (
63
104
  }
64
105
  },
65
106
  llmsTxt: { ...DEFAULT_CONFIG.llmsTxt, ...userConfig.llmsTxt },
66
- client: { ...DEFAULT_CONFIG.client, ...userConfig.client }
107
+ client: { ...DEFAULT_CONFIG.client, ...userConfig.client },
108
+ github: userConfig.github,
109
+ include: userConfig.include,
110
+ exclude: userConfig.exclude,
111
+ mcp: { ...DEFAULT_CONFIG.mcp, ...userConfig.mcp },
112
+ analytics: userConfig.analytics != null
113
+ ? {
114
+ ...userConfig.analytics,
115
+ traffic: userConfig.analytics.traffic != null
116
+ ? { enabled: false, console: false, ...userConfig.analytics.traffic }
117
+ : userConfig.analytics.traffic
118
+ }
119
+ : userConfig.analytics,
120
+ csp: userConfig.csp != null
121
+ ? { ...DEFAULT_CONFIG.csp, ...userConfig.csp }
122
+ : DEFAULT_CONFIG.csp,
123
+ cache: {
124
+ enabled: userConfig.cache?.enabled ?? false,
125
+ maxAge: userConfig.cache?.maxAge ?? 300,
126
+ maxAgeMarkdown: userConfig.cache?.maxAgeMarkdown ?? 300,
127
+ staleWhileRevalidate: userConfig.cache?.staleWhileRevalidate ?? 0,
128
+ versionTag: userConfig.cache?.versionTag
129
+ }
67
130
  }
68
131
  }