Skip to content

Commit

Permalink
feat: enhance lunr search for modern template (dotnet#9264)
Browse files Browse the repository at this point in the history
  • Loading branch information
yufeih authored and p-kostov committed Jun 28, 2024
1 parent b93aae3 commit 4e9c472
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 304 deletions.
3 changes: 3 additions & 0 deletions templates/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ async function buildModernTemplate() {
'modern/src/docfx.ts',
'modern/src/search-worker.ts',
],
external: [
'./main.js'
],
plugins: [
sassPlugin()
],
Expand Down
1 change: 0 additions & 1 deletion templates/modern/layout/_master.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{{!include(/^public/.*/)}}
{{!include(favicon.ico)}}
{{!include(logo.svg)}}
{{!include(search-stopwords.json)}}
<!DOCTYPE html>
<html {{#_lang}}lang="{{_lang}}"{{/_lang}}>
<head>
Expand Down
4 changes: 4 additions & 0 deletions templates/modern/src/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import BootstrapIcons from 'bootstrap-icons/font/bootstrap-icons.json'
import { HLJSApi } from 'highlight.js'
import { AnchorJSOptions } from 'anchor-js'
import { MermaidConfig } from 'mermaid'
import lunr from 'lunr'

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

Expand Down Expand Up @@ -37,4 +38,7 @@ export type DocfxOptions = {

/** Configures [hightlight.js](https://highlightjs.org/) */
configureHljs?: (hljs: HLJSApi) => void,

/** Configures [lunr](https://lunrjs.com/docs/index.html) */
configureLunr?: (lunr: lunr.Builder) => void,
}
105 changes: 44 additions & 61 deletions templates/modern/src/search-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,64 @@
// The .NET Foundation licenses this file to you under the MIT license.

import lunr from 'lunr'
import { get, set, createStore } from 'idb-keyval'
import { DocfxOptions } from './options'

let lunrIndex

let stopWords = null
let searchData = {}

lunr.tokenizer.separator = /[\s\-.()]+/

const stopWordsRequest = new XMLHttpRequest()
stopWordsRequest.open('GET', '../search-stopwords.json')
stopWordsRequest.onload = function() {
if (this.status !== 200) {
return
}
stopWords = JSON.parse(this.responseText)
buildIndex()
type SearchHit = {
href: string
title: string
keywords: string
}
stopWordsRequest.send()

const searchDataRequest = new XMLHttpRequest()

searchDataRequest.open('GET', '../index.json')
searchDataRequest.onload = function() {
if (this.status !== 200) {
return
}
searchData = JSON.parse(this.responseText)

buildIndex()
let search: (q: string) => SearchHit[]

async function loadIndex() {
const { index, data } = await loadIndexCore()
search = q => index.search(q).map(({ ref }) => data[ref])
postMessage({ e: 'index-ready' })
}
searchDataRequest.send()

onmessage = function(oEvent) {
const q = oEvent.data.q
const results = []
if (lunrIndex) {
const hits = lunrIndex.search(q)
hits.forEach(function(hit) {
const item = searchData[hit.ref]
results.push({ href: item.href, title: item.title, keywords: item.keywords })
})
async function loadIndexCore() {
const res = await fetch('../index.json')
const etag = res.headers.get('etag')
const data = await res.json() as { [key: string]: SearchHit }
const cache = createStore('docfx', 'lunr')

if (etag) {
const value = JSON.parse(await get('index', cache) || '{}')
if (value && value.etag === etag) {
return { index: lunr.Index.load(value), data }
}
}
postMessage({ e: 'query-ready', q, d: results })
}

function buildIndex() {
if (stopWords !== null && !isEmpty(searchData)) {
lunrIndex = lunr(function() {
this.pipeline.remove(lunr.stopWordFilter)
this.ref('href')
this.field('title', { boost: 50 })
this.field('keywords', { boost: 20 })
const main = await import('./main.js')
const docfx = main.default as DocfxOptions

const index = lunr(function() {
this.pipeline.remove(lunr.stopWordFilter)
this.ref('href')
this.field('title', { boost: 50 })
this.field('keywords', { boost: 20 })

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

const docfxStopWordFilter = lunr.generateStopWordFilter(stopWords)
lunr.Pipeline.registerFunction(docfxStopWordFilter, 'docfxStopWordFilter')
this.pipeline.add(docfxStopWordFilter)
this.searchPipeline.add(docfxStopWordFilter)
})
for (const key in data) {
this.add(data[key])
}
})

if (etag) {
await set('index', JSON.stringify(Object.assign(index.toJSON(), { etag })), cache)
}

return { index, data }
}

function isEmpty(obj) {
if (!obj) return true
loadIndex().catch(console.error)

for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) { return false }
onmessage = function(e) {
if (search) {
postMessage({ e: 'query-ready', d: search(e.data.q) })
}

return true
}
24 changes: 24 additions & 0 deletions templates/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.1",
"highlight.js": "^11.8.0",
"idb-keyval": "^6.2.1",
"jquery": "3.7.0",
"lit-html": "^2.8.0",
"lunr": "2.3.9",
Expand All @@ -38,6 +39,7 @@
},
"devDependencies": {
"@types/jest": "^29.5.5",
"@types/lunr": "^2.3.5",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"browser-sync": "^2.29.3",
Expand Down

This file was deleted.

Loading

0 comments on commit 4e9c472

Please sign in to comment.