GET https://library.freie-volksmission.de/3rdparty/foliatejs/view.js

HTTP Client

1 Total requests
0 HTTP errors

Clients

http_client 1

Requests

GET https://johnfactotum.github.io/foliate-js/view.js
Response 200
[
  "info" => [
    "start_time" => 1768903721.7818
    "connect_time" => 0.072431087493896
    "pretransfer_time" => 0.19158005714417
    "starttransfer_time" => 0.19142317771912
    "total_time" => 0.1924090385437
    "namelookup_time" => 0.0093581676483154
    "size_download" => 5684
    "primary_ip" => "185.199.109.153"
    "primary_port" => 443
    "debug" => """
      * Enable the curl extension for better performance\n
      * Hostname was NOT found in DNS cache\n
      * Added johnfactotum.github.io:0:185.199.109.153 to DNS cache\n
      *   Trying 185.199.109.153...\n
      > GET /foliate-js/view.js HTTP/1.1 \r\n
      Accept: */*\r\n
      Accept-Encoding: gzip\r\n
      User-Agent: Symfony HttpClient (Native)\r\n
      Host: johnfactotum.github.io\r\n
      \r\n
      < HTTP/1.1 200 OK\r\n
      < Connection: close\r\n
      < Content-Length: 5684\r\n
      < Server: GitHub.com\r\n
      < Content-Type: application/javascript; charset=utf-8\r\n
      < Last-Modified: Fri, 28 Nov 2025 22:40:32 GMT\r\n
      < Access-Control-Allow-Origin: *\r\n
      < Strict-Transport-Security: max-age=31556952\r\n
      < ETag: W/"692a24e0-5894"\r\n
      < expires: Tue, 20 Jan 2026 10:18:41 GMT\r\n
      < Cache-Control: max-age=600\r\n
      < Content-Encoding: gzip\r\n
      < x-proxy-cache: MISS\r\n
      < X-GitHub-Request-Id: 0EA6:1A4CBD:408935A:41A4EF2:696F5428\r\n
      < Accept-Ranges: bytes\r\n
      < Age: 0\r\n
      < Date: Tue, 20 Jan 2026 10:08:41 GMT\r\n
      < Via: 1.1 varnish\r\n
      < X-Served-By: cache-fra-etou8220159-FRA\r\n
      < X-Cache: MISS\r\n
      < X-Cache-Hits: 0\r\n
      < X-Timer: S1768903722.856273,VS0,VE115\r\n
      < Vary: Accept-Encoding\r\n
      < X-Fastly-Request-ID: 0d4fd2eef7c0133b43d84c594d1ce28dd4150439\r\n
      < \r\n
      """
    "original_url" => "https://johnfactotum.github.io/foliate-js/view.js"
    "pause_handler" => Closure(float $duration) {#653
      class: "Symfony\Component\HttpClient\Response\NativeResponse"
      use: {
        $pauseExpiry: 0.0
      }
    }
  ]
  "response_headers" => [
    "HTTP/1.1 200 OK"
    "Connection: close"
    "Content-Length: 5684"
    "Server: GitHub.com"
    "Content-Type: application/javascript; charset=utf-8"
    "Last-Modified: Fri, 28 Nov 2025 22:40:32 GMT"
    "Access-Control-Allow-Origin: *"
    "Strict-Transport-Security: max-age=31556952"
    "ETag: W/"692a24e0-5894""
    "expires: Tue, 20 Jan 2026 10:18:41 GMT"
    "Cache-Control: max-age=600"
    "Content-Encoding: gzip"
    "x-proxy-cache: MISS"
    "X-GitHub-Request-Id: 0EA6:1A4CBD:408935A:41A4EF2:696F5428"
    "Accept-Ranges: bytes"
    "Age: 0"
    "Date: Tue, 20 Jan 2026 10:08:41 GMT"
    "Via: 1.1 varnish"
    "X-Served-By: cache-fra-etou8220159-FRA"
    "X-Cache: MISS"
    "X-Cache-Hits: 0"
    "X-Timer: S1768903722.856273,VS0,VE115"
    "Vary: Accept-Encoding"
    "X-Fastly-Request-ID: 0d4fd2eef7c0133b43d84c594d1ce28dd4150439"
  ]
  "response_content" => [
    """
      import * as CFI from './epubcfi.js'\n
      import { TOCProgress, SectionProgress } from './progress.js'\n
      import { Overlayer } from './overlayer.js'\n
      import { textWalker } from './text-walker.js'\n
      \n
      const SEARCH_PREFIX = 'foliate-search:'\n
      \n
      const isZip = async file => {\n
          const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer())\n
          return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04\n
      }\n
      \n
      const isPDF = async file => {\n
          const arr = new Uint8Array(await file.slice(0, 5).arrayBuffer())\n
          return arr[0] === 0x25\n
              && arr[1] === 0x50 && arr[2] === 0x44 && arr[3] === 0x46\n
              && arr[4] === 0x2d\n
      }\n
      \n
      const isCBZ = ({ name, type }) =>\n
          type === 'application/vnd.comicbook+zip' || name.endsWith('.cbz')\n
      \n
      const isFB2 = ({ name, type }) =>\n
          type === 'application/x-fictionbook+xml' || name.endsWith('.fb2')\n
      \n
      const isFBZ = ({ name, type }) =>\n
          type === 'application/x-zip-compressed-fb2'\n
          || name.endsWith('.fb2.zip') || name.endsWith('.fbz')\n
      \n
      const makeZipLoader = async file => {\n
          const { configure, ZipReader, BlobReader, TextWriter, BlobWriter } =\n
              await import('./vendor/zip.js')\n
          configure({ useWebWorkers: false })\n
          const reader = new ZipReader(new BlobReader(file))\n
          const entries = await reader.getEntries()\n
          const map = new Map(entries.map(entry => [entry.filename, entry]))\n
          const load = f => (name, ...args) =>\n
              map.has(name) ? f(map.get(name), ...args) : null\n
          const loadText = load(entry => entry.getData(new TextWriter()))\n
          const loadBlob = load((entry, type) => entry.getData(new BlobWriter(type)))\n
          const getSize = name => map.get(name)?.uncompressedSize ?? 0\n
          return { entries, loadText, loadBlob, getSize }\n
      }\n
      \n
      const getFileEntries = async entry => entry.isFile ? entry\n
          : (await Promise.all(Array.from(\n
              await new Promise((resolve, reject) => entry.createReader()\n
                  .readEntries(entries => resolve(entries), error => reject(error))),\n
              getFileEntries))).flat()\n
      \n
      const makeDirectoryLoader = async entry => {\n
          const entries = await getFileEntries(entry)\n
          const files = await Promise.all(\n
              entries.map(entry => new Promise((resolve, reject) =>\n
                  entry.file(file => resolve([file, entry.fullPath]),\n
                      error => reject(error)))))\n
          const map = new Map(files.map(([file, path]) =>\n
              [path.replace(entry.fullPath + '/', ''), file]))\n
          const decoder = new TextDecoder()\n
          const decode = x => x ? decoder.decode(x) : null\n
          const getBuffer = name => map.get(name)?.arrayBuffer() ?? null\n
          const loadText = async name => decode(await getBuffer(name))\n
          const loadBlob = name => map.get(name)\n
          const getSize = name => map.get(name)?.size ?? 0\n
          return { loadText, loadBlob, getSize }\n
      }\n
      \n
      export class ResponseError extends Error {}\n
      export class NotFoundError extends Error {}\n
      export class UnsupportedTypeError extends Error {}\n
      \n
      const fetchFile = async url => {\n
          const res = await fetch(url)\n
          if (!res.ok) throw new ResponseError(\n
              `${res.status} ${res.statusText}`, { cause: res })\n
          return new File([await res.blob()], new URL(res.url).pathname)\n
      }\n
      \n
      export const makeBook = async file => {\n
          if (typeof file === 'string') file = await fetchFile(file)\n
          let book\n
          if (file.isDirectory) {\n
              const loader = await makeDirectoryLoader(file)\n
              const { EPUB } = await import('./epub.js')\n
              book = await new EPUB(loader).init()\n
          }\n
          else if (!file.size) throw new NotFoundError('File not found')\n
          else if (await isZip(file)) {\n
              const loader = await makeZipLoader(file)\n
              if (isCBZ(file)) {\n
                  const { makeComicBook } = await import('./comic-book.js')\n
                  book = makeComicBook(loader, file)\n
              }\n
              else if (isFBZ(file)) {\n
                  const { makeFB2 } = await import('./fb2.js')\n
                  const { entries } = loader\n
                  const entry = entries.find(entry => entry.filename.endsWith('.fb2'))\n
                  const blob = await loader.loadBlob((entry ?? entries[0]).filename)\n
                  book = await makeFB2(blob)\n
              }\n
              else {\n
                  const { EPUB } = await import('./epub.js')\n
                  book = await new EPUB(loader).init()\n
              }\n
          }\n
          else if (await isPDF(file)) {\n
              const { makePDF } = await import('./pdf.js')\n
              book = await makePDF(file)\n
          }\n
          else {\n
              const { isMOBI, MOBI } = await import('./mobi.js')\n
              if (await isMOBI(file)) {\n
                  const fflate = await import('./vendor/fflate.js')\n
                  book = await new MOBI({ unzlib: fflate.unzlibSync }).open(file)\n
              }\n
              else if (isFB2(file)) {\n
                  const { makeFB2 } = await import('./fb2.js')\n
                  book = await makeFB2(file)\n
              }\n
          }\n
          if (!book) throw new UnsupportedTypeError('File type not supported')\n
          return book\n
      }\n
      \n
      class CursorAutohider {\n
          #timeout\n
          #el\n
          #check\n
          #state\n
          constructor(el, check, state = {}) {\n
              this.#el = el\n
              this.#check = check\n
              this.#state = state\n
              if (this.#state.hidden) this.hide()\n
              this.#el.addEventListener('mousemove', ({ screenX, screenY }) => {\n
                  // check if it actually moved\n
                  if (screenX === this.#state.x && screenY === this.#state.y) return\n
                  this.#state.x = screenX, this.#state.y = screenY\n
                  this.show()\n
                  if (this.#timeout) clearTimeout(this.#timeout)\n
                  if (check()) this.#timeout = setTimeout(this.hide.bind(this), 1000)\n
              }, false)\n
          }\n
          cloneFor(el) {\n
              return new CursorAutohider(el, this.#check, this.#state)\n
          }\n
          hide() {\n
              this.#el.style.cursor = 'none'\n
              this.#state.hidden = true\n
          }\n
          show() {\n
              this.#el.style.removeProperty('cursor')\n
              this.#state.hidden = false\n
          }\n
      }\n
      \n
      class History extends EventTarget {\n
          #arr = []\n
          #index = -1\n
          pushState(x) {\n
              const last = this.#arr[this.#index]\n
              if (last === x || last?.fraction && last.fraction === x.fraction) return\n
              this.#arr[++this.#index] = x\n
              this.#arr.length = this.#index + 1\n
              this.dispatchEvent(new Event('index-change'))\n
          }\n
          replaceState(x) {\n
              const index = this.#index\n
              this.#arr[index] = x\n
          }\n
          back() {\n
              const index = this.#index\n
              if (index <= 0) return\n
              const detail = { state: this.#arr[index - 1] }\n
              this.#index = index - 1\n
              this.dispatchEvent(new CustomEvent('popstate', { detail }))\n
              this.dispatchEvent(new Event('index-change'))\n
          }\n
          forward() {\n
              const index = this.#index\n
              if (index >= this.#arr.length - 1) return\n
              const detail = { state: this.#arr[index + 1] }\n
              this.#index = index + 1\n
              this.dispatchEvent(new CustomEvent('popstate', { detail }))\n
              this.dispatchEvent(new Event('index-change'))\n
          }\n
          get canGoBack() {\n
              return this.#index > 0\n
          }\n
          get canGoForward() {\n
              return this.#index < this.#arr.length - 1\n
          }\n
          clear() {\n
              this.#arr = []\n
              this.#index = -1\n
          }\n
      }\n
      \n
      const languageInfo = lang => {\n
          if (!lang) return {}\n
          try {\n
              const canonical = Intl.getCanonicalLocales(lang)[0]\n
              const locale = new Intl.Locale(canonical)\n
              const isCJK = ['zh', 'ja', 'kr'].includes(locale.language)\n
              const direction = (locale.getTextInfo?.() ?? locale.textInfo)?.direction\n
              return { canonical, locale, isCJK, direction }\n
          } catch (e) {\n
              console.warn(e)\n
              return {}\n
          }\n
      }\n
      \n
      export class View extends HTMLElement {\n
          #root = this.attachShadow({ mode: 'closed' })\n
          #sectionProgress\n
          #tocProgress\n
          #pageProgress\n
          #searchResults = new Map()\n
          #cursorAutohider = new CursorAutohider(this, () =>\n
              this.hasAttribute('autohide-cursor'))\n
          isFixedLayout = false\n
          lastLocation\n
          history = new History()\n
          constructor() {\n
              super()\n
              this.history.addEventListener('popstate', ({ detail }) => {\n
                  const resolved = this.resolveNavigation(detail.state)\n
                  this.renderer.goTo(resolved)\n
              })\n
          }\n
          async open(book) {\n
              if (typeof book === 'string'\n
              || typeof book.arrayBuffer === 'function'\n
              || book.isDirectory) book = await makeBook(book)\n
              this.book = book\n
              this.language = languageInfo(book.metadata?.language)\n
      \n
              if (book.splitTOCHref && book.getTOCFragment) {\n
                  const ids = book.sections.map(s => s.id)\n
                  this.#sectionProgress = new SectionProgress(book.sections, 1500, 1600)\n
                  const splitHref = book.splitTOCHref.bind(book)\n
                  const getFragment = book.getTOCFragment.bind(book)\n
                  this.#tocProgress = new TOCProgress()\n
                  await this.#tocProgress.init({\n
                      toc: book.toc ?? [], ids, splitHref, getFragment })\n
                  this.#pageProgress = new TOCProgress()\n
                  await this.#pageProgress.init({\n
                      toc: book.pageList ?? [], ids, splitHref, getFragment })\n
              }\n
      \n
              this.isFixedLayout = this.book.rendition?.layout === 'pre-paginated'\n
              if (this.isFixedLayout) {\n
                  await import('./fixed-layout.js')\n
                  this.renderer = document.createElement('foliate-fxl')\n
              } else {\n
                  await import('./paginator.js')\n
                  this.renderer = document.createElement('foliate-paginator')\n
              }\n
              this.renderer.setAttribute('exportparts', 'head,foot,filter')\n
              this.renderer.addEventListener('load', e => this.#onLoad(e.detail))\n
              this.renderer.addEventListener('relocate', e => this.#onRelocate(e.detail))\n
              this.renderer.addEventListener('create-overlayer', e =>\n
                  e.detail.attach(this.#createOverlayer(e.detail)))\n
              this.renderer.open(book)\n
              this.#root.append(this.renderer)\n
      \n
              if (book.sections.some(section => section.mediaOverlay)) {\n
                  const activeClass = book.media.activeClass\n
                  const playbackActiveClass = book.media.playbackActiveClass\n
                  this.mediaOverlay = book.getMediaOverlay()\n
                  let lastActive\n
                  this.mediaOverlay.addEventListener('highlight', e => {\n
                      const resolved = this.resolveNavigation(e.detail.text)\n
                      this.renderer.goTo(resolved)\n
                          .then(() => {\n
                              const { doc } = this.renderer.getContents()\n
                                  .find(x => x.index = resolved.index)\n
                              const el = resolved.anchor(doc)\n
                              el.classList.add(activeClass)\n
                              if (playbackActiveClass) el.ownerDocument\n
                                  .documentElement.classList.add(playbackActiveClass)\n
                              lastActive = new WeakRef(el)\n
                          })\n
                  })\n
                  this.mediaOverlay.addEventListener('unhighlight', () => {\n
                      const el = lastActive?.deref()\n
                      if (el) {\n
                          el.classList.remove(activeClass)\n
                          if (playbackActiveClass) el.ownerDocument\n
                              .documentElement.classList.remove(playbackActiveClass)\n
                      }\n
                  })\n
              }\n
          }\n
          close() {\n
              this.renderer?.destroy()\n
              this.renderer?.remove()\n
              this.#sectionProgress = null\n
              this.#tocProgress = null\n
              this.#pageProgress = null\n
              this.#searchResults = new Map()\n
              this.lastLocation = null\n
              this.history.clear()\n
              this.tts = null\n
              this.mediaOverlay = null\n
          }\n
          goToTextStart() {\n
              return this.goTo(this.book.landmarks\n
                  ?.find(m => m.type.includes('bodymatter') || m.type.includes('text'))\n
                  ?.href ?? this.book.sections.findIndex(s => s.linear !== 'no'))\n
          }\n
          async init({ lastLocation, showTextStart }) {\n
              const resolved = lastLocation ? this.resolveNavigation(lastLocation) : null\n
              if (resolved) {\n
                  await this.renderer.goTo(resolved)\n
                  this.history.pushState(lastLocation)\n
              }\n
              else if (showTextStart) await this.goToTextStart()\n
              else {\n
                  this.history.pushState(0)\n
                  await this.next()\n
              }\n
          }\n
          #emit(name, detail, cancelable) {\n
              return this.dispatchEvent(new CustomEvent(name, { detail, cancelable }))\n
          }\n
          #onRelocate({ reason, range, index, fraction, size }) {\n
              const progress = this.#sectionProgress?.getProgress(index, fraction, size) ?? {}\n
              const tocItem = this.#tocProgress?.getProgress(index, range)\n
              const pageItem = this.#pageProgress?.getProgress(index, range)\n
              const cfi = this.getCFI(index, range)\n
              this.lastLocation = { ...progress, tocItem, pageItem, cfi, range }\n
              if (reason === 'snap' || reason === 'page' || reason === 'scroll')\n
                  this.history.replaceState(cfi)\n
              this.#emit('relocate', this.lastLocation)\n
          }\n
          #onLoad({ doc, index }) {\n
              // set language and dir if not already set\n
              doc.documentElement.lang ||= this.language.canonical ?? ''\n
              if (!this.language.isCJK)\n
                  doc.documentElement.dir ||= this.language.direction ?? ''\n
      \n
              this.#handleLinks(doc, index)\n
              this.#cursorAutohider.cloneFor(doc.documentElement)\n
      \n
              this.#emit('load', { doc, index })\n
          }\n
          #handleLinks(doc, index) {\n
              const { book } = this\n
              const section = book.sections[index]\n
              doc.addEventListener('click', e => {\n
                  const a = e.target.closest('a[href]')\n
                  if (!a) return\n
                  e.preventDefault()\n
                  const href_ = a.getAttribute('href')\n
                  const href = section?.resolveHref?.(href_) ?? href_\n
                  if (book?.isExternal?.(href))\n
                      Promise.resolve(this.#emit('external-link', { a, href }, true))\n
                          .then(x => x ? globalThis.open(href, '_blank') : null)\n
                          .catch(e => console.error(e))\n
                  else Promise.resolve(this.#emit('link', { a, href }, true))\n
                      .then(x => x ? this.goTo(href) : null)\n
                      .catch(e => console.error(e))\n
              })\n
          }\n
          async addAnnotation(annotation, remove) {\n
              const { value } = annotation\n
              if (value.startsWith(SEARCH_PREFIX)) {\n
                  const cfi = value.replace(SEARCH_PREFIX, '')\n
                  const { index, anchor } = await this.resolveNavigation(cfi)\n
                  const obj = this.#getOverlayer(index)\n
                  if (obj) {\n
                      const { overlayer, doc } = obj\n
                      if (remove) {\n
                          overlayer.remove(value)\n
                          return\n
                      }\n
                      const range = doc ? anchor(doc) : anchor\n
                      overlayer.add(value, range, Overlayer.outline)\n
                  }\n
                  return\n
              }\n
              const { index, anchor } = await this.resolveNavigation(value)\n
              const obj = this.#getOverlayer(index)\n
              if (obj) {\n
                  const { overlayer, doc } = obj\n
                  overlayer.remove(value)\n
                  if (!remove) {\n
                      const range = doc ? anchor(doc) : anchor\n
                      const draw = (func, opts) => overlayer.add(value, range, func, opts)\n
                      this.#emit('draw-annotation', { draw, annotation, doc, range })\n
                  }\n
              }\n
              const label = this.#tocProgress.getProgress(index)?.label ?? ''\n
              return { index, label }\n
          }\n
          deleteAnnotation(annotation) {\n
              return this.addAnnotation(annotation, true)\n
          }\n
          #getOverlayer(index) {\n
              return this.renderer.getContents()\n
                  .find(x => x.index === index && x.overlayer)\n
          }\n
          #createOverlayer({ doc, index }) {\n
              const overlayer = new Overlayer()\n
              doc.addEventListener('click', e => {\n
                  const [value, range] = overlayer.hitTest(e)\n
                  if (value && !value.startsWith(SEARCH_PREFIX)) {\n
                      this.#emit('show-annotation', { value, index, range })\n
                  }\n
              }, false)\n
      \n
              const list = this.#searchResults.get(index)\n
              if (list) for (const item of list) this.addAnnotation(item)\n
      \n
              this.#emit('create-overlay', { index })\n
              return overlayer\n
          }\n
          async showAnnotation(annotation) {\n
              const { value } = annotation\n
              const resolved = await this.goTo(value)\n
              if (resolved) {\n
                  const { index, anchor } = resolved\n
                  const { doc } =  this.#getOverlayer(index)\n
                  const range = anchor(doc)\n
                  this.#emit('show-annotation', { value, index, range })\n
              }\n
          }\n
          getCFI(index, range) {\n
              const baseCFI = this.book.sections[index].cfi ?? CFI.fake.fromIndex(index)\n
              if (!range) return baseCFI\n
              return CFI.joinIndir(baseCFI, CFI.fromRange(range))\n
          }\n
          resolveCFI(cfi) {\n
              if (this.book.resolveCFI)\n
                  return this.book.resolveCFI(cfi)\n
              else {\n
                  const parts = CFI.parse(cfi)\n
                  const index = CFI.fake.toIndex((parts.parent ?? parts).shift())\n
                  const anchor = doc => CFI.toRange(doc, parts)\n
                  return { index, anchor }\n
              }\n
          }\n
          resolveNavigation(target) {\n
              try {\n
                  if (typeof target === 'number') return { index: target }\n
                  if (typeof target.fraction === 'number') {\n
                      const [index, anchor] = this.#sectionProgress.getSection(target.fraction)\n
                      return { index, anchor }\n
                  }\n
                  if (CFI.isCFI.test(target)) return this.resolveCFI(target)\n
                  return this.book.resolveHref(target)\n
              } catch (e) {\n
                  console.error(e)\n
                  console.error(`Could not resolve target ${target}`)\n
              }\n
          }\n
          async goTo(target) {\n
              const resolved = this.resolveNavigation(target)\n
              try {\n
                  await this.renderer.goTo(resolved)\n
                  this.history.pushState(target)\n
                  return resolved\n
              } catch(e) {\n
                  console.error(e)\n
                  console.error(`Could not go to ${target}`)\n
              }\n
          }\n
          async goToFraction(frac) {\n
              const [index, anchor] = this.#sectionProgress.getSection(frac)\n
              await this.renderer.goTo({ index, anchor })\n
              this.history.pushState({ fraction: frac })\n
          }\n
          async select(target) {\n
              try {\n
                  const obj = await this.resolveNavigation(target)\n
                  await this.renderer.goTo({ ...obj, select: true })\n
                  this.history.pushState(target)\n
              } catch(e) {\n
                  console.error(e)\n
                  console.error(`Could not go to ${target}`)\n
              }\n
          }\n
          deselect() {\n
              for (const { doc } of this.renderer.getContents())\n
                  doc.defaultView.getSelection().removeAllRanges()\n
          }\n
          getSectionFractions() {\n
              return (this.#sectionProgress?.sectionFractions ?? [])\n
                  .map(x => x + Number.EPSILON)\n
          }\n
          getProgressOf(index, range) {\n
              const tocItem = this.#tocProgress?.getProgress(index, range)\n
              const pageItem = this.#pageProgress?.getProgress(index, range)\n
              return { tocItem, pageItem }\n
          }\n
          async getTOCItemOf(target) {\n
              try {\n
                  const { index, anchor } = await this.resolveNavigation(target)\n
                  const doc = await this.book.sections[index].createDocument()\n
                  const frag = anchor(doc)\n
                  const isRange = frag instanceof Range\n
                  const range = isRange ? frag : doc.createRange()\n
                  if (!isRange) range.selectNodeContents(frag)\n
                  return this.#tocProgress.getProgress(index, range)\n
              } catch(e) {\n
                  console.error(e)\n
                  console.error(`Could not get ${target}`)\n
              }\n
          }\n
          async prev(distance) {\n
              await this.renderer.prev(distance)\n
          }\n
          async next(distance) {\n
              await this.renderer.next(distance)\n
          }\n
          goLeft() {\n
              return this.book.dir === 'rtl' ? this.next() : this.prev()\n
          }\n
          goRight() {\n
              return this.book.dir === 'rtl' ? this.prev() : this.next()\n
          }\n
          async * #searchSection(matcher, query, index) {\n
              const doc = await this.book.sections[index].createDocument()\n
              for (const { range, excerpt } of matcher(doc, query))\n
                  yield { cfi: this.getCFI(index, range), excerpt }\n
          }\n
          async * #searchBook(matcher, query) {\n
              const { sections } = this.book\n
              for (const [index, { createDocument }] of sections.entries()) {\n
                  if (!createDocument) continue\n
                  const doc = await createDocument()\n
                  const subitems = Array.from(matcher(doc, query), ({ range, excerpt }) =>\n
                      ({ cfi: this.getCFI(index, range), excerpt }))\n
                  const progress = (index + 1) / sections.length\n
                  yield { progress }\n
                  if (subitems.length) yield { index, subitems }\n
              }\n
          }\n
          async * search(opts) {\n
              this.clearSearch()\n
              const { searchMatcher } = await import('./search.js')\n
              const { query, index } = opts\n
              const matcher = searchMatcher(textWalker,\n
                  { defaultLocale: this.language, ...opts })\n
              const iter = index != null\n
                  ? this.#searchSection(matcher, query, index)\n
                  : this.#searchBook(matcher, query)\n
      \n
              const list = []\n
              this.#searchResults.set(index, list)\n
      \n
              for await (const result of iter) {\n
                  if (result.subitems){\n
                      const list = result.subitems\n
                          .map(({ cfi }) => ({ value: SEARCH_PREFIX + cfi }))\n
                      this.#searchResults.set(result.index, list)\n
                      for (const item of list) this.addAnnotation(item)\n
                      yield {\n
                          label: this.#tocProgress.getProgress(result.index)?.label ?? '',\n
                          subitems: result.subitems,\n
                      }\n
                  }\n
                  else {\n
                      if (result.cfi) {\n
                          const item = { value: SEARCH_PREFIX + result.cfi }\n
                          list.push(item)\n
                          this.addAnnotation(item)\n
                      }\n
                      yield result\n
                  }\n
              }\n
              yield 'done'\n
          }\n
          clearSearch() {\n
              for (const list of this.#searchResults.values())\n
                  for (const item of list) this.deleteAnnotation(item)\n
              this.#searchResults.clear()\n
          }\n
          async initTTS(granularity = 'word', highlight) {\n
              const doc = this.renderer.getContents()[0].doc\n
              if (this.tts && this.tts.doc === doc) return\n
              const { TTS } = await import('./tts.js')\n
              this.tts = new TTS(doc, textWalker, highlight || (range =>\n
                  this.renderer.scrollToAnchor(range, true)), granularity)\n
          }\n
          startMediaOverlay() {\n
              const { index } = this.renderer.getContents()[0]\n
              return this.mediaOverlay.start(index)\n
          }\n
      }\n
      \n
      customElements.define('foliate-view', View)\n
      """
  ]
]