@vuepress-plume/plugin-search 1.0.0-rc.212 → 1.0.0-rc.213
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/dist/client/components/Search.vue +43 -3
- package/dist/client/components/SearchBox.vue +68 -36
- package/dist/client/composables/index.d.ts +47 -9
- package/dist/client/composables/index.js +21 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/client/shim.d.ts +3 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +82 -20
- package/dist/shared/index.d.ts +17 -2
- package/dist/shared/index.js +9 -2
- package/package.json +8 -8
- package/dist/client/utils/index.d.ts +0 -84
- package/dist/client/utils/index.js +0 -35
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import type { SearchBoxLocales, SearchOptions } from '../../shared/index.js'
|
|
3
3
|
import { onKeyStroke } from '@vueuse/core'
|
|
4
4
|
import {
|
|
5
|
+
computed,
|
|
5
6
|
defineAsyncComponent,
|
|
6
7
|
ref,
|
|
8
|
+
toRef,
|
|
7
9
|
} from 'vue'
|
|
10
|
+
import { useLocale } from '../composables/index.js'
|
|
8
11
|
import SearchButton from './SearchButton.vue'
|
|
9
12
|
|
|
10
|
-
defineProps<{
|
|
13
|
+
const props = defineProps<{
|
|
11
14
|
locales: SearchBoxLocales
|
|
12
15
|
options: SearchOptions
|
|
13
16
|
}>()
|
|
@@ -16,15 +19,42 @@ const SearchBox = defineAsyncComponent(() => import('./SearchBox.vue'))
|
|
|
16
19
|
|
|
17
20
|
const showSearch = ref(false)
|
|
18
21
|
|
|
22
|
+
const locale = useLocale(toRef(() => props.locales))
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Text shown when JavaScript is disabled.
|
|
26
|
+
*
|
|
27
|
+
* 禁用 JavaScript 时展示的提示文本。
|
|
28
|
+
*
|
|
29
|
+
* `<noscript>` 仅在脚本被禁用时渲染,且必须是纯文本:启用脚本时浏览器会把
|
|
30
|
+
* 其中的内容当作原始文本,若包含元素或 HTML 实体会造成水合不匹配。
|
|
31
|
+
* `<noscript>` only renders with scripting disabled, and must stay plain text:
|
|
32
|
+
* with scripting enabled browsers parse its content as raw text, so elements or
|
|
33
|
+
* HTML entities would cause a hydration mismatch.
|
|
34
|
+
*/
|
|
35
|
+
const noscriptText = computed(
|
|
36
|
+
() => locale.value.noscriptText || 'Search is unavailable without JavaScript.',
|
|
37
|
+
)
|
|
38
|
+
|
|
19
39
|
onKeyStroke('k', (event) => {
|
|
20
|
-
|
|
40
|
+
// 在输入框 / 可编辑区域内不劫持 Ctrl/Cmd + K(如 macOS 的删除至行尾)。
|
|
41
|
+
// Do not hijack Ctrl/Cmd + K while editing content (e.g. macOS delete-to-end-of-line).
|
|
42
|
+
if ((event.ctrlKey || event.metaKey) && !isEditingContent(event)) {
|
|
21
43
|
event.preventDefault()
|
|
22
44
|
showSearch.value = true
|
|
23
45
|
}
|
|
24
46
|
})
|
|
25
47
|
|
|
26
48
|
onKeyStroke('/', (event) => {
|
|
27
|
-
|
|
49
|
+
// 仅在无修饰键且不在编辑内容时劫持,避免与浏览器/系统组合键冲突。
|
|
50
|
+
// Only hijack without modifiers and outside editable content to avoid clashing
|
|
51
|
+
// with browser/system shortcuts.
|
|
52
|
+
if (
|
|
53
|
+
!event.ctrlKey
|
|
54
|
+
&& !event.metaKey
|
|
55
|
+
&& !event.altKey
|
|
56
|
+
&& !isEditingContent(event)
|
|
57
|
+
) {
|
|
28
58
|
event.preventDefault()
|
|
29
59
|
showSearch.value = true
|
|
30
60
|
}
|
|
@@ -55,6 +85,8 @@ function isEditingContent(event: KeyboardEvent): boolean {
|
|
|
55
85
|
<div id="local-search">
|
|
56
86
|
<SearchButton :locales="locales" @click="showSearch = true" />
|
|
57
87
|
</div>
|
|
88
|
+
|
|
89
|
+
<noscript>{{ noscriptText }}</noscript>
|
|
58
90
|
</div>
|
|
59
91
|
</template>
|
|
60
92
|
|
|
@@ -64,6 +96,14 @@ function isEditingContent(event: KeyboardEvent): boolean {
|
|
|
64
96
|
align-items: center;
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
/* 仅在禁用 JavaScript 时可见的降级提示。
|
|
100
|
+
Fallback hint that is only visible when JavaScript is disabled. */
|
|
101
|
+
.search-wrapper noscript {
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
line-height: 1.4;
|
|
104
|
+
color: var(--vp-c-text-2);
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
@media (min-width: 768px) {
|
|
68
108
|
.search-wrapper {
|
|
69
109
|
flex-grow: 1;
|
|
@@ -26,8 +26,7 @@ import {
|
|
|
26
26
|
} from 'vue'
|
|
27
27
|
import { usePageLang, useRouteLocale, useRouter, withBase } from 'vuepress/client'
|
|
28
28
|
import { createTokenizer } from '../../shared/index.js'
|
|
29
|
-
import {
|
|
30
|
-
import { LRUCache } from '../utils/index.js'
|
|
29
|
+
import { loadSearchIndexJSON, useLocale } from '../composables/index.js'
|
|
31
30
|
import BackIcon from './icons/BackIcon.vue'
|
|
32
31
|
import ClearIcon from './icons/ClearIcon.vue'
|
|
33
32
|
import SearchIcon from './icons/SearchIcon.vue'
|
|
@@ -48,12 +47,9 @@ const lang = usePageLang()
|
|
|
48
47
|
const el = shallowRef<HTMLElement>()
|
|
49
48
|
const resultsEl = shallowRef<HTMLElement>()
|
|
50
49
|
|
|
51
|
-
const searchIndexData = useSearchIndex()
|
|
52
|
-
|
|
53
50
|
interface Result {
|
|
54
51
|
title: string
|
|
55
52
|
titles: string[]
|
|
56
|
-
text?: string
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
const { activate } = useFocusTrap(el, { immediate: true })
|
|
@@ -105,19 +101,27 @@ const searchIndex = computedAsync(async (onCancel) => {
|
|
|
105
101
|
// Use the same tokenizer as the node build side so query tokens always match the index.
|
|
106
102
|
const tokenize = createTokenizer(lang.value)
|
|
107
103
|
|
|
108
|
-
const loadIndex = searchIndexData.value[routeLocale.value]
|
|
109
|
-
// 当前语言缺少索引文件时回退到空索引,并给出可见提示,避免静默无结果。
|
|
110
|
-
// Fall back to an empty index and show a visible message when the locale has no index.
|
|
111
|
-
if (!loadIndex) {
|
|
112
|
-
if (!canceled)
|
|
113
|
-
searchIndexError.value = searchIndexErrorText.value
|
|
114
|
-
return markRaw(createEmptyIndex())
|
|
115
|
-
}
|
|
116
|
-
|
|
117
104
|
try {
|
|
118
|
-
|
|
105
|
+
// 分片索引在客户端按片合并并让出主线程,避免大索引长时间阻塞渲染。
|
|
106
|
+
// Sharded indexes are merged piece by piece on the client, yielding to the main
|
|
107
|
+
// thread, so a large index never blocks rendering for long.
|
|
108
|
+
const indexJson = await loadSearchIndexJSON(routeLocale.value)
|
|
109
|
+
|
|
110
|
+
// 当前语言缺少索引文件时回退到空索引,并给出可见提示,避免静默无结果。
|
|
111
|
+
// Fall back to an empty index and show a visible message when the locale has no index.
|
|
112
|
+
if (!indexJson) {
|
|
113
|
+
if (!canceled)
|
|
114
|
+
searchIndexError.value = searchIndexErrorText.value
|
|
115
|
+
return markRaw(createEmptyIndex())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 使用公开的异步反序列化 API(`loadJSAsync` 为 `@ignore` 的内部 API),
|
|
119
|
+
// 它在批次之间让出主线程,避免大索引同步解析长时间阻塞。
|
|
120
|
+
// Use the public async deserialization API (`loadJSAsync` is an `@ignore`d
|
|
121
|
+
// internal API). It yields to the main thread between batches, so a large index
|
|
122
|
+
// never blocks rendering for long.
|
|
119
123
|
return markRaw(
|
|
120
|
-
MiniSearch.
|
|
124
|
+
await MiniSearch.loadJSONAsync<Result>(indexJson, {
|
|
121
125
|
fields: ['title', 'titles', 'text'],
|
|
122
126
|
storeFields: ['title', 'titles'],
|
|
123
127
|
searchOptions: {
|
|
@@ -162,16 +166,9 @@ const mark = computedAsync(async () => {
|
|
|
162
166
|
return markRaw(new Mark(resultsEl.value))
|
|
163
167
|
}, null)
|
|
164
168
|
|
|
165
|
-
const cache = new LRUCache<string, Map<string, string>>(64) // 64 files
|
|
166
|
-
|
|
167
169
|
watchDebounced(
|
|
168
170
|
() => [searchIndex.value, filterText.value] as const,
|
|
169
|
-
async ([index, filterTextValue],
|
|
170
|
-
if (old?.[0] !== index) {
|
|
171
|
-
// in case of hmr
|
|
172
|
-
cache.clear()
|
|
173
|
-
}
|
|
174
|
-
|
|
171
|
+
async ([index, filterTextValue], _old, onCleanup) => {
|
|
175
172
|
let canceled = false
|
|
176
173
|
onCleanup(() => {
|
|
177
174
|
canceled = true
|
|
@@ -191,16 +188,10 @@ watchDebounced(
|
|
|
191
188
|
enableNoResults.value = true
|
|
192
189
|
|
|
193
190
|
const terms = new Set<string>()
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const [id, anchor] = r.id.split('#')
|
|
197
|
-
const map = cache.get(id)
|
|
198
|
-
const text = map?.get(anchor) ?? ''
|
|
199
|
-
for (const term in r.match)
|
|
191
|
+
for (const result of results.value) {
|
|
192
|
+
for (const term in result.match)
|
|
200
193
|
terms.add(term)
|
|
201
|
-
|
|
202
|
-
return { ...r, text }
|
|
203
|
-
})
|
|
194
|
+
}
|
|
204
195
|
|
|
205
196
|
await nextTick()
|
|
206
197
|
if (canceled)
|
|
@@ -306,6 +297,30 @@ onKeyStroke(['n', 'N'], (event) => {
|
|
|
306
297
|
|
|
307
298
|
const router = useRouter()
|
|
308
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Whether the browser back button already closed the search box.
|
|
302
|
+
*
|
|
303
|
+
* 浏览器后退键是否已关闭搜索框。
|
|
304
|
+
*
|
|
305
|
+
* When true the extra history entry created on mount has been consumed, so the
|
|
306
|
+
* unmount handler must not roll history back again.
|
|
307
|
+
*
|
|
308
|
+
* 为 true 时说明挂载时压入的额外历史条目已被消费,卸载时不应再回退历史。
|
|
309
|
+
*/
|
|
310
|
+
let closedByPopstate = false
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Whether the search box is being closed because a result is being opened.
|
|
314
|
+
*
|
|
315
|
+
* 搜索框是否因跳转到搜索结果而关闭。
|
|
316
|
+
*
|
|
317
|
+
* In that case the URL must stay on the target page, so history must not be
|
|
318
|
+
* rolled back on unmount.
|
|
319
|
+
*
|
|
320
|
+
* 此时 URL 必须停留在目标页面,卸载时不能回退历史。
|
|
321
|
+
*/
|
|
322
|
+
let navigatingToResult = false
|
|
323
|
+
|
|
309
324
|
onKeyStroke('Enter', (e) => {
|
|
310
325
|
if (e.isComposing)
|
|
311
326
|
return
|
|
@@ -320,7 +335,11 @@ onKeyStroke('Enter', (e) => {
|
|
|
320
335
|
}
|
|
321
336
|
|
|
322
337
|
if (selectedPackage) {
|
|
323
|
-
|
|
338
|
+
navigatingToResult = true
|
|
339
|
+
// 用 replace 顶替挂载时 pushState 的临时历史条目,避免从结果页返回时多一次 Back。
|
|
340
|
+
// Replace the temporary history entry pushed on mount, so returning from the
|
|
341
|
+
// result page does not require an extra Back press.
|
|
342
|
+
router.replace(selectedPackage.id)
|
|
324
343
|
emit('close')
|
|
325
344
|
}
|
|
326
345
|
})
|
|
@@ -338,6 +357,7 @@ onMounted(() => {
|
|
|
338
357
|
|
|
339
358
|
useEventListener('popstate', (event) => {
|
|
340
359
|
event.preventDefault()
|
|
360
|
+
closedByPopstate = true
|
|
341
361
|
emit('close')
|
|
342
362
|
})
|
|
343
363
|
|
|
@@ -353,6 +373,15 @@ onMounted(() => {
|
|
|
353
373
|
|
|
354
374
|
onBeforeUnmount(() => {
|
|
355
375
|
isLocked.value = false
|
|
376
|
+
// 配对挂载时的 pushState,避免反复开关搜索框累积历史条目。
|
|
377
|
+
// 若该条目已被浏览器后退键(popstate)消费,或正在跳转到搜索结果,则不能回退。
|
|
378
|
+
//
|
|
379
|
+
// Pair the pushState made on mount so repeated open/close does not pile up
|
|
380
|
+
// history entries. Skip the rollback when the entry was already consumed by
|
|
381
|
+
// the browser back button (popstate), or when opening a result (the URL must
|
|
382
|
+
// stay on the target page).
|
|
383
|
+
if (!closedByPopstate && !navigatingToResult)
|
|
384
|
+
window.history.back()
|
|
356
385
|
})
|
|
357
386
|
|
|
358
387
|
function resetSearch() {
|
|
@@ -376,7 +405,10 @@ function formMarkRegex(terms: Set<string>) {
|
|
|
376
405
|
|
|
377
406
|
function selectedClick(e: MouseEvent, p: SearchResult & Result) {
|
|
378
407
|
e.preventDefault()
|
|
379
|
-
|
|
408
|
+
navigatingToResult = true
|
|
409
|
+
// 同 Enter 键路径:replace 消费掉挂载时压入的临时历史条目。
|
|
410
|
+
// Same as the Enter path: replace consumes the temp entry pushed on mount.
|
|
411
|
+
router.replace(p.id)
|
|
380
412
|
emit('close')
|
|
381
413
|
}
|
|
382
414
|
</script>
|
|
@@ -389,7 +421,7 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
|
|
|
389
421
|
:aria-owns="results?.length ? 'localsearch-list' : undefined"
|
|
390
422
|
aria-expanded="true"
|
|
391
423
|
aria-haspopup="listbox"
|
|
392
|
-
aria-labelledby="
|
|
424
|
+
aria-labelledby="localsearch-label"
|
|
393
425
|
class="VPLocalSearchBox"
|
|
394
426
|
>
|
|
395
427
|
<div class="backdrop" @click="$emit('close')" />
|
|
@@ -14,21 +14,36 @@ import {SearchBoxLocales, SearchLocaleOptions} from '../../shared/index.js'
|
|
|
14
14
|
export declare function useLocale(
|
|
15
15
|
locales: MaybeRef<SearchBoxLocales>,
|
|
16
16
|
): ComputedRef<Partial<SearchLocaleOptions>>
|
|
17
|
+
/**
|
|
18
|
+
* Shape of a dynamically imported index module.
|
|
19
|
+
*
|
|
20
|
+
* 动态导入的索引模块的形状。
|
|
21
|
+
*
|
|
22
|
+
* Large locales are split into shards: the default export then holds the index
|
|
23
|
+
* metadata (with an empty `index`), and `shards` exposes lazy loaders for the
|
|
24
|
+
* remaining term entries.
|
|
25
|
+
*
|
|
26
|
+
* 较大的语言会被拆分为分片:此时默认导出承载索引元数据(`index` 为空),
|
|
27
|
+
* `shards` 则提供其余词元条目的延迟加载函数。
|
|
28
|
+
*/
|
|
29
|
+
export interface SearchIndexModule {
|
|
30
|
+
/** Serialized index JSON, or the index metadata when sharded / 序列化索引 JSON;分片时为索引元数据 */
|
|
31
|
+
default: string
|
|
32
|
+
/** Lazy loaders of the remaining shards / 其余分片的延迟加载函数 */
|
|
33
|
+
shards?: (() => Promise<{
|
|
34
|
+
default: string
|
|
35
|
+
}>)[]
|
|
36
|
+
}
|
|
17
37
|
/**
|
|
18
38
|
* Type definition for search index data.
|
|
19
39
|
*
|
|
20
40
|
* 搜索索引数据的类型定义。
|
|
21
41
|
*
|
|
22
|
-
* Maps locale paths to functions that load the corresponding search index.
|
|
42
|
+
* Maps locale paths to functions that load the corresponding search index module.
|
|
23
43
|
*
|
|
24
|
-
*
|
|
44
|
+
* 将语言路径映射到加载相应搜索索引模块的函数。
|
|
25
45
|
*/
|
|
26
|
-
type SearchIndexData = Record<
|
|
27
|
-
string,
|
|
28
|
-
() => Promise<{
|
|
29
|
-
default: string
|
|
30
|
-
}>
|
|
31
|
-
>
|
|
46
|
+
export type SearchIndexData = Record<string, () => Promise<SearchIndexModule>>
|
|
32
47
|
/**
|
|
33
48
|
* Get the search index data for all locales.
|
|
34
49
|
*
|
|
@@ -37,6 +52,29 @@ type SearchIndexData = Record<
|
|
|
37
52
|
* @returns Shallow ref to the search index data / 搜索索引数据的浅层引用
|
|
38
53
|
* @example
|
|
39
54
|
* const indexData = useSearchIndex()
|
|
40
|
-
* const
|
|
55
|
+
* const loadModule = indexData.value['/zh/']
|
|
41
56
|
*/
|
|
42
57
|
export declare function useSearchIndex(): ShallowRef<SearchIndexData>
|
|
58
|
+
/**
|
|
59
|
+
* Load and assemble the JSON-serialized search index of a locale.
|
|
60
|
+
*
|
|
61
|
+
* 加载并组装某个语言的 JSON 序列化搜索索引。
|
|
62
|
+
*
|
|
63
|
+
* The returned string can be passed to `MiniSearch.loadJSONAsync()`, which
|
|
64
|
+
* deserializes the index in batches without blocking the main thread.
|
|
65
|
+
*
|
|
66
|
+
* 返回的字符串可直接交给 `MiniSearch.loadJSONAsync()`,
|
|
67
|
+
* 它会分批反序列化索引,避免阻塞主线程。
|
|
68
|
+
*
|
|
69
|
+
* Sharded indexes are loaded in parallel and then merged one shard at a time,
|
|
70
|
+
* yielding to the main thread between shards. This keeps the first paint and
|
|
71
|
+
* input handling responsive while a large index is being assembled.
|
|
72
|
+
*
|
|
73
|
+
* 分片索引会并行加载,再逐片合并,并在分片之间让出主线程。
|
|
74
|
+
* 这样在组装大索引时仍能保持首屏渲染与输入的响应性。
|
|
75
|
+
*
|
|
76
|
+
* @param locale - Locale path (e.g. `/`, `/zh/`) / 语言路径
|
|
77
|
+
* @returns Serialized index JSON, or `undefined` when the locale has no index /
|
|
78
|
+
* 序列化索引 JSON;该语言没有索引时返回 `undefined`
|
|
79
|
+
*/
|
|
80
|
+
export declare function loadSearchIndexJSON(locale: string): Promise<string | undefined>
|
|
@@ -9,6 +9,7 @@ const defaultLocales = {
|
|
|
9
9
|
backButtonTitle: 'Close search',
|
|
10
10
|
noResultsText: 'No results for',
|
|
11
11
|
searchIndexErrorText: 'Failed to load the search index.',
|
|
12
|
+
noscriptText: 'Search is unavailable without JavaScript.',
|
|
12
13
|
footer: {
|
|
13
14
|
selectText: 'to select',
|
|
14
15
|
selectKeyAriaLabel: 'enter',
|
|
@@ -37,9 +38,28 @@ const searchIndexData = shallowRef(searchIndex)
|
|
|
37
38
|
function useSearchIndex() {
|
|
38
39
|
return searchIndexData
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
function yieldToMainThread() {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadSearchIndexJSON(locale) {
|
|
47
|
+
const loadIndexModule = searchIndexData.value[locale]
|
|
48
|
+
if (!loadIndexModule) return void 0
|
|
49
|
+
const indexModule = await loadIndexModule()
|
|
50
|
+
if (!indexModule.shards?.length) return indexModule.default
|
|
51
|
+
const serialized = JSON.parse(indexModule.default)
|
|
52
|
+
const shards = await Promise.all(indexModule.shards.map((loadShard) => loadShard()))
|
|
53
|
+
for (const shard of shards) {
|
|
54
|
+
const entries = JSON.parse(shard.default)
|
|
55
|
+
for (const entry of entries) serialized.index.push(entry)
|
|
56
|
+
await yieldToMainThread()
|
|
57
|
+
}
|
|
58
|
+
return JSON.stringify(serialized)
|
|
59
|
+
}
|
|
40
60
|
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot))
|
|
41
61
|
__VUE_HMR_RUNTIME__.updateSearchIndex = (data) => {
|
|
42
62
|
searchIndexData.value = data
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
export {useLocale, useSearchIndex}
|
|
65
|
+
export {loadSearchIndexJSON, useLocale, useSearchIndex}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import SearchBox from './components/Search.vue'
|
|
2
|
-
import {useSearchIndex} from './composables/index.js'
|
|
3
|
-
export {SearchBox, useSearchIndex}
|
|
2
|
+
import {loadSearchIndexJSON, useSearchIndex} from './composables/index.js'
|
|
3
|
+
export {SearchBox, loadSearchIndexJSON, useSearchIndex}
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import SearchBox from './components/Search.vue'
|
|
2
|
-
import {useSearchIndex} from './composables/index.js'
|
|
2
|
+
import {loadSearchIndexJSON, useSearchIndex} from './composables/index.js'
|
|
3
3
|
|
|
4
|
-
export {SearchBox, useSearchIndex}
|
|
4
|
+
export {SearchBox, loadSearchIndexJSON, useSearchIndex}
|
package/dist/client/shim.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ declare module '*.vue' {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
declare module '@internal/minisearchIndex' {
|
|
9
|
-
|
|
9
|
+
import type { SearchIndexData } from './composables/searchIndex.js'
|
|
10
|
+
|
|
11
|
+
const searchIndex: SearchIndexData
|
|
10
12
|
export {
|
|
11
13
|
searchIndex,
|
|
12
14
|
}
|
package/dist/node/index.d.ts
CHANGED
package/dist/node/index.js
CHANGED
|
@@ -8,9 +8,15 @@ export * from '../shared/index.js'
|
|
|
8
8
|
|
|
9
9
|
const SEARCH_INDEX_DIR = 'internal/minisearchIndex/'
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const MAX_SEARCHABLE_TEXT_LENGTH = 8e3
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const SEARCH_INDEX_SHARD_SIZE = 5e4
|
|
14
|
+
|
|
15
|
+
const indexByLocales = /* @__PURE__ */ new Map()
|
|
16
|
+
|
|
17
|
+
const indexCache = /* @__PURE__ */ new Map()
|
|
18
|
+
|
|
19
|
+
const writtenIndexFingerprints = /* @__PURE__ */ new Map()
|
|
14
20
|
|
|
15
21
|
function getIndexByLocale(locale, lang, options) {
|
|
16
22
|
let index = indexByLocales.get(locale)
|
|
@@ -43,6 +49,7 @@ async function prepareSearchIndex({app, isSearchable, searchOptions}) {
|
|
|
43
49
|
const start = performance.now()
|
|
44
50
|
indexByLocales.clear()
|
|
45
51
|
indexCache.clear()
|
|
52
|
+
writtenIndexFingerprints.clear()
|
|
46
53
|
await pMap(app.pages, (p) => indexFile(p, searchOptions, isSearchable), {concurrency: 64})
|
|
47
54
|
await writeTemp(app)
|
|
48
55
|
if (app.env.isBuild) {
|
|
@@ -95,25 +102,62 @@ async function onSearchIndexRemoved(app, {page, isSearchable, searchOptions}) {
|
|
|
95
102
|
})
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
function splitSearchIndex(serialized, shardSize = SEARCH_INDEX_SHARD_SIZE) {
|
|
106
|
+
const {index} = serialized
|
|
107
|
+
if (shardSize <= 0 || index.length <= shardSize)
|
|
108
|
+
return {
|
|
109
|
+
meta: serialized,
|
|
110
|
+
shards: [],
|
|
111
|
+
}
|
|
112
|
+
const shards = []
|
|
113
|
+
for (let i = 0; i < index.length; i += shardSize) shards.push(index.slice(i, i + shardSize))
|
|
114
|
+
return {
|
|
115
|
+
meta: {
|
|
116
|
+
...serialized,
|
|
117
|
+
index: [],
|
|
118
|
+
},
|
|
119
|
+
shards,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildIndexFiles(localeName, serialized, shardSize = SEARCH_INDEX_SHARD_SIZE) {
|
|
124
|
+
const {meta, shards} = splitSearchIndex(serialized, shardSize)
|
|
125
|
+
const files = {}
|
|
126
|
+
shards.forEach((entries, i) => {
|
|
127
|
+
files[`searchBox-${localeName}-${i}.js`] =
|
|
128
|
+
`export default ${JSON.stringify(JSON.stringify(entries))}`
|
|
129
|
+
})
|
|
130
|
+
const shardImports = shards.length
|
|
131
|
+
? `\nexport const shards = [${shards.map((_, i) => `() => import('@${SEARCH_INDEX_DIR}searchBox-${localeName}-${i}.js')`).join(', ')}]`
|
|
132
|
+
: ''
|
|
133
|
+
files[`searchBox-${localeName}.js`] =
|
|
134
|
+
`export default ${JSON.stringify(JSON.stringify(meta))}${shardImports}`
|
|
135
|
+
return files
|
|
136
|
+
}
|
|
137
|
+
|
|
98
138
|
async function writeTemp(app) {
|
|
99
139
|
const records = []
|
|
100
140
|
const promises = []
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
records.push(`${JSON.stringify(locale)}: () => import('@${SEARCH_INDEX_DIR}${filename}')`)
|
|
141
|
+
const writeIfChanged = (filename, content) => {
|
|
142
|
+
const filePath = `${SEARCH_INDEX_DIR}${filename}`
|
|
143
|
+
if (writtenIndexFingerprints.get(filePath) === content) return
|
|
105
144
|
promises.push(
|
|
106
|
-
app.writeTemp(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
),
|
|
145
|
+
app.writeTemp(filePath, content).then(() => {
|
|
146
|
+
writtenIndexFingerprints.set(filePath, content)
|
|
147
|
+
}),
|
|
110
148
|
)
|
|
111
149
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
150
|
+
for (const [locale, localeIndex] of indexByLocales) {
|
|
151
|
+
const localeName = locale.replace(/^\/|\/$/g, '').replace(/\//g, '_') || 'default'
|
|
152
|
+
records.push(
|
|
153
|
+
`${JSON.stringify(locale)}: () => import('@${SEARCH_INDEX_DIR}searchBox-${localeName}.js')`,
|
|
154
|
+
)
|
|
155
|
+
const files = buildIndexFiles(localeName, localeIndex.toJSON())
|
|
156
|
+
for (const [filename, content] of Object.entries(files)) writeIfChanged(filename, content)
|
|
157
|
+
}
|
|
158
|
+
writeIfChanged(
|
|
159
|
+
'index.js',
|
|
160
|
+
`export const searchIndex = {${records.join(',')}}${app.env.isDev ? `\n${genHmrCode('searchIndex')}` : ''}`,
|
|
117
161
|
)
|
|
118
162
|
await Promise.all(promises)
|
|
119
163
|
}
|
|
@@ -126,7 +170,12 @@ async function indexFile(page, options, isSearchable) {
|
|
|
126
170
|
const cache = getIndexCache(page.filePathRelative ?? fileId)
|
|
127
171
|
try {
|
|
128
172
|
if (cache.length) index.removeAll(cache)
|
|
129
|
-
} catch {
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logger.error(
|
|
175
|
+
`${colors.green('[@vuepress-plume/plugin-search]')} failed to remove stale index entries:`,
|
|
176
|
+
error,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
130
179
|
cache.length = 0
|
|
131
180
|
for (const item of createPageSectionItems(page))
|
|
132
181
|
if (index.has(item.id)) {
|
|
@@ -149,6 +198,7 @@ function* createPageSectionItems(page) {
|
|
|
149
198
|
const fileId = page.path
|
|
150
199
|
for (const section of splitPageIntoSections(html)) {
|
|
151
200
|
const {anchor, text, titles} = section
|
|
201
|
+
|
|
152
202
|
if (!text && !titles.length) continue
|
|
153
203
|
yield {
|
|
154
204
|
id: anchor ? `${fileId}#${anchor}` : fileId,
|
|
@@ -159,9 +209,9 @@ function* createPageSectionItems(page) {
|
|
|
159
209
|
}
|
|
160
210
|
}
|
|
161
211
|
|
|
162
|
-
const headingRegex = /<h(\d
|
|
212
|
+
const headingRegex = /<h(\d)[^>]*>(<a.*? href="#[^"]*"[^>]*>[\s\S]*?<\/a>)<\/h\1>/gi
|
|
163
213
|
|
|
164
|
-
const headingContentRegex = /<a.*? href="#(
|
|
214
|
+
const headingContentRegex = /<a.*? href="#([^"]*)"[^>]*><span>([\s\S]*?)<\/span><\/a>/i
|
|
165
215
|
|
|
166
216
|
const ignoreHeadingRegex = /<template[^>]*>[\s\S]*<\/template>/gi
|
|
167
217
|
|
|
@@ -178,7 +228,10 @@ function* splitPageIntoSections(html) {
|
|
|
178
228
|
const content = result[i + 2]
|
|
179
229
|
if (!title || !content) continue
|
|
180
230
|
if (level === 0) parentTitles = [title]
|
|
181
|
-
else
|
|
231
|
+
else {
|
|
232
|
+
parentTitles.length = level
|
|
233
|
+
parentTitles[level] = title
|
|
234
|
+
}
|
|
182
235
|
let titles = parentTitles.slice(0, level)
|
|
183
236
|
titles[level] = title
|
|
184
237
|
titles = titles.filter(Boolean)
|
|
@@ -192,7 +245,9 @@ function* splitPageIntoSections(html) {
|
|
|
192
245
|
|
|
193
246
|
function getSearchableText(content) {
|
|
194
247
|
content = clearHtmlTags(content)
|
|
195
|
-
return content
|
|
248
|
+
return content.length > MAX_SEARCHABLE_TEXT_LENGTH
|
|
249
|
+
? content.slice(0, MAX_SEARCHABLE_TEXT_LENGTH)
|
|
250
|
+
: content
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
function clearHtmlTags(str) {
|
|
@@ -230,6 +285,7 @@ const SEARCH_LOCALES = [
|
|
|
230
285
|
backButtonTitle: 'Close search',
|
|
231
286
|
noResultsText: 'No results for',
|
|
232
287
|
searchIndexErrorText: 'Failed to load the search index.',
|
|
288
|
+
noscriptText: 'Search is unavailable without JavaScript.',
|
|
233
289
|
footer: {
|
|
234
290
|
selectText: 'to select',
|
|
235
291
|
selectKeyAriaLabel: 'enter',
|
|
@@ -249,6 +305,7 @@ const SEARCH_LOCALES = [
|
|
|
249
305
|
backButtonTitle: '关闭',
|
|
250
306
|
noResultsText: '无搜索结果:',
|
|
251
307
|
searchIndexErrorText: '搜索索引加载失败。',
|
|
308
|
+
noscriptText: '未启用 JavaScript,搜索不可用。',
|
|
252
309
|
footer: {
|
|
253
310
|
selectText: '选择',
|
|
254
311
|
selectKeyAriaLabel: '输入',
|
|
@@ -268,6 +325,7 @@ const SEARCH_LOCALES = [
|
|
|
268
325
|
backButtonTitle: '關閉',
|
|
269
326
|
noResultsText: '無搜尋結果:',
|
|
270
327
|
searchIndexErrorText: '搜尋索引載入失敗。',
|
|
328
|
+
noscriptText: '未啟用 JavaScript,搜尋無法使用。',
|
|
271
329
|
footer: {
|
|
272
330
|
selectText: '選擇',
|
|
273
331
|
selectKeyAriaLabel: '輸入',
|
|
@@ -287,6 +345,7 @@ const SEARCH_LOCALES = [
|
|
|
287
345
|
backButtonTitle: 'Schließen',
|
|
288
346
|
noResultsText: 'Keine Suchergebnisse:',
|
|
289
347
|
searchIndexErrorText: 'Der Suchindex konnte nicht geladen werden.',
|
|
348
|
+
noscriptText: 'Die Suche ist ohne JavaScript nicht verfügbar.',
|
|
290
349
|
footer: {
|
|
291
350
|
selectText: 'Auswählen',
|
|
292
351
|
selectKeyAriaLabel: 'Eingabe',
|
|
@@ -306,6 +365,7 @@ const SEARCH_LOCALES = [
|
|
|
306
365
|
backButtonTitle: 'Fermer',
|
|
307
366
|
noResultsText: 'Aucun résultat trouvé :',
|
|
308
367
|
searchIndexErrorText: 'Échec du chargement de l’index de recherche.',
|
|
368
|
+
noscriptText: 'La recherche est indisponible sans JavaScript.',
|
|
309
369
|
footer: {
|
|
310
370
|
selectText: 'sélectionner',
|
|
311
371
|
selectKeyAriaLabel: 'Entrée',
|
|
@@ -325,6 +385,7 @@ const SEARCH_LOCALES = [
|
|
|
325
385
|
backButtonTitle: 'Закрыть',
|
|
326
386
|
noResultsText: 'Нет результатов поиска:',
|
|
327
387
|
searchIndexErrorText: 'Не удалось загрузить поисковый индекс.',
|
|
388
|
+
noscriptText: 'Поиск недоступен без JavaScript.',
|
|
328
389
|
footer: {
|
|
329
390
|
selectText: 'Выбрать',
|
|
330
391
|
selectKeyAriaLabel: 'Ввод',
|
|
@@ -344,6 +405,7 @@ const SEARCH_LOCALES = [
|
|
|
344
405
|
backButtonTitle: '閉じる',
|
|
345
406
|
noResultsText: '検索結果がありません:',
|
|
346
407
|
searchIndexErrorText: '検索インデックスの読み込みに失敗しました。',
|
|
408
|
+
noscriptText: 'JavaScript が無効のため、検索を利用できません。',
|
|
347
409
|
footer: {
|
|
348
410
|
selectText: '選択',
|
|
349
411
|
selectKeyAriaLabel: '入力',
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -22,6 +22,19 @@ export interface SearchLocaleOptions {
|
|
|
22
22
|
* 搜索索引缺失或加载失败时显示的文本。
|
|
23
23
|
*/
|
|
24
24
|
searchIndexErrorText?: string
|
|
25
|
+
/**
|
|
26
|
+
* Fallback text shown when JavaScript is disabled.
|
|
27
|
+
*
|
|
28
|
+
* 禁用 JavaScript 时显示的降级提示文本。
|
|
29
|
+
*
|
|
30
|
+
* 请注意:该文本会渲染在 `<noscript>` 中,必须为不含 `<`、`>`、`&`、引号等
|
|
31
|
+
* 需要 HTML 转义的纯文本,否则在启用脚本的浏览器中水合时会与原文不一致。
|
|
32
|
+
*
|
|
33
|
+
* Note: this text is rendered inside `<noscript>`, so it must be plain text
|
|
34
|
+
* without characters requiring HTML escaping (`<`, `>`, `&`, quotes), otherwise
|
|
35
|
+
* it would mismatch during hydration in browsers with scripting enabled.
|
|
36
|
+
*/
|
|
37
|
+
noscriptText?: string
|
|
25
38
|
/** Footer keyboard shortcut hints / 底部键盘快捷键提示 */
|
|
26
39
|
footer: {
|
|
27
40
|
/** Text for select action / 选择操作的文本 */
|
|
@@ -54,16 +67,18 @@ export type SearchBoxLocales = LocaleConfig<SearchLocaleOptions>
|
|
|
54
67
|
* MiniSearch requires the tokenizer used to build the index to be identical to
|
|
55
68
|
* the one used at query time. Defining it in one place guarantees both sides
|
|
56
69
|
* use the same strategy: `Intl.Segmenter` when available, otherwise the same
|
|
57
|
-
* deterministic whitespace fallback.
|
|
70
|
+
* deterministic whitespace fallback. Blank and punctuation-only tokens are
|
|
71
|
+
* discarded on both sides, so they never pollute the index or the query.
|
|
58
72
|
*
|
|
59
73
|
* MiniSearch 要求构建索引与查询时使用完全一致的分词器。集中定义可保证两端策略一致:
|
|
60
74
|
* 支持 `Intl.Segmenter` 时使用它分词,否则统一退化为确定性的空白符分词。
|
|
75
|
+
* 两端都会丢弃空白与纯标点词元,避免污染索引与查询。
|
|
61
76
|
*
|
|
62
77
|
* @param lang - Language code for word segmentation / 用于分词的语言代码
|
|
63
78
|
* @returns Tokenizer that splits a string into tokens / 将字符串切分为词元的函数
|
|
64
79
|
* @example
|
|
65
80
|
* const tokenize = createTokenizer('en')
|
|
66
|
-
* tokenize('Hello world') // ['Hello', '
|
|
81
|
+
* tokenize('Hello world') // ['Hello', 'world'] with Intl.Segmenter
|
|
67
82
|
*/
|
|
68
83
|
export declare function createTokenizer(lang: string): (text: string) => string[]
|
|
69
84
|
/**
|
package/dist/shared/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
function isWordToken(token) {
|
|
2
|
+
return /\p{L}|\p{N}/u.test(token)
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
function createTokenizer(lang) {
|
|
2
6
|
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
3
7
|
const segmenter = new Intl.Segmenter(lang, {granularity: 'word'})
|
|
4
|
-
return (text) =>
|
|
8
|
+
return (text) =>
|
|
9
|
+
Array.from(segmenter.segment(text))
|
|
10
|
+
.map((segment) => segment.segment)
|
|
11
|
+
.filter(isWordToken)
|
|
5
12
|
}
|
|
6
|
-
return (text) => text.split(/\s+/).filter(
|
|
13
|
+
return (text) => text.split(/\s+/).filter(isWordToken)
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
export {createTokenizer}
|
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.213",
|
|
5
5
|
"description": "The Plugin for VuePress 2 - local search",
|
|
6
6
|
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
|
|
16
16
|
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"VuePress",
|
|
19
|
+
"vuepress plugin",
|
|
20
|
+
"mini search",
|
|
21
|
+
"vuepress-plugin-search"
|
|
22
|
+
],
|
|
17
23
|
"exports": {
|
|
18
24
|
".": {
|
|
19
25
|
"types": "./dist/node/index.d.ts",
|
|
@@ -42,19 +48,13 @@
|
|
|
42
48
|
"mark.js": "^8.11.1",
|
|
43
49
|
"minisearch": "^7.2.0",
|
|
44
50
|
"p-map": "^7.0.8",
|
|
45
|
-
"sanitize-html": "^2.
|
|
51
|
+
"sanitize-html": "^2.18.0",
|
|
46
52
|
"vue": "^3.5.43"
|
|
47
53
|
},
|
|
48
54
|
"publishConfig": {
|
|
49
55
|
"access": "public",
|
|
50
56
|
"provenance": true
|
|
51
57
|
},
|
|
52
|
-
"keyword": [
|
|
53
|
-
"VuePress",
|
|
54
|
-
"vuepress plugin",
|
|
55
|
-
"mini search",
|
|
56
|
-
"vuepress-plugin-search"
|
|
57
|
-
],
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "pnpm run tsdown && pnpm run copy",
|
|
60
60
|
"clean": "rimraf --glob ./dist",
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LRU (Least Recently Used) Cache Implementation
|
|
3
|
-
*
|
|
4
|
-
* LRU(最近最少使用)缓存实现
|
|
5
|
-
*
|
|
6
|
-
* Adapted from https://stackoverflow.com/a/46432113/11613622
|
|
7
|
-
*
|
|
8
|
-
* @module plugin-search/client/utils/lru
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Generic LRU Cache implementation using Map.
|
|
12
|
-
*
|
|
13
|
-
* 使用 Map 实现的通用 LRU 缓存。
|
|
14
|
-
*
|
|
15
|
-
* Automatically evicts the least recently used item when the cache reaches
|
|
16
|
-
* its maximum size.
|
|
17
|
-
*
|
|
18
|
-
* 当缓存达到最大容量时,自动淘汰最近最少使用的项。
|
|
19
|
-
*
|
|
20
|
-
* @template K - Key type / 键类型
|
|
21
|
-
* @template V - Value type / 值类型
|
|
22
|
-
* @example
|
|
23
|
-
* const cache = new LRUCache<string, number>(3)
|
|
24
|
-
* cache.set('a', 1)
|
|
25
|
-
* cache.set('b', 2)
|
|
26
|
-
* cache.set('c', 3)
|
|
27
|
-
* cache.set('d', 4) // 'a' is evicted
|
|
28
|
-
* cache.get('b') // returns 2, 'b' becomes most recent
|
|
29
|
-
*/
|
|
30
|
-
export declare class LRUCache<K, V> {
|
|
31
|
-
/** Maximum number of items in the cache / 缓存中的最大项数 */
|
|
32
|
-
private max
|
|
33
|
-
/** Internal Map storage / 内部 Map 存储 */
|
|
34
|
-
private cache
|
|
35
|
-
/**
|
|
36
|
-
* Create a new LRU Cache instance.
|
|
37
|
-
*
|
|
38
|
-
* 创建新的 LRU 缓存实例。
|
|
39
|
-
*
|
|
40
|
-
* @param max - Maximum cache size (default: 10) / 最大缓存大小(默认:10)
|
|
41
|
-
*/
|
|
42
|
-
constructor(max?: number)
|
|
43
|
-
/**
|
|
44
|
-
* Get a value from the cache.
|
|
45
|
-
*
|
|
46
|
-
* 从缓存中获取值。
|
|
47
|
-
*
|
|
48
|
-
* Accessing an item moves it to the end (most recently used position).
|
|
49
|
-
*
|
|
50
|
-
* 访问项会将其移动到末尾(最近使用的位置)。
|
|
51
|
-
*
|
|
52
|
-
* @param key - Cache key / 缓存键
|
|
53
|
-
* @returns Cached value or undefined if not found / 缓存值,未找到则返回 undefined
|
|
54
|
-
*/
|
|
55
|
-
get(key: K): V | undefined
|
|
56
|
-
/**
|
|
57
|
-
* Set a value in the cache.
|
|
58
|
-
*
|
|
59
|
-
* 在缓存中设置值。
|
|
60
|
-
*
|
|
61
|
-
* If the key already exists, it is moved to the end. If the cache is full,
|
|
62
|
-
* the oldest item is evicted.
|
|
63
|
-
*
|
|
64
|
-
* 如果键已存在,则将其移动到末尾。如果缓存已满,则淘汰最旧的项。
|
|
65
|
-
*
|
|
66
|
-
* @param key - Cache key / 缓存键
|
|
67
|
-
* @param val - Value to cache / 要缓存的值
|
|
68
|
-
*/
|
|
69
|
-
set(key: K, val: V): void
|
|
70
|
-
/**
|
|
71
|
-
* Get the first (oldest) key in the cache.
|
|
72
|
-
*
|
|
73
|
-
* 获取缓存中的第一个(最旧的)键。
|
|
74
|
-
*
|
|
75
|
-
* @returns The oldest key or undefined if cache is empty / 最旧的键,缓存为空则返回 undefined
|
|
76
|
-
*/
|
|
77
|
-
first(): K | undefined
|
|
78
|
-
/**
|
|
79
|
-
* Clear all items from the cache.
|
|
80
|
-
*
|
|
81
|
-
* 清空缓存中的所有项。
|
|
82
|
-
*/
|
|
83
|
-
clear(): void
|
|
84
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
var LRUCache = class {
|
|
2
|
-
max
|
|
3
|
-
|
|
4
|
-
cache
|
|
5
|
-
|
|
6
|
-
constructor(max = 10) {
|
|
7
|
-
this.max = max
|
|
8
|
-
this.cache = new Map()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
get(key) {
|
|
12
|
-
const item = this.cache.get(key)
|
|
13
|
-
if (item !== void 0) {
|
|
14
|
-
this.cache.delete(key)
|
|
15
|
-
this.cache.set(key, item)
|
|
16
|
-
}
|
|
17
|
-
return item
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
set(key, val) {
|
|
21
|
-
if (this.cache.has(key)) this.cache.delete(key)
|
|
22
|
-
else if (this.cache.size === this.max) this.cache.delete(this.first())
|
|
23
|
-
this.cache.set(key, val)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
first() {
|
|
27
|
-
return this.cache.keys().next().value
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
clear() {
|
|
31
|
-
this.cache.clear()
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export {LRUCache}
|