Skip to main content

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.

epubcfi.js is a self-contained EPUB Canonical Fragment Identifier (CFI) implementation. It parses CFI strings into plain JavaScript objects, generates CFIs from DOM Ranges, resolves CFIs back to Ranges, and compares or joins CFIs. The parser uses a hand-written state machine and handles escaped characters and text assertions correctly. The module has no dependencies and can be used outside a browser if you only need parsing or comparison.

Import

import * as CFI from './foliate-js/epubcfi.js'
// or selectively:
import { parse, fromRange, toRange, compare, joinIndir, isCFI, fake } from './foliate-js/epubcfi.js'

Parsed CFI structure

parse() returns plain objects. A collapsed (non-range) CFI is an array of path arrays, where each path array contains part objects:
// epubcfi(/6/4!/4) →
[
    [{ index: 6 }, { index: 4 }],
    [{ index: 4 }]
]
A range CFI (containing a comma) is an object:
// epubcfi(/6/4!/2,/2,/4) →
{
    parent: [[{ index: 6 }, { index: 4 }], [{ index: 2 }]],
    start:  [[{ index: 2 }]],
    end:    [[{ index: 4 }]]
}
Each part object may have these properties:
PropertyTypeDescription
indexnumberStep index (from / token).
idstringElement ID assertion (from [id]).
offsetnumberCharacter offset (from : token).
temporalnumberTemporal offset (from ~ token).
spatialnumber[]Spatial offset coordinates (from @ tokens).
textstring[]Text location assertions.
sidestringSide bias (from ;s= parameter).

Exports

isCFI

A RegExp that tests whether a string is a wrapped CFI (i.e. matches epubcfi(...)).
isCFI.test('epubcfi(/6/4!/4/2:10)') // true
isCFI.test('/6/4!/4/2:10')          // false

joinIndir(...cfis)

Joins two or more CFIs with the step indirection operator (!), wrapping the result in epubcfi(...).
cfis
string[]
required
Two or more CFI strings (wrapped or bare). The wrapper is stripped before joining.
const sectionCFI = 'epubcfi(/6/4)'
const localCFI   = '/4/2:10'
joinIndir(sectionCFI, localCFI) // 'epubcfi(/6/4!/4/2:10)'

parse(cfi)

Parses a CFI string into a plain object or array. Accepts both wrapped (epubcfi(...)) and bare forms.
cfi
string
required
A CFI string to parse.
Returns a collapsed CFI (array of path arrays) for non-range CFIs, or { parent, start, end } for range CFIs.
const parsed = parse('epubcfi(/6/4!/4/2:10)')
// [
//   [{ index: 6 }, { index: 4 }],
//   [{ index: 4 }, { index: 2, offset: 10 }]
// ]

fromRange(range, filter?)

Converts a DOM Range to a CFI string. Produces a range CFI when the range is not collapsed.
range
Range
required
A DOM Range within a section document.
filter
function
An optional node filter with the same signature as a TreeWalker filter: (node: Node) => NodeFilter.FILTER_ACCEPT | NodeFilter.FILTER_REJECT | NodeFilter.FILTER_SKIP. Use this to exclude injected nodes (annotations, highlights) from the CFI path.
const cfi = fromRange(range)
// 'epubcfi(/6/4!/4/2:10)'

// Exclude annotation nodes from the path
const filter = node => node.classList?.contains('annotation')
    ? NodeFilter.FILTER_REJECT
    : NodeFilter.FILTER_ACCEPT
const cfi = fromRange(range, filter)

toRange(doc, cfi, filter?)

Resolves a CFI string to a DOM Range within a document.
doc
Document
required
The section Document in which to resolve the CFI.
cfi
string | object
required
A CFI string (wrapped or bare) or a parsed CFI object.
filter
function
Same node filter as fromRange. Must match the filter used when the CFI was created if injected nodes are present.
const range = toRange(doc, 'epubcfi(/6/4!/4/2:10)')
// DOM Range at character offset 10 inside the matching element

compare(a, b)

Comparator for sorting CFIs. Returns a negative number, zero, or a positive number following the standard comparator convention, suitable for use with Array.prototype.sort.
a
string | object
required
A CFI string or parsed object.
b
string | object
required
A CFI string or parsed object.
const cfis = ['epubcfi(/6/8!/4:20)', 'epubcfi(/6/4!/2:0)', 'epubcfi(/6/6!/6:5)']
cfis.sort(compare)
// sorted in document order

fake.fromIndex(index)

Creates a fake base CFI for non-EPUB books (e.g. CBZ, FB2) that don’t have a real package document. The result can be used as the section.cfi property when constructing a book object.
index
number
required
Zero-based section index.
fake.fromIndex(0) // 'epubcfi(/6/2)'
fake.fromIndex(1) // 'epubcfi(/6/4)'
fake.fromIndex(2) // 'epubcfi(/6/6)'

fake.toIndex(parts)

Extracts the zero-based section index from a parsed fake CFI. The inverse of fake.fromIndex.
parts
object
required
A parsed CFI (the result of parse()), or the parts array directly.
const parsed = parse(fake.fromIndex(3))
fake.toIndex(parsed) // 3

Filter function

Both fromRange and toRange accept an optional filter that mirrors the TreeWalker filter API:
const filter = node => {
    if (node.nodeType !== 1) return NodeFilter.FILTER_ACCEPT
    if (node.matches('.reject')) return NodeFilter.FILTER_REJECT  // exclude subtree
    if (node.matches('.skip'))   return NodeFilter.FILTER_SKIP    // skip node, keep children
    return NodeFilter.FILTER_ACCEPT
}

const cfi = fromRange(range, filter)
const restored = toRange(doc, cfi, filter)
Pass the same filter to both functions to ensure roundtrip consistency when the document contains injected elements.

Build docs developers (and LLMs) love