Skip to content

Commit 5ff3096

Browse files
committed
improvement: Use existing match highlighter for search matches.
1 parent f71fcd6 commit 5ff3096

File tree

4 files changed

+70
-58
lines changed

4 files changed

+70
-58
lines changed

assets/js/autocomplete/suggestions.js

+2-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getSidebarNodes } from '../globals'
2-
import { escapeRegexModifiers, escapeHtmlEntities, isBlank } from '../helpers'
2+
import { isBlank } from '../helpers'
3+
import { highlightMatches } from '../highlighter'
34

45
/**
56
* @typedef Suggestion
@@ -285,27 +286,3 @@ function startsWith (text, subtext) {
285286
function tokenize (query) {
286287
return query.trim().split(/\s+/)
287288
}
288-
289-
/**
290-
* Returns an HTML string highlighting the individual tokens from the query string.
291-
*/
292-
function highlightMatches (text, query) {
293-
// Sort terms length, so that the longest are highlighted first.
294-
const terms = tokenize(query).sort((term1, term2) => term2.length - term1.length)
295-
return highlightTerms(text, terms)
296-
}
297-
298-
function highlightTerms (text, terms) {
299-
if (terms.length === 0) return text
300-
301-
const [firstTerm, ...otherTerms] = terms
302-
const match = text.match(new RegExp(`(.*)(${escapeRegexModifiers(firstTerm)})(.*)`, 'i'))
303-
304-
if (match) {
305-
const [, before, matching, after] = match
306-
// Note: this has exponential complexity, but we expect just a few terms, so that's fine.
307-
return highlightTerms(before, terms) + '<em>' + escapeHtmlEntities(matching) + '</em>' + highlightTerms(after, terms)
308-
} else {
309-
return highlightTerms(text, otherTerms)
310-
}
311-
}

assets/js/highlighter.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { escapeRegexModifiers, escapeHtmlEntities } from './helpers'
2+
3+
/**
4+
* Returns an HTML string highlighting the individual tokens from the query string.
5+
*/
6+
export function highlightMatches (text, query, opts = {}) {
7+
// Sort terms length, so that the longest are highlighted first.
8+
if (typeof query === 'string') {
9+
query = query.split(/\s+/)
10+
}
11+
const terms = query.sort((term1, term2) => term2.length - term1.length)
12+
return highlightTerms(text, terms, opts)
13+
}
14+
15+
function highlightTerms (text, terms, opts) {
16+
if (terms.length === 0) return text
17+
18+
let flags = 'i'
19+
20+
if (opts.multiline) {
21+
flags = 'is'
22+
}
23+
24+
const [firstTerm, ...otherTerms] = terms
25+
const match = text.match(new RegExp(`(.*)(${escapeRegexModifiers(firstTerm)})(.*)`, flags))
26+
27+
if (match) {
28+
const [, before, matching, after] = match
29+
// Note: this has exponential complexity, but we expect just a few terms, so that's fine.
30+
return highlightTerms(before, terms, opts) + '<em>' + escapeHtmlEntities(matching) + '</em>' + highlightTerms(after, terms, opts)
31+
} else {
32+
return highlightTerms(text, otherTerms, opts)
33+
}
34+
}

assets/js/search-page.js

+24-23
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { qs, escapeHtmlEntities, isBlank, getQueryParamByName, getProjectNameAnd
55
import { setSearchInputValue } from './search-bar'
66
import searchResultsTemplate from './handlebars/templates/search-results.handlebars'
77
import { getSearchNodes } from './globals'
8+
import { highlightMatches } from './highlighter'
89

910
const EXCERPT_RADIUS = 80
1011
const SEARCH_CONTAINER_SELECTOR = '#search'
@@ -24,7 +25,7 @@ lunr.Pipeline.registerFunction(docTrimmerFunction, 'docTrimmer')
2425
window.addEventListener('swup:page:view', initialize)
2526
initialize()
2627

27-
function initialize () {
28+
function initialize() {
2829
const pathname = window.location.pathname
2930
if (pathname.endsWith('/search.html') || pathname.endsWith('/search')) {
3031
const query = getQueryParamByName('q')
@@ -33,7 +34,7 @@ function initialize () {
3334
}
3435
}
3536

36-
async function search (value, queryType) {
37+
async function search(value, queryType) {
3738
if (isBlank(value)) {
3839
renderResults({ value })
3940
} else {
@@ -56,7 +57,7 @@ async function search (value, queryType) {
5657
}
5758
}
5859

59-
async function localSearch (value) {
60+
async function localSearch(value) {
6061
const index = await getIndex()
6162

6263
// We cannot match on atoms :foo because that would be considered
@@ -65,7 +66,7 @@ async function localSearch (value) {
6566
return searchResultsToDecoratedSearchItems(index.search(fixedValue))
6667
}
6768

68-
async function remoteSearch (value, queryType, searchNodes) {
69+
async function remoteSearch(value, queryType, searchNodes) {
6970
let filterNodes = searchNodes
7071

7172
if (queryType === 'latest') {
@@ -86,7 +87,7 @@ async function remoteSearch (value, queryType, searchNodes) {
8687
return payload.hits.map(result => {
8788
const [packageName, packageVersion] = result.document.package.split('-')
8889

89-
const doc = result.document.doc
90+
const doc = highlightMatches(result.document.doc, value, { multiline: true })
9091
const excerpts = [doc]
9192
const metadata = {}
9293
const ref = `https://hexdocs.pm/${packageName}/${packageVersion}/${result.document.ref}`
@@ -107,13 +108,13 @@ async function remoteSearch (value, queryType, searchNodes) {
107108
}
108109
}
109110

110-
function renderResults ({ value, results, errorMessage }) {
111+
function renderResults({ value, results, errorMessage }) {
111112
const searchContainer = qs(SEARCH_CONTAINER_SELECTOR)
112113
const resultsHtml = searchResultsTemplate({ value, results, errorMessage })
113114
searchContainer.innerHTML = resultsHtml
114115
}
115116

116-
async function getIndex () {
117+
async function getIndex() {
117118
const cachedIndex = await loadIndex()
118119
if (cachedIndex) { return cachedIndex }
119120

@@ -122,7 +123,7 @@ async function getIndex () {
122123
return index
123124
}
124125

125-
async function loadIndex () {
126+
async function loadIndex() {
126127
try {
127128
const serializedIndex = sessionStorage.getItem(indexStorageKey())
128129
if (serializedIndex) {
@@ -137,7 +138,7 @@ async function loadIndex () {
137138
}
138139
}
139140

140-
async function saveIndex (index) {
141+
async function saveIndex(index) {
141142
try {
142143
const serializedIndex = await compress(index)
143144
sessionStorage.setItem(indexStorageKey(), serializedIndex)
@@ -146,7 +147,7 @@ async function saveIndex (index) {
146147
}
147148
}
148149

149-
async function compress (index) {
150+
async function compress(index) {
150151
const stream = new Blob([JSON.stringify(index)], {
151152
type: 'application/json'
152153
}).stream().pipeThrough(new window.CompressionStream('gzip'))
@@ -156,7 +157,7 @@ async function compress (index) {
156157
return b64encode(buffer)
157158
}
158159

159-
async function decompress (index) {
160+
async function decompress(index) {
160161
const stream = new Blob([b64decode(index)], {
161162
type: 'application/json'
162163
}).stream().pipeThrough(new window.DecompressionStream('gzip'))
@@ -165,7 +166,7 @@ async function decompress (index) {
165166
return JSON.parse(blob)
166167
}
167168

168-
function b64encode (buffer) {
169+
function b64encode(buffer) {
169170
let binary = ''
170171
const bytes = new Uint8Array(buffer)
171172
const len = bytes.byteLength
@@ -175,7 +176,7 @@ function b64encode (buffer) {
175176
return window.btoa(binary)
176177
}
177178

178-
function b64decode (str) {
179+
function b64decode(str) {
179180
const binaryString = window.atob(str)
180181
const len = binaryString.length
181182
const bytes = new Uint8Array(new ArrayBuffer(len))
@@ -185,11 +186,11 @@ function b64decode (str) {
185186
return bytes
186187
}
187188

188-
function indexStorageKey () {
189+
function indexStorageKey() {
189190
return `idv5:${getProjectNameAndVersion()}`
190191
}
191192

192-
function createIndex () {
193+
function createIndex() {
193194
return lunr(function () {
194195
this.ref('ref')
195196
this.field('title', { boost: 3 })
@@ -207,11 +208,11 @@ function createIndex () {
207208
})
208209
}
209210

210-
function docTokenSplitter (builder) {
211+
function docTokenSplitter(builder) {
211212
builder.pipeline.before(lunr.stemmer, docTokenFunction)
212213
}
213214

214-
function docTokenFunction (token) {
215+
function docTokenFunction(token) {
215216
// If we have something with an arity, we split on : . to make partial
216217
// matches easier. We split only when tokenizing, not when searching.
217218
// Below we use ExDoc.Markdown.to_ast/2 as an example.
@@ -275,11 +276,11 @@ function docTokenFunction (token) {
275276
return tokens
276277
}
277278

278-
function docTrimmer (builder) {
279+
function docTrimmer(builder) {
279280
builder.pipeline.before(lunr.stemmer, docTrimmerFunction)
280281
}
281282

282-
function docTrimmerFunction (token) {
283+
function docTrimmerFunction(token) {
283284
// Preserve @ and : at the beginning of tokens,
284285
// and ? and ! at the end of tokens. It needs to
285286
// be done before stemming, otherwise search and
@@ -289,7 +290,7 @@ function docTrimmerFunction (token) {
289290
})
290291
}
291292

292-
function searchResultsToDecoratedSearchItems (results) {
293+
function searchResultsToDecoratedSearchItems(results) {
293294
return results
294295
// If the docs are regenerated without changing its version,
295296
// a reference may have been doc'ed false in the code but
@@ -306,11 +307,11 @@ function searchResultsToDecoratedSearchItems (results) {
306307
})
307308
}
308309

309-
function getSearchItemByRef (ref) {
310+
function getSearchItemByRef(ref) {
310311
return searchData.items.find(searchItem => searchItem.ref === ref) || null
311312
}
312313

313-
function getExcerpts (searchItem, metadata) {
314+
function getExcerpts(searchItem, metadata) {
314315
const { doc } = searchItem
315316
const searchTerms = Object.keys(metadata)
316317

@@ -331,7 +332,7 @@ function getExcerpts (searchItem, metadata) {
331332
return excerpts.slice(0, 1)
332333
}
333334

334-
function excerpt (doc, sliceStart, sliceLength) {
335+
function excerpt(doc, sliceStart, sliceLength) {
335336
const startPos = Math.max(sliceStart - EXCERPT_RADIUS, 0)
336337
const endPos = Math.min(sliceStart + sliceLength + EXCERPT_RADIUS, doc.length)
337338
return [

0 commit comments

Comments
 (0)