Skip to content

Commit af0eaa8

Browse files
authored
feat: enhance lunr search for modern template (#9264)
1 parent d260495 commit af0eaa8

File tree

8 files changed

+77
-304
lines changed

8 files changed

+77
-304
lines changed

templates/build.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ async function buildModernTemplate() {
5252
'modern/src/docfx.ts',
5353
'modern/src/search-worker.ts',
5454
],
55+
external: [
56+
'./main.js'
57+
],
5558
plugins: [
5659
sassPlugin()
5760
],

templates/modern/layout/_master.tmpl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
{{!include(/^public/.*/)}}
33
{{!include(favicon.ico)}}
44
{{!include(logo.svg)}}
5-
{{!include(search-stopwords.json)}}
65
<!DOCTYPE html>
76
<html {{#_lang}}lang="{{_lang}}"{{/_lang}}>
87
<head>

templates/modern/src/options.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import BootstrapIcons from 'bootstrap-icons/font/bootstrap-icons.json'
55
import { HLJSApi } from 'highlight.js'
66
import { AnchorJSOptions } from 'anchor-js'
77
import { MermaidConfig } from 'mermaid'
8+
import lunr from 'lunr'
89

910
export type Theme = 'light' | 'dark' | 'auto'
1011

@@ -37,4 +38,7 @@ export type DocfxOptions = {
3738

3839
/** Configures [hightlight.js](https://highlightjs.org/) */
3940
configureHljs?: (hljs: HLJSApi) => void,
41+
42+
/** Configures [lunr](https://lunrjs.com/docs/index.html) */
43+
configureLunr?: (lunr: lunr.Builder) => void,
4044
}

templates/modern/src/search-worker.ts

Lines changed: 44 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,64 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
import lunr from 'lunr'
5+
import { get, set, createStore } from 'idb-keyval'
6+
import { DocfxOptions } from './options'
57

6-
let lunrIndex
7-
8-
let stopWords = null
9-
let searchData = {}
10-
11-
lunr.tokenizer.separator = /[\s\-.()]+/
12-
13-
const stopWordsRequest = new XMLHttpRequest()
14-
stopWordsRequest.open('GET', '../search-stopwords.json')
15-
stopWordsRequest.onload = function() {
16-
if (this.status !== 200) {
17-
return
18-
}
19-
stopWords = JSON.parse(this.responseText)
20-
buildIndex()
8+
type SearchHit = {
9+
href: string
10+
title: string
11+
keywords: string
2112
}
22-
stopWordsRequest.send()
23-
24-
const searchDataRequest = new XMLHttpRequest()
25-
26-
searchDataRequest.open('GET', '../index.json')
27-
searchDataRequest.onload = function() {
28-
if (this.status !== 200) {
29-
return
30-
}
31-
searchData = JSON.parse(this.responseText)
3213

33-
buildIndex()
14+
let search: (q: string) => SearchHit[]
3415

16+
async function loadIndex() {
17+
const { index, data } = await loadIndexCore()
18+
search = q => index.search(q).map(({ ref }) => data[ref])
3519
postMessage({ e: 'index-ready' })
3620
}
37-
searchDataRequest.send()
3821

39-
onmessage = function(oEvent) {
40-
const q = oEvent.data.q
41-
const results = []
42-
if (lunrIndex) {
43-
const hits = lunrIndex.search(q)
44-
hits.forEach(function(hit) {
45-
const item = searchData[hit.ref]
46-
results.push({ href: item.href, title: item.title, keywords: item.keywords })
47-
})
22+
async function loadIndexCore() {
23+
const res = await fetch('../index.json')
24+
const etag = res.headers.get('etag')
25+
const data = await res.json() as { [key: string]: SearchHit }
26+
const cache = createStore('docfx', 'lunr')
27+
28+
if (etag) {
29+
const value = JSON.parse(await get('index', cache) || '{}')
30+
if (value && value.etag === etag) {
31+
return { index: lunr.Index.load(value), data }
32+
}
4833
}
49-
postMessage({ e: 'query-ready', q, d: results })
50-
}
5134

52-
function buildIndex() {
53-
if (stopWords !== null && !isEmpty(searchData)) {
54-
lunrIndex = lunr(function() {
55-
this.pipeline.remove(lunr.stopWordFilter)
56-
this.ref('href')
57-
this.field('title', { boost: 50 })
58-
this.field('keywords', { boost: 20 })
35+
const main = await import('./main.js')
36+
const docfx = main.default as DocfxOptions
37+
38+
const index = lunr(function() {
39+
this.pipeline.remove(lunr.stopWordFilter)
40+
this.ref('href')
41+
this.field('title', { boost: 50 })
42+
this.field('keywords', { boost: 20 })
5943

60-
for (const prop in searchData) {
61-
if (Object.prototype.hasOwnProperty.call(searchData, prop)) {
62-
this.add(searchData[prop])
63-
}
64-
}
44+
lunr.tokenizer.separator = /[\s\-.()]+/
45+
docfx.configureLunr?.(this)
6546

66-
const docfxStopWordFilter = lunr.generateStopWordFilter(stopWords)
67-
lunr.Pipeline.registerFunction(docfxStopWordFilter, 'docfxStopWordFilter')
68-
this.pipeline.add(docfxStopWordFilter)
69-
this.searchPipeline.add(docfxStopWordFilter)
70-
})
47+
for (const key in data) {
48+
this.add(data[key])
49+
}
50+
})
51+
52+
if (etag) {
53+
await set('index', JSON.stringify(Object.assign(index.toJSON(), { etag })), cache)
7154
}
55+
56+
return { index, data }
7257
}
7358

74-
function isEmpty(obj) {
75-
if (!obj) return true
59+
loadIndex().catch(console.error)
7660

77-
for (const prop in obj) {
78-
if (Object.prototype.hasOwnProperty.call(obj, prop)) { return false }
61+
onmessage = function(e) {
62+
if (search) {
63+
postMessage({ e: 'query-ready', d: search(e.data.q) })
7964
}
80-
81-
return true
8265
}

templates/package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"bootstrap": "^5.3.2",
3131
"bootstrap-icons": "^1.11.1",
3232
"highlight.js": "^11.8.0",
33+
"idb-keyval": "^6.2.1",
3334
"jquery": "3.7.0",
3435
"lit-html": "^2.8.0",
3536
"lunr": "2.3.9",
@@ -38,6 +39,7 @@
3839
},
3940
"devDependencies": {
4041
"@types/jest": "^29.5.5",
42+
"@types/lunr": "^2.3.5",
4143
"@typescript-eslint/eslint-plugin": "^6.7.4",
4244
"@typescript-eslint/parser": "^6.7.4",
4345
"browser-sync": "^2.29.3",

test/docfx.Snapshot.Tests/SamplesTest.Extensions/search-stopwords.verified.json

Lines changed: 0 additions & 121 deletions
This file was deleted.

0 commit comments

Comments
 (0)