Complete API documentation for rebook.
- Parser Registry
- First Sections Export
- ReaderView
- Plugins
- Book
- Section
- Document Model
- Pretext Layout
- Search
- MCP Tools
- Parser Interface
- Renderer Interface
- Adapter System
- Error Types
- Metadata Normalization
- Styles
- Default Format Styles
- Parser Detection Priority
searchBook() searches readable text from parsed books. By default it scans
every section. Use scope: 'chapter' and chapterIndex to search within one
chapter/section. Reader instances expose the same current-book capability as
reader.search() and reader.searchChapters().
import { searchBook, searchChapters } from 'rebook'
const results = await searchBook(book, 'platform cooperative', {
maxResults: 20,
contextChars: 80,
})
const chapterResults = await searchBook(book, 'ownership', {
scope: 'chapter',
chapterIndex: 2,
})
const grouped = await searchChapters(book, 'worker')
const readerResults = await reader.search('platform cooperative')Search prefers section.getBlocks() so browser renderer extraction rules are
respected. Inline footnote markers remain searchable as markers, while hidden
footnote content is not mixed into the visible body text.
Installed packages also expose a stdio MCP server executable:
{
"mcpServers": {
"book": {
"command": "npx",
"args": ["-y", "--package", "rebook", "rebook-mcp", "/absolute/path/book.epub"]
}
}
}If rebook is installed globally or the MCP client runs inside a project where
rebook is installed, command can be rebook-mcp directly. The CLI supports
EPUB, MOBI/AZW3, FB2, and CBZ files and exposes tools for the selected book over
stdio.
For embedding, createBookMCPTools() returns SDK-agnostic Model Context
Protocol style tool definitions for a parsed Book. Adapters can register the
name, description, inputSchema, and call handler(args) from any MCP
server implementation.
import { createBookMCPTools, callBookMCPTool } from 'rebook/mcp'
const tools = createBookMCPTools(book)
await callBookMCPTool(tools, 'search_book', {
query: 'cooperative',
chapterIndex: 1,
maxResults: 5,
})Built-in tools:
list_chapters: returns section indexes, ids, titles, sizes, and linear flags.search_book: searches the whole book or one chapter viachapterIndex.get_chapter_text: returns readable text for one chapter with truncation.
The central hub for registering and auto-detecting parsers.
import { registry } from 'rebook'
// Register a parser factory
registry.register('epub', epub)
registry.register('mobi', mobi)
registry.register('fb2', fb2)
registry.register('cbz', cbz)
// Register with custom priority (higher = checked first)
registry.register('cbz', cbz, 20)
// Auto-detect format and parse
const book = await registry.open(file, { domAdapter, urlFactory })
// Detect format only (without parsing)
const parser = await registry.detect(file)
// List registered parsers
registry.list() // ['epub', 'mobi', 'fb2', 'cbz']Auto-detect format and parse. input can be a File, Blob, ArrayBuffer, or URL string.
Returns the first parser whose canParse() returns true, or null.
Returns an array of registered parser names.
Exporters convert a normalized Book back into a downloadable file. Built-in formats are epub, cbz, txt, and html, and the API is format-neutral so additional output formats can be added later without changing call sites.
exportFirstSections() parses any registered supported input format and exports the first N linear reading sections using the requested exporter format. The API works in browsers and Node-compatible runtimes.
Important: this count is not based on rendered visual pages. For CBZ one section usually means one image page. For EPUB/MOBI/FB2 it means the parser's linear spine/reading section, often a chapter or parser-split section. Visual-page export needs renderer layout inputs such as viewport size, font size, line height, margins, and spread mode.
import {
registry,
exporterRegistry,
epub,
mobi,
fb2,
cbz,
exportFirstSections,
} from 'rebook'
registry.register('epub', epub)
registry.register('mobi', mobi)
registry.register('fb2', fb2)
registry.register('cbz', cbz)
const blob = await exportFirstSections(file, 5, {
format: 'epub',
parserOptions: { domAdapter, urlFactory },
})
exporterRegistry.list() // ['epub', 'cbz', 'txt', 'html']For an already parsed Book, use exportBook() with an explicit selection:
import { exportBook, firstSectionsSelection } from 'rebook'
const book = await registry.open(file, { domAdapter, urlFactory })
const blob = await exportBook(book, firstSectionsSelection(5), { format: 'epub' })In Node, write the exported buffer:
import { exportBookAsBuffer, firstSectionsSelection } from 'rebook'
const buffer = await exportBookAsBuffer(book, firstSectionsSelection(5), { format: 'epub' })
await fs.writeFile('first-5.epub', Buffer.from(buffer))Non-linear sections are skipped by default.
Register output formats with exporterRegistry:
import type { Exporter } from 'rebook'
import { exporterRegistry } from 'rebook'
const txtExporter: Exporter = {
format: 'txt',
mediaType: 'text/plain',
extension: '.txt',
canExport: (_book, selection) => selection.type === 'first-sections',
exportBook: async (book, selection) => {
const sections = book.sections.slice(0, selection.count)
const text = await Promise.all(sections.map(section => section.loadText?.() ?? section.load()))
return new Blob([text.join('\n\n')], { type: 'text/plain' })
},
}
exporterRegistry.register('txt', () => txtExporter)High-level API combining parser registry, renderer, and navigation.
import { createReader } from 'rebook'
const reader = createReader({
container: document.getElementById('viewer')!,
styles: {
fontSize: '18px',
lineHeight: 1.7,
maxInlineSize: '720px',
},
})The default browser backend is BrowserRenderer: XHTML AST -> structural blocks -> preset styles -> Pretext line ranges -> visible DOM rows.
await reader.open(file) // File, Blob, ArrayBuffer, or URL string
await reader.openBook(book) // Pre-parsed Book instanceawait reader.next() // Next section/page
await reader.prev() // Previous section/page
await reader.goLeft() // Respects RTL direction
await reader.goRight() // Respects RTL direction
await reader.goTo(href) // Navigate to TOC href
await reader.goToFraction(0.5) // Navigate to 50% progressreader.setStyles({ fontSize: '18px', theme: 'dark' })
reader.setLayout('scrolled')
// Control spread (two-page layout)
reader.setSpread(2) // Auto-spread: 2 pages on wide screens, 1 on narrow
reader.setSpread(1) // Force single pageMarks are transient render annotations for reader UI state such as the current TTS segment, search hits, translation state, or annotation previews. They are not persisted by rebook.
reader.setMark({
id: 'tts-current',
kind: 'tts',
range: {
sectionIndex,
blockId: segment.blockId,
startOffset: segment.startOffset,
endOffset: segment.endOffset,
},
className: 'rebook-tts-current',
data: { segmentId: segment.id },
})
reader.removeMark('tts-current')
reader.clearMarks('tts')Browser marks are rendered as classes on matching visible line nodes. Mini
Program marks are exposed on snapshot line nodes as className, markIds,
markKinds, and markData.
In the default browser renderer, wide containers display two text columns side-by-side using the Pretext line list.
- Container width >=2 x
maxInlineSize+gap: Shows 2 pages (spread) - Container width < 2 x
maxInlineSize+gap: Shows 1 page (single) - Resizing: Recomputes the grid span and switches between spread and single-page
The maxColumnCount config option (default: 2) controls the maximum number of visible pages. Set to 1 to always use single-page layout.
reader.on('load', (e) => {
// Section loaded
console.log('index:', e.index)
})
reader.on('relocate', (e) => {
// Location changed
console.log('section index:', e.index)
console.log('fraction within section:', e.fraction)
console.log('overall progress fraction:', e.totalFraction)
console.log('CFI of current position:', e.cfi)
// Automatically tracked active TOC item (chapter)
if (e.tocItem) {
console.log('active chapter:', e.tocItem.label, e.tocItem.href)
}
})
reader.on('link', (e) => {
// Link clicked
if (e.external) window.open(e.href)
else reader.goTo(e.href)
})const metadata = reader.getMetadata() // BookMetadata
const toc = reader.getTOC() // TOCItem[]
const location = reader.getLocation() // Current location (contains index, fraction, tocItem, cfi)
const fractions = reader.getSectionFractions() // Per-section progress ticks
const matches = await reader.search('ownership') // SearchResult[]reader.destroy() // Release all resources
RebookPluginis the low-levelBook -> Bookmiddleware API. For installable packages, manifests, permissions, commands, settings, marketplace distribution, and lifecycle, see Rebook extensions.
Plugins are Book middleware. They run after parsing or openBook() and before rendering in the shared reader session used by browser and Mini Program readers.
import { createReader, withTrialLimit, type TrialLimitedBook } from 'rebook'
const reader = createReader({
container,
plugins: [withTrialLimit({ maxPages: 20 })],
})
const book = await reader.open(file) as TrialLimitedBook
if (!reader.canGoTo('chapter-20.xhtml')) {
// Show locked-state UI in the host app.
}The trial-limit plugin adds book.trialLimit, a controller for trial reader
state: estimated page count, limit fraction, page step fraction, TOC access
items, target navigation checks, and next-page checks. Reader instances expose
canGoTo(), canGoNext(), and trial-aware TOC helpers so host apps usually do
not need to pass section fractions into the controller directly. Page estimates use
explicit page lists first, pre-paginated section counts second, and sampled text
density for reflowable books before falling back to section sizes.
import { createWechatMiniProgramReader } from 'rebook/renderers/wechat-miniprogram'
import { withTrialLimit } from 'rebook/plugins/trial-limit'
const reader = createWechatMiniProgramReader({
width,
height,
wx,
plugins: [withTrialLimit({ maxPages: 20 })],
setData: snapshot => page.setData({ reader: snapshot }),
})
await reader.openBook(book)withTranslation() keeps the lightweight local mode. It translates
renderer-requested text blocks lazily with the supplied AI SDK LanguageModel.
Browser and Mini Program renderers emit block-window events for the current
viewport plus configured prefetch pages. ReaderSession forwards those windows
to book-level blockWindowConsumers, so translation work follows reading
progress instead of translating the whole book up front. Apps can still listen
to block-window for diagnostics, but they do not need to call translation
plugin methods to start block translation.
import { createOpenAI } from '@ai-sdk/openai'
import { createReader } from 'rebook'
import { withTranslation } from 'rebook/plugins/translation'
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY })
const reader = createReader({
container,
plugins: [
withTranslation({
model: openai.chat('gpt-4o-mini'),
targetLanguage: 'zh-CN',
mode: 'bilingual',
translateTOC: true,
prefetchPages: 2,
onUpdate: ({ sectionIndex }) => {
if (reader.getLocation()?.index === sectionIndex) reader.refresh()
},
}),
],
})withProfessionalTranslation() uses rebook-service instead of running a local
model. The backend owns the LangGraph workflow, glossary, review/refine loop,
persistence, and progress state. The frontend plugin attaches to a server-side
book and renders translated chunks when they become available.
The bookId must refer to a book that has already been uploaded or imported
into rebook-service.
import { createReader } from 'rebook'
import { withProfessionalTranslation } from 'rebook/plugins/translation'
const reader = createReader({
container,
plugins: [
withProfessionalTranslation({
serviceBaseUrl: 'http://127.0.0.1:8083',
bookId: serverBookId,
targetLanguage: 'zh-CN',
mode: 'bilingual',
pipeline: {
bookType: 'technical textbook',
audience: 'professional readers',
style: 'Faithful, precise, publication-quality Chinese.',
maxReviewLoops: 1,
},
onStatus: status => {
console.log(status.phase, status.progress, status.message)
},
onUpdate: ({ sectionIndex }) => {
if (reader.getLocation()?.index === sectionIndex) reader.refresh()
},
}),
],
})The returned Book exposes professionalTranslationStatus for backend job
state, including jobId, stage, progress, completedChunks, and
totalChunks.
withTTS() adds a book.tts controller. It converts section blocks into stable
speech segments with sectionIndex, blockId, startOffset, and endOffset,
then calls a provider-based TTS backend such as the companion rebook-tts
service. Segment offsets are relative to blockId, so they can be used directly
with reader marks.
import { createBrowserTTSAudioPlayer, createReader, withTTS } from 'rebook'
const ttsPlayer = createBrowserTTSAudioPlayer()
const reader = createReader({
container,
plugins: [
withTTS({
endpoint: 'http://127.0.0.1:4177',
provider: 'edge',
voice: 'zh-CN-XiaoyiNeural',
maxSegmentChars: 500,
player: ttsPlayer,
}),
],
})
const book = await reader.open(file)
const sectionIndex = reader.getLocation()?.index ?? 0
const prefetch = await book.tts.prefetchSection(sectionIndex, { concurrency: 2 })
await book.tts.playPrefetchedSection(prefetch, {
preloadAhead: 3,
onSegmentStart: ({ segment }) => {
reader.setMark({
id: 'tts-current',
kind: 'tts',
range: {
sectionIndex: segment.sectionIndex,
blockId: segment.blockId,
startOffset: segment.startOffset,
endOffset: segment.endOffset,
},
className: 'tts-current',
})
},
onSegmentError: ({ segment, error }) => {
console.warn('Skipped TTS segment', segment.id, error)
},
})prefetchSection() starts an async backend job for the whole section, so the
next segments can be generated while the current audio is playing.
createBrowserTTSAudioPlayer() uses AudioContext scheduling when available
and falls back to HTMLAudioElement playback. You can pass any object matching
TTSAudioPlayer to withTTS({ player }) for other runtimes. If one generated
segment is missing or contains invalid audio, browser playback reports it through
onSegmentError and continues with the next segment.
For web-novel style multi-voice narration, pass the same AI SDK model shape used
by withTranslation() and enable multiSpeaker. The AI analysis runs in the
plugin with a compact offset-only output schema; the TTS backend still only needs
to synthesize prepared segments. Speaker to voice assignments are remembered by
the plugin, so the same character keeps the same voice across prepared sections.
Narration uses speakerId = 0; inferred characters receive stable positive
speaker ids that are sent back into later AI analysis requests. For providers
without designed voices, the plugin caches the backend voice catalog and lets the
model choose a voice for newly identified speakers; known speaker voice ids are
reused in later requests.
When the selected provider supports designed voices, such as rebook-tts with
provider: 'mimo', the plugin first plans recurring speakers as compact role
cards, including gender, apparent age, occupation or identity, personality, and
voice style, then runs compact text segmentation. The role card is sent as the
voice design prompt for character segments; the backend can turn that role card
into a reusable voice sample and synthesize dialogue with voice clone. Narration
uses the provider's normal default voice. The frontend API stays the same: pass the provider, model, and
multiSpeaker: true. Long-running models can be bounded with
speakerAnalysis: { timeoutMs }; failures are surfaced through the analysis log
hook and the replay scripts.
import { openai } from '@ai-sdk/openai'
withTTS({
endpoint: 'http://127.0.0.1:4177',
provider: 'edge',
model: openai('gpt-4o-mini'),
multiSpeaker: true,
voiceProfile: {
narrator: 'zh-CN-YunxiNeural',
male: ['zh-CN-YunjianNeural', 'zh-CN-YunxiNeural'],
female: ['zh-CN-XiaoyiNeural', 'zh-CN-XiaoxiaoNeural'],
other: 'zh-CN-XiaoxiaoNeural',
},
player: ttsPlayer,
})For advanced job management, use the lower-level job APIs directly:
const job = await book.tts.createSectionJob(sectionIndex, { concurrency: 2 })
const latest = await book.tts.getJob(job.id)The output of parsers and input to renderers. This is the central contract.
interface Book {
sections: Section[] // Ordered list of sections (chapters)
dir?: 'ltr' | 'rtl' // Page progression direction
toc?: TOCItem[] // Table of contents
landmarks?: Landmark[] // Named locations (cover, toc, bodymatter...)
metadata?: BookMetadata // Title, author, etc.
rendition?: Rendition // Layout hints
resolveHref?(href: string): ResolvedNavigation | null
getCover?(): Promise<Blob | null>
destroy(): void // Release all resources
}All parsers normalize metadata to these consistent types:
| Field | Type | Notes |
|---|---|---|
title |
string |
Plain string |
subtitle |
string |
Plain string |
author |
Contributor[] |
Array of { name, sortAs?, role? } |
translator |
Contributor[] |
Array |
editor |
Contributor[] |
Array |
publisher |
string |
Plain string |
language |
string |
Single string (first if multiple) |
subject |
string[] |
Array of strings |
identifier |
string |
Plain string |
published |
string |
Date string |
modified |
string |
Date string |
description |
string |
May contain HTML |
belongsTo |
{ series?, collection? } |
Series info with { name, position?, total? } |
interface TOCItem {
label: string
href: string
subitems?: TOCItem[]
}interface Landmark {
label: string
href: string
type: string // 'cover', 'toc', 'bodymatter', etc.
}interface Rendition {
layout?: 'reflowable' | 'pre-paginated'
flow?: 'paginated' | 'scrolled' | 'continuous'
spread?: 'none' | 'auto' | 'landscape'
}A single chapter or content unit.
type SectionFormat = 'xhtml' | 'html' | 'image'
interface Section {
id: string | number
size: number // Byte size for progress
format?: SectionFormat // Content type (default: 'xhtml')
load(): Promise<string> | string // Returns content string
unload?(): void // Free cached resources
getDocument?(): Promise<SectionDocument | null> // AI-friendly doc tree
getSegments?(): Promise<TextSegment[]> | TextSegment[] // Styled text segments
getBlocks?(): Promise<TextBlock[]> | TextBlock[] // Structural reading blocks
createDocument?(): Promise<string> // Raw HTML string for searching
}Returns a content string - the renderer decides how to display it:
'xhtml'/'html': Full HTML/XHTML document or fragment'image': Data URI or base64 string
Returns a SectionDocument with query and mutation APIs. Returns null for formats without document structure (e.g., 'image'). See Document Model.
Returns styled text fragments extracted from the parsed XHTML tree. EPUB sections use this as the input to the Pretext adapter so renderers can avoid full chapter DOM layout during font or viewport changes.
Returns AST-derived reading blocks such as chapter, heading, paragraph, listItem, blockquote, and pre. The browser renderer uses these blocks as its primary input and applies preset styles instead of trusting arbitrary EPUB CSS.
SlateJS-inspired tree structure for AI-friendly content manipulation.
A node in the document tree:
interface DocumentNode {
type: string // 'p', 'h1', 'img', 'text', etc.
attrs?: Record<string, string> // class, id, src, href, etc.
children?: DocumentNode[] // Child nodes (element nodes)
text?: string // Text content (text nodes only)
}Text nodes have type: 'text' and text set. Element nodes have type set to the tag name and optional children.
interface SectionDocument {
nodes: DocumentNode[]
// Query
query(selector: string): DocumentNode[]
// Content extraction
getText(): string
getImages(): DocumentResource[]
// Immutable mutations (return new SectionDocument)
insertNode(path: number[], node: DocumentNode): SectionDocument
removeNode(path: number[]): SectionDocument
setNode(path: number[], attrs: Record<string, string>): SectionDocument
replaceText(path: number[], text: string): SectionDocument
// Output
serialize(): string
}CSS-like selectors:
- Tag name:
'p','h1','img' - Class:
'.intro','.highlight' - ID:
'#chapter1' - Attribute:
'[href]','[src="image.jpg"]' - Multiple:
'h1, h2, h3'
Returns plain text content of the entire document.
Returns all images as DocumentResource[]:
interface DocumentResource {
id: string
type: string // 'image', 'font', 'style'
mimeType: string
url: string
}All mutations are immutable - they return a new SectionDocument without modifying the original.
import { textNode, elementNode } from 'rebook'
// Path is an array of indices into the tree
// [0] = first root node
// [0, 1] = second child of first root node
const doc = await section.getDocument()
// Insert at root level (after first paragraph)
const withNewPara = doc.insertNode([1], elementNode('p', {}, [textNode('New paragraph')]))
// Remove a node
const withoutFirst = doc.removeNode([0])
// Update attributes
const highlighted = doc.setNode([0], { class: 'highlight' })
// Replace text content
const translated = doc.replaceText([0, 0], 'Translated text')
// Chain mutations
const result = doc
.setNode([0], { lang: 'en' })
.replaceText([0, 0], 'Hello')
.insertNode([1], elementNode('p', {}, [textNode('World')]))Converts the document tree back to an HTML string.
import {
textNode,
elementNode,
isTextNode,
isElementNode,
parseHTML,
createSectionDocument,
} from 'rebook'
// Create nodes
const text = textNode('Hello world')
const para = elementNode('p', { class: 'intro' }, [text])
const img = elementNode('img', { src: 'cover.jpg', alt: 'Cover' })
// Type guards
isTextNode(text) // true
isElementNode(para) // true
// Parse HTML string into DocumentNode[]
const nodes = parseHTML('<p>Hello</p><p>World</p>', domAdapter)
// Create SectionDocument from nodes
const doc = createSectionDocument(nodes, domAdapter)The Document Model enables these AI-powered workflows:
| Use Case | How |
|---|---|
| Translation | getText() -> translate -> replaceText() per text node |
| Content summary | getText() -> summarize |
| Annotation | setNode([path], { class: 'annotation' }) |
| Accessibility | Walk tree, add alt attrs to images via setNode() |
| Content injection | insertNode() to add AI-generated content |
| Restructuring | removeNode() + insertNode() to reorder sections |
| Image enhancement | getImages() -> process -> replace via setNode() |
rebook integrates the community package @chenglou/pretext instead of implementing text measurement itself. The library extracts EPUB/XHTML structure into styled TextSegment[], then delegates measurement and line breaking to Pretext.
import { prepareBlocks, layout, getVisibleLines } from 'rebook'
const blocks = await section.getBlocks!()
const prepared = prepareBlocks(blocks, {
baseStyle: {
fontFamily: 'Georgia, serif',
fontSize: 18,
lineHeight: 1.6,
},
})
const lines = layout(prepared, {
inlineSize: 680,
lineHeight: 29,
blockGap: 8,
})
const visible = getVisibleLines(lines, scrollTop, viewportHeight, 2)interface TextSegment {
text: string
style?: TextStyle
break?: 'normal' | 'never'
extraWidth?: number
source?: {
nodeType?: string
attrs?: Record<string, string>
}
}extractDocumentSegments(nodes) converts a DocumentNode[] tree into these segments, preserving inline style markers such as bold, italic, color, font size, and text decoration.
type TextBlockType = 'container' | 'chapter' | 'heading' | 'paragraph' | 'listItem' | 'blockquote' | 'pre' | 'image'
interface TextImage {
src: string
originalSrc?: string
alt?: string
title?: string
width?: number
height?: number
aspectRatio?: number
isCover?: boolean
role?: string
style?: ImageStyle
}
interface TextBlock {
id: string
type: TextBlockType
depth?: number
attrs?: Record<string, string>
style?: TextStyle
blockGapBefore?: number
blockGapAfter?: number
image?: TextImage
segments: TextSegment[]
}These types are defined in the core parser-renderer contract, not inside the browser renderer. Any renderer can consume section.getBlocks() without depending on the Pretext adapter. Image blocks carry a renderable src; EPUB sections also preserve originalSrc so renderers or tooling can distinguish OPF cover images from regular illustrations.
Compiles structural reading blocks into Pretext rich-inline items. This is the preferred path for browser rendering because block type and depth drive preset styles and spacing.
Compiles segment text into Pretext rich-inline items. This is the expensive step because Pretext uses Canvas text measurement. Run it when the text or font changes, not on every resize.
Runs the cheap layout step for a given inline size and line height. It returns LineRange[]:
interface LineRange {
index: number
start: LinePosition | null
end: LinePosition | null
text: string
width: number
top: number
height: number
segments: LineSegmentRange[]
}Each LineSegmentRange maps a rendered fragment back to the original EPUB segment index and Pretext cursor range, which is the data a windowed list, Canvas renderer, or SVG renderer needs to render only visible lines.
Returns a visible line window:
{
startIndex: number
endIndex: number
offsetTop: number
totalHeight: number
lines: LineRange[]
}This is intentionally framework-agnostic. React, Vue, Canvas, and custom renderers can consume the same line window.
interface Parser {
parse(input: ParserInput, options?: ParserOptions): Promise<Book>
canParse(input: ParserInput): Promise<boolean>
priority?: number // Higher = checked first (default 0)
}
type ParserInput = string | File | Blob | ArrayBuffer
interface ParserOptions {
domAdapter?: DOMAdapter
urlFactory?: URLFactory
sha1?: (data: ArrayBuffer) => Promise<ArrayBuffer>
onProgress?: (progress: number, message?: string) => void
}import { epub } from 'rebook/parsers/epub' // or from 'rebook'
import { mobi } from 'rebook/parsers/mobi' // or from 'rebook'
import { fb2 } from 'rebook/parsers/fb2' // or from 'rebook'
import { cbz } from 'rebook/parsers/cbz' // or from 'rebook'
// Each returns a Parser instance
const parser = epub()import { EPUBParser } from 'rebook'
const parser = new EPUBParser()
if (await parser.canParse(file)) {
const book = await parser.parse(file, { domAdapter, urlFactory })
}interface Renderer {
open(book: Book): Promise<void>
goTo(target: string | number): Promise<void>
next(): Promise<void>
prev(): Promise<void>
setStyles(styles: RendererStyles): void
setLayout(layout: LayoutMode): void
setSpread(maxColumns: number): void // Control two-page layout
on(event: string, listener: (e: any) => void): void
off(event: string, listener: (e: any) => void): void
destroy(): void
}BrowserRenderer consumes section.getBlocks(), Pretext prepared blocks, and LineRange[] to render only the visible text rows as simple DOM spans. It supports auto-spread two-column layout through maxColumnCount, maxInlineSize, gap, and setSpread().
import { createBrowserRenderer } from 'rebook'
const renderer = createBrowserRenderer({
container: document.getElementById('viewer')!,
styles: {
fontSize: '18px',
lineHeight: 1.6,
maxInlineSize: '680px',
},
})
await renderer.open(book)
await renderer.goTo(0)In paginated mode, this renderer hides free scrolling, maps wheel/next()/prev() to page turns, and keeps page-block padding so text is not clipped at the viewport edge. In scrolled mode it falls back to continuous vertical scrolling.
Use createWechatMiniProgramReader() for app/page integration. It provides Mini Program parser adapters, installs the Pretext measurement polyfill by default, and emits a serializable snapshot instead of DOM nodes.
import { registry } from 'rebook'
import { epub } from 'rebook/parsers/epub'
import { createWechatMiniProgramReader } from 'rebook/renderers/wechat-miniprogram'
registry.register('epub', epub)
const fs = wx.getFileSystemManager()
const arrayBuffer = fs.readFileSync(filePath) as ArrayBuffer
const unitlessStyles = new Set(['fontWeight', 'opacity', 'zIndex'])
const toStyleText = (style: Record<string, string | number> = {}) =>
Object.entries(style)
.map(([key, value]) => {
const cssKey = key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
const cssValue = typeof value === 'number' && !unitlessStyles.has(key)
? `${value}px`
: String(value)
return `${cssKey}:${cssValue}`
})
.join(';')
const reader = createWechatMiniProgramReader({
width: 375,
height: 667,
wx,
styles: { fontSize: '18px', lineHeight: 1.6 },
setData: snapshot => this.setData({
reader: {
...snapshot,
lines: snapshot.lines.map(line => ({
...line,
styleText: toStyleText(line.style),
fragments: 'fragments' in line
? line.fragments.map(fragment => ({
...fragment,
styleText: toStyleText(fragment.style),
}))
: undefined,
})),
},
}),
})
await reader.open(arrayBuffer)
await reader.goTo(0)
await reader.next()Mini Program pages can render snapshot.lines in WXML:
<view class="reader" style="width: {{reader.width}}px; height: {{reader.height}}px;">
<block wx:for="{{reader.lines}}" wx:key="key">
<view
wx:if="{{item.kind === 'text'}}"
class="reader-line"
style="{{item.styleText}}"
>
<text
wx:for="{{item.fragments}}"
wx:for-item="fragment"
wx:key="key"
style="{{fragment.styleText}}"
>{{fragment.text}}</text>
</view>
<image
wx:elif="{{item.kind === 'image'}}"
class="reader-image"
style="{{item.styleText}}"
src="{{item.image.src}}"
mode="widthFix"
/>
</block>
</view>For lower-level integrations, createWechatMiniProgramRenderer() exposes the renderer directly. Its constructor installs the platform-neutral installPretextMeasurementPolyfill(wx) by default so @chenglou/pretext can measure text through the host createOffscreenCanvas implementation in Mini Program runtimes. Pass installPretextPolyfill: false if the host has already installed a compatible OffscreenCanvas global.
Parsers are environment-agnostic via dependency injection.
Abstracts DOM parsing and querying:
interface DOMAdapter {
parseXML(str: string): XMLDocument
parseHTML(str: string, mimeType?: string): XMLDocument
serialize(doc: XMLDocument): string
// Optional: document manipulation (for Document Model)
getChildNodes?(element: XMLElement): XMLNode[]
createDocument?(): XMLDocument
createElement?(doc: XMLDocument, tag: string): XMLElement
createTextNode?(doc: XMLDocument, text: string): XMLNode
appendChild?(parent: XMLElement, child: XMLNode): void
}Abstracts blob URL creation:
interface URLFactory {
createURL(data: string | ArrayBuffer | Blob, mimeType?: string): string
revokeURL(url: string): void
}| Adapter | Import | Environment |
|---|---|---|
BrowserDOMAdapter |
rebook |
Browser |
BrowserURLFactory |
rebook |
Browser |
NodeDOMAdapter |
rebook/adapters/node |
Node.js (@xmldom/xmldom) |
NodeURLFactory |
rebook/adapters/node |
Node.js (fake URLs) |
import { createReader, registry } from 'rebook'
import { epub } from 'rebook/parsers/epub'
registry.register('epub', epub)
const reader = createReader({ container: element })
await reader.open(file) // Browser adapters auto-injectedimport { registry } from 'rebook'
import { epub } from 'rebook/parsers/epub'
import { NodeDOMAdapter, NodeURLFactory } from 'rebook/adapters/node'
registry.register('epub', epub)
const book = await registry.open(arrayBuffer, {
domAdapter: new NodeDOMAdapter(),
urlFactory: new NodeURLFactory(),
})import {
EBookError, // Base class for all rebook errors
ParseError, // Parsing failed (malformed content)
UnsupportedFormatError, // Format not recognized
CorruptedFileError, // File is severely corrupted
AdapterRequiredError, // Required adapter not provided
UnsupportedInputError, // Input type not supported
} from 'rebook'| Property | Type | Description |
|---|---|---|
message |
string |
Human-readable description |
code |
string |
Machine-readable code (e.g., 'PARSE_ERROR') |
name |
string |
Error class name |
format |
string |
Format name (on ParseError, CorruptedFileError) |
try {
const book = await registry.open(file, options)
} catch (e) {
if (e instanceof UnsupportedFormatError) {
console.error('Unsupported file format')
} else if (e instanceof ParseError) {
console.error(`Parse error (${e.format}): ${e.message}`)
} else if (e instanceof EBookError) {
console.error(`[${e.code}] ${e.message}`)
} else {
throw e
}
}All parsers normalize metadata to consistent types:
const book = await registry.open(file, options)
// Title: always a string
book.metadata?.title // "My Book"
// Author: always Contributor[]
const authors = book.metadata?.author ?? []
for (const a of authors) {
a.name // "John Doe"
a.sortAs // "Doe, John" (optional)
a.role // "aut" (optional)
}
// Language: always a single string
book.metadata?.language // "en"
// Subject: always string[]
book.metadata?.subject // ["Fiction", "Fantasy"]import {
normalizeLanguage, // (input) => string
normalizeTitle, // (input) => string
normalizePublisher, // (input) => string
normalizeContributors, // (input) => Contributor[]
normalizeSubjects, // (input) => string[]
} from 'rebook'interface RendererStyles {
fontFamily?: string
fontSize?: string
lineHeight?: number | string
textAlign?: 'start' | 'justify' | 'center'
hyphenate?: boolean
css?: string // Custom CSS
theme?: 'light' | 'dark' | 'sepia'
color?: string
background?: string
gap?: string // Column gap (paginated)
maxInlineSize?: string // Max column width
maxBlockSize?: string // Max page height
margin?: string // Header/footer margin
}reader.setStyles({
fontFamily: 'Georgia, serif',
fontSize: '18px',
lineHeight: 1.6,
theme: 'sepia',
css: 'p { text-indent: 2em; }',
})FB2 is pure XML without embedded CSS. Use the default stylesheet:
import { fb2DefaultStyles } from 'rebook'
reader.setStyles({ css: fb2DefaultStyles })
// Or combine with custom overrides
reader.setStyles({ css: fb2DefaultStyles + 'p { color: #333; }' })Legacy MOBI6 files (.mobi) lack embedded stylesheets:
import { mobi6DefaultStyles } from 'rebook'
reader.setStyles({ css: mobi6DefaultStyles })Modern KF8/AZW3 files (.azw3) have embedded styles - mobi6DefaultStyles won't conflict.
When using registry.open() or registry.detect(), parsers are checked in priority order (highest first):
| Parser | Default Priority | Detection Method |
|---|---|---|
| EPUB | 10 | PK magic + mimetype entry = application/epub+zip |
| MOBI | 5 | BOOKMOBI magic at bytes 60-68 |
| FB2 | 5 | <FictionBook XML or .fb2 in zip |
| CBZ | 0 | .cbz extension or zip containing image files |
Override priority when registering:
registry.register('cbz', cbz, 20) // Check CBZ first