@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.
@@ -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/lru.js'
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/locale.js'
4
+ import { useLocale } from '../composables/index.js'
5
5
 
6
6
  const props = defineProps<{
7
7
  locales: SearchBoxLocales
@@ -1 +1,12 @@
1
- export * from './searchIndex.js';
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
- export * from './searchIndex.js';
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
+ };
@@ -1,3 +1,5 @@
1
- import type { ClientConfig } from 'vuepress/client';
1
+ import { ClientConfig } from 'vuepress/client';
2
+
2
3
  declare const _default: ClientConfig;
3
- export default _default;
4
+
5
+ export { _default as default };
@@ -1,14 +1,18 @@
1
- import { defineClientConfig } from 'vuepress/client';
2
- import { h } from 'vue';
3
- import Search from './components/Search.vue';
4
- const locales = __SEARCH_LOCALES__;
5
- const searchOptions = __SEARCH_OPTIONS__;
6
- export default defineClientConfig({
7
- enhance({ app }) {
8
- app.component('SearchBox', props => h(Search, {
9
- locales,
10
- options: searchOptions,
11
- ...props,
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
+ };
@@ -1,3 +1,2 @@
1
- import SearchBox from './components/Search.vue';
2
- import { useSearchIndex } from './composables/index.js';
3
- export { SearchBox, useSearchIndex, };
1
+ export { default as SearchBox } from './components/Search.vue';
2
+ export { useSearchIndex } from './composables/index.js';
@@ -1,3 +1,7 @@
1
- import SearchBox from './components/Search.vue';
2
- import { useSearchIndex } from './composables/index.js';
3
- export { SearchBox, useSearchIndex, };
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
+ };
@@ -1,4 +1,4 @@
1
- export declare class LRUCache<K, V> {
1
+ declare class LRUCache<K, V> {
2
2
  private max;
3
3
  private cache;
4
4
  constructor(max?: number);
@@ -7,3 +7,5 @@ export declare class LRUCache<K, V> {
7
7
  first(): K | undefined;
8
8
  clear(): void;
9
9
  }
10
+
11
+ export { LRUCache };
@@ -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
+ };
@@ -1,4 +1,14 @@
1
- import { searchPlugin } from './searchPlugin.js';
2
- export { prepareSearchIndex } from './prepareSearchIndex.js';
1
+ import { Plugin, App } from 'vuepress/core';
2
+ import { SearchPluginOptions, SearchOptions } from '../shared/index.js';
3
3
  export * from '../shared/index.js';
4
- export { searchPlugin, };
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
- import { searchPlugin } from './searchPlugin.js';
2
- export { prepareSearchIndex } from './prepareSearchIndex.js';
3
- export * from '../shared/index.js';
4
- export { searchPlugin, };
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
+ };
@@ -1,6 +1,7 @@
1
- import type { LocaleConfig, Page } from 'vuepress/core';
2
- import type { Options as MiniSearchOptions } from 'minisearch';
3
- export interface SearchLocaleOptions {
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
- export type SearchBoxLocales = LocaleConfig<SearchLocaleOptions>;
20
- export interface SearchPluginOptions extends SearchOptions {
20
+ type SearchBoxLocales = LocaleConfig<SearchLocaleOptions>;
21
+ interface SearchPluginOptions extends SearchOptions {
21
22
  locales?: SearchBoxLocales;
22
23
  isSearchable?: (page: Page) => boolean;
23
24
  }
24
- export interface SearchOptions {
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<MiniSearchOptions, 'extractField' | 'tokenize' | 'processTerm'>;
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?: MiniSearchOptions['searchOptions'];
38
+ searchOptions?: Options['searchOptions'];
38
39
  };
39
40
  }
41
+
42
+ export type { SearchBoxLocales, SearchLocaleOptions, SearchOptions, SearchPluginOptions };
@@ -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.80",
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.38",
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 ts",
58
- "clean": "rimraf --glob ./lib ./*.tsbuildinfo",
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
- "ts": "tsc -b tsconfig.build.json"
60
+ "tsup": "tsup --config tsup.config.ts"
61
61
  }
62
62
  }
@@ -1,3 +0,0 @@
1
- import type { MaybeRef } from 'vue';
2
- import type { SearchBoxLocales } from '../../shared/index.js';
3
- export declare function useLocale(locales: MaybeRef<SearchBoxLocales>): import("vue").ComputedRef<Partial<import("../../shared/index.js").SearchLocaleOptions>>;
@@ -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,3 +0,0 @@
1
- export declare function useSearchIndex(): import("vue").ShallowRef<Record<string, () => Promise<{
2
- default: string;
3
- }>>>;
@@ -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
- }
@@ -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
- }
@@ -1,3 +0,0 @@
1
- import type { Plugin } from 'vuepress/core';
2
- import type { SearchPluginOptions } from '../shared/index.js';
3
- export declare function searchPlugin({ locales, isSearchable, ...searchOptions }?: SearchPluginOptions): Plugin;
@@ -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
- }