Files
OrbbecSDK_ROS2/docs/_ext/static/section_search.js
T
2026-03-26 16:08:29 +08:00

612 lines
18 KiB
JavaScript

"use strict";
(function () {
const INDEX_PATH = "_static/section-search-index.json";
const INDEX_SCRIPT_PATH = "_static/section-search-index.js";
const SEARCH_APP_ID = "section-search-app";
const SEARCH_RESULTS_ID = "section-search-results";
const SEARCH_STATUS_ID = "section-search-status";
const SEARCH_HISTORY_ID = "section-search-history";
const HIT_CLASS = "section-search-hit";
const HISTORY_LIMIT = 8;
let searchIndexPromise = null;
document.addEventListener("DOMContentLoaded", () => {
initSearchPage();
initContentHighlight();
});
function initSearchPage() {
const app = document.getElementById(SEARCH_APP_ID);
if (!app) {
return;
}
const query = getQueryParam("q").trim();
const form = app.querySelector("form");
const input = form ? form.querySelector('input[name="q"]') : null;
const resultsRoot = document.getElementById(SEARCH_RESULTS_ID);
const statusRoot = document.getElementById(SEARCH_STATUS_ID);
const historyRoot = document.getElementById(SEARCH_HISTORY_ID);
if (input) {
input.value = query;
}
initSearchHistory(app, form, input, historyRoot, query);
if (!query) {
if (statusRoot) {
statusRoot.textContent = app.dataset.emptyQuery || "";
}
return;
}
if (statusRoot) {
statusRoot.textContent = app.dataset.loading || "";
}
loadSearchIndex()
.then((payload) => {
const results = searchEntries(payload.entries || [], query);
saveSearchHistory(query);
renderSearchHistory(app, historyRoot, query, input ? input.value.trim() : "");
renderResults(resultsRoot, statusRoot, app, query, results);
})
.catch((error) => {
console.error("Failed to load section search index.", error);
if (statusRoot) {
statusRoot.textContent = app.dataset.loadError || "Search index failed to load.";
}
});
}
function initContentHighlight() {
const app = document.getElementById(SEARCH_APP_ID);
if (app) {
return;
}
const query = getQueryParam("highlight").trim();
if (!query) {
return;
}
const container = document.querySelector('[itemprop="articleBody"]');
if (!container) {
return;
}
const terms = buildHighlightTerms(query);
if (!terms.length) {
return;
}
highlightMatches(container, terms);
scrollToClosestHit();
}
function loadSearchIndex() {
if (window.SECTION_SEARCH_INDEX) {
return Promise.resolve(window.SECTION_SEARCH_INDEX);
}
if (!searchIndexPromise) {
searchIndexPromise = loadSearchIndexScript().catch(() => loadSearchIndexJson());
}
return searchIndexPromise;
}
function loadSearchIndexScript() {
return new Promise((resolve, reject) => {
if (window.SECTION_SEARCH_INDEX) {
resolve(window.SECTION_SEARCH_INDEX);
return;
}
const existing = document.querySelector('script[data-section-search-index="true"]');
if (existing) {
existing.addEventListener("load", () => resolve(window.SECTION_SEARCH_INDEX));
existing.addEventListener("error", reject);
return;
}
const script = document.createElement("script");
script.src = getUrlRoot() + INDEX_SCRIPT_PATH;
script.async = true;
script.dataset.sectionSearchIndex = "true";
script.onload = () => {
if (window.SECTION_SEARCH_INDEX) {
resolve(window.SECTION_SEARCH_INDEX);
} else {
reject(new Error("Section search index script loaded without payload."));
}
};
script.onerror = () => reject(new Error("Failed to load section search index script."));
document.head.appendChild(script);
});
}
function loadSearchIndexJson() {
return fetch(getUrlRoot() + INDEX_PATH).then((response) => {
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
return response.json();
});
}
function searchEntries(entries, rawQuery) {
const query = normalizeText(rawQuery);
const tokens = buildSearchTokens(rawQuery);
return entries
.map((entry) => scoreEntry(entry, query, tokens))
.filter(Boolean)
.sort(compareResults)
.slice(0, 80);
}
function scoreEntry(entry, query, tokens) {
const title = normalizeText((entry.title || "") + " " + (entry.page_title || ""));
const text = normalizeText(entry.text || "");
const combined = (title + "\n" + text).trim();
if (!combined) {
return null;
}
const phraseMatch = query && combined.includes(query);
const matchedTokens = tokens.filter((token) => title.includes(token) || text.includes(token));
if (!phraseMatch && tokens.length && matchedTokens.length < tokens.length) {
return null;
}
if (!phraseMatch && !matchedTokens.length) {
return null;
}
let score = entry.kind === "section" ? 20 : 5;
if (query && title.includes(query)) {
score += 160;
}
if (query && text.includes(query)) {
score += 90;
}
matchedTokens.forEach((token) => {
if (title.includes(token)) {
score += 45;
} else {
score += 18;
}
});
if (entry.page_title && entry.title && entry.page_title !== entry.title) {
score += 10;
}
return {
entry,
score,
snippet: makeSnippet(entry.text || entry.page_title || entry.title || "", query, tokens),
};
}
function compareResults(left, right) {
if (right.score !== left.score) {
return right.score - left.score;
}
const leftTitle = ((left.entry.title || "") + " " + (left.entry.page_title || "")).toLowerCase();
const rightTitle = ((right.entry.title || "") + " " + (right.entry.page_title || "")).toLowerCase();
return leftTitle.localeCompare(rightTitle);
}
function renderResults(root, statusRoot, app, query, results) {
if (!root) {
return;
}
const highlightTerms = buildHighlightTerms(query);
root.replaceChildren();
if (!results.length) {
if (statusRoot) {
statusRoot.textContent = app.dataset.noResults || "";
}
return;
}
if (statusRoot) {
statusRoot.textContent = formatCountMessage(app, results.length);
}
const list = document.createElement("ul");
list.className = "search";
list.setAttribute("role", "list");
results.forEach((result) => {
list.appendChild(renderResultItem(result, query, highlightTerms));
});
root.appendChild(list);
}
function initSearchHistory(app, form, input, historyRoot, query) {
if (!form || !input || !historyRoot) {
return;
}
renderSearchHistory(app, historyRoot, query, input.value.trim());
form.addEventListener("submit", () => {
saveSearchHistory(input.value);
});
input.addEventListener("focus", () => {
renderSearchHistory(app, historyRoot, query, input.value.trim());
toggleHistoryVisibility(historyRoot, true);
});
input.addEventListener("input", () => {
renderSearchHistory(app, historyRoot, query, input.value.trim());
toggleHistoryVisibility(historyRoot, true);
});
document.addEventListener("click", (event) => {
if (!historyRoot.contains(event.target) && !form.contains(event.target)) {
toggleHistoryVisibility(historyRoot, false);
}
});
}
function renderSearchHistory(app, historyRoot, currentQuery, draftQuery) {
if (!historyRoot) {
return;
}
const history = getSearchHistory();
const keyword = normalizeText(draftQuery || "");
const filtered = keyword
? history.filter((item) => normalizeText(item).includes(keyword))
: history.slice();
historyRoot.replaceChildren();
const header = document.createElement("div");
header.className = "section-search-history__header";
const title = document.createElement("div");
title.className = "section-search-history__title";
title.textContent = app.dataset.historyTitle || "Recent searches";
header.appendChild(title);
if (history.length) {
const clearButton = document.createElement("button");
clearButton.type = "button";
clearButton.className = "section-search-history__clear";
clearButton.textContent = app.dataset.clearHistory || "Clear history";
clearButton.addEventListener("click", () => {
clearSearchHistory();
renderSearchHistory(app, historyRoot, currentQuery, draftQuery);
toggleHistoryVisibility(historyRoot, true);
});
header.appendChild(clearButton);
}
historyRoot.appendChild(header);
const list = document.createElement("div");
list.className = "section-search-history__list";
if (!filtered.length) {
const empty = document.createElement("div");
empty.className = "section-search-history__empty";
empty.textContent = keyword ? app.dataset.noResults || "" : app.dataset.historyEmpty || "";
list.appendChild(empty);
} else {
filtered.forEach((item) => {
const link = document.createElement("a");
link.className = "section-search-history__item";
link.href = "?q=" + encodeURIComponent(item);
link.textContent = item;
if (normalizeText(item) === normalizeText(currentQuery)) {
link.setAttribute("aria-current", "true");
}
list.appendChild(link);
});
}
historyRoot.appendChild(list);
toggleHistoryVisibility(historyRoot, Boolean(history.length || keyword));
}
function renderResultItem(result, query, highlightTerms) {
const item = document.createElement("li");
item.className = "kind-text section-search-result";
const header = document.createElement("div");
header.className = "section-search-result__header";
const link = document.createElement("a");
link.className = "section-search-result__title";
link.href = buildResultUrl(result.entry.url, query);
appendHighlightedText(link, result.entry.title || result.entry.page_title || "", highlightTerms);
header.appendChild(link);
item.appendChild(header);
if (result.entry.page_title && result.entry.page_title !== result.entry.title) {
const context = document.createElement("div");
context.className = "section-search-result__meta";
context.appendChild(createHighlightedFragment(result.entry.page_title, highlightTerms));
item.appendChild(context);
}
if (result.snippet) {
const summary = document.createElement("p");
summary.className = "search-snippet section-search-result__snippet";
summary.appendChild(createHighlightedFragment(result.snippet, highlightTerms));
item.appendChild(summary);
}
return item;
}
function buildResultUrl(url, query) {
const hashIndex = url.indexOf("#");
const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
const separator = base.includes("?") ? "&" : "?";
return base + separator + "highlight=" + encodeURIComponent(query) + hash;
}
function makeSnippet(text, query, tokens) {
const source = collapseWhitespace(text);
if (!source) {
return "";
}
const candidates = [query].concat(tokens).filter(Boolean);
let index = -1;
for (const term of candidates) {
index = source.toLowerCase().indexOf(term.toLowerCase());
if (index >= 0) {
break;
}
}
if (index < 0) {
return source.slice(0, 180) + (source.length > 180 ? "..." : "");
}
const start = Math.max(index - 70, 0);
const end = Math.min(index + 110, source.length);
const prefix = start > 0 ? "..." : "";
const suffix = end < source.length ? "..." : "";
return prefix + source.slice(start, end) + suffix;
}
function buildSearchTokens(query) {
const normalized = normalizeText(query);
const latinTokens = normalized
.split(/[^\p{Letter}\p{Number}_]+/u)
.map((token) => token.trim())
.filter(Boolean);
const cjkTokens = normalized.match(/[\u3400-\u9fff]+/gu) || [];
return Array.from(new Set(latinTokens.concat(cjkTokens)));
}
function buildHighlightTerms(query) {
const normalized = normalizeText(query);
const tokens = buildSearchTokens(query);
const ordered = [normalized]
.concat(tokens)
.filter(Boolean)
.sort((left, right) => right.length - left.length);
return Array.from(new Set(ordered));
}
function highlightMatches(container, terms) {
const matcher = new RegExp("(" + terms.map(escapeRegExp).join("|") + ")", "giu");
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const parent = node.parentElement;
if (!parent || !node.nodeValue || !node.nodeValue.trim()) {
return NodeFilter.FILTER_REJECT;
}
if (parent.closest("script, style, noscript, mark, .headerlink")) {
return NodeFilter.FILTER_REJECT;
}
return containsAny(node.nodeValue, terms) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
},
});
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
textNodes.forEach((node) => {
const value = node.nodeValue;
if (!value) {
return;
}
matcher.lastIndex = 0;
let lastIndex = 0;
let match = null;
const fragment = document.createDocumentFragment();
while ((match = matcher.exec(value)) !== null) {
if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
}
const mark = document.createElement("mark");
mark.className = HIT_CLASS;
mark.textContent = match[0];
fragment.appendChild(mark);
lastIndex = match.index + match[0].length;
}
if (lastIndex === 0) {
return;
}
if (lastIndex < value.length) {
fragment.appendChild(document.createTextNode(value.slice(lastIndex)));
}
node.parentNode.replaceChild(fragment, node);
});
}
function scrollToClosestHit() {
const hash = decodeURIComponent(window.location.hash || "").replace(/^#/, "");
let target = null;
if (hash) {
const anchor = document.getElementById(hash);
if (anchor) {
const scope = anchor.closest("section") || anchor;
target = scope.querySelector("." + HIT_CLASS);
}
}
if (!target) {
target = document.querySelector("." + HIT_CLASS);
}
if (target) {
window.requestAnimationFrame(() => {
target.scrollIntoView({ block: "center" });
});
}
}
function formatCountMessage(app, count) {
const template = count === 1 ? app.dataset.resultOne : app.dataset.resultMany;
return (template || "").replace("{count}", String(count));
}
function getQueryParam(name) {
return new URLSearchParams(window.location.search).get(name) || "";
}
function getSearchHistory() {
try {
const raw = window.localStorage.getItem(getHistoryStorageKey());
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed)
? parsed.map((item) => collapseWhitespace(String(item))).filter(Boolean).slice(0, HISTORY_LIMIT)
: [];
} catch (error) {
return [];
}
}
function saveSearchHistory(query) {
const normalized = collapseWhitespace(query);
if (!normalized) {
return;
}
const history = getSearchHistory().filter((item) => item !== normalized);
history.unshift(normalized);
try {
window.localStorage.setItem(getHistoryStorageKey(), JSON.stringify(history.slice(0, HISTORY_LIMIT)));
} catch (error) {
console.warn("Failed to save search history.", error);
}
}
function clearSearchHistory() {
try {
window.localStorage.removeItem(getHistoryStorageKey());
} catch (error) {
console.warn("Failed to clear search history.", error);
}
}
function getHistoryStorageKey() {
const scope =
window.location.pathname.replace(/search\.html$/i, "").replace(/index\.html$/i, "") ||
getUrlRoot() ||
"default";
return "section_search_recent_queries::" + scope;
}
function getUrlRoot() {
return (window.DOCUMENTATION_OPTIONS && window.DOCUMENTATION_OPTIONS.URL_ROOT) || "";
}
function toggleHistoryVisibility(historyRoot, visible) {
if (!historyRoot) {
return;
}
historyRoot.hidden = !visible;
}
function containsAny(text, terms) {
const normalized = text.toLowerCase();
return terms.some((term) => normalized.includes(term.toLowerCase()));
}
function appendHighlightedText(target, text, terms) {
target.appendChild(createHighlightedFragment(text, terms));
}
function createHighlightedFragment(text, terms) {
const fragment = document.createDocumentFragment();
const source = text || "";
const filteredTerms = (terms || []).filter(Boolean);
if (!source || !filteredTerms.length) {
fragment.appendChild(document.createTextNode(source));
return fragment;
}
const matcher = new RegExp("(" + filteredTerms.map(escapeRegExp).join("|") + ")", "giu");
let lastIndex = 0;
let match = null;
while ((match = matcher.exec(source)) !== null) {
if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(source.slice(lastIndex, match.index)));
}
const mark = document.createElement("mark");
mark.className = "section-search-match";
mark.textContent = match[0];
fragment.appendChild(mark);
lastIndex = match.index + match[0].length;
}
if (lastIndex < source.length) {
fragment.appendChild(document.createTextNode(source.slice(lastIndex)));
}
return fragment;
}
function normalizeText(text) {
return collapseWhitespace(text).toLowerCase();
}
function collapseWhitespace(text) {
return (text || "").replace(/\s+/g, " ").trim();
}
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
})();