cheatsheets/_js/search/index.js

123 lines
2.2 KiB
JavaScript

/**
* Lunr-based searching
*
* @example
*
* <meta name="search:index" content="https://search.devhints.io/index.json">
*
* s = new Search()
* s.inferFromMeta()
* s.search('hello').then(() => ...)
*
* s.loadFromData({ index, store })
*/
class Search {
constructor (opts) {
const once = require('once')
const Emitter = require('events')
this.indexPath = opts && opts.index
this.load = once(this.load)
this.index = null
this.store = null
this.emitter = new Emitter()
}
load () {
return this.loadRemote()
}
/**
* Infers `indexPath` from meta tags.
*/
inferFromMeta () {
// Safeguard, in case we're not loaded in a browser context
if (typeof document === 'undefined') return this
const meta = document.querySelector('meta[name="search:index"]')
if (!meta) return this
const value = meta.getAttribute('content')
this.indexPath = value
return this
}
/**
* Loads the search index remotely (via `indexPath`).
*/
loadRemote () {
return window.fetch(this.indexPath)
.then(res => res.json())
.then(data => {
this.loadFromData(data)
})
}
/**
* Loads the search index locally.
*/
loadFromData (data) {
const lunr = require('lunr')
const { index, store } = data
this.index = lunr.Index.load(index)
this.store = store
this.emitter.emit('load')
}
/**
* Waits until the data is loaded, then resolves.
*
* - When called when the data is still loading, it'll wait until the data is
* loaded.
*/
loaded () {
return new Promise((resolve, reject) => {
if (this.index) return resolve(this)
this.emitter.on('load', () => {
resolve(this)
})
this.load()
})
}
/*
* Searches for keywords.
*/
async search (keywords) {
await this.loaded()
const results = this.index.search(keywords)
const fullResults = results.map(result => {
return {
...result,
object: this.store[result.ref]
}
})
return fullResults
}
}
/*
* The primary instance
*/
const search = new Search().inferFromMeta()
/*
* Export
*/
module.exports = {
Search,
search
}