From 4a43f704fb1248f8bc3fa1015179eeac559456dc Mon Sep 17 00:00:00 2001 From: xihale Date: Sat, 20 Sep 2025 19:16:19 +0800 Subject: [PATCH 1/6] fix: add article component space --- layouts/index.shtml | 4 +++- layouts/learn.shtml | 6 ++++-- layouts/monthly.shtml | 4 +++- layouts/page.shtml | 4 +++- layouts/post.shtml | 6 ++++-- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/layouts/index.shtml b/layouts/index.shtml index 9cfbdff..6782053 100644 --- a/layouts/index.shtml +++ b/layouts/index.shtml @@ -3,5 +3,7 @@ -
+
+
+
\ No newline at end of file diff --git a/layouts/learn.shtml b/layouts/learn.shtml index 70ae568..a85c4f3 100644 --- a/layouts/learn.shtml +++ b/layouts/learn.shtml @@ -7,7 +7,9 @@

Table of Contents

-
+
+
+
@@ -20,4 +22,4 @@
- + \ No newline at end of file diff --git a/layouts/monthly.shtml b/layouts/monthly.shtml index 06a3d9c..58320c3 100644 --- a/layouts/monthly.shtml +++ b/layouts/monthly.shtml @@ -2,5 +2,7 @@ -
+
+
+
\ No newline at end of file diff --git a/layouts/page.shtml b/layouts/page.shtml index 06a3d9c..58320c3 100644 --- a/layouts/page.shtml +++ b/layouts/page.shtml @@ -2,5 +2,7 @@ -
+
+
+
\ No newline at end of file diff --git a/layouts/post.shtml b/layouts/post.shtml index a0d9be4..e822865 100644 --- a/layouts/post.shtml +++ b/layouts/post.shtml @@ -14,5 +14,7 @@ -
- +
+
+
+ \ No newline at end of file From d910467ff678aad861e4cd79b8a07e8aab821f3a Mon Sep 17 00:00:00 2001 From: xihale Date: Sat, 20 Sep 2025 19:16:48 +0800 Subject: [PATCH 2/6] feat: search modal & highlight --- assets/search.css | 198 +++++++++++++++++++++++++++ assets/search.js | 257 +++++++++++++++++++++++++++++++++++ layouts/templates/base.shtml | 33 ++++- 3 files changed, 482 insertions(+), 6 deletions(-) create mode 100644 assets/search.css create mode 100644 assets/search.js diff --git a/assets/search.css b/assets/search.css new file mode 100644 index 0000000..f6e3dcd --- /dev/null +++ b/assets/search.css @@ -0,0 +1,198 @@ +/* Ensure [hidden] attribute hides the modal even if other rules set display */ +.search-modal[hidden] { + display: none !important; +} + +/* search modal and result styles */ +.search-modal { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 48px 16px; + /* Ensure the modal covers full viewport and prevent scroll chaining to background */ + height: 100vh; + overscroll-behavior: none; /* 阻止滚动穿透到页面主体 */ + -webkit-overflow-scrolling: touch; +} +.search-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.45); + /* also prevent scroll chaining on the overlay */ + overscroll-behavior: none; +} +.search-panel { + position: relative; + width: 100%; + max-width: 1100px; +} +.search-panel .container { + background: var(--bg); + color: var(--fg); + border-radius: 6px; + padding: 18px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} +.search-header { + display: flex; + gap: 8px; + align-items: center; +} +#search-input { + flex: 1; + padding: 10px 12px; + font-size: 16px; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +#search-input:focus { + outline: none; + border-color: #0066cc; + box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2); +} +#search-close { + background: transparent; + border: 0; + font-size: 20px; + cursor: pointer; + color: var(--fg); +} +#search-results { + margin-top: 12px; + max-height: 60vh; + padding-right: 10px; + overflow: auto; +} +.search-item { + padding: 10px 8px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; + transition: background-color 0.2s ease; +} + +.search-item:hover, +.search-item:focus-within { + background-color: rgba(0, 0, 0, 0.03); +} + +.search-item a { + color: inherit; + text-decoration: none; + flex: 1; + border-radius: 2px; + padding: 2px; +} + +.search-item h4 { + margin: 0; + font-size: 16px; +} +.search-item p { + margin: 6px 0 0 0; + font-size: 13px; + color: #3c3c3c; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; +} +.search-item .label { + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + background: var(--bg); + color: var(--border); +} +.search-highlight { + background: #fff5c2; + transition: background 1.2s ease; +} + +@media (max-width: 767px) and (orientation: portrait) { + .search-modal { + padding: 0; + align-items: stretch; + } + .search-panel { + display: flex; + flex-direction: column; + } + /* 使 container 占满可视高度,内部结果区做独立滚动,避免背景滚动 */ + .search-panel .container { + border-radius: 0; + display: flex; + flex-direction: column; + padding: 16px; + max-height: 100vh; + } + + #search-results { + overflow: auto; + flex: 0.98; + max-height: none; + } +} + +@media (min-width: 768px) { + .search-panel .container { + max-width: 800px; + margin: 0 auto; + } +} + +/* search button in header */ +#search-btn { + background: transparent; + border: 0; + cursor: pointer; + color: currentColor; + margin-left: 20px; + padding: 8px; +} + +/* align SVG icon with text baseline */ +#search-btn svg { + vertical-align: middle; +} + +/* dark mode support */ +@media (prefers-color-scheme: dark) { + .search-item p { + color: #e6e6e69f; + } + .search-item .label { + color: #6c757d; + } + /* dark-mode highlight for search results and matched elements */ + .search-highlight { + background: #665c00; + transition: background 1.2s ease; + } + + .search-item:hover, + .search-item:focus-within { + background-color: rgba(255, 255, 255, 0.05); + } + + #search-input:focus { + border-color: #4d9eff; + box-shadow: 0 0 0 2px rgba(77, 158, 255, 0.2); + } + + .search-item a:focus { + outline-color: #4d9eff; + } +} diff --git a/assets/search.js b/assets/search.js new file mode 100644 index 0000000..550708d --- /dev/null +++ b/assets/search.js @@ -0,0 +1,257 @@ +import Fuse from "https://registry.npmmirror.com/fuse.js/7.1.0/files/dist/fuse.min.mjs"; + +const DEFAULT_FEEDS = [ + { url: "/learn/index.xml", module: "学习" }, + { url: "/monthly/index.xml", module: "月刊" }, + { url: "/post/index.xml", module: "文章" }, +]; + +class Search { + constructor({ feeds = DEFAULT_FEEDS } = {}) { + this.feeds = feeds; + this.fuse = null; + + this.dom = { + modal: document.getElementById("search-modal"), + input: document.getElementById("search-input"), + results: document.getElementById("search-results"), + btn: document.getElementById("search-btn"), + closeBtn: document.getElementById("search-close"), + overlay: document.getElementById("search-overlay"), + }; + + this._debouncedSearch = this.debounce((q) => this.performSearch(q), 300); + } + + // ---- utilities ---- + static stripHtml(s) { + return s ? s.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() : ""; + } + + static escapeRegExp(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + debounce(fn, wait = 300) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(...args), wait); + }; + } + + // ---- feed / fuse ---- + async fetchFeed(feed) { + try { + const res = await fetch(feed.url, { cache: "no-store" }); + if (!res.ok) { + throw new Error(`无法获取 Feed: ${feed.url} (HTTP ${res.status})`); + } + const text = await res.text(); + const doc = new DOMParser().parseFromString(text, "application/xml"); + const items = Array.from(doc.querySelectorAll("item")); + return items.map((item) => ({ + title: Search.stripHtml(item.querySelector("title")?.textContent || ""), + description: Search.stripHtml(item.querySelector("description")?.textContent || ""), + url: Search.stripHtml(item.querySelector("link")?.textContent || ""), + module: feed.module, + })); + } catch (e) { + // 向上抛出错误,供上层提示使用 + throw new Error(e?.message ? e.message : `无法获取 Feed: ${feed.url}`); + } + } + + async initSearch() { + if (this.fuse) return this.fuse; + const all = await Promise.all(this.feeds.map((f) => this.fetchFeed(f))); + const index = all.flat(); + this.fuse = new Fuse(index, { keys: ["title", "description"], threshold: 0.4 }); + return this.fuse; + } + + // ---- rendering / highlighting ---- + highlightText(text, query) { + if (!query || !text) return text; + try { + const regex = new RegExp(Search.escapeRegExp(query), "gi"); + return text.replace(regex, (m) => `${m}`); + } catch (e) { + return text; + } + } + + renderResults(results = [], query = "") { + const container = this.dom.results; + if (!container) return; + if (!results?.length) { + container.innerHTML = "

没有找到结果

"; + return; + } + + container.innerHTML = results.slice(0, 20).map((res) => { + const item = res.item || res; + return ` + + `; + }).join(""); + } + + // ---- modal controls ---- + async open() { + try { + await this.initSearch(); + if (this.dom.modal) this.dom.modal.hidden = false; + this.dom.input?.focus(); + } catch (e) { + if (this.dom.modal) this.dom.modal.hidden = false; + if (this.dom.results) this.dom.results.innerHTML = `

无法获取 Feed:${e.message}

`; + this.dom.input?.focus(); + } + } + + close() { + if (this.dom.modal) this.dom.modal.hidden = true; + if (this.dom.results) this.dom.results.innerHTML = ""; + history.replaceState(null, "", location.pathname + location.search); + } + + // ---- search action ---- + async performSearch(query) { + if (!query) { + this.renderResults([]); + return; + } + try { + await this.initSearch(); + const results = this.fuse.search(query); + this.renderResults(results, query); + } catch (e) { + if (this.dom.results) this.dom.results.innerHTML = `

无法获取 Feed:${e.message}

`; + console.error(e); + } + } + + // ---- DOM text highlighting on page ---- + clearHighlights(container) { + if (!container) return; + const highlights = container.querySelectorAll('.search-highlight'); + highlights.forEach((span) => { + const textNode = document.createTextNode(span.textContent); + span.parentNode.replaceChild(textNode, span); + }); + } + + domHighlight(container, query) { + if (!container || !query) return; + this.clearHighlights(container); + const regex = new RegExp(Search.escapeRegExp(query), 'gi'); + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { + acceptNode: (node) => { + if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + const forbidden = ['SCRIPT', 'STYLE']; + if (forbidden.includes(parent.tagName)) return NodeFilter.FILTER_REJECT; + return NodeFilter.FILTER_ACCEPT; + } + }); + + const nodes = []; + let n; + while ((n = walker.nextNode())) nodes.push(n); + + nodes.forEach((textNode) => { + const text = textNode.nodeValue; + regex.lastIndex = 0; + let match; + let lastIndex = 0; + const frag = document.createDocumentFragment(); + let found = false; + + while ((match = regex.exec(text)) !== null) { + found = true; + const before = text.slice(lastIndex, match.index); + if (before) frag.appendChild(document.createTextNode(before)); + const span = document.createElement('span'); + span.className = 'search-highlight'; + span.textContent = match[0]; + frag.appendChild(span); + lastIndex = match.index + match[0].length; + } + + if (!found) return; + const after = text.slice(lastIndex); + if (after) frag.appendChild(document.createTextNode(after)); + textNode.parentNode.replaceChild(frag, textNode); + }); + } + + highlightPageContent() { + if (!location.hash?.startsWith("#search=")) return; + const query = decodeURIComponent(location.hash.slice(8)); + if (!query) return; + const article = document.querySelector("article"); + if (article) { + this.domHighlight(article, query); + document.querySelector(".search-highlight")?.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } + + // ---- event bindings ---- + attachHandlers() { + this.dom.btn?.addEventListener("click", () => this.open()); + this.dom.closeBtn?.addEventListener("click", () => this.close()); + this.dom.overlay?.addEventListener("click", () => this.close()); + + if (this.dom.input) { + this.dom.input.addEventListener("input", (e) => this._debouncedSearch(e.target.value.trim())); + this.dom.input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + const first = document.querySelector("#search-results .search-item a"); + first?.click(); + if (location.pathname == first?.pathname) { + location.reload(); + } + } + }); + } + + this.dom.input.addEventListener("keydown", (e) => { + if (e.key === "Escape") this.close(); + }); + + document.addEventListener("keydown", (e) => { + if (e.key.toLowerCase() === "s" && !e.repeat && !e.metaKey && !e.ctrlKey && !e.altKey) { + const active = document.activeElement; + const inputs = ["INPUT", "TEXTAREA", "SELECT"]; + if (!inputs.includes(active?.tagName)) { + e.preventDefault(); + this.open(); + } + } + }); + + this.dom.results.addEventListener("click", () => { + const active = document.activeElement; + if (location.pathname == active.pathname) { + active.click(); + location.reload(); + } + }); + } +} + +// 实例化并导出兼容旧接口 +const searchInstance = new Search({ feeds: DEFAULT_FEEDS }); + +window.addEventListener("DOMContentLoaded", () => { + searchInstance.attachHandlers(); + searchInstance.highlightPageContent(); +}); diff --git a/layouts/templates/base.shtml b/layouts/templates/base.shtml index 4c3760a..0a87149 100644 --- a/layouts/templates/base.shtml +++ b/layouts/templates/base.shtml @@ -25,8 +25,8 @@ fetch('https://en.liujiacai.net/pv/write?' + new URLSearchParams(payload), {mode: 'no-cors'}).catch(console.log); - +
@@ -37,17 +37,38 @@ 博客 贡献 +
+ + + - + \ No newline at end of file From b2b924b4a6d31332da4662b17dad709fc7f9a652 Mon Sep 17 00:00:00 2001 From: xihale Date: Sat, 20 Sep 2025 19:30:32 +0800 Subject: [PATCH 3/6] fix: use local fuse.js --- assets/libs/fuse.min.js | 9 +++++++++ assets/search.js | 2 +- zine.ziggy | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 assets/libs/fuse.min.js diff --git a/assets/libs/fuse.min.js b/assets/libs/fuse.min.js new file mode 100644 index 0000000..60a7cfd --- /dev/null +++ b/assets/libs/fuse.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v7.1.0 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2025 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +function t(t){return Array.isArray?Array.isArray(t):"[object Array]"===h(t)}const e=1/0;function n(t){return null==t?"":function(t){if("string"==typeof t)return t;let n=t+"";return"0"==n&&1/t==-e?"-0":n}(t)}function s(t){return"string"==typeof t}function i(t){return"number"==typeof t}function r(t){return!0===t||!1===t||function(t){return u(t)&&null!==t}(t)&&"[object Boolean]"==h(t)}function u(t){return"object"==typeof t}function c(t){return null!=t}function o(t){return!t.trim().length}function h(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":Object.prototype.toString.call(t)}const a=t=>`Missing ${t} property in key`,l=t=>`Property 'weight' in key '${t}' must be a positive integer`,d=Object.prototype.hasOwnProperty;class g{constructor(t){this._keys=[],this._keyMap={};let e=0;t.forEach((t=>{let n=f(t);this._keys.push(n),this._keyMap[n.id]=n,e+=n.weight})),this._keys.forEach((t=>{t.weight/=e}))}get(t){return this._keyMap[t]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}}function f(e){let n=null,i=null,r=null,u=1,c=null;if(s(e)||t(e))r=e,n=A(e),i=p(e);else{if(!d.call(e,"name"))throw new Error(a("name"));const t=e.name;if(r=t,d.call(e,"weight")&&(u=e.weight,u<=0))throw new Error(l(t));n=A(t),i=p(t),c=e.getFn}return{path:n,id:i,weight:u,src:r,getFn:c}}function A(e){return t(e)?e:e.split(".")}function p(e){return t(e)?e.join("."):e}var C={isCaseSensitive:!1,ignoreDiacritics:!1,includeScore:!1,keys:[],shouldSort:!0,sortFn:(t,e)=>t.score===e.score?t.idx{if(c(e))if(u[l]){const d=e[u[l]];if(!c(d))return;if(l===u.length-1&&(s(d)||i(d)||r(d)))o.push(n(d));else if(t(d)){h=!0;for(let t=0,e=d.length;t{this._keysMap[t.id]=e}))}create(){!this.isCreated&&this.docs.length&&(this.isCreated=!0,s(this.docs[0])?this.docs.forEach(((t,e)=>{this._addString(t,e)})):this.docs.forEach(((t,e)=>{this._addObject(t,e)})),this.norm.clear())}add(t){const e=this.size();s(t)?this._addString(t,e):this._addObject(t,e)}removeAt(t){this.records.splice(t,1);for(let e=t,n=this.size();e{let u=n.getFn?n.getFn(e):this.getFn(e,n.path);if(c(u))if(t(u)){let e=[];const n=[{nestedArrIndex:-1,value:u}];for(;n.length;){const{nestedArrIndex:i,value:r}=n.pop();if(c(r))if(s(r)&&!o(r)){let t={v:r,i:i,n:this.norm.get(r)};e.push(t)}else t(r)&&r.forEach(((t,e)=>{n.push({nestedArrIndex:e,value:t})}))}i.$[r]=e}else if(s(u)&&!o(u)){let t={v:u,n:this.norm.get(u)};i.$[r]=t}})),this.records.push(i)}toJSON(){return{keys:this.keys,records:this.records}}}function M(t,e,{getFn:n=C.getFn,fieldNormWeight:s=C.fieldNormWeight}={}){const i=new F({getFn:n,fieldNormWeight:s});return i.setKeys(t.map(f)),i.setSources(e),i.create(),i}function E(t,{errors:e=0,currentLocation:n=0,expectedLocation:s=0,distance:i=C.distance,ignoreLocation:r=C.ignoreLocation}={}){const u=e/t.length;if(r)return u;const c=Math.abs(s-n);return i?u+c/i:c?1:u}const D=32;function B(t,e,n,{location:s=C.location,distance:i=C.distance,threshold:r=C.threshold,findAllMatches:u=C.findAllMatches,minMatchCharLength:c=C.minMatchCharLength,includeMatches:o=C.includeMatches,ignoreLocation:h=C.ignoreLocation}={}){if(e.length>D)throw new Error(`Pattern length exceeds max of ${D}.`);const a=e.length,l=t.length,d=Math.max(0,Math.min(s,l));let g=r,f=d;const A=c>1||o,p=A?Array(l):[];let m;for(;(m=t.indexOf(e,f))>-1;){let t=E(e,{currentLocation:m,expectedLocation:d,distance:i,ignoreLocation:h});if(g=Math.min(t,g),f=m+a,A){let t=0;for(;t=o;r-=1){let u=r-1,c=n[t.charAt(u)];if(A&&(p[u]=+!!c),m[r]=(m[r+1]<<1|1)&c,s&&(m[r]|=(F[r+1]|F[r])<<1|1|F[r+1]),m[r]&x&&(M=E(e,{errors:s,currentLocation:u,expectedLocation:d,distance:i,ignoreLocation:h}),M<=g)){if(g=M,f=u,f<=d)break;o=Math.max(1,2*d-f)}}if(E(e,{errors:s+1,currentLocation:d,expectedLocation:d,distance:i,ignoreLocation:h})>g)break;F=m}const y={isMatch:f>=0,score:Math.max(.001,M)};if(A){const t=function(t=[],e=C.minMatchCharLength){let n=[],s=-1,i=-1,r=0;for(let u=t.length;r=e&&n.push([s,i]),s=-1)}return t[r-1]&&r-s>=e&&n.push([s,r-1]),n}(p,c);t.length?o&&(y.indices=t):y.isMatch=!1}return y}function x(t){let e={};for(let n=0,s=t.length;nt.normalize("NFD").replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,""):t=>t;class L{constructor(t,{location:e=C.location,threshold:n=C.threshold,distance:s=C.distance,includeMatches:i=C.includeMatches,findAllMatches:r=C.findAllMatches,minMatchCharLength:u=C.minMatchCharLength,isCaseSensitive:c=C.isCaseSensitive,ignoreDiacritics:o=C.ignoreDiacritics,ignoreLocation:h=C.ignoreLocation}={}){if(this.options={location:e,threshold:n,distance:s,includeMatches:i,findAllMatches:r,minMatchCharLength:u,isCaseSensitive:c,ignoreDiacritics:o,ignoreLocation:h},t=c?t:t.toLowerCase(),t=o?y(t):t,this.pattern=t,this.chunks=[],!this.pattern.length)return;const a=(t,e)=>{this.chunks.push({pattern:t,alphabet:x(t),startIndex:e})},l=this.pattern.length;if(l>D){let t=0;const e=l%D,n=l-e;for(;t{const{isMatch:f,score:A,indices:p}=B(t,e,n,{location:i+g,distance:r,threshold:u,findAllMatches:c,minMatchCharLength:o,includeMatches:s,ignoreLocation:h});f&&(d=!0),l+=A,f&&p&&(a=[...a,...p])}));let g={isMatch:d,score:d?l/this.chunks.length:1};return d&&s&&(g.indices=a),g}}class k{constructor(t){this.pattern=t}static isMultiMatch(t){return _(t,this.multiRegex)}static isSingleMatch(t){return _(t,this.singleRegex)}search(){}}function _(t,e){const n=t.match(e);return n?n[1]:null}class v extends k{constructor(t,{location:e=C.location,threshold:n=C.threshold,distance:s=C.distance,includeMatches:i=C.includeMatches,findAllMatches:r=C.findAllMatches,minMatchCharLength:u=C.minMatchCharLength,isCaseSensitive:c=C.isCaseSensitive,ignoreDiacritics:o=C.ignoreDiacritics,ignoreLocation:h=C.ignoreLocation}={}){super(t),this._bitapSearch=new L(t,{location:e,threshold:n,distance:s,includeMatches:i,findAllMatches:r,minMatchCharLength:u,isCaseSensitive:c,ignoreDiacritics:o,ignoreLocation:h})}static get type(){return"fuzzy"}static get multiRegex(){return/^"(.*)"$/}static get singleRegex(){return/^(.*)$/}search(t){return this._bitapSearch.searchIn(t)}}class S extends k{constructor(t){super(t)}static get type(){return"include"}static get multiRegex(){return/^'"(.*)"$/}static get singleRegex(){return/^'(.*)$/}search(t){let e,n=0;const s=[],i=this.pattern.length;for(;(e=t.indexOf(this.pattern,n))>-1;)n=e+i,s.push([e,n-1]);const r=!!s.length;return{isMatch:r,score:r?0:1,indices:s}}}const I=[class extends k{constructor(t){super(t)}static get type(){return"exact"}static get multiRegex(){return/^="(.*)"$/}static get singleRegex(){return/^=(.*)$/}search(t){const e=t===this.pattern;return{isMatch:e,score:e?0:1,indices:[0,this.pattern.length-1]}}},S,class extends k{constructor(t){super(t)}static get type(){return"prefix-exact"}static get multiRegex(){return/^\^"(.*)"$/}static get singleRegex(){return/^\^(.*)$/}search(t){const e=t.startsWith(this.pattern);return{isMatch:e,score:e?0:1,indices:[0,this.pattern.length-1]}}},class extends k{constructor(t){super(t)}static get type(){return"inverse-prefix-exact"}static get multiRegex(){return/^!\^"(.*)"$/}static get singleRegex(){return/^!\^(.*)$/}search(t){const e=!t.startsWith(this.pattern);return{isMatch:e,score:e?0:1,indices:[0,t.length-1]}}},class extends k{constructor(t){super(t)}static get type(){return"inverse-suffix-exact"}static get multiRegex(){return/^!"(.*)"\$$/}static get singleRegex(){return/^!(.*)\$$/}search(t){const e=!t.endsWith(this.pattern);return{isMatch:e,score:e?0:1,indices:[0,t.length-1]}}},class extends k{constructor(t){super(t)}static get type(){return"suffix-exact"}static get multiRegex(){return/^"(.*)"\$$/}static get singleRegex(){return/^(.*)\$$/}search(t){const e=t.endsWith(this.pattern);return{isMatch:e,score:e?0:1,indices:[t.length-this.pattern.length,t.length-1]}}},class extends k{constructor(t){super(t)}static get type(){return"inverse-exact"}static get multiRegex(){return/^!"(.*)"$/}static get singleRegex(){return/^!(.*)$/}search(t){const e=-1===t.indexOf(this.pattern);return{isMatch:e,score:e?0:1,indices:[0,t.length-1]}}},v],w=I.length,$=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;const b=new Set([v.type,S.type]);class N{constructor(t,{isCaseSensitive:e=C.isCaseSensitive,ignoreDiacritics:n=C.ignoreDiacritics,includeMatches:s=C.includeMatches,minMatchCharLength:i=C.minMatchCharLength,ignoreLocation:r=C.ignoreLocation,findAllMatches:u=C.findAllMatches,location:c=C.location,threshold:o=C.threshold,distance:h=C.distance}={}){this.query=null,this.options={isCaseSensitive:e,ignoreDiacritics:n,includeMatches:s,minMatchCharLength:i,findAllMatches:u,ignoreLocation:r,location:c,threshold:o,distance:h},t=e?t:t.toLowerCase(),t=n?y(t):t,this.pattern=t,this.query=function(t,e={}){return t.split("|").map((t=>{let n=t.trim().split($).filter((t=>t&&!!t.trim())),s=[];for(let t=0,i=n.length;t!(!t[j]&&!t[W]),q=t=>({[j]:Object.keys(t).map((e=>({[e]:t[e]})))});function J(e,n,{auto:i=!0}={}){const r=e=>{let c=Object.keys(e);const o=(t=>!!t[z])(e);if(!o&&c.length>1&&!P(e))return r(q(e));if((e=>!t(e)&&u(e)&&!P(e))(e)){const t=o?e[z]:c[0],r=o?e[K]:e[t];if(!s(r))throw new Error((t=>`Invalid value for key ${t}`)(t));const u={keyId:p(t),pattern:r};return i&&(u.searcher=O(r,n)),u}let h={children:[],operator:c[0]};return c.forEach((n=>{const s=e[n];t(s)&&s.forEach((t=>{h.children.push(r(t))}))})),h};return P(e)||(e=q(e)),r(e)}function V(t,e){const n=t.matches;e.matches=[],c(n)&&n.forEach((t=>{if(!c(t.indices)||!t.indices.length)return;const{indices:n,value:s}=t;let i={indices:n,value:s};t.key&&(i.key=t.key.src),t.idx>-1&&(i.refIndex=t.idx),e.matches.push(i)}))}function U(t,e){e.score=t.score}class G{constructor(t,e={},n){this.options={...C,...e},this.options.useExtendedSearch,this._keyStore=new g(this.options.keys),this.setCollection(t,n)}setCollection(t,e){if(this._docs=t,e&&!(e instanceof F))throw new Error("Incorrect 'index' type");this._myIndex=e||M(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}add(t){c(t)&&(this._docs.push(t),this._myIndex.add(t))}remove(t=(()=>!1)){const e=[];for(let n=0,s=this._docs.length;n{let n=1;t.matches.forEach((({key:t,norm:s,score:i})=>{const r=t?t.weight:null;n*=Math.pow(0===i&&r?Number.EPSILON:i,(r||1)*(e?1:s))})),t.score=n}))}(h,{ignoreFieldNorm:o}),u&&h.sort(c),i(e)&&e>-1&&(h=h.slice(0,e)),function(t,e,{includeMatches:n=C.includeMatches,includeScore:s=C.includeScore}={}){const i=[];return n&&i.push(V),s&&i.push(U),t.map((t=>{const{idx:n}=t,s={item:e[n],refIndex:n};return i.length&&i.forEach((e=>{e(t,s)})),s}))}(h,this._docs,{includeMatches:n,includeScore:r})}_searchStringList(t){const e=O(t,this.options),{records:n}=this._myIndex,s=[];return n.forEach((({v:t,i:n,n:i})=>{if(!c(t))return;const{isMatch:r,score:u,indices:o}=e.searchIn(t);r&&s.push({item:t,idx:n,matches:[{score:u,value:t,norm:i,indices:o}]})})),s}_searchLogical(t){const e=J(t,this.options),n=(t,e,s)=>{if(!t.children){const{keyId:n,searcher:i}=t,r=this._findMatches({key:this._keyStore.get(n),value:this._myIndex.getValueForItemAtKeyId(e,n),searcher:i});return r&&r.length?[{idx:s,item:e,matches:r}]:[]}const i=[];for(let r=0,u=t.children.length;r{if(c(t)){let u=n(e,t,s);u.length&&(i[s]||(i[s]={idx:s,item:t,matches:[]},r.push(i[s])),u.forEach((({matches:t})=>{i[s].matches.push(...t)})))}})),r}_searchObjectList(t){const e=O(t,this.options),{keys:n,records:s}=this._myIndex,i=[];return s.forEach((({$:t,i:s})=>{if(!c(t))return;let r=[];n.forEach(((n,s)=>{r.push(...this._findMatches({key:n,value:t[s],searcher:e}))})),r.length&&i.push({idx:s,item:t,matches:r})})),i}_findMatches({key:e,value:n,searcher:s}){if(!c(n))return[];let i=[];if(t(n))n.forEach((({v:t,i:n,n:r})=>{if(!c(t))return;const{isMatch:u,score:o,indices:h}=s.searchIn(t);u&&i.push({score:o,key:e,value:t,idx:n,norm:r,indices:h})}));else{const{v:t,n:r}=n,{isMatch:u,score:c,indices:o}=s.searchIn(t);u&&i.push({score:c,key:e,value:t,norm:r,indices:o})}return i}}G.version="7.1.0",G.createIndex=M,G.parseIndex=function(t,{getFn:e=C.getFn,fieldNormWeight:n=C.fieldNormWeight}={}){const{keys:s,records:i}=t,r=new F({getFn:e,fieldNormWeight:n});return r.setKeys(s),r.setIndexRecords(i),r},G.config=C,function(...t){R.push(...t)}(N);export{G as default}; \ No newline at end of file diff --git a/assets/search.js b/assets/search.js index 550708d..05430ff 100644 --- a/assets/search.js +++ b/assets/search.js @@ -1,4 +1,4 @@ -import Fuse from "https://registry.npmmirror.com/fuse.js/7.1.0/files/dist/fuse.min.mjs"; +import Fuse from "/libs/fuse.min.js"; const DEFAULT_FEEDS = [ { url: "/learn/index.xml", module: "学习" }, diff --git a/zine.ziggy b/zine.ziggy index 2a8bd5f..63a0c2d 100644 --- a/zine.ziggy +++ b/zine.ziggy @@ -5,6 +5,7 @@ Site { .layouts_dir_path = "layouts", .assets_dir_path = "assets", .static_assets = [ - "404.html" + "404.html", + "libs/fuse.min.js", ], } From d6b203bbccfdbc419076b26c6987a8bde203abcc Mon Sep 17 00:00:00 2001 From: xihale Date: Sat, 20 Sep 2025 19:44:02 +0800 Subject: [PATCH 4/6] refactor: move css & js location of search module --- layouts/templates/base.shtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layouts/templates/base.shtml b/layouts/templates/base.shtml index 0a87149..9e9a651 100644 --- a/layouts/templates/base.shtml +++ b/layouts/templates/base.shtml @@ -25,8 +25,9 @@ fetch('https://en.liujiacai.net/pv/write?' + new URLSearchParams(payload), {mode: 'no-cors'}).catch(console.log); - + +
@@ -58,7 +59,6 @@
-