Documentation Index
Fetch the complete documentation index at: https://mintlify.com/johnfactotum/foliate-js/llms.txt
Use this file to discover all available pages before exploring further.
Full-text search in foliate-js scans each section of the book in order, yielding results as they are found rather than waiting for the entire book to be processed. Search results are automatically drawn as highlighted outlines in the reading view using the same overlayer system as annotations. Because searching can be slow for large books, the async generator API lets you update your UI incrementally.
Running a search
view.search(opts) is an async generator. Use it in a for await loop:
for await (const result of view.search({ query: 'whale' })) {
if (result === 'done') {
console.log('Search complete')
break
}
if (result.progress != null) {
// update a progress bar
console.log(`Scanning: ${Math.round(result.progress * 100)}%`)
}
if (result.subitems) {
// results for one section
for (const { cfi, excerpt } of result.subitems) {
const { pre, match, post } = excerpt
console.log(`${pre}[${match}]${post}`)
}
}
}
Search options
| Option | Type | Description |
|---|
query | string | The text to search for. Required. |
index | number | If provided, searches only the section at this index. Omit to search all sections. |
draw | function | Draw function for highlights. Defaults to Overlayer.outline. |
drawOptions | object | Options passed to draw. |
locales | string | BCP 47 locale tag used for collation. Defaults to the book’s language. |
sensitivity | string | 'base' ignores case and diacritics (default), 'accent' distinguishes diacritics, 'case' distinguishes case, 'variant' is an exact match. |
granularity | string | 'grapheme' (default) or 'word'. Use 'word' to match whole words only. |
matchDiacritics | boolean | Shorthand to require diacritic-exact matches. |
matchCase | boolean | Shorthand to require case-exact matches. |
matchWholeWords | boolean | Shorthand to require whole-word matches. |
Yielded values
The generator yields three kinds of values:
Progress updates:
{ progress: 0.42 } // number between 0 and 1
Results for a section:
{
index: 3, // section index (only present when subitems exist)
subitems: [
{
cfi: 'epubcfi(/6/8!/4/2/1:147,/1:152)',
excerpt: {
pre: 'Call me Ishmael. Some years ago—',
match: 'whale',
post: ' ship; little money in my purse'
}
}
]
}
Completion signal:
The index property on section results corresponds to the section index in book.sections. It is only present on objects that contain subitems; progress objects do not have an index.
Calling view.search() automatically calls view.clearSearch() first, so any previous search highlights are removed before the new search begins.
Clearing search highlights
This removes all search overlay annotations from every section. Call it when the user closes the search panel or starts a new search.
Searching a single section
Pass index to restrict the search to one section:
const { index } = view.lastLocation
for await (const result of view.search({ query: 'albatross', index })) {
if (result === 'done') break
if (result.subitems) {
for (const item of result.subitems) {
console.log(item.cfi, item.excerpt)
}
}
}
Single-section search still uses the async generator API. The only difference is that progress events are not emitted and only one section result can appear.
Customizing search highlight appearance
By default, search matches are highlighted with Overlayer.outline (a rounded rectangle border). You can change this:
import { Overlayer } from './foliate-js/overlayer.js'
for await (const result of view.search({
query: 'Ahab',
draw: Overlayer.highlight,
drawOptions: { color: '#fbbf24' },
})) {
// ...
}
Complete search UI example
import './foliate-js/view.js'
const view = document.createElement('foliate-view')
document.body.append(view)
await view.open('moby-dick.epub')
const input = document.getElementById('search-input')
const resultsContainer = document.getElementById('search-results')
const progressBar = document.getElementById('search-progress')
let searchAbortController = null
async function runSearch(query) {
// cancel any previous search rendering loop
searchAbortController?.abort()
searchAbortController = new AbortController()
const { signal } = searchAbortController
resultsContainer.innerHTML = ''
progressBar.style.width = '0%'
for await (const result of view.search({ query, granularity: 'word' })) {
if (signal.aborted) break
if (result === 'done') {
progressBar.style.width = '100%'
break
}
if (result.progress != null) {
progressBar.style.width = `${Math.round(result.progress * 100)}%`
continue
}
if (result.subitems) {
const section = document.createElement('div')
section.className = 'search-section'
for (const { cfi, excerpt } of result.subitems) {
const item = document.createElement('button')
item.innerHTML = `
<span class="pre">${excerpt.pre}</span>
<mark>${excerpt.match}</mark>
<span class="post">${excerpt.post}</span>
`
item.addEventListener('click', () => view.goTo(cfi))
section.append(item)
}
resultsContainer.append(section)
}
}
}
input.addEventListener('input', e => {
const query = e.target.value.trim()
if (query.length >= 2) runSearch(query)
else view.clearSearch()
})
Search is CPU-intensive and can block the main thread for large sections. There is no built-in Web Worker support. If you notice jank, consider adding await new Promise(r => setTimeout(r, 0)) between sections to yield to the browser.
Using searchMatcher directly
For advanced use cases — searching across an arbitrary array of strings rather than a loaded book — you can import searchMatcher from search.js and use it standalone:
import { searchMatcher } from './foliate-js/search.js'
import { textWalker } from './foliate-js/text-walker.js'
const matcher = searchMatcher(textWalker, {
defaultLocale: 'en',
matchWholeWords: true,
matchCase: false,
matchDiacritics: false,
})
// matcher returns a generator given a Document and a query string
for (const { range, excerpt } of matcher(doc, 'whale')) {
console.log(excerpt.match, range)
}
searchMatcher takes a textWalker function and an options object, and returns a function (doc, query) that yields { range, excerpt } objects. The range here is a DOM Range on the section document, not a CFI. <foliate-view> converts these ranges to CFIs internally.