docnest 0.1.17 → 0.1.19

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
@@ -66,6 +66,9 @@ export default defineConfig({
66
66
  `search-index.json`,并保留页面上的静态站点标记,让结果链接指向 `/doc/<path>.html`。
67
67
  受限模式不生成搜索索引,也不显示搜索入口,避免摘要绕过阅读限制。
68
68
 
69
+ 搜索框为空时显示最近搜索:仅记录有结果的关键词,去重后保留最近 10 条,点击即可再次搜索。
70
+ 历史保存在当前浏览器,按 `site.storageKeyPrefix` 隔离;不上传服务器。
71
+
69
72
  PDF、页面水印和图表外带能力由一个开关统一控制,默认使用普通阅读模式:
70
73
 
71
74
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2306,8 +2306,9 @@ body[data-theme="dark"] .tree-nav::-webkit-scrollbar-thumb:hover {
2306
2306
  }
2307
2307
 
2308
2308
  .doc-search-status {
2309
+ flex-shrink: 0;
2309
2310
  min-height: 1.25rem;
2310
- padding: 0.75rem 1.25rem 0.5rem;
2311
+ padding: 1rem 1.25rem 0.875rem;
2311
2312
  color: var(--linear-text-muted);
2312
2313
  font-size: 0.75rem;
2313
2314
  }
@@ -2319,6 +2320,44 @@ body[data-theme="dark"] .tree-nav::-webkit-scrollbar-thumb:hover {
2319
2320
  overscroll-behavior: contain;
2320
2321
  }
2321
2322
 
2323
+ .doc-search-history {
2324
+ min-height: 0;
2325
+ overflow-y: auto;
2326
+ padding: 0.25rem 1.25rem 1.5rem;
2327
+ }
2328
+
2329
+ .doc-search-history__title {
2330
+ margin-bottom: 0.75rem;
2331
+ color: var(--linear-text-muted);
2332
+ font-size: 0.75rem;
2333
+ }
2334
+
2335
+ .doc-search-history__items {
2336
+ display: flex;
2337
+ flex-wrap: wrap;
2338
+ gap: 0.625rem;
2339
+ }
2340
+
2341
+ .doc-search-history__item {
2342
+ max-width: 100%;
2343
+ padding: 0.4rem 0.75rem;
2344
+ border: 1px solid var(--linear-border);
2345
+ border-radius: 8px;
2346
+ background: var(--linear-bg-subtle);
2347
+ color: var(--linear-text);
2348
+ font: inherit;
2349
+ font-size: 0.8125rem;
2350
+ overflow-wrap: anywhere;
2351
+ text-align: left;
2352
+ cursor: pointer;
2353
+ }
2354
+
2355
+ .doc-search-history__item:hover,
2356
+ .doc-search-history__item:focus-visible {
2357
+ border-color: var(--linear-accent);
2358
+ background: var(--doc-accent-soft);
2359
+ }
2360
+
2322
2361
  .doc-search-result {
2323
2362
  position: relative;
2324
2363
  display: block;
@@ -23,7 +23,8 @@
23
23
  crumbs.push({ name: displayName, isLast: true, url: '' });
24
24
  } else {
25
25
  var dirPath = parts.slice(0, i + 1).join('/');
26
- crumbs.push({ name: displayName, isLast: false, url: '/doc?path=' + encodeURIComponent(dirPath + '/README.md') });
26
+ // 服务端会根据目录是否存在 README.md 决定跳转到文档或目录列表。
27
+ crumbs.push({ name: displayName, isLast: false, url: '/dir?path=' + encodeURIComponent(dirPath) });
27
28
  }
28
29
  }
29
30
  return crumbs;
@@ -54,7 +55,17 @@
54
55
  return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
55
56
  }
56
57
 
57
- function updateBreadcrumbs(docPath) {
58
+ function updateBreadcrumbs(docPath, loadedDocument) {
59
+ var currentBreadcrumbs = document.querySelector('.breadcrumbs');
60
+ if (!currentBreadcrumbs) return;
61
+
62
+ // 服务端已经掌握真实目录结构,优先复用它生成的面包屑,避免客户端猜测 README.md。
63
+ var serverBreadcrumbs = loadedDocument && loadedDocument.querySelector('.breadcrumbs');
64
+ if (serverBreadcrumbs) {
65
+ currentBreadcrumbs.innerHTML = serverBreadcrumbs.innerHTML;
66
+ return;
67
+ }
68
+
58
69
  var crumbs = buildBreadcrumbsFromPath(docPath);
59
70
  renderBreadcrumbs(crumbs);
60
71
  }
@@ -194,7 +205,7 @@
194
205
  if (newTitle) document.title = newTitle.textContent;
195
206
  window.currentDocPath = pathFromUrl || pathFromHref(absoluteUrl);
196
207
  updateActiveLink(window.currentDocPath);
197
- updateBreadcrumbs(window.currentDocPath);
208
+ updateBreadcrumbs(window.currentDocPath, doc);
198
209
  if (typeof window.buildDocToc === 'function') window.buildDocToc();
199
210
  if (typeof window.scrollToDocHash === 'function') window.scrollToDocHash();
200
211
 
@@ -2,6 +2,8 @@
2
2
  'use strict';
3
3
 
4
4
  var MAX_RESULTS = 12;
5
+ var MAX_HISTORY = 10;
6
+ var HISTORY_DELAY = 800;
5
7
  var SNIPPET_LENGTH = 160;
6
8
  var SEARCH_INDEX_PATH = '/search-index.json';
7
9
  var state = {
@@ -12,8 +14,77 @@
12
14
  lastFocus: null,
13
15
  results: [],
14
16
  selectedIndex: -1,
17
+ history: null,
18
+ pendingQuery: '',
19
+ historyTimer: null,
20
+ composing: false,
15
21
  };
16
22
 
23
+ function historyKey() {
24
+ return String((window.DOCNEST_CONFIG || {}).storageKeyPrefix || 'docnest') + ':search-history';
25
+ }
26
+
27
+ function readHistory() {
28
+ if (state.history) return state.history;
29
+ var saved;
30
+ try {
31
+ saved = JSON.parse(window.localStorage.getItem(historyKey()));
32
+ } catch (_) {}
33
+ var seen = new Set();
34
+ state.history = (Array.isArray(saved) ? saved : []).filter(function(query) {
35
+ if (typeof query !== 'string' || !query.trim()) return false;
36
+ var key = getQueryTerms(query).join(' ');
37
+ if (seen.has(key)) return false;
38
+ seen.add(key);
39
+ return true;
40
+ }).map(function(query) { return query.trim(); }).slice(0, MAX_HISTORY);
41
+ return state.history;
42
+ }
43
+
44
+ function savePendingQuery() {
45
+ window.clearTimeout(state.historyTimer);
46
+ state.historyTimer = null;
47
+ var query = state.pendingQuery;
48
+ state.pendingQuery = '';
49
+ if (!query || state.composing || searchDocuments(query).length === 0) return;
50
+ var key = getQueryTerms(query).join(' ');
51
+ state.history = [query].concat(readHistory().filter(function(item) {
52
+ return getQueryTerms(item).join(' ') !== key;
53
+ })).slice(0, MAX_HISTORY);
54
+ try {
55
+ window.localStorage.setItem(historyKey(), JSON.stringify(state.history));
56
+ } catch (_) {}
57
+ }
58
+
59
+ function renderHistory() {
60
+ state.modal.history.textContent = '';
61
+ var queries = readHistory().filter(function(query) { return searchDocuments(query).length > 0; });
62
+ state.modal.history.hidden = queries.length === 0;
63
+ if (!queries.length) return false;
64
+
65
+ var heading = document.createElement('div');
66
+ heading.className = 'doc-search-history__title';
67
+ heading.textContent = '最近搜索';
68
+ var items = document.createElement('div');
69
+ items.className = 'doc-search-history__items';
70
+ queries.forEach(function(query) {
71
+ var button = document.createElement('button');
72
+ button.type = 'button';
73
+ button.className = 'doc-search-history__item';
74
+ button.textContent = query;
75
+ button.addEventListener('click', function() {
76
+ state.modal.input.value = query;
77
+ renderResults();
78
+ savePendingQuery();
79
+ state.modal.input.focus();
80
+ });
81
+ items.appendChild(button);
82
+ });
83
+ state.modal.history.appendChild(heading);
84
+ state.modal.history.appendChild(items);
85
+ return true;
86
+ }
87
+
17
88
  function getAssetUrl(path) {
18
89
  var base = document.querySelector('base');
19
90
  if (base && base.getAttribute('href')) {
@@ -241,12 +312,22 @@
241
312
  function renderResults() {
242
313
  if (!state.modal) return;
243
314
  var query = state.modal.input.value.trim();
315
+ window.clearTimeout(state.historyTimer);
316
+ if (!query) savePendingQuery();
317
+ state.pendingQuery = '';
244
318
  state.modal.results.textContent = '';
319
+ state.modal.results.hidden = false;
320
+ state.modal.history.hidden = true;
321
+ state.modal.input.removeAttribute('aria-activedescendant');
245
322
  state.results = [];
246
323
  state.selectedIndex = -1;
247
324
 
248
325
  if (!query) {
249
326
  setStatus('输入标题、目录或正文关键词');
327
+ if (renderHistory()) {
328
+ state.modal.results.hidden = true;
329
+ return;
330
+ }
250
331
  state.modal.results.appendChild(createEmptyState('支持中文关键词、英文词组和文档路径搜索'));
251
332
  return;
252
333
  }
@@ -260,6 +341,10 @@
260
341
  }
261
342
 
262
343
  state.results = matches.slice(0, MAX_RESULTS);
344
+ if (!state.composing) {
345
+ state.pendingQuery = query;
346
+ state.historyTimer = window.setTimeout(savePendingQuery, HISTORY_DELAY);
347
+ }
263
348
  setStatus(matches.length > MAX_RESULTS
264
349
  ? '找到 ' + matches.length + ' 篇文档,显示最相关的 ' + MAX_RESULTS + ' 篇'
265
350
  : '找到 ' + matches.length + ' 篇文档');
@@ -287,6 +372,7 @@
287
372
  '<kbd class="doc-search-input-key">Esc</kbd>',
288
373
  '</div>',
289
374
  '<div class="doc-search-status" id="doc-search-status" role="status" aria-live="polite"></div>',
375
+ '<div class="doc-search-history" role="group" aria-label="最近搜索" hidden></div>',
290
376
  '<div class="doc-search-results" id="doc-search-results" role="listbox" aria-label="搜索结果"></div>',
291
377
  '<div class="doc-search-hint"><span>↑↓ 选择</span><span>Enter 打开</span><span>Esc 关闭</span></div>',
292
378
  '</div>',
@@ -297,9 +383,19 @@
297
383
  input: overlay.querySelector('.doc-search-input'),
298
384
  status: overlay.querySelector('.doc-search-status'),
299
385
  results: overlay.querySelector('.doc-search-results'),
386
+ history: overlay.querySelector('.doc-search-history'),
300
387
  close: overlay.querySelector('.doc-search-modal__close'),
301
388
  };
302
389
  modal.input.addEventListener('input', renderResults);
390
+ modal.input.addEventListener('compositionstart', function() {
391
+ state.composing = true;
392
+ window.clearTimeout(state.historyTimer);
393
+ state.pendingQuery = '';
394
+ });
395
+ modal.input.addEventListener('compositionend', function() {
396
+ state.composing = false;
397
+ renderResults();
398
+ });
303
399
  modal.close.addEventListener('click', closeSearch);
304
400
  overlay.addEventListener('click', function(event) {
305
401
  if (event.target === overlay) closeSearch();
@@ -336,6 +432,10 @@
336
432
 
337
433
  function openSearch() {
338
434
  if (!state.modal) createModal();
435
+ if (!state.modal.overlay.hidden) {
436
+ state.modal.input.focus();
437
+ return;
438
+ }
339
439
  state.lastFocus = document.activeElement;
340
440
  state.modal.overlay.hidden = false;
341
441
  state.modal.overlay.setAttribute('aria-hidden', 'false');
@@ -343,6 +443,8 @@
343
443
  state.modal.input.disabled = true;
344
444
  state.modal.input.value = '';
345
445
  state.modal.results.textContent = '';
446
+ state.modal.results.hidden = false;
447
+ state.modal.history.hidden = true;
346
448
  setStatus('正在加载文档索引…');
347
449
  window.requestAnimationFrame(function() { state.modal.input.focus(); });
348
450
 
@@ -361,6 +463,8 @@
361
463
 
362
464
  function closeSearch() {
363
465
  if (!state.modal || state.modal.overlay.hidden) return;
466
+ savePendingQuery();
467
+ state.composing = false;
364
468
  state.modal.overlay.hidden = true;
365
469
  state.modal.overlay.setAttribute('aria-hidden', 'true');
366
470
  document.body.classList.remove('doc-search-open');
@@ -383,6 +487,8 @@
383
487
  if (event.key === 'Escape') {
384
488
  event.preventDefault();
385
489
  closeSearch();
490
+ } else if (event.target !== state.modal.input) {
491
+ return;
386
492
  } else if (event.key === 'ArrowDown') {
387
493
  event.preventDefault();
388
494
  moveSelection(1);
@@ -414,6 +520,7 @@
414
520
  var isMac = /Mac|iPhone|iPad|iPod/i.test((window.navigator && window.navigator.platform) || '');
415
521
  if (shortcut) shortcut.textContent = isMac ? '⌘ K' : 'Ctrl K';
416
522
  document.addEventListener('keydown', onKeyDown);
523
+ window.addEventListener('pagehide', savePendingQuery);
417
524
  }
418
525
 
419
526
  if (document.readyState === 'loading') {