The Colibrio Reader Framework FOSS EPUB CFI implementation
Vendored from https://bitbucket.org/colibrio/colibrio-web-epubcfi/.
A spec. compliant EPUB CFI parser that also performs validation and error reporting.
In addition to the CFI parser, the library also contains classes to resolve CFIs to Elements and DOMRanges, build CFIs from XML documents, and serialize CFI node trees to string.
This library should have everything you need to make your reading system EPUB CFI compliant and interoperable!
Usage
The library can be imported using both ES module syntax and CommonJS syntax.
Node / Deno / Bun support
To run the EpubCfiResolver and the EpubCfiBuilder you will need to use some library to parse the XHTML documents,
such as JSDOM (https://github.com/jsdom/jsdom).
Requirements
Node version 20 and above
Installation
Run npm install to install all dependencies.
Building
This library is built with the Typescript compiler.
Build artifacts (Javascript code and Typescript definitions) output to the ./build folder.
npm run build
Running the tests
npm run test
The unit test spec files are located together with their corresponding class.
In other words, you will find the EpubCfiBuilder.spec.ts test file side by side with the EpubCfiBuilder.ts file.
Tests are written using Vitest (https://vitest.dev/).
The Playground
npm run playground
A small app that can be used to create, parse, and resolve EPUB CFIs.
Sources are available in the playground folder.
Getting started
Parse your CFI and get back a tree of nodes, each representing a part of the CFI.
1 const epubCfiRootNode = EpubCfiParser.parse("epubcfi(/6/4[chap01ref]!/4[body01]/10[para05])");
The EpubCfiResolver will now be used to resolve the target Element or DOMRange that the CFI addresses.
2 const resolver = new EpubCfiResolver(epubCfiRootNode);
The process of resolving has a minimum of two steps depending on the number of so-called indirections that the CFI contains.
An indirection is an instruction (signified by an !) in the EPUB CFI that redirects to a new XHTML or SVG document.
epubcfi(/6/4[chap01ref]!/4[body01]/10[para05])
^---- OPF ----^ ^---- XHTML Doc ----^
As the resolver encounters an indirection, it will return an IEpubCfiIndirectionResult object. This object contains
the DOM Element that represents the "link" to the other document context. Using this result data you
will then load the appropriate target Document and instruct the resolver to continue resolving by
again calling continueResolving with the new Document as a parameter.
The process starts out in the EPUB's OPF XML document, so this is the first document instance we provide to the resolver.
const opfIndirectionResult = resolver.continueResolving(opfXmlDocument, pathToOpfDocumentAsString);
if(opfIndirectionResult === null) {
throw new Error('The EPUB OPF does not contain an <itemref> element matching the CFI.');
}
We can assume that there is only one indirection (!) in the CFI. This will be the one redirecting us from the OPF XML,
to the EPUB Content Document.
You get a reference to the element that references the new document context through the EpubCfiIndirectionResult.element
property. This will be of type Element (more specifically an itemref element). Using this data, we now load the new XHTML
document and pass that in to the next call to resolver.continueResolving().
const manifestItem = opfXmlDocument.getElementById(opfIndirectionResult.element.getAttribute('idref'));
const contentDocument = await myEpubLib.fetchManifestItemResource(manifestItem.getAttribute('href'));
resolver.continueResolving(contentDocument, manifestItem.href);
const resolvedTarget = resolver.getResolvedTarget();
if(resolvedTarget.hasErrors()) {
console.warn('The resolving process produced errors.', resolvedTarget.getResolverErrors());
}
When resolving has been completed, if no irreparable errors have occurred, we have a resolved target. The target will either be a position or a range in the DOM.
if(resolvedTarget.isDomRange()) {
const targetDomRange = resolvedTarget.createDomRange();
const selection = window.getSelection();
selection.removeAllRanges();
selection.setBaseAndExtent(targetDomRange.startContainer, targetDomRange.startOffset, targetDomRange.endContainer, targetDomRange.endOffset);
} else if(resolvedTarget.isTargetingElement()) {
resolvedTarget.getTargetElement().focus();
}
About the EPUB CFI specification
Link to specification:
Purpose
From the specification Abstract:
This specification, EPUB Canonical Fragment Identifier (epubcfi), defines a standardized method for referencing arbitrary content within an EPUB® Publication through the use of fragment identifiers.
The web has proven that the concept of hyperlinking is tremendously powerful, but EPUB publications have been denied much of the benefit that hyperlinking makes possible because of the lack of a standardized scheme to link into them.
Features
- Enables lossless interchange of location between EPUB3 compatible reading systems.
- Powerful, deterministic addressing of content, regardless of publication complexity.
- Serializable to URI fragment selector.
- Supports so-called assertions to make CFIs less sensitive to underlying changes to content structure.
- Sortable without needing to be resolved.
- Extendable to vendor-specific processing instructions for assertions.
Syntax
The syntax is somewhat similar to XPointer when using position indexes to address element nodes. EPUB CFI has a very important addition to its syntax, however. It uses odd / even rule to its position index syntax to make it possible to differentiate between element nodes and text nodes. In XPointer you can only address element nodes using positions.
The odd / even rule
At the heart of the EPUB CFI's addressing scheme is the rule that even numbers point to element nodes (<body>, <p>,
<span> etc) whereas odd numbers point to text nodes (<p> This is a text node </p>).
This feature, combined with the support for offsets and ranges, makes it possible to create CFIs that target any position within an EPUB in a unified, succinct way.
Here's a minimal HTML document.
<html>
<head>
<title>
Chapter 1
</title>
</head>
<body>
Hello there!
</body>
</html>
From a CFI syntax perspective this is how we would address each element.
<html>
<head> => /2
<title> => /2/2
Chapter 1 => /2/2/1
</title>
</head>
<body> => /4
Hello there! => /4/1
</body>
</html>
Resolving starts from the root of the EPUB
To simplify explanation, the previous examples the CFI path started at the root of the XHTML content document. In reality a spec compliant CFI always start in the EPUB's entry point: the OPF file.
The way we address the target <itemref> node should be familiar now. But, see if you can spot something different with
the path.
<opf>
<metadata></metadata> => /2
<manifest> => /4
<item id="chapter1" href="c1.xhtml"/>
</manifest>
<spine> => /6
<itemref idref="chapter1"> => /6/2!
</spine>
</opf>
That's right, the path to the <itemref> node ends with an exclamation mark !. This special character is called an
indirection token. It tells the resolver that it should load a new XML document and continue the resolving in the new
document context. In the example above the new document to be loaded is c1.xhtml.
So, let's combine this OPF path component with the XHTML document path component from the previous examples:
/6/2!/4/1
The CFI above addresses the text within the <body> of the c1.xhtml content document.
Examples
Addressing of text range
ℹ️ Note that to resolve Text Assertions, you need to provide an
IEpubCfiResolverOptionsobject withprocessTextAssertionsset totrue. Read more about this in the Resolver section below.
If we want to address the text range "Bruno jumped up next to him" in the document below:
/4/4/1:39,:66
<html>
<head>
<title>
Addressing of text range
</title>
</head>
<body>
<h1>
Chapter 1.
</h1>
<p>
Ben sat down on the bench. Bruno jumped up next to him and put his paw on Bens lap.
|------------------------------------>^---------------------------^
ℹ️ Note that CFI takes into account all white spaces in the HTML.
</p>
</body>
</html>
Addressing using id assertion
If we want to address the second number in the addition below, we could use the full path to the element:
/4/4/2/6.
Another, shorter way to express this is to use the id attribute of the element.
/0[mn2]
<html>
<head>
<title>
Addressing using id assertion
</title>
</head>
<body>
<h1>
ID Assertion
</h1>
<p>
<math>
<mn>1</mn>
<mo>+</mo>
<mn id="mn2">2</mn>
</math>
</p>
</body>
</html>
Addressing using text assertion
If we want to address the text range "Ben sat down on the bench. Bruno jumped up next to him" in the document below, with an added text assertion:
/4/4/1:27[Chapter 1.,Ben sat down],:54[next to him, and put his paw]
The text assertion is an extra fallback that will be used to try to resolve the location, even if the document has been structurally modified.
<html>
<head>
<title>
Addressing of text range
</title>
</head>
<body>
<h1>
Chapter 1.
</h1>
<p>
Ben sat down on the bench. Bruno jumped up next to him and put his paw on Bens lap.
</p>
</body>
</html>
Implementation notes
This implementation uses common steps and concepts that you will be familiar with if you have ever built a language parser. Firstly, it breaks down the source into tokens using lexical analysis. The parser consumes these tokens to build a so-called Abstract Syntax Tree (AST) which in turn can be used by an interpreter.
In this implementation these steps map to the following components:
- Lexer
- Parser
- Resolver
In addition to parsing and resolving EPUB CFIs, this library also lets you build CFIs programmatically using XML nodes.
Components
Lexer
The EpubCfiLexer class performs a so-called lexical analysis of the CFI string and creates a stream of tokens for the
parser to use.
Characters and strings that the EpubCfiLexer recognize as IEpubCfiTokens include epubcfi(, /, !, [, :,
], ).
The lexer does not perform any validation of syntax. This is up to the parser.
Parser
The EpubCfiParser utilizes the output stream of tokens from the lexer to build what is commonly referred to as a
"Abstract Syntax Tree". This tree contains the actual semantic structure that you expect from a parser, meaning that you
have nodes such as IEpubCfiPathNode, IEpubCfiStepNode, IEpubCfiAssertionNode etc.
The parser does enforce syntactical rules but is forgiving and will recover from many types of common errors.
All encountered errors are available in the errors property of the IEpubCfiRootNode returned from the parse
method.
Resolver
The EpubCfiResolver is a utility class that takes either a IEpubCfiRootNode or a CFI string and exposes an API to
resolve the CFI to an Element or a DOMRange. Because EPUB is a format composed of inter-linked
documents, resolving is procedural. The process involves calling continueResolving multiple times,
providing the XML document to be used for continued resolving. Each time continueResolving
returns it serves as a request for a new XML document to be provided according to the CFI data.
The initial call to continueResolving passes a reference to the EPUB OPF XML file.
In 99% of cases your EPUB CFI will contain only one so-called "indirection" to a new document context. This will be the
redirection from the OPF XML file to the Content Document referenced by the spine items itemref attribute.
Indirections can point into IFrame elements within Content Documents.
When the continueResolving method call returns null this means that the resolving has finished and that the target
will be available in the EpubCfiResolvedTarget object returned from the getResolvedTarget method.
This EpubCfiResolvedTarget object has a range of methods to perform DOM API operations according to the instructions
specified by the target CFI node. Methods include for example, createDomRange which creates a DOMRange based on the
CFI node's range path component, and
getTargetElement which retrieves the targeted Element if the CFI targets a single node.
It also has methods to interrogate the error state of the resolving process and much, much more.
ℹ️ Note that to resolve Text Assertions you must provide
IEpubCfiResolverOptionsto theEpubCfiResolverconstructor. Example:const epubCfiResolver = new EpubCfiResolver(epubCfiRootNode, {processTextAssertions: true,textAssertionSearchDistance: 10000,customTextAssertionMatcher: (params: IEpubCfiCustomTextAssertionMatcherParams): number => {...}});
Handling EPUB CFI vendor-specific extensions in the EpubCfiResolver
Vendor-specific parameters (https://w3c.github.io/epub-specs/epub33/epubcfi/#sec-extensions) is an extension mechanism that lets users add custom data to the fragment selector.
From the specification:
"This specification encourages the development of custom functions to assist with CFI correction where the intrinsic functionality is insufficient. Refer to Extending EPUB CFIs for more information on how to develop such functionality."
The Parser supports all these nodes and they are represented in the AST (Abstract Syntax Tree). and so it is just a matter of implementing the resolving of the DOM position.
Why not do it and send a pull request! 🤓
Builder
The EpubCfiBuilder is a utility class that helps you build CFIs from an XML content document and a reference to a
target Element in the same document.
The code example below creates a CFI from the current user selection.
const selectionRange = window.getSelection().getRangeAt(0);
const cfiBuilder = new EpubCfi.EpubCfiBuilder();
cfiBuilder.appendLocalPath(appState.activeOpfSpineItemRefElement)
cfiBuilder.appendTerminalDomRange(selectionRange);
const serializedCfi = cfiBuilder.toString();
// Now save this in your CFI in your storage service of choice 🙂
ℹ️ Note that to include Text Assertions you must set the
IEpubCfiBuilderTextAssertionOptionsby calling theEpubCfiBuilder.setTextAssertionOptions()method.epubCfiBuilder.setTextAssertionOptions({preLength: 10,postLength: 10,snapToWordBoundaries: true,wordBoundaryMaxExpansion: 16,});
License
MIT License
Copyright (c) 2025, Andreas Duchen, Colibrio Software AB
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.