fuc-new/doc/node_modules/@vuepress-plume/plugin-search/lib/node/index.js
hellcat 6adbc04d4b 1
2025-10-03 17:17:08 +08:00

362 lines
11 KiB
JavaScript

import MiniSearch from "minisearch";
import pMap from "p-map";
import { colors, getDirname, logger, path } from "vuepress/utils";
import { addViteOptimizeDepsInclude, getFullLocaleConfig } from "@vuepress/helper";
import chokidar from "chokidar";
export * from "../shared/index.js"
//#region src/node/prepareSearchIndex.ts
const SEARCH_INDEX_DIR = "internal/minisearchIndex/";
const indexByLocales = /* @__PURE__ */ new Map();
const indexCache = /* @__PURE__ */ new Map();
function getIndexByLocale(locale, options) {
let index = indexByLocales.get(locale);
if (!index) {
index = new MiniSearch({
fields: [
"title",
"titles",
"text"
],
storeFields: ["title", "titles"],
...options.miniSearch?.options
});
indexByLocales.set(locale, index);
}
return index;
}
function getIndexCache(filepath) {
let index = indexCache.get(filepath);
if (!index) {
index = [];
indexCache.set(filepath, index);
}
return index;
}
async function prepareSearchIndex({ app, isSearchable, searchOptions }) {
const start = performance.now();
indexByLocales.clear();
indexCache.clear();
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
await pMap(pages, (p) => indexFile(p, searchOptions), { concurrency: 64 });
await writeTemp(app);
if (app.env.isDebug) logger.info(`\n[${colors.green("@vuepress-plume/plugin-search")}] prepare search time spent: ${(performance.now() - start).toFixed(2)}ms`);
}
async function onSearchIndexUpdated(filepath, { app, isSearchable, searchOptions }) {
if ((isSearchable ? app.pages.filter(isSearchable) : app.pages).some((p) => p.filePathRelative?.endsWith(filepath))) {
await indexFile(app.pages.find((p) => p.filePathRelative?.endsWith(filepath)), searchOptions);
await writeTemp(app);
}
}
async function onSearchIndexRemoved(filepath, { app, isSearchable, searchOptions }) {
if ((isSearchable ? app.pages.filter(isSearchable) : app.pages).some((p) => p.filePathRelative?.endsWith(filepath))) {
const page = app.pages.find((p) => p.filePathRelative?.endsWith(filepath));
const fileId = page.path;
const locale = page.pathLocale;
const index = getIndexByLocale(locale, searchOptions);
const cache = getIndexCache(fileId);
if (cache && cache.length) index.removeAll(cache);
await writeTemp(app);
}
}
async function writeTemp(app) {
const records = [];
for (const [locale] of indexByLocales) {
const index = indexByLocales.get(locale);
const filename = `searchBox-${locale.replace(/^\/|\/$/g, "").replace(/\//g, "_") || "default"}.js`;
records.push(`${JSON.stringify(locale)}: () => import('@${SEARCH_INDEX_DIR}${filename}')`);
await app.writeTemp(`${SEARCH_INDEX_DIR}${filename}`, `export default ${JSON.stringify(JSON.stringify(index) ?? {})}`);
}
await app.writeTemp(`${SEARCH_INDEX_DIR}index.js`, `export const searchIndex = {${records.join(",")}}${app.env.isDev ? `\n${genHmrCode("searchIndex")}` : ""}`);
}
async function indexFile(page, options) {
if (!page.filePath) return;
if (page.frontmatter?.search === false) return;
const fileId = page.path;
const locale = page.pathLocale;
const index = getIndexByLocale(locale, options);
const cache = getIndexCache(fileId);
const html = `<h1><a href="#"><span>${page.frontmatter.title || page.title}</span></a></h1>
${page.contentRendered}`;
const sections = splitPageIntoSections(html);
if (cache && cache.length) index.removeAll(cache);
for await (const section of sections) {
if (!section || !(section.text || section.titles)) break;
const { anchor, text, titles } = section;
const id = anchor ? [fileId, anchor].join("#") : fileId;
if (index.has(id)) if (anchor) logger.error(`${colors.green("[@vuepress-plume/plugin-search]")} duplicate heading anchor : ${colors.cyan(titles.join(" >> "))} \n at ${colors.cyan(fileId)}`);
else logger.error(`${colors.green("[@vuepress-plume/plugin-search]")} duplicate page permalink : ${colors.cyan(fileId)}`);
else {
const item = {
id,
text,
title: titles.at(-1),
titles: titles.slice(0, -1)
};
index.add(item);
cache.push(item);
}
}
}
const headingRegex = /<h(\d*).*?>(<a.*? href="#.*?".*?>[\s\S]*?<\/a>)<\/h\1>/gi;
const headingContentRegex = /<a.*? href="#(.*?)".*?><span>([\s\S]*?)<\/span><\/a>/i;
const ignoreHeadingRegex = /<template[^>]*>[\s\S]*<\/template>/gi;
/**
* Splits HTML into sections based on headings
*/
function* splitPageIntoSections(html) {
const result = html.split(headingRegex);
result.shift();
let parentTitles = [];
for (let i = 0; i < result.length; i += 3) {
const level = Number.parseInt(result[i]) - 1;
const heading = result[i + 1];
const headingResult = headingContentRegex.exec(heading);
const title = clearHtmlTags(headingResult?.[2] ?? "").trim();
const anchor = headingResult?.[1] ?? "";
const content = result[i + 2];
if (!title || !content) continue;
if (level === 0) parentTitles = [title];
else parentTitles[level] = title;
let titles = parentTitles.slice(0, level);
titles[level] = title;
titles = titles.filter(Boolean);
yield {
anchor,
titles,
text: getSearchableText(content)
};
}
}
function getSearchableText(content) {
content = clearHtmlTags(content);
return content;
}
function clearHtmlTags(str) {
str = str.replace(ignoreHeadingRegex, "");
return str.replace(/<[^>]*>/g, "");
}
function genHmrCode(m) {
const func = `update${m[0].toUpperCase()}${m.slice(1)}`;
return `
if (import.meta.webpackHot) {
import.meta.webpackHot.accept()
if (__VUE_HMR_RUNTIME__.${m}) {
__VUE_HMR_RUNTIME__.${func}(${m})
}
}
if (import.meta.hot) {
import.meta.hot.accept(({ ${m} }) => {
__VUE_HMR_RUNTIME__.${func}(${m})
})
}
`;
}
//#endregion
//#region src/node/locales/de.ts
const deSearchLocale = {
placeholder: "Dokumente durchsuchen",
resetButtonTitle: "Suche zurücksetzen",
backButtonTitle: "Schließen",
noResultsText: "Keine Suchergebnisse:",
footer: {
selectText: "Auswählen",
selectKeyAriaLabel: "Eingabe",
navigateText: "Wechseln",
navigateUpKeyAriaLabel: "Nach oben",
navigateDownKeyAriaLabel: "Nach unten",
closeText: "Schließen",
closeKeyAriaLabel: "Beenden"
}
};
//#endregion
//#region src/node/locales/en.ts
const enSearchLocale = {
placeholder: "Search",
resetButtonTitle: "Reset search",
backButtonTitle: "Close search",
noResultsText: "No results for",
footer: {
selectText: "to select",
selectKeyAriaLabel: "enter",
navigateText: "to navigate",
navigateUpKeyAriaLabel: "up arrow",
navigateDownKeyAriaLabel: "down arrow",
closeText: "to close",
closeKeyAriaLabel: "escape"
}
};
//#endregion
//#region src/node/locales/fr.ts
const frSearchLocale = {
placeholder: "Rechercher dans la documentation",
resetButtonTitle: "Réinitialiser la recherche",
backButtonTitle: "Fermer",
noResultsText: "Aucun résultat trouvé :",
footer: {
selectText: "sélectionner",
selectKeyAriaLabel: "Entrée",
navigateText: "naviguer",
navigateUpKeyAriaLabel: "haut",
navigateDownKeyAriaLabel: "bas",
closeText: "fermer",
closeKeyAriaLabel: "sortie"
}
};
//#endregion
//#region src/node/locales/ja.ts
const jaSearchLocale = {
placeholder: "ドキュメントを検索",
resetButtonTitle: "検索をリセット",
backButtonTitle: "閉じる",
noResultsText: "検索結果がありません:",
footer: {
selectText: "選択",
selectKeyAriaLabel: "入力",
navigateText: "切り替え",
navigateUpKeyAriaLabel: "上へ",
navigateDownKeyAriaLabel: "下へ",
closeText: "閉じる",
closeKeyAriaLabel: "終了"
}
};
//#endregion
//#region src/node/locales/ru.ts
const ruSearchLocale = {
placeholder: "Поиск по документации",
resetButtonTitle: "Сбросить поиск",
backButtonTitle: "Закрыть",
noResultsText: "Нет результатов поиска:",
footer: {
selectText: "Выбрать",
selectKeyAriaLabel: "Ввод",
navigateText: "Переключить",
navigateUpKeyAriaLabel: "Вверх",
navigateDownKeyAriaLabel: "Вниз",
closeText: "Закрыть",
closeKeyAriaLabel: "Выход"
}
};
//#endregion
//#region src/node/locales/zh-tw.ts
const zhTwSearchLocale = {
placeholder: "搜索文檔",
resetButtonTitle: "重置搜索",
backButtonTitle: "關閉",
noResultsText: "無搜索結果:",
footer: {
selectText: "選擇",
selectKeyAriaLabel: "輸入",
navigateText: "切換",
navigateUpKeyAriaLabel: "向上",
navigateDownKeyAriaLabel: "向下",
closeText: "關閉",
closeKeyAriaLabel: "退出"
}
};
//#endregion
//#region src/node/locales/zh.ts
const zhSearchLocale = {
placeholder: "搜索文档",
resetButtonTitle: "重置搜索",
backButtonTitle: "关闭",
noResultsText: "无搜索结果:",
footer: {
selectText: "选择",
selectKeyAriaLabel: "输入",
navigateText: "切换",
navigateUpKeyAriaLabel: "向上",
navigateDownKeyAriaLabel: "向下",
closeText: "关闭",
closeKeyAriaLabel: "退出"
}
};
//#endregion
//#region src/node/locales/index.ts
const SEARCH_LOCALES = [
[["en", "en-US"], enSearchLocale],
[[
"zh",
"zh-CN",
"zh-Hans",
"zh-Hant"
], zhSearchLocale],
[["zh-TW"], zhTwSearchLocale],
[["de", "de-DE"], deSearchLocale],
[["fr", "fr-FR"], frSearchLocale],
[["ru", "ru-RU"], ruSearchLocale],
[["ja", "ja-JP"], jaSearchLocale]
];
//#endregion
//#region src/node/searchPlugin.ts
const __dirname = getDirname(import.meta.url);
function searchPlugin({ locales = {}, isSearchable,...searchOptions } = {}) {
return (app) => ({
name: "@vuepress-plume/plugin-search",
clientConfigFile: path.resolve(__dirname, "../client/config.js"),
define: {
__SEARCH_LOCALES__: getFullLocaleConfig({
app,
name: "@vuepress-plume/plugin-search",
default: SEARCH_LOCALES,
config: locales
}),
__SEARCH_OPTIONS__: searchOptions
},
extendsBundlerOptions(bundlerOptions) {
addViteOptimizeDepsInclude(bundlerOptions, app, [
"mark.js/src/vanilla.js",
"@vueuse/integrations/useFocusTrap",
"minisearch"
]);
},
onPrepared: (app$1) => prepareSearchIndex({
app: app$1,
isSearchable,
searchOptions
}),
onWatched: (app$1, watchers) => {
const searchIndexWatcher = chokidar.watch("pages", {
cwd: app$1.dir.temp(),
ignoreInitial: true,
ignored: (filepath, stats) => Boolean(stats?.isFile()) && !filepath.endsWith(".js")
});
searchIndexWatcher.on("add", (filepath) => {
onSearchIndexUpdated(filepath, {
app: app$1,
isSearchable,
searchOptions
});
});
searchIndexWatcher.on("change", (filepath) => {
onSearchIndexUpdated(filepath, {
app: app$1,
isSearchable,
searchOptions
});
});
searchIndexWatcher.on("unlink", (filepath) => {
onSearchIndexRemoved(filepath, {
app: app$1,
isSearchable,
searchOptions
});
});
watchers.push(searchIndexWatcher);
}
});
}
//#endregion
export { prepareSearchIndex, searchPlugin };