From nobody Fri Apr 3 22:32:11 2026 Received: from sender4-op-o15.zoho.com (sender4-op-o15.zoho.com [136.143.188.15]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E0A723321DC; Sun, 22 Mar 2026 18:18:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=pass smtp.client-ip=136.143.188.15 ARC-Seal: i=2; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774203505; cv=pass; b=JPjUK7hJd4SM0SsocPFTVO76L7h61Ix0Wb51ryZcNXqnM2HWPaNPoPK7c7ZZbnDK3fUUKV880NvBvIo+6uSpqMwwxOWoaUhZkzfS3szrfsdftKm+FqCRRav6x9deeDmplk64Fh+Euo/NLehj5TdAcpt8Af3wSRj/gDsF9M2frPU= ARC-Message-Signature: i=2; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774203505; c=relaxed/simple; bh=XEhdN3OdD1Z+pBfBJhnRvqfGHi7ZM2s7vJmq3apR+3M=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Mlo7go3lbStTLXTHUr1uVE63qhNGB92COiaodQ4dh3FXma14BKoosw1qwvfDewMtJGre+g4Fnvox5oXXM0fzMrfh0eiZ714xsN2tGiEy16mn6m64ESCEfEmbuas3MYQzO89pfmpFWYNHnDCeY0BK9V9+V6bWM96RD3VOog5We9c= ARC-Authentication-Results: i=2; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=ritovision.com; spf=pass smtp.mailfrom=ritovision.com; dkim=pass (1024-bit key) header.d=ritovision.com header.i=rito@ritovision.com header.b=EAipuI2F; arc=pass smtp.client-ip=136.143.188.15 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=ritovision.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=ritovision.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=ritovision.com header.i=rito@ritovision.com header.b="EAipuI2F" ARC-Seal: i=1; a=rsa-sha256; t=1774203487; cv=none; d=zohomail.com; s=zohoarc; b=As16kYtPGmLylXdRUyGzaB48WoZd88iR8nn6d89d/jEXM3KeQfHffmtuOwu9DA9xRn3vLqoaa9ze7gY0zQyU5DxSEmq5wyQ7neI1Mnd9lG8Zobrbg8BwgI1flyke4kDpBbjnKit8rS42+T+E5YVFbe+KQJzr1GAEL7I2RHtrsRQ= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1774203487; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:MIME-Version:Message-ID:References:Subject:Subject:To:To:Message-Id:Reply-To; bh=QLgZmCRpCZxE5slTyyGy57JzkCFvi4wjlEwYXo00EU8=; b=Zl2qrc1iAn/gSyLWqiGFI1Jg3ZezPsoyl0gIpQRpmugdd/Zi9l7gQqGbIScrroOszktTprN8+NSredNjuuWB8z2/2e1KdznPFxr9pqeQFJiJArOKrIBMAsE4/OO0ut8L6nLkRi3Saq91SHygJd5Y4ftFOpwOHsI9d6E3wKL7070= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass header.i=ritovision.com; spf=pass smtp.mailfrom=rito@ritovision.com; dmarc=pass header.from= DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; t=1774203487; s=zmail; d=ritovision.com; i=rito@ritovision.com; h=From:From:To:To:Cc:Cc:Subject:Subject:Date:Date:Message-ID:In-Reply-To:References:MIME-Version:Content-Transfer-Encoding:Message-Id:Reply-To; bh=QLgZmCRpCZxE5slTyyGy57JzkCFvi4wjlEwYXo00EU8=; b=EAipuI2FLHoGos5sp5rEV6exNx7fEl6EvOlLO452dd+rdJkfzBOTPKUUXD6vyP3k ChJ7ZbaHX0zYNgjK9iO6ys6wGn2R/Qm8xAdkvBpMnSNH04KMfxZjRKSgibG956otU2M awS1bYuf/8OMMsuXpdpk1bo8th7ow03p3oXVtpPI= Received: by mx.zohomail.com with SMTPS id 1774203484757329.68050884779404; Sun, 22 Mar 2026 11:18:04 -0700 (PDT) From: Rito Rhymes To: Jonathan Corbet , Mauro Carvalho Chehab , linux-doc@vger.kernel.org Cc: Shuah Khan , linux-kernel@vger.kernel.org, rdunlap@infradead.org, Rito Rhymes Subject: [PATCH v2] docs: add advanced search for kernel documentation Date: Sun, 22 Mar 2026 14:17:59 -0400 Message-ID: <20260322181759.47889-1-rito@ritovision.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260321181511.11706-1-rito@ritovision.com> References: <20260321181511.11706-1-rito@ritovision.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-ZohoMailClient: External Content-Type: text/plain; charset="utf-8" Replace the stock Sphinx search page with one that reuses the existing searchindex.js while adding structured result grouping, filtering, and exact identifier matching. Results are grouped into Symbols, Sections, Index entries, and Pages, each in a collapsible section with a count. An Advanced panel exposes filters for documentation area, object type, result kind, and exact match mode. All state is URL-encoded so searches remain shareable. Page summary snippets are lazy-loaded via IntersectionObserver to avoid fetching every matching page up front. The sidebar keeps the existing quick-search box and adds an "Advanced search" link below it. Signed-off-by: Rito Rhymes Assisted-by: Codex:GPT-5.4 Assisted-by: Claude:Opus-4.6 --- v2: add Assisted-by attribution Documentation/sphinx-static/custom.css | 163 ++++ Documentation/sphinx-static/kernel-search.js | 746 ++++++++++++++++++ Documentation/sphinx/templates/search.html | 106 +++ Documentation/sphinx/templates/searchbox.html | 18 + 4 files changed, 1033 insertions(+) create mode 100644 Documentation/sphinx-static/kernel-search.js create mode 100644 Documentation/sphinx/templates/search.html create mode 100644 Documentation/sphinx/templates/searchbox.html diff --git a/Documentation/sphinx-static/custom.css b/Documentation/sphinx-= static/custom.css index db24f4344..dd7cc221e 100644 --- a/Documentation/sphinx-static/custom.css +++ b/Documentation/sphinx-static/custom.css @@ -169,3 +169,166 @@ a.manpage { font-weight: bold; font-family: "Courier New", Courier, monospace; } + +/* Keep the quick search box as-is and add a secondary advanced search lin= k. */ +div.sphinxsidebar p.search-advanced-link { + margin: 0.5em 0 0 0; + font-size: 0.95em; +} + +/* + * The enhanced search page keeps the stock GET workflow but adds + * filter controls and grouped results. + */ +form.kernel-search-form { + margin-bottom: 2em; +} + +div.kernel-search-query-row { + display: flex; + flex-wrap: wrap; + gap: 1em; + align-items: end; +} + +div.kernel-search-query-field { + flex: 1 1 26em; +} + +div.kernel-search-query-field label, +div.kernel-search-field label, +fieldset.kernel-search-kind-filters legend { + display: block; + font-weight: bold; + margin-bottom: 0.35em; +} + +div.kernel-search-query-field input[type=3D"text"], +div.kernel-search-field select { + width: 100%; + box-sizing: border-box; +} + +div.kernel-search-query-actions { + display: flex; + align-items: center; + gap: 0.75em; +} + +span.kernel-search-progress { + min-height: 1.2em; + color: #666; +} + +details.kernel-search-advanced { + margin-top: 1em; + padding: 0.75em 1em 1em 1em; + border: 1px solid #cccccc; + background: #f7f7f7; +} + +details.kernel-search-advanced summary { + cursor: pointer; + font-weight: bold; +} + +div.kernel-search-advanced-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16em, 1fr)); + gap: 1em 1.25em; + margin-top: 1em; +} + +fieldset.kernel-search-kind-filters { + margin: 0; + padding: 0; + border: none; +} + +label.kernel-search-checkbox { + display: flex; + align-items: flex-start; + gap: 0.5em; + margin-bottom: 0.35em; +} + +div.kernel-search-results { + margin-top: 1.5em; +} + +p.kernel-search-status { + margin-bottom: 1.5em; +} + +details.kernel-search-group { + margin-top: 2em; +} + +summary.kernel-search-group-summary { + cursor: pointer; + font-size: 150%; + margin: 0 0 0.6em 0; +} + +summary.kernel-search-group-summary h2.kernel-search-group-title { + display: inline; + margin: 0; + font-size: inherit; + font-weight: normal; +} + +span.kernel-search-group-count { + color: #666666; + margin-left: 0.35em; +} + +ol.kernel-search-list { + list-style: none; + margin: 0; + padding: 0; +} + +li.kernel-search-result { + padding: 0.9em 0; + border-top: 1px solid #dddddd; +} + +li.kernel-search-result:first-child { + border-top: none; +} + +div.kernel-search-result-heading { + font-weight: bold; +} + +div.kernel-search-path, +div.kernel-search-meta, +p.kernel-search-summary { + margin-top: 0.3em; + color: #555555; +} + +div.kernel-search-path, +div.kernel-search-meta { + font-size: 0.95em; +} + +p.kernel-search-summary { + margin-bottom: 0; +} + +@media screen and (max-width: 65em) { + div.kernel-search-query-actions { + width: 100%; + justify-content: flex-start; + } +} + +@media screen and (min-width: 65em) { + div.kernel-search-result-heading, + div.kernel-search-path, + div.kernel-search-meta, + p.kernel-search-summary { + margin-left: 2rem; + } +} diff --git a/Documentation/sphinx-static/kernel-search.js b/Documentation/s= phinx-static/kernel-search.js new file mode 100644 index 000000000..bcf79f820 --- /dev/null +++ b/Documentation/sphinx-static/kernel-search.js @@ -0,0 +1,746 @@ +"use strict"; + +(() =3D> { + const RESULT_KIND_ORDER =3D ["object", "title", "index", "text"]; + const RESULT_KIND_LABELS =3D { + object: "Symbols", + title: "Sections", + index: "Index entries", + text: "Pages", + }; + const TOP_LEVEL_AREA =3D "__top_level__"; + const OBJECT_PRIORITY =3D { + 0: 15, + 1: 5, + 2: -5, + }; + const SUMMARY_ROOT_MARGIN =3D "300px 0px"; + const documentTextCache =3D new Map(); + const summaryTargets =3D new WeakMap(); + let summaryObserver =3D null; + + window.Search =3D window.Search || {}; + window.Search._callbacks =3D window.Search._callbacks || []; + window.Search._index =3D window.Search._index || null; + window.Search.setIndex =3D (index) =3D> { + window.Search._index =3D index; + const callbacks =3D window.Search._callbacks.slice(); + window.Search._callbacks.length =3D 0; + callbacks.forEach((callback) =3D> callback(index)); + }; + window.Search.whenReady =3D (callback) =3D> { + if (window.Search._index) callback(window.Search._index); + else window.Search._callbacks.push(callback); + }; + + const splitQuery =3D (query) =3D> + query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter((term) =3D> term); + + const getStemmer =3D () =3D> + typeof Stemmer =3D=3D=3D "function" ? new Stemmer() : { stemWord: (wor= d) =3D> word }; + + const hasOwn =3D (object, key) =3D> + Object.prototype.hasOwnProperty.call(object, key); + + const getContentRoot =3D () =3D> + document.documentElement.dataset.content_root + || (typeof DOCUMENTATION_OPTIONS !=3D=3D "undefined" ? DOCUMENTATION_O= PTIONS.URL_ROOT || "" : ""); + + const compareResults =3D (left, right) =3D> { + if (left.score =3D=3D=3D right.score) { + const leftTitle =3D left.title.toLowerCase(); + const rightTitle =3D right.title.toLowerCase(); + if (leftTitle =3D=3D=3D rightTitle) return 0; + return leftTitle < rightTitle ? -1 : 1; + } + return right.score - left.score; + }; + + const getAreaValue =3D (docName) =3D> + docName.includes("/") ? docName.split("/", 1)[0] : TOP_LEVEL_AREA; + + const getAreaLabel =3D (area) =3D> + area =3D=3D=3D TOP_LEVEL_AREA ? "Top level" : area; + + const matchArea =3D (docName, area) =3D> { + if (!area) return true; + if (area =3D=3D=3D TOP_LEVEL_AREA) return !docName.includes("/"); + return docName =3D=3D=3D area || docName.startsWith(area + "/"); + }; + + const buildDocUrls =3D (docName) =3D> { + const contentRoot =3D getContentRoot(); + const builder =3D DOCUMENTATION_OPTIONS.BUILDER; + const fileSuffix =3D DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const linkSuffix =3D DOCUMENTATION_OPTIONS.LINK_SUFFIX; + + if (builder =3D=3D=3D "dirhtml") { + let dirname =3D docName + "/"; + if (dirname.match(/\/index\/$/)) dirname =3D dirname.substring(0, di= rname.length - 6); + else if (dirname =3D=3D=3D "index/") dirname =3D ""; + + return { + requestUrl: contentRoot + dirname, + linkUrl: contentRoot + dirname, + }; + } + + return { + requestUrl: contentRoot + docName + fileSuffix, + linkUrl: docName + linkSuffix, + }; + }; + + const htmlToText =3D (htmlString, anchor) =3D> { + const htmlElement =3D new DOMParser().parseFromString(htmlString, "tex= t/html"); + for (const selector of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(selector).forEach((element) =3D> elemen= t.remove()); + } + + if (anchor) { + const anchorId =3D anchor[0] =3D=3D=3D "#" ? anchor.substring(1) : a= nchor; + const anchorContent =3D htmlElement.getElementById(anchorId); + if (anchorContent) return anchorContent.textContent; + } + + const docContent =3D htmlElement.querySelector('[role=3D"main"]'); + return docContent ? docContent.textContent : ""; + }; + + const makeSummary =3D (htmlText, keywords, anchor) =3D> { + const text =3D htmlToText(htmlText, anchor); + if (!text) return null; + + const lowered =3D text.toLowerCase(); + const positions =3D keywords + .map((keyword) =3D> lowered.indexOf(keyword.toLowerCase())) + .filter((position) =3D> position > -1); + const actualStart =3D positions.length ? positions[0] : 0; + const start =3D Math.max(actualStart - 120, 0); + const prefix =3D start =3D=3D=3D 0 ? "" : "..."; + const suffix =3D start + 240 < text.length ? "..." : ""; + + const summary =3D document.createElement("p"); + summary.className =3D "kernel-search-summary"; + summary.textContent =3D prefix + text.substring(start, start + 240).tr= im() + suffix; + return summary; + }; + + const loadDocumentText =3D (requestUrl) =3D> { + if (!documentTextCache.has(requestUrl)) { + documentTextCache.set( + requestUrl, + fetch(requestUrl) + .then((response) =3D> (response.ok ? response.text() : "")) + .catch(() =3D> ""), + ); + } + + return documentTextCache.get(requestUrl); + }; + + const pushBest =3D (resultMap, result) =3D> { + const key =3D [result.kind, result.docName, result.anchor || "", resul= t.title].join("|"); + const existing =3D resultMap.get(key); + if (!existing || existing.score < result.score) resultMap.set(key, res= ult); + }; + + const buildQueryState =3D (query, exact) =3D> { + const rawTerms =3D splitQuery(query.trim()); + const rawTermsLower =3D rawTerms.map((term) =3D> term.toLowerCase()); + const objectTerms =3D new Set(rawTermsLower); + const highlightTerms =3D exact ? rawTermsLower : []; + const searchTerms =3D new Set(); + const excludedTerms =3D new Set(); + + if (!exact) { + const stemmer =3D getStemmer(); + rawTerms.forEach((term) =3D> { + const lowered =3D term.toLowerCase(); + if ((typeof stopwords !=3D=3D "undefined" && stopwords.has(lowered= )) || /^\d+$/.test(term)) { + return; + } + + const word =3D stemmer.stemWord(lowered); + if (!word) return; + + if (word[0] =3D=3D=3D "-") excludedTerms.add(word.substring(1)); + else { + searchTerms.add(word); + highlightTerms.push(lowered); + } + }); + } else { + rawTermsLower.forEach((term) =3D> searchTerms.add(term)); + } + + if (typeof SPHINX_HIGHLIGHT_ENABLED !=3D=3D "undefined" && SPHINX_HIGH= LIGHT_ENABLED) { + localStorage.setItem("sphinx_highlight_terms", [...new Set(highlight= Terms)].join(" ")); + } + + return { + exact, + query, + queryLower: query.toLowerCase().trim(), + rawTerms: rawTermsLower, + objectTerms, + searchTerms, + excludedTerms, + highlightTerms: [...new Set(highlightTerms)], + }; + }; + + const candidateMatches =3D (candidateLower, state) =3D> { + if (!state.queryLower) return false; + if (state.exact) return candidateLower =3D=3D=3D state.queryLower; + + if ( + candidateLower.includes(state.queryLower) + && state.queryLower.length >=3D Math.ceil(candidateLower.length / 2) + ) { + return true; + } + + return state.rawTerms.length > 0 + && state.rawTerms.every((term) =3D> candidateLower.includes(term)); + }; + + const scoreLabelMatch =3D (candidateLower, state, baseScore, partialScor= e) =3D> { + if (state.exact) return baseScore + 20; + if (candidateLower =3D=3D=3D state.queryLower) return baseScore + 10; + if (candidateLower.includes(state.queryLower)) { + return Math.max(partialScore, Math.round((baseScore * state.queryLow= er.length) / candidateLower.length)); + } + + return partialScore * Math.max(1, state.rawTerms.filter((term) =3D> ca= ndidateLower.includes(term)).length); + }; + + const collectObjectResults =3D (index, state, filters) =3D> { + const resultMap =3D new Map(); + const objects =3D index.objects || {}; + const objNames =3D index.objnames || {}; + const objTypes =3D index.objtypes || {}; + + Object.keys(objects).forEach((prefix) =3D> { + objects[prefix].forEach((match) =3D> { + const fileIndex =3D match[0]; + const typeIndex =3D match[1]; + const priority =3D match[2]; + const anchorValue =3D match[3]; + const name =3D match[4]; + const docName =3D index.docnames[fileIndex]; + const fileName =3D index.filenames[fileIndex]; + const pageTitle =3D index.titles[fileIndex]; + const objectLabel =3D objNames[typeIndex] ? objNames[typeIndex][2]= : "Object"; + const objectType =3D objTypes[typeIndex]; + + if (!matchArea(docName, filters.area)) return; + if (filters.objtype && filters.objtype !=3D=3D objectType) return; + + const fullName =3D prefix ? prefix + "." + name : name; + const fullNameLower =3D fullName.toLowerCase(); + const lastNameLower =3D fullNameLower.split(".").slice(-1)[0]; + const nameLower =3D name.toLowerCase(); + + let score =3D 0; + if (state.exact) { + if ( + fullNameLower !=3D=3D state.queryLower + && lastNameLower !=3D=3D state.queryLower + && nameLower !=3D=3D state.queryLower + ) { + return; + } + score =3D 120; + } else { + const haystack =3D `${fullName} ${objectLabel} ${pageTitle}`.toL= owerCase(); + if (state.objectTerms.size =3D=3D=3D 0) return; + if ([...state.objectTerms].some((term) =3D> !haystack.includes(t= erm))) return; + const matchedNameTerms =3D state.rawTerms.filter( + (term) =3D> + fullNameLower.includes(term) + || lastNameLower.includes(term) + || nameLower.includes(term), + ).length; + + if ( + fullNameLower =3D=3D=3D state.queryLower + || lastNameLower =3D=3D=3D state.queryLower + || nameLower =3D=3D=3D state.queryLower + ) { + score +=3D 11; + } else if ( + lastNameLower.includes(state.queryLower) + || nameLower.includes(state.queryLower) + ) { + score +=3D 6; + } else if (fullNameLower.includes(state.queryLower)) { + score +=3D 4; + } else if (matchedNameTerms > 0) { + score +=3D matchedNameTerms; + } else { + return; + } + } + + score +=3D OBJECT_PRIORITY[priority] || 0; + + let anchor =3D anchorValue; + if (anchor =3D=3D=3D "") anchor =3D fullName; + else if (anchor =3D=3D=3D "-" && objNames[typeIndex]) anchor =3D o= bjNames[typeIndex][1] + "-" + fullName; + + pushBest(resultMap, { + kind: "object", + docName, + fileName, + title: fullName, + anchor: anchor ? "#" + anchor : "", + description: `${objectLabel}, in ${pageTitle}`, + score, + }); + }); + }); + + return [...resultMap.values()].sort(compareResults); + }; + + const collectSectionResults =3D (index, state, filters) =3D> { + const resultMap =3D new Map(); + const allTitles =3D index.alltitles || {}; + + Object.entries(allTitles).forEach(([sectionTitle, entries]) =3D> { + const lowered =3D sectionTitle.toLowerCase().trim(); + if (!candidateMatches(lowered, state)) return; + + entries.forEach(([fileIndex, anchorId]) =3D> { + const docName =3D index.docnames[fileIndex]; + const fileName =3D index.filenames[fileIndex]; + const pageTitle =3D index.titles[fileIndex]; + if (!matchArea(docName, filters.area)) return; + + if (anchorId =3D=3D=3D null && sectionTitle =3D=3D=3D pageTitle) r= eturn; + + pushBest(resultMap, { + kind: "title", + docName, + fileName, + title: pageTitle !=3D=3D sectionTitle ? `${pageTitle} > ${sectio= nTitle}` : sectionTitle, + anchor: anchorId ? "#" + anchorId : "", + description: pageTitle, + score: scoreLabelMatch(lowered, state, 15, 7), + }); + }); + }); + + return [...resultMap.values()].sort(compareResults); + }; + + const collectIndexResults =3D (index, state, filters) =3D> { + const resultMap =3D new Map(); + const entries =3D index.indexentries || {}; + + Object.entries(entries).forEach(([entry, matches]) =3D> { + const lowered =3D entry.toLowerCase().trim(); + if (!candidateMatches(lowered, state)) return; + + matches.forEach(([fileIndex, anchorId, isMain]) =3D> { + const docName =3D index.docnames[fileIndex]; + const fileName =3D index.filenames[fileIndex]; + const pageTitle =3D index.titles[fileIndex]; + if (!matchArea(docName, filters.area)) return; + + let score =3D scoreLabelMatch(lowered, state, 20, 8); + if (!isMain) score -=3D 5; + + pushBest(resultMap, { + kind: "index", + docName, + fileName, + title: entry, + anchor: anchorId ? "#" + anchorId : "", + description: pageTitle, + score, + }); + }); + }); + + return [...resultMap.values()].sort(compareResults); + }; + + const collectTextResults =3D (index, state, filters) =3D> { + const resultMap =3D new Map(); + const terms =3D index.terms || {}; + const titleTerms =3D index.titleterms || {}; + const searchTerms =3D [...state.searchTerms]; + + if (searchTerms.length =3D=3D=3D 0) return []; + + const scoreMap =3D new Map(); + const fileMap =3D new Map(); + + searchTerms.forEach((word) =3D> { + const files =3D []; + const candidates =3D [ + { + files: hasOwn(terms, word) ? terms[word] : undefined, + score: 5, + }, + { + files: hasOwn(titleTerms, word) ? titleTerms[word] : undefined, + score: 15, + }, + ]; + + if (!state.exact && word.length > 2) { + if (!hasOwn(terms, word)) { + Object.keys(terms).forEach((term) =3D> { + if (term.includes(word)) candidates.push({ files: terms[term],= score: 2 }); + }); + } + if (!hasOwn(titleTerms, word)) { + Object.keys(titleTerms).forEach((term) =3D> { + if (term.includes(word)) candidates.push({ files: titleTerms[t= erm], score: 7 }); + }); + } + } + + if (candidates.every((candidate) =3D> candidate.files =3D=3D=3D unde= fined)) return; + + candidates.forEach((candidate) =3D> { + if (candidate.files =3D=3D=3D undefined) return; + + let recordFiles =3D candidate.files; + if (recordFiles.length =3D=3D=3D undefined) recordFiles =3D [recor= dFiles]; + files.push(...recordFiles); + + recordFiles.forEach((fileIndex) =3D> { + if (!scoreMap.has(fileIndex)) scoreMap.set(fileIndex, new Map()); + const currentScore =3D scoreMap.get(fileIndex).get(word) || 0; + scoreMap.get(fileIndex).set(word, Math.max(currentScore, candida= te.score)); + }); + }); + + files.forEach((fileIndex) =3D> { + if (!fileMap.has(fileIndex)) fileMap.set(fileIndex, [word]); + else if (!fileMap.get(fileIndex).includes(word)) fileMap.get(fileI= ndex).push(word); + }); + }); + + const filteredTermCount =3D state.exact + ? searchTerms.length + : searchTerms.filter((term) =3D> term.length > 2).length; + + for (const [fileIndex, matchedWords] of fileMap.entries()) { + const docName =3D index.docnames[fileIndex]; + const fileName =3D index.filenames[fileIndex]; + if (!matchArea(docName, filters.area)) continue; + + if (matchedWords.length !=3D=3D searchTerms.length && matchedWords.l= ength !=3D=3D filteredTermCount) { + continue; + } + + if ( + [...state.excludedTerms].some( + (term) =3D> + terms[term] =3D=3D=3D fileIndex + || titleTerms[term] =3D=3D=3D fileIndex + || (terms[term] || []).includes(fileIndex) + || (titleTerms[term] || []).includes(fileIndex), + ) + ) { + continue; + } + + let score =3D Math.max(...matchedWords.map((word) =3D> scoreMap.get(= fileIndex).get(word))); + if (state.exact && index.titles[fileIndex].toLowerCase() =3D=3D=3D s= tate.queryLower) score +=3D 10; + + pushBest(resultMap, { + kind: "text", + docName, + fileName, + title: index.titles[fileIndex], + anchor: "", + description: null, + score, + }); + } + + return [...resultMap.values()].sort(compareResults); + }; + + const buildFilters =3D (state) =3D> ({ + area: state.area, + objtype: state.objtype, + }); + + const ensureSummaryObserver =3D () =3D> { + if (summaryObserver || typeof IntersectionObserver !=3D=3D "function")= return summaryObserver; + + summaryObserver =3D new IntersectionObserver((entries) =3D> { + entries.forEach((entry) =3D> { + if (!entry.isIntersecting) return; + + const target =3D entry.target; + summaryObserver.unobserve(target); + const payload =3D summaryTargets.get(target); + if (!payload || payload.loaded) return; + + payload.loaded =3D true; + loadDocumentText(payload.requestUrl).then((htmlText) =3D> { + if (!htmlText) return; + + const summary =3D makeSummary(htmlText, payload.keywords, payloa= d.anchor); + if (!summary) return; + + payload.item.appendChild(summary); + }); + }); + }, { rootMargin: SUMMARY_ROOT_MARGIN }); + + return summaryObserver; + }; + + const queueSummaryLoad =3D (result, item, keywords) =3D> { + const urls =3D buildDocUrls(result.docName); + const payload =3D { + anchor: result.anchor, + item, + keywords, + loaded: false, + requestUrl: urls.requestUrl, + }; + + const observer =3D ensureSummaryObserver(); + if (!observer) { + payload.loaded =3D true; + loadDocumentText(payload.requestUrl).then((htmlText) =3D> { + if (!htmlText) return; + + const summary =3D makeSummary(htmlText, keywords, result.anchor); + if (summary) item.appendChild(summary); + }); + return; + } + + summaryTargets.set(item, payload); + observer.observe(item); + }; + + const createResultItem =3D (result, keywords) =3D> { + const urls =3D buildDocUrls(result.docName); + const item =3D document.createElement("li"); + item.className =3D `kernel-search-result kind-${result.kind}`; + + const heading =3D item.appendChild(document.createElement("div")); + heading.className =3D "kernel-search-result-heading"; + + const link =3D heading.appendChild(document.createElement("a")); + link.href =3D urls.linkUrl + result.anchor; + link.dataset.score =3D String(result.score); + link.textContent =3D result.title; + + const path =3D item.appendChild(document.createElement("div")); + path.className =3D "kernel-search-path"; + path.textContent =3D result.fileName; + + if (result.description) { + const meta =3D item.appendChild(document.createElement("div")); + meta.className =3D "kernel-search-meta"; + meta.textContent =3D result.description; + } + + if (result.kind =3D=3D=3D "text") { + queueSummaryLoad(result, item, keywords); + } + return item; + }; + + const renderResults =3D (state, resultsByKind) =3D> { + const container =3D document.getElementById("kernel-search-results"); + const totalResults =3D RESULT_KIND_ORDER.reduce( + (count, kind) =3D> count + resultsByKind[kind].length, + 0, + ); + if (summaryObserver) { + summaryObserver.disconnect(); + summaryObserver =3D null; + } + container.replaceChildren(); + + const summary =3D document.createElement("p"); + summary.className =3D "kernel-search-status"; + if (!state.queryLower) { + summary.textContent =3D "Enter a search query to browse kernel docum= entation."; + container.appendChild(summary); + return; + } + + if (!totalResults) { + summary.textContent =3D + "No matching results were found for the current query and filters.= "; + container.appendChild(summary); + return; + } + + summary.textContent =3D + `Found ${totalResults} result${totalResults =3D=3D=3D 1 ? "" : "s"} = for "${state.query}".`; + container.appendChild(summary); + + RESULT_KIND_ORDER.forEach((kind) =3D> { + const results =3D resultsByKind[kind]; + if (!results.length) return; + + const group =3D container.appendChild(document.createElement("detail= s")); + group.className =3D `kernel-search-group kind-${kind}`; + group.open =3D true; + + const summary =3D group.appendChild(document.createElement("summary"= )); + summary.className =3D "kernel-search-group-summary"; + + const heading =3D summary.appendChild(document.createElement("h2")); + heading.className =3D "kernel-search-group-title"; + heading.textContent =3D RESULT_KIND_LABELS[kind]; + + const count =3D summary.appendChild(document.createElement("span")); + count.className =3D "kernel-search-group-count"; + count.textContent =3D `(${results.length})`; + + const list =3D group.appendChild(document.createElement("ol")); + list.className =3D "kernel-search-list"; + + results.forEach((result) =3D> { + list.appendChild(createResultItem(result, state.highlightTerms)); + }); + }); + }; + + const populateAreaOptions =3D (select, state) =3D> { + const areas =3D new Set(); + window.Search._index.docnames.forEach((docName) =3D> areas.add(getArea= Value(docName))); + + const options =3D [new Option("All documentation areas", "", false, !s= tate.area)]; + [...areas] + .sort((left, right) =3D> { + if (left =3D=3D=3D TOP_LEVEL_AREA) return -1; + if (right =3D=3D=3D TOP_LEVEL_AREA) return 1; + return left.localeCompare(right); + }) + .forEach((area) =3D> { + options.push(new Option(getAreaLabel(area), area, false, area =3D= =3D=3D state.area)); + }); + + select.replaceChildren(...options); + }; + + const populateObjectTypeOptions =3D (select, state) =3D> { + const objTypes =3D window.Search._index.objtypes || {}; + const objNames =3D window.Search._index.objnames || {}; + const entries =3D Object.keys(objTypes) + .map((key) =3D> ({ + value: objTypes[key], + label: objNames[key] ? objNames[key][2] : objTypes[key], + })) + .sort((left, right) =3D> left.label.localeCompare(right.label)); + + const seen =3D new Set(); + const options =3D [new Option("All object types", "", false, !state.ob= jtype)]; + entries.forEach((entry) =3D> { + if (seen.has(entry.value)) return; + seen.add(entry.value); + options.push(new Option(entry.label, entry.value, false, entry.value= =3D=3D=3D state.objtype)); + }); + + select.replaceChildren(...options); + }; + + const parseState =3D () =3D> { + const params =3D new URLSearchParams(window.location.search); + const kinds =3D params.getAll("kind").filter((kind) =3D> RESULT_KIND_O= RDER.includes(kind)); + + return { + query: params.get("q") || "", + queryLower: (params.get("q") || "").toLowerCase().trim(), + exact: params.get("exact") =3D=3D=3D "1", + area: params.get("area") || "", + objtype: params.get("objtype") || "", + advanced: params.get("advanced") =3D=3D=3D "1", + kinds: kinds.length ? new Set(kinds) : new Set(RESULT_KIND_ORDER), + }; + }; + + const shouldOpenAdvanced =3D (state) =3D> + state.advanced + || state.exact + || state.area !=3D=3D "" + || state.objtype !=3D=3D "" + || RESULT_KIND_ORDER.some((kind) =3D> !state.kinds.has(kind)); + + const bindFormState =3D (state) =3D> { + document.getElementById("kernel-search-query").value =3D state.query; + document.getElementById("kernel-search-exact").checked =3D state.exact; + RESULT_KIND_ORDER.forEach((kind) =3D> { + const checkbox =3D document.getElementById(`kernel-search-kind-${kin= d}`); + if (checkbox) checkbox.checked =3D state.kinds.has(kind); + }); + + const advanced =3D document.getElementById("kernel-search-advanced"); + const advancedFlag =3D document.getElementById("kernel-search-advanced= -flag"); + const open =3D shouldOpenAdvanced(state); + advanced.open =3D open; + advancedFlag.disabled =3D !open; + advanced.addEventListener("toggle", () =3D> { + advancedFlag.disabled =3D !advanced.open; + }); + }; + + const runSearch =3D () =3D> { + const baseState =3D parseState(); + bindFormState(baseState); + populateAreaOptions(document.getElementById("kernel-search-area"), bas= eState); + populateObjectTypeOptions(document.getElementById("kernel-search-objty= pe"), baseState); + + const queryState =3D buildQueryState(baseState.query, baseState.exact); + const filters =3D buildFilters(baseState); + const resultsByKind =3D { + object: [], + title: [], + index: [], + text: [], + }; + + if (!baseState.queryLower) { + renderResults(baseState, resultsByKind); + return; + } + + if (baseState.kinds.has("object")) { + resultsByKind.object =3D collectObjectResults(window.Search._index, = queryState, filters); + } + if (baseState.kinds.has("title")) { + resultsByKind.title =3D collectSectionResults(window.Search._index, = queryState, filters); + } + if (baseState.kinds.has("index")) { + resultsByKind.index =3D collectIndexResults(window.Search._index, qu= eryState, filters); + } + if (baseState.kinds.has("text")) { + resultsByKind.text =3D collectTextResults(window.Search._index, quer= yState, filters); + } + + renderResults(baseState, resultsByKind); + }; + + document.addEventListener("DOMContentLoaded", () =3D> { + const container =3D document.getElementById("kernel-search-results"); + if (!container) return; + + const progress =3D document.getElementById("search-progress"); + if (progress) progress.textContent =3D "Preparing search..."; + + window.Search.whenReady(() =3D> { + if (progress) progress.textContent =3D ""; + runSearch(); + }); + }); +})(); diff --git a/Documentation/sphinx/templates/search.html b/Documentation/sph= inx/templates/search.html new file mode 100644 index 000000000..966740c12 --- /dev/null +++ b/Documentation/sphinx/templates/search.html @@ -0,0 +1,106 @@ +{# SPDX-License-Identifier: GPL-2.0 #} + +{# Enhanced search page for kernel documentation. #} +{%- extends "layout.html" %} +{% set title =3D _('Search') %} +{%- block scripts %} + {{ super() }} + + +{%- endblock %} +{% block extrahead %} +