Skip to content

Latest commit

 

History

History
1357 lines (1049 loc) · 39.4 KB

File metadata and controls

1357 lines (1049 loc) · 39.4 KB

API Reference

Complete API documentation for rebook.

Table of Contents


Search

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.


MCP Tools

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 via chapterIndex.
  • get_chapter_text: returns readable text for one chapter with truncation.

Parser Registry

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']

registry.open(input, options?)

Auto-detect format and parse. input can be a File, Blob, ArrayBuffer, or URL string.

registry.detect(input)

Returns the first parser whose canParse() returns true, or null.

registry.list()

Returns an array of registered parser names.


First Sections Export

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.

Custom Exporters

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)

ReaderView

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.

Opening

await reader.open(file)           // File, Blob, ArrayBuffer, or URL string
await reader.openBook(book)       // Pre-parsed Book instance

Navigation

await 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% progress

Styling & Layout

reader.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 page

Marks

Marks 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.

Auto-Spread Layout

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.

Events

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)
})

State

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[]

Cleanup

reader.destroy() // Release all resources

Plugins

RebookPlugin is the low-level Book -> Book middleware 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)

Translation

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.

Text To Speech

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)

Book

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
}

BookMetadata

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? }

TOCItem

interface TOCItem {
    label: string
    href: string
    subitems?: TOCItem[]
}

Landmark

interface Landmark {
    label: string
    href: string
    type: string // 'cover', 'toc', 'bodymatter', etc.
}

Rendition

interface Rendition {
    layout?: 'reflowable' | 'pre-paginated'
    flow?: 'paginated' | 'scrolled' | 'continuous'
    spread?: 'none' | 'auto' | 'landscape'
}

Section

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
}

load()

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

getDocument()

Returns a SectionDocument with query and mutation APIs. Returns null for formats without document structure (e.g., 'image'). See Document Model.

getSegments()

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.

getBlocks()

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.


Document Model

SlateJS-inspired tree structure for AI-friendly content manipulation.

DocumentNode

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.

SectionDocument

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
}

query(selector)

CSS-like selectors:

  • Tag name: 'p', 'h1', 'img'
  • Class: '.intro', '.highlight'
  • ID: '#chapter1'
  • Attribute: '[href]', '[src="image.jpg"]'
  • Multiple: 'h1, h2, h3'

getText()

Returns plain text content of the entire document.

getImages()

Returns all images as DocumentResource[]:

interface DocumentResource {
    id: string
    type: string    // 'image', 'font', 'style'
    mimeType: string
    url: string
}

Mutation Operations

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')]))

serialize()

Converts the document tree back to an HTML string.

Node Helpers

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)

AI Use Cases

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()

Pretext Layout

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)

TextSegment

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.

TextBlock

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.

prepareBlocks(blocks, options?)

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.

prepare(segments, options?)

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.

layout(prepared, options)

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.

getVisibleLines(lines, scrollTop, viewportHeight, overscan?)

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.


Parser Interface

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
}

Parser Factories

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()

Direct Parser Usage

import { EPUBParser } from 'rebook'

const parser = new EPUBParser()
if (await parser.canParse(file)) {
    const book = await parser.parse(file, { domAdapter, urlFactory })
}

Renderer Interface

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
}

Browser Renderer

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.

WeChat Mini Program Reader

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.


Adapter System

Parsers are environment-agnostic via dependency injection.

DOMAdapter

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
}

URLFactory

Abstracts blob URL creation:

interface URLFactory {
    createURL(data: string | ArrayBuffer | Blob, mimeType?: string): string
    revokeURL(url: string): void
}

Built-in Adapters

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)

Browser Usage (auto-wired)

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-injected

Node.js / Worker Usage

import { 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(),
})

Error Types

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'

Error Properties

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)

Usage

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
    }
}

Metadata Normalization

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"]

Normalization Helpers

import {
    normalizeLanguage,      // (input) => string
    normalizeTitle,         // (input) => string
    normalizePublisher,     // (input) => string
    normalizeContributors,  // (input) => Contributor[]
    normalizeSubjects,      // (input) => string[]
} from 'rebook'

Styles

RendererStyles

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
}

Usage

reader.setStyles({
    fontFamily: 'Georgia, serif',
    fontSize: '18px',
    lineHeight: 1.6,
    theme: 'sepia',
    css: 'p { text-indent: 2em; }',
})

Default Format Styles

FB2 Styles

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; }' })

MOBI6 Styles

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.


Parser Detection Priority

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