diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92b841f4..706b710f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -236,6 +236,10 @@ jobs: run: | tar -C "spec${baseURL}" --strip-components=1 -xzf openapi.tar.gz + - name: "🔍 pagefind indexing" + run: | + npx -y pagefind --site "spec${baseURL}" + - name: "📦 Tarball creation" run: | cd spec diff --git a/assets/js/offline-search.js b/assets/js/offline-search.js new file mode 100644 index 00000000..9a1f6211 --- /dev/null +++ b/assets/js/offline-search.js @@ -0,0 +1,198 @@ +/* +Copyright 2026 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Adapted from [1] to combine Docsy"s built-in search UI with the Pagefind +search backend. + + [1]: https://github.com/matrix-org/docsy/blob/71d103ebb20ace3d528178c4b6d92b6cc4f7fd53/assets/js/offline-search.js +*/ + +(function ($) { + "use strict"; + + $(document).ready(async function () { + const pagefind = await import("/pagefind/pagefind.js"); + const $searchInput = $(".td-search input"); + + // + // Lazily initialise Pagefind only when the user is about to start a search. + // + + $searchInput.focus(() => { + pagefind.init(); + }); + + // + // Register handler + // + + $searchInput.on("change", (event) => { + render($(event.target)); + + // Hide keyboard on mobile browser + $searchInput.blur(); + }); + + // Prevent reloading page by enter key on sidebar search. + $searchInput.closest("form").on("submit", () => { + return false; + }); + + // + // Pagefind + // + + const render = async ($targetSearchInput) => { + // + // Dispose existing popover + // + + { + let popover = bootstrap.Popover.getInstance($targetSearchInput[0]); + if (popover !== null) { + popover.dispose(); + } + } + + // + // Search + // + + const searchQuery = $targetSearchInput.val(); + if (searchQuery === "") { + return; + } + + const search = await pagefind.debouncedSearch(searchQuery); + if (search === null) { + // A more recent search call has been made, nothing to do. + return; + } + const results = await Promise.all(search.results.slice(0, 20).map(r => r.data())); + + // + // Make result html + // + + const $html = $("
").text(`No results found for query "${searchQuery}"`)
+ );
+ } else {
+ results.forEach((r, index_r) => {
+ // Add the main result"s page title.
+ $searchResultBody.append(
+ $("")
+ .addClass("d-block")
+ .css({
+ fontSize: "1.2rem",
+ })
+ .attr("href", r.url)
+ .text(r.meta.title)
+ );
+
+ // Render the first 3 subresults per page and wrap the rest
+ // in a collapsed container.
+ const LIMIT = 3;
+ let $wrapper = null;
+
+ r.sub_results.forEach((s, index_s) => {
+
+
+ if (index_s === LIMIT) {
+ const wrapper_id = `collapssible-subresults-${index_r}`;
+ const $expander = $("")
+ .attr("data-bs-toggle", "collapse")
+ .attr("data-bs-target", `#${wrapper_id}`)
+ .attr("href", "#")
+ .attr("role", "button")
+ .attr("aria-expanded", "false")
+ .attr("aria-controls", wrapper_id)
+ .css("margin-left", "0.5rem")
+ .text(`${r.sub_results.length - index_s} more result(s) from ${r.meta.title}`);
+
+ $searchResultBody.append($(" ").append($expander));
+ $wrapper = $("