@vuepress-plume/plugin-search 1.0.0-rc.80 → 1.0.0-rc.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client/components/SearchBox.vue +2 -3
- package/lib/client/components/SearchButton.vue +1 -1
- package/lib/client/composables/index.d.ts +12 -1
- package/lib/client/composables/index.js +46 -1
- package/lib/client/config.d.ts +4 -2
- package/lib/client/config.js +17 -13
- package/lib/client/index.d.ts +2 -3
- package/lib/client/index.js +7 -3
- package/lib/client/utils/{lru.d.ts → index.d.ts} +3 -1
- package/lib/client/utils/index.js +33 -0
- package/lib/node/index.d.ts +13 -3
- package/lib/node/index.js +213 -4
- package/lib/shared/index.d.ts +11 -8
- package/lib/shared/index.js +0 -1
- package/package.json +5 -5
- package/lib/client/composables/locale.d.ts +0 -3
- package/lib/client/composables/locale.js +0 -25
- package/lib/client/composables/searchIndex.d.ts +0 -3
- package/lib/client/composables/searchIndex.js +0 -14
- package/lib/client/utils/lru.js +0 -33
- package/lib/node/prepareSearchIndex.d.ts +0 -10
- package/lib/node/prepareSearchIndex.js +0 -145
- package/lib/node/searchPlugin.d.ts +0 -3
- package/lib/node/searchPlugin.js +0 -35
|
@@ -23,10 +23,9 @@ import {
|
|
|
23
23
|
import Mark from 'mark.js/src/vanilla.js'
|
|
24
24
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
|
25
25
|
import MiniSearch, { type SearchResult } from 'minisearch'
|
|
26
|
-
import { useSearchIndex } from '../composables/index.js'
|
|
26
|
+
import { useLocale, useSearchIndex } from '../composables/index.js'
|
|
27
27
|
import type { SearchBoxLocales, SearchOptions } from '../../shared/index.js'
|
|
28
|
-
import { LRUCache } from '../utils/
|
|
29
|
-
import { useLocale } from '../composables/locale.js'
|
|
28
|
+
import { LRUCache } from '../utils/index.js'
|
|
30
29
|
import SearchIcon from './icons/SearchIcon.vue'
|
|
31
30
|
import ClearIcon from './icons/ClearIcon.vue'
|
|
32
31
|
import BackIcon from './icons/BackIcon.vue'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { toRef } from 'vue'
|
|
3
3
|
import type { SearchBoxLocales } from '../../shared/index.js'
|
|
4
|
-
import { useLocale } from '../composables/
|
|
4
|
+
import { useLocale } from '../composables/index.js'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
7
|
locales: SearchBoxLocales
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
import { MaybeRef } from 'vue';
|
|
3
|
+
import * as ______shared_index_js from '../../shared/index.js';
|
|
4
|
+
import { SearchBoxLocales } from '../../shared/index.js';
|
|
5
|
+
|
|
6
|
+
declare function useSearchIndex(): vue.ShallowRef<Record<string, () => Promise<{
|
|
7
|
+
default: string;
|
|
8
|
+
}>>>;
|
|
9
|
+
|
|
10
|
+
declare function useLocale(locales: MaybeRef<SearchBoxLocales>): vue.ComputedRef<Partial<______shared_index_js.SearchLocaleOptions>>;
|
|
11
|
+
|
|
12
|
+
export { useLocale, useSearchIndex };
|
|
@@ -1 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
// src/client/composables/searchIndex.ts
|
|
2
|
+
import { searchIndex } from "@internal/minisearchIndex";
|
|
3
|
+
import { shallowRef } from "vue";
|
|
4
|
+
var searchIndexData = shallowRef(searchIndex);
|
|
5
|
+
function useSearchIndex() {
|
|
6
|
+
return searchIndexData;
|
|
7
|
+
}
|
|
8
|
+
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
|
|
9
|
+
__VUE_HMR_RUNTIME__.updateSearchIndex = (data) => {
|
|
10
|
+
searchIndexData.value = data;
|
|
11
|
+
};
|
|
12
|
+
__VUE_HMR_RUNTIME__.updateSearchIndex = (data) => {
|
|
13
|
+
searchIndexData.value = data;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/client/composables/locale.ts
|
|
18
|
+
import { useRouteLocale } from "vuepress/client";
|
|
19
|
+
import { computed, toRef } from "vue";
|
|
20
|
+
var defaultLocales = {
|
|
21
|
+
"/": {
|
|
22
|
+
placeholder: "Search",
|
|
23
|
+
resetButtonTitle: "Reset search",
|
|
24
|
+
backButtonTitle: "Close search",
|
|
25
|
+
noResultsText: "No results for",
|
|
26
|
+
footer: {
|
|
27
|
+
selectText: "to select",
|
|
28
|
+
selectKeyAriaLabel: "enter",
|
|
29
|
+
navigateText: "to navigate",
|
|
30
|
+
navigateUpKeyAriaLabel: "up arrow",
|
|
31
|
+
navigateDownKeyAriaLabel: "down arrow",
|
|
32
|
+
closeText: "to close",
|
|
33
|
+
closeKeyAriaLabel: "escape"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function useLocale(locales) {
|
|
38
|
+
const localesRef = toRef(locales);
|
|
39
|
+
const routeLocale = useRouteLocale();
|
|
40
|
+
const locale = computed(() => localesRef.value[routeLocale.value] ?? defaultLocales[routeLocale.value] ?? defaultLocales["/"]);
|
|
41
|
+
return locale;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
useLocale,
|
|
45
|
+
useSearchIndex
|
|
46
|
+
};
|
package/lib/client/config.d.ts
CHANGED
package/lib/client/config.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
1
|
+
// src/client/config.ts
|
|
2
|
+
import { defineClientConfig } from "vuepress/client";
|
|
3
|
+
import { h } from "vue";
|
|
4
|
+
import Search from "./components/Search.vue";
|
|
5
|
+
var locales = __SEARCH_LOCALES__;
|
|
6
|
+
var searchOptions = __SEARCH_OPTIONS__;
|
|
7
|
+
var config_default = defineClientConfig({
|
|
8
|
+
enhance({ app }) {
|
|
9
|
+
app.component("SearchBox", (props) => h(Search, {
|
|
10
|
+
locales,
|
|
11
|
+
options: searchOptions,
|
|
12
|
+
...props
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
14
15
|
});
|
|
16
|
+
export {
|
|
17
|
+
config_default as default
|
|
18
|
+
};
|
package/lib/client/index.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export { SearchBox, useSearchIndex, };
|
|
1
|
+
export { default as SearchBox } from './components/Search.vue';
|
|
2
|
+
export { useSearchIndex } from './composables/index.js';
|
package/lib/client/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
// src/client/index.ts
|
|
2
|
+
import SearchBox from "./components/Search.vue";
|
|
3
|
+
import { useSearchIndex } from "./composables/index.js";
|
|
4
|
+
export {
|
|
5
|
+
SearchBox,
|
|
6
|
+
useSearchIndex
|
|
7
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/client/utils/lru.ts
|
|
2
|
+
var LRUCache = class {
|
|
3
|
+
max;
|
|
4
|
+
cache;
|
|
5
|
+
constructor(max = 10) {
|
|
6
|
+
this.max = max;
|
|
7
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
get(key) {
|
|
10
|
+
const item = this.cache.get(key);
|
|
11
|
+
if (item !== void 0) {
|
|
12
|
+
this.cache.delete(key);
|
|
13
|
+
this.cache.set(key, item);
|
|
14
|
+
}
|
|
15
|
+
return item;
|
|
16
|
+
}
|
|
17
|
+
set(key, val) {
|
|
18
|
+
if (this.cache.has(key))
|
|
19
|
+
this.cache.delete(key);
|
|
20
|
+
else if (this.cache.size === this.max)
|
|
21
|
+
this.cache.delete(this.first());
|
|
22
|
+
this.cache.set(key, val);
|
|
23
|
+
}
|
|
24
|
+
first() {
|
|
25
|
+
return this.cache.keys().next().value;
|
|
26
|
+
}
|
|
27
|
+
clear() {
|
|
28
|
+
this.cache.clear();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export {
|
|
32
|
+
LRUCache
|
|
33
|
+
};
|
package/lib/node/index.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { Plugin, App } from 'vuepress/core';
|
|
2
|
+
import { SearchPluginOptions, SearchOptions } from '../shared/index.js';
|
|
3
3
|
export * from '../shared/index.js';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
declare function searchPlugin({ locales, isSearchable, ...searchOptions }?: SearchPluginOptions): Plugin;
|
|
6
|
+
|
|
7
|
+
interface SearchIndexOptions {
|
|
8
|
+
app: App;
|
|
9
|
+
searchOptions: SearchOptions;
|
|
10
|
+
isSearchable: SearchPluginOptions['isSearchable'];
|
|
11
|
+
}
|
|
12
|
+
declare function prepareSearchIndex({ app, isSearchable, searchOptions, }: SearchIndexOptions): Promise<void>;
|
|
13
|
+
|
|
14
|
+
export { prepareSearchIndex, searchPlugin };
|
package/lib/node/index.js
CHANGED
|
@@ -1,4 +1,213 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/node/searchPlugin.ts
|
|
2
|
+
import chokidar from "chokidar";
|
|
3
|
+
import { getDirname, path } from "vuepress/utils";
|
|
4
|
+
import { addViteOptimizeDepsInclude } from "@vuepress/helper";
|
|
5
|
+
|
|
6
|
+
// src/node/prepareSearchIndex.ts
|
|
7
|
+
import MiniSearch from "minisearch";
|
|
8
|
+
import pMap from "p-map";
|
|
9
|
+
import { colors, logger } from "vuepress/utils";
|
|
10
|
+
var SEARCH_INDEX_DIR = "internal/minisearchIndex/";
|
|
11
|
+
var indexByLocales = /* @__PURE__ */ new Map();
|
|
12
|
+
var indexCache = /* @__PURE__ */ new Map();
|
|
13
|
+
function getIndexByLocale(locale, options) {
|
|
14
|
+
let index = indexByLocales.get(locale);
|
|
15
|
+
if (!index) {
|
|
16
|
+
index = new MiniSearch({
|
|
17
|
+
fields: ["title", "titles", "text"],
|
|
18
|
+
storeFields: ["title", "titles"],
|
|
19
|
+
...options.miniSearch?.options
|
|
20
|
+
});
|
|
21
|
+
indexByLocales.set(locale, index);
|
|
22
|
+
}
|
|
23
|
+
return index;
|
|
24
|
+
}
|
|
25
|
+
function getIndexCache(filepath) {
|
|
26
|
+
let index = indexCache.get(filepath);
|
|
27
|
+
if (!index) {
|
|
28
|
+
index = [];
|
|
29
|
+
indexCache.set(filepath, index);
|
|
30
|
+
}
|
|
31
|
+
return index;
|
|
32
|
+
}
|
|
33
|
+
async function prepareSearchIndex({
|
|
34
|
+
app,
|
|
35
|
+
isSearchable,
|
|
36
|
+
searchOptions
|
|
37
|
+
}) {
|
|
38
|
+
const start = performance.now();
|
|
39
|
+
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
|
|
40
|
+
await pMap(pages, (p) => indexFile(p, searchOptions), {
|
|
41
|
+
concurrency: 64
|
|
42
|
+
});
|
|
43
|
+
await writeTemp(app);
|
|
44
|
+
if (app.env.isDebug) {
|
|
45
|
+
logger.info(
|
|
46
|
+
`
|
|
47
|
+
[${colors.green("@vuepress-plume/plugin-search")}] prepare search time spent: ${(performance.now() - start).toFixed(2)}ms`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function onSearchIndexUpdated(filepath, {
|
|
52
|
+
app,
|
|
53
|
+
isSearchable,
|
|
54
|
+
searchOptions
|
|
55
|
+
}) {
|
|
56
|
+
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
|
|
57
|
+
if (pages.some((p) => p.filePathRelative?.endsWith(filepath))) {
|
|
58
|
+
await indexFile(app.pages.find((p) => p.filePathRelative?.endsWith(filepath)), searchOptions);
|
|
59
|
+
await writeTemp(app);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function onSearchIndexRemoved(filepath, {
|
|
63
|
+
app,
|
|
64
|
+
isSearchable,
|
|
65
|
+
searchOptions
|
|
66
|
+
}) {
|
|
67
|
+
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
|
|
68
|
+
if (pages.some((p) => p.filePathRelative?.endsWith(filepath))) {
|
|
69
|
+
const page = app.pages.find((p) => p.filePathRelative?.endsWith(filepath));
|
|
70
|
+
const fileId = page.path;
|
|
71
|
+
const locale = page.pathLocale;
|
|
72
|
+
const index = getIndexByLocale(locale, searchOptions);
|
|
73
|
+
const cache = getIndexCache(fileId);
|
|
74
|
+
index.removeAll(cache);
|
|
75
|
+
await writeTemp(app);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function writeTemp(app) {
|
|
79
|
+
const records = [];
|
|
80
|
+
for (const [locale] of indexByLocales) {
|
|
81
|
+
const index = indexByLocales.get(locale);
|
|
82
|
+
const localeName = locale.replace(/^\/|\/$/g, "").replace(/\//g, "_") || "default";
|
|
83
|
+
const filename = `searchBox-${localeName}.js`;
|
|
84
|
+
records.push(`${JSON.stringify(locale)}: () => import('@${SEARCH_INDEX_DIR}${filename}')`);
|
|
85
|
+
await app.writeTemp(
|
|
86
|
+
`${SEARCH_INDEX_DIR}${filename}`,
|
|
87
|
+
`export default ${JSON.stringify(
|
|
88
|
+
JSON.stringify(index) ?? {}
|
|
89
|
+
)}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
await app.writeTemp(
|
|
93
|
+
`${SEARCH_INDEX_DIR}index.js`,
|
|
94
|
+
`export const searchIndex = {${records.join(",")}}${app.env.isDev ? `
|
|
95
|
+
${genHmrCode("searchIndex")}` : ""}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async function indexFile(page, options) {
|
|
99
|
+
const fileId = page.path;
|
|
100
|
+
const locale = page.pathLocale;
|
|
101
|
+
const index = getIndexByLocale(locale, options);
|
|
102
|
+
const cache = getIndexCache(fileId);
|
|
103
|
+
const html = page.contentRendered;
|
|
104
|
+
const sections = splitPageIntoSections(html);
|
|
105
|
+
if (cache && cache.length)
|
|
106
|
+
index.removeAll(cache);
|
|
107
|
+
for await (const section of sections) {
|
|
108
|
+
if (!section || !(section.text || section.titles))
|
|
109
|
+
break;
|
|
110
|
+
const { anchor, text, titles } = section;
|
|
111
|
+
const id = anchor ? [fileId, anchor].join("#") : fileId;
|
|
112
|
+
const item = {
|
|
113
|
+
id,
|
|
114
|
+
text,
|
|
115
|
+
title: titles.at(-1),
|
|
116
|
+
titles: titles.slice(0, -1)
|
|
117
|
+
};
|
|
118
|
+
index.add(item);
|
|
119
|
+
cache.push(item);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
var headingRegex = /<h(\d*).*?>(<a.*? href="#.*?".*?>.*?<\/a>)<\/h\1>/gi;
|
|
123
|
+
var headingContentRegex = /<a.*? href="#(.*?)".*?>(.*?)<\/a>/i;
|
|
124
|
+
function* splitPageIntoSections(html) {
|
|
125
|
+
const result = html.split(headingRegex);
|
|
126
|
+
result.shift();
|
|
127
|
+
let parentTitles = [];
|
|
128
|
+
for (let i = 0; i < result.length; i += 3) {
|
|
129
|
+
const level = Number.parseInt(result[i]) - 1;
|
|
130
|
+
const heading = result[i + 1];
|
|
131
|
+
const headingResult = headingContentRegex.exec(heading);
|
|
132
|
+
const title = clearHtmlTags(headingResult?.[2] ?? "").trim();
|
|
133
|
+
const anchor = headingResult?.[1] ?? "";
|
|
134
|
+
const content = result[i + 2];
|
|
135
|
+
if (!title || !content)
|
|
136
|
+
continue;
|
|
137
|
+
const titles = parentTitles.slice(0, level);
|
|
138
|
+
titles[level] = title;
|
|
139
|
+
yield { anchor, titles, text: getSearchableText(content) };
|
|
140
|
+
if (level === 0)
|
|
141
|
+
parentTitles = [title];
|
|
142
|
+
else
|
|
143
|
+
parentTitles[level] = title;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function getSearchableText(content) {
|
|
147
|
+
content = clearHtmlTags(content);
|
|
148
|
+
return content;
|
|
149
|
+
}
|
|
150
|
+
function clearHtmlTags(str) {
|
|
151
|
+
return str.replace(/<[^>]*>/g, "");
|
|
152
|
+
}
|
|
153
|
+
function genHmrCode(m) {
|
|
154
|
+
const func = `update${m[0].toUpperCase()}${m.slice(1)}`;
|
|
155
|
+
return `
|
|
156
|
+
if (import.meta.webpackHot) {
|
|
157
|
+
import.meta.webpackHot.accept()
|
|
158
|
+
if (__VUE_HMR_RUNTIME__.${m}) {
|
|
159
|
+
__VUE_HMR_RUNTIME__.${func}(${m})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (import.meta.hot) {
|
|
164
|
+
import.meta.hot.accept(({ ${m} }) => {
|
|
165
|
+
__VUE_HMR_RUNTIME__.${func}(${m})
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/node/searchPlugin.ts
|
|
172
|
+
var __dirname = getDirname(import.meta.url);
|
|
173
|
+
function searchPlugin({
|
|
174
|
+
locales = {},
|
|
175
|
+
isSearchable,
|
|
176
|
+
...searchOptions
|
|
177
|
+
} = {}) {
|
|
178
|
+
return (app) => ({
|
|
179
|
+
name: "@vuepress-plume/plugin-search",
|
|
180
|
+
clientConfigFile: path.resolve(__dirname, "../client/config.js"),
|
|
181
|
+
define: {
|
|
182
|
+
__SEARCH_LOCALES__: locales,
|
|
183
|
+
__SEARCH_OPTIONS__: searchOptions
|
|
184
|
+
},
|
|
185
|
+
extendsBundlerOptions(bundlerOptions) {
|
|
186
|
+
addViteOptimizeDepsInclude(bundlerOptions, app, ["mark.js/src/vanilla.js", "@vueuse/integrations/useFocusTrap", "minisearch"]);
|
|
187
|
+
},
|
|
188
|
+
onPrepared: (app2) => prepareSearchIndex({ app: app2, isSearchable, searchOptions }),
|
|
189
|
+
onWatched: (app2, watchers) => {
|
|
190
|
+
const searchIndexWatcher = chokidar.watch("pages/**/*.js", {
|
|
191
|
+
cwd: app2.dir.temp(),
|
|
192
|
+
ignoreInitial: true
|
|
193
|
+
});
|
|
194
|
+
searchIndexWatcher.on("add", (filepath) => {
|
|
195
|
+
onSearchIndexUpdated(filepath, { app: app2, isSearchable, searchOptions });
|
|
196
|
+
});
|
|
197
|
+
searchIndexWatcher.on("change", (filepath) => {
|
|
198
|
+
onSearchIndexUpdated(filepath, { app: app2, isSearchable, searchOptions });
|
|
199
|
+
});
|
|
200
|
+
searchIndexWatcher.on("unlink", (filepath) => {
|
|
201
|
+
onSearchIndexRemoved(filepath, { app: app2, isSearchable, searchOptions });
|
|
202
|
+
});
|
|
203
|
+
watchers.push(searchIndexWatcher);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/node/index.ts
|
|
209
|
+
export * from "../shared/index.js";
|
|
210
|
+
export {
|
|
211
|
+
prepareSearchIndex,
|
|
212
|
+
searchPlugin
|
|
213
|
+
};
|
package/lib/shared/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { LocaleConfig, Page } from 'vuepress/core';
|
|
2
|
+
import { Options } from 'minisearch';
|
|
3
|
+
|
|
4
|
+
interface SearchLocaleOptions {
|
|
4
5
|
placeholder: string;
|
|
5
6
|
buttonText: string;
|
|
6
7
|
resetButtonTitle: string;
|
|
@@ -16,12 +17,12 @@ export interface SearchLocaleOptions {
|
|
|
16
17
|
closeKeyAriaLabel: string;
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
type SearchBoxLocales = LocaleConfig<SearchLocaleOptions>;
|
|
21
|
+
interface SearchPluginOptions extends SearchOptions {
|
|
21
22
|
locales?: SearchBoxLocales;
|
|
22
23
|
isSearchable?: (page: Page) => boolean;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
interface SearchOptions {
|
|
25
26
|
/**
|
|
26
27
|
* @default false
|
|
27
28
|
*/
|
|
@@ -30,10 +31,12 @@ export interface SearchOptions {
|
|
|
30
31
|
/**
|
|
31
32
|
* @see https://lucaong.github.io/minisearch/modules/_minisearch_.html#options
|
|
32
33
|
*/
|
|
33
|
-
options?: Pick<
|
|
34
|
+
options?: Pick<Options, 'extractField' | 'tokenize' | 'processTerm'>;
|
|
34
35
|
/**
|
|
35
36
|
* @see https://lucaong.github.io/minisearch/modules/_minisearch_.html#searchoptions-1
|
|
36
37
|
*/
|
|
37
|
-
searchOptions?:
|
|
38
|
+
searchOptions?: Options['searchOptions'];
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
export type { SearchBoxLocales, SearchLocaleOptions, SearchOptions, SearchPluginOptions };
|
package/lib/shared/index.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vuepress-plume/plugin-search",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.81",
|
|
5
5
|
"description": "The Plugin for VuePress 2 - local search",
|
|
6
6
|
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"vuepress": "2.0.0-rc.14"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@vuepress/helper": "2.0.0-rc.
|
|
37
|
+
"@vuepress/helper": "2.0.0-rc.39",
|
|
38
38
|
"@vueuse/core": "^10.11.0",
|
|
39
39
|
"@vueuse/integrations": "^10.11.0",
|
|
40
40
|
"chokidar": "^3.6.0",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"vuepress-plugin-search"
|
|
55
55
|
],
|
|
56
56
|
"scripts": {
|
|
57
|
-
"build": "pnpm run copy && pnpm run
|
|
58
|
-
"clean": "rimraf --glob ./lib
|
|
57
|
+
"build": "pnpm run copy && pnpm run tsup",
|
|
58
|
+
"clean": "rimraf --glob ./lib",
|
|
59
59
|
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
|
|
60
|
-
"
|
|
60
|
+
"tsup": "tsup --config tsup.config.ts"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { useRouteLocale } from 'vuepress/client';
|
|
2
|
-
import { computed, toRef } from 'vue';
|
|
3
|
-
const defaultLocales = {
|
|
4
|
-
'/': {
|
|
5
|
-
placeholder: 'Search',
|
|
6
|
-
resetButtonTitle: 'Reset search',
|
|
7
|
-
backButtonTitle: 'Close search',
|
|
8
|
-
noResultsText: 'No results for',
|
|
9
|
-
footer: {
|
|
10
|
-
selectText: 'to select',
|
|
11
|
-
selectKeyAriaLabel: 'enter',
|
|
12
|
-
navigateText: 'to navigate',
|
|
13
|
-
navigateUpKeyAriaLabel: 'up arrow',
|
|
14
|
-
navigateDownKeyAriaLabel: 'down arrow',
|
|
15
|
-
closeText: 'to close',
|
|
16
|
-
closeKeyAriaLabel: 'escape',
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
export function useLocale(locales) {
|
|
21
|
-
const localesRef = toRef(locales);
|
|
22
|
-
const routeLocale = useRouteLocale();
|
|
23
|
-
const locale = computed(() => localesRef.value[routeLocale.value] ?? defaultLocales[routeLocale.value] ?? defaultLocales['/']);
|
|
24
|
-
return locale;
|
|
25
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { searchIndex } from '@internal/minisearchIndex';
|
|
2
|
-
import { shallowRef } from 'vue';
|
|
3
|
-
const searchIndexData = shallowRef(searchIndex);
|
|
4
|
-
export function useSearchIndex() {
|
|
5
|
-
return searchIndexData;
|
|
6
|
-
}
|
|
7
|
-
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
|
|
8
|
-
__VUE_HMR_RUNTIME__.updateSearchIndex = (data) => {
|
|
9
|
-
searchIndexData.value = data;
|
|
10
|
-
};
|
|
11
|
-
__VUE_HMR_RUNTIME__.updateSearchIndex = (data) => {
|
|
12
|
-
searchIndexData.value = data;
|
|
13
|
-
};
|
|
14
|
-
}
|
package/lib/client/utils/lru.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// adapted from https://stackoverflow.com/a/46432113/11613622
|
|
2
|
-
export class LRUCache {
|
|
3
|
-
max;
|
|
4
|
-
cache;
|
|
5
|
-
constructor(max = 10) {
|
|
6
|
-
this.max = max;
|
|
7
|
-
this.cache = new Map();
|
|
8
|
-
}
|
|
9
|
-
get(key) {
|
|
10
|
-
const item = this.cache.get(key);
|
|
11
|
-
if (item !== undefined) {
|
|
12
|
-
// refresh key
|
|
13
|
-
this.cache.delete(key);
|
|
14
|
-
this.cache.set(key, item);
|
|
15
|
-
}
|
|
16
|
-
return item;
|
|
17
|
-
}
|
|
18
|
-
set(key, val) {
|
|
19
|
-
// refresh key
|
|
20
|
-
if (this.cache.has(key))
|
|
21
|
-
this.cache.delete(key);
|
|
22
|
-
// evict oldest
|
|
23
|
-
else if (this.cache.size === this.max)
|
|
24
|
-
this.cache.delete(this.first());
|
|
25
|
-
this.cache.set(key, val);
|
|
26
|
-
}
|
|
27
|
-
first() {
|
|
28
|
-
return this.cache.keys().next().value;
|
|
29
|
-
}
|
|
30
|
-
clear() {
|
|
31
|
-
this.cache.clear();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { App } from 'vuepress/core';
|
|
2
|
-
import type { SearchOptions, SearchPluginOptions } from '../shared/index.js';
|
|
3
|
-
export interface SearchIndexOptions {
|
|
4
|
-
app: App;
|
|
5
|
-
searchOptions: SearchOptions;
|
|
6
|
-
isSearchable: SearchPluginOptions['isSearchable'];
|
|
7
|
-
}
|
|
8
|
-
export declare function prepareSearchIndex({ app, isSearchable, searchOptions, }: SearchIndexOptions): Promise<void>;
|
|
9
|
-
export declare function onSearchIndexUpdated(filepath: string, { app, isSearchable, searchOptions, }: SearchIndexOptions): Promise<void>;
|
|
10
|
-
export declare function onSearchIndexRemoved(filepath: string, { app, isSearchable, searchOptions, }: SearchIndexOptions): Promise<void>;
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import MiniSearch from 'minisearch';
|
|
2
|
-
import pMap from 'p-map';
|
|
3
|
-
import { colors, logger } from 'vuepress/utils';
|
|
4
|
-
const SEARCH_INDEX_DIR = 'internal/minisearchIndex/';
|
|
5
|
-
const indexByLocales = new Map();
|
|
6
|
-
const indexCache = new Map();
|
|
7
|
-
function getIndexByLocale(locale, options) {
|
|
8
|
-
let index = indexByLocales.get(locale);
|
|
9
|
-
if (!index) {
|
|
10
|
-
index = new MiniSearch({
|
|
11
|
-
fields: ['title', 'titles', 'text'],
|
|
12
|
-
storeFields: ['title', 'titles'],
|
|
13
|
-
...options.miniSearch?.options,
|
|
14
|
-
});
|
|
15
|
-
indexByLocales.set(locale, index);
|
|
16
|
-
}
|
|
17
|
-
return index;
|
|
18
|
-
}
|
|
19
|
-
function getIndexCache(filepath) {
|
|
20
|
-
let index = indexCache.get(filepath);
|
|
21
|
-
if (!index) {
|
|
22
|
-
index = [];
|
|
23
|
-
indexCache.set(filepath, index);
|
|
24
|
-
}
|
|
25
|
-
return index;
|
|
26
|
-
}
|
|
27
|
-
export async function prepareSearchIndex({ app, isSearchable, searchOptions, }) {
|
|
28
|
-
const start = performance.now();
|
|
29
|
-
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
|
|
30
|
-
await pMap(pages, p => indexFile(p, searchOptions), {
|
|
31
|
-
concurrency: 64,
|
|
32
|
-
});
|
|
33
|
-
await writeTemp(app);
|
|
34
|
-
if (app.env.isDebug) {
|
|
35
|
-
logger.info(`\n[${colors.green('@vuepress-plume/plugin-search')}] prepare search time spent: ${(performance.now() - start).toFixed(2)}ms`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export async function onSearchIndexUpdated(filepath, { app, isSearchable, searchOptions, }) {
|
|
39
|
-
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
|
|
40
|
-
if (pages.some(p => p.filePathRelative?.endsWith(filepath))) {
|
|
41
|
-
await indexFile(app.pages.find(p => p.filePathRelative?.endsWith(filepath)), searchOptions);
|
|
42
|
-
await writeTemp(app);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
export async function onSearchIndexRemoved(filepath, { app, isSearchable, searchOptions, }) {
|
|
46
|
-
const pages = isSearchable ? app.pages.filter(isSearchable) : app.pages;
|
|
47
|
-
if (pages.some(p => p.filePathRelative?.endsWith(filepath))) {
|
|
48
|
-
const page = app.pages.find(p => p.filePathRelative?.endsWith(filepath));
|
|
49
|
-
const fileId = page.path;
|
|
50
|
-
const locale = page.pathLocale;
|
|
51
|
-
const index = getIndexByLocale(locale, searchOptions);
|
|
52
|
-
const cache = getIndexCache(fileId);
|
|
53
|
-
index.removeAll(cache);
|
|
54
|
-
await writeTemp(app);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function writeTemp(app) {
|
|
58
|
-
const records = [];
|
|
59
|
-
for (const [locale] of indexByLocales) {
|
|
60
|
-
const index = indexByLocales.get(locale);
|
|
61
|
-
const localeName = locale.replace(/^\/|\/$/g, '').replace(/\//g, '_') || 'default';
|
|
62
|
-
const filename = `searchBox-${localeName}.js`;
|
|
63
|
-
records.push(`${JSON.stringify(locale)}: () => import('@${SEARCH_INDEX_DIR}${filename}')`);
|
|
64
|
-
await app.writeTemp(`${SEARCH_INDEX_DIR}${filename}`, `export default ${JSON.stringify(JSON.stringify(index) ?? {})}`);
|
|
65
|
-
}
|
|
66
|
-
await app.writeTemp(`${SEARCH_INDEX_DIR}index.js`, `export const searchIndex = {${records.join(',')}}${app.env.isDev ? `\n${genHmrCode('searchIndex')}` : ''}`);
|
|
67
|
-
}
|
|
68
|
-
async function indexFile(page, options) {
|
|
69
|
-
// get file metadata
|
|
70
|
-
const fileId = page.path;
|
|
71
|
-
const locale = page.pathLocale;
|
|
72
|
-
const index = getIndexByLocale(locale, options);
|
|
73
|
-
const cache = getIndexCache(fileId);
|
|
74
|
-
// retrieve file and split into "sections"
|
|
75
|
-
const html = page.contentRendered;
|
|
76
|
-
const sections = splitPageIntoSections(html);
|
|
77
|
-
if (cache && cache.length)
|
|
78
|
-
index.removeAll(cache);
|
|
79
|
-
// add sections to the locale index
|
|
80
|
-
for await (const section of sections) {
|
|
81
|
-
if (!section || !(section.text || section.titles))
|
|
82
|
-
break;
|
|
83
|
-
const { anchor, text, titles } = section;
|
|
84
|
-
const id = anchor ? [fileId, anchor].join('#') : fileId;
|
|
85
|
-
const item = {
|
|
86
|
-
id,
|
|
87
|
-
text,
|
|
88
|
-
title: titles.at(-1),
|
|
89
|
-
titles: titles.slice(0, -1),
|
|
90
|
-
};
|
|
91
|
-
index.add(item);
|
|
92
|
-
cache.push(item);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const headingRegex = /<h(\d*).*?>(<a.*? href="#.*?".*?>.*?<\/a>)<\/h\1>/gi;
|
|
96
|
-
const headingContentRegex = /<a.*? href="#(.*?)".*?>(.*?)<\/a>/i;
|
|
97
|
-
/**
|
|
98
|
-
* Splits HTML into sections based on headings
|
|
99
|
-
*/
|
|
100
|
-
function* splitPageIntoSections(html) {
|
|
101
|
-
const result = html.split(headingRegex);
|
|
102
|
-
result.shift();
|
|
103
|
-
let parentTitles = [];
|
|
104
|
-
for (let i = 0; i < result.length; i += 3) {
|
|
105
|
-
const level = Number.parseInt(result[i]) - 1;
|
|
106
|
-
const heading = result[i + 1];
|
|
107
|
-
const headingResult = headingContentRegex.exec(heading);
|
|
108
|
-
const title = clearHtmlTags(headingResult?.[2] ?? '').trim();
|
|
109
|
-
const anchor = headingResult?.[1] ?? '';
|
|
110
|
-
const content = result[i + 2];
|
|
111
|
-
if (!title || !content)
|
|
112
|
-
continue;
|
|
113
|
-
const titles = parentTitles.slice(0, level);
|
|
114
|
-
titles[level] = title;
|
|
115
|
-
yield { anchor, titles, text: getSearchableText(content) };
|
|
116
|
-
if (level === 0)
|
|
117
|
-
parentTitles = [title];
|
|
118
|
-
else
|
|
119
|
-
parentTitles[level] = title;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function getSearchableText(content) {
|
|
123
|
-
content = clearHtmlTags(content);
|
|
124
|
-
return content;
|
|
125
|
-
}
|
|
126
|
-
function clearHtmlTags(str) {
|
|
127
|
-
return str.replace(/<[^>]*>/g, '');
|
|
128
|
-
}
|
|
129
|
-
function genHmrCode(m) {
|
|
130
|
-
const func = `update${m[0].toUpperCase()}${m.slice(1)}`;
|
|
131
|
-
return `
|
|
132
|
-
if (import.meta.webpackHot) {
|
|
133
|
-
import.meta.webpackHot.accept()
|
|
134
|
-
if (__VUE_HMR_RUNTIME__.${m}) {
|
|
135
|
-
__VUE_HMR_RUNTIME__.${func}(${m})
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (import.meta.hot) {
|
|
140
|
-
import.meta.hot.accept(({ ${m} }) => {
|
|
141
|
-
__VUE_HMR_RUNTIME__.${func}(${m})
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
`;
|
|
145
|
-
}
|
package/lib/node/searchPlugin.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import chokidar from 'chokidar';
|
|
2
|
-
import { getDirname, path } from 'vuepress/utils';
|
|
3
|
-
import { addViteOptimizeDepsInclude } from '@vuepress/helper';
|
|
4
|
-
import { onSearchIndexRemoved, onSearchIndexUpdated, prepareSearchIndex } from './prepareSearchIndex.js';
|
|
5
|
-
const __dirname = getDirname(import.meta.url);
|
|
6
|
-
export function searchPlugin({ locales = {}, isSearchable, ...searchOptions } = {}) {
|
|
7
|
-
return app => ({
|
|
8
|
-
name: '@vuepress-plume/plugin-search',
|
|
9
|
-
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
|
|
10
|
-
define: {
|
|
11
|
-
__SEARCH_LOCALES__: locales,
|
|
12
|
-
__SEARCH_OPTIONS__: searchOptions,
|
|
13
|
-
},
|
|
14
|
-
extendsBundlerOptions(bundlerOptions) {
|
|
15
|
-
addViteOptimizeDepsInclude(bundlerOptions, app, ['mark.js/src/vanilla.js', '@vueuse/integrations/useFocusTrap', 'minisearch']);
|
|
16
|
-
},
|
|
17
|
-
onPrepared: app => prepareSearchIndex({ app, isSearchable, searchOptions }),
|
|
18
|
-
onWatched: (app, watchers) => {
|
|
19
|
-
const searchIndexWatcher = chokidar.watch('pages/**/*.js', {
|
|
20
|
-
cwd: app.dir.temp(),
|
|
21
|
-
ignoreInitial: true,
|
|
22
|
-
});
|
|
23
|
-
searchIndexWatcher.on('add', (filepath) => {
|
|
24
|
-
onSearchIndexUpdated(filepath, { app, isSearchable, searchOptions });
|
|
25
|
-
});
|
|
26
|
-
searchIndexWatcher.on('change', (filepath) => {
|
|
27
|
-
onSearchIndexUpdated(filepath, { app, isSearchable, searchOptions });
|
|
28
|
-
});
|
|
29
|
-
searchIndexWatcher.on('unlink', (filepath) => {
|
|
30
|
-
onSearchIndexRemoved(filepath, { app, isSearchable, searchOptions });
|
|
31
|
-
});
|
|
32
|
-
watchers.push(searchIndexWatcher);
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
}
|