Bible IO

Bible IO is a Dart content and state-of-truth layer for Bible applications. It loads validated translation data into immutable, edition-aware models and provides stable navigation, multilingual references, UI-ready search results, and lossless JSON round-tripping.

Features

  • Immutable Bible, Book, Chapter, and Verse models with exact source labels
  • Versioned content schema with explicit canon order and extensible annotations
  • Strict validation with path-aware BibleDataFormatError failures
  • Background JSON processing and structured progress for responsive UIs
  • Eager, lazy, or disabled search indexes to fit different memory budgets
  • Canonically normalized multilingual search, including unspaced scripts
  • Paginated, UI-ready hits with grapheme-safe snippets and accurate ranges
  • Multilingual reference and passage parsing through bible_io_references 1.2
  • Subdivided verses (5a, 5b) and combined source entries (29-30)
  • Edition-aware keys for bookmarks, highlights, notes, and reading progress
  • Translation metadata and catalog helpers for multi-edition applications
  • Result-based lookup helpers for application boundaries

Installation

Bible IO supports Dart 3.4 and later.

dependencies:
  bible_io: ^1.2.0

bible_io_references is exported by this package. Importing package:bible_io/bible_io.dart provides both Bible IO and reference types.

Quick start

import 'package:bible_io/bible_io.dart';

Future<void> main() async {
  final bible = await Bible.load('path/to/en_kjv.json');

  final genesis11 = bible.getVerse(BibleBookEnum.genesis, 1, 1);
  final john316 = bible.getVerseByRef('John 3:16');

  print(genesis11.text);
  print(john316.text);
}

Loading without blocking the UI

Bible.load() reads files on platforms with dart:io. JSON decoding, model validation, and eager index construction run in a background isolate by default where isolates are available:

final bible = await Bible.load(
  'assets/en_kjv.json',
  onLoadProgress: (progress) {
    print('${progress.phase}: ${(progress.fraction * 100).round()}%');
  },
);

Flutter and other platform-neutral applications can load through an asset bundle, UTF-8 bytes, a decoded map, or a JSON string:

final fromAsset = await Bible.loadAsset(rootBundle, 'assets/kjv.json');
final fromBytes = Bible.fromUtf8Bytes(bytes);
final fromMap = Bible.fromDecodedJson(decodedJson);
final fromString = await Bible.fromJsonAsync(jsonString);

On platforms without isolate support, async construction falls back to the current isolate. Use parseInBackground: false only when synchronous processing is intentional.

Search indexes can be built eagerly, on first indexed search, or not retained:

final options = const BibleLoadOptions(
  searchIndexMode: SearchIndexMode.disabled,
);
final bible = Bible.fromDecodedJson(data, options: options);

print(bible.hasSearchIndex);
bible.prewarmSearchIndex();
bible.clearSearchIndex();

With SearchIndexMode.disabled, searches scan the content and prewarmSearchIndex() does not retain an index.

With SearchIndexMode.lazy, the first indexed search builds synchronously. Call await bible.prewarmSearchIndexAsync() ahead of that search when native UI responsiveness matters.

Edition identity and persisted UI state

Give every translation or revision a stable metadata id. A verse location is only unique inside one edition, so persist BibleVerseKey values for bookmarks, highlights, notes, and reading progress:

final verse = bible.getVerseByRef('John 3:16');
final editionId = bible.metadata.id;

if (editionId != null) {
  final key = BibleVerseKey.fromVerse(editionId, verse);
  final stored = key.toJson();
  final restored = BibleVerseKey.fromJson(stored);
  print(restored);
}

BibleLocation also supports copyWith(), toJson(), fromJson(), and conversion to VerseRef. Model values and nested collections are immutable; construct a new value with copyWith() instead of mutating shared state.

final genesis = bible[BibleBookEnum.genesis] as Book;
final genesis1 = bible[(BibleBookEnum.genesis, 1)] as Chapter;
final genesis11 = bible[(BibleBookEnum.genesis, 1, 1)] as Verse;

final current = genesis11.location;
final next = bible.nextVerse(current);
final previous = bible.previousVerse(current);

Navigation follows the edition's declared bookOrder and actual chapter and verse labels. Sparse numbering is supported.

Subdivided and combined verses

Verse maps can contain subdivided or combined labels alongside ordinary numbers:

{
  "28": "Previous verse",
  "29-30": "One source text entry covering both verses",
  "31": "Next verse"
}

The loader retains the exact key and text as one immutable Verse. Both numeric lookups below find that same entry:

final combined = bible.getVerseByLabel(BibleBookEnum.genesis, 24, '29-30');
final sameEntry = bible.getVerse(BibleBookEnum.genesis, 24, 30);
final passage = bible.getPassage('Genesis 24:29-30'); // One entry.

print(combined.verseLabel);     // 29-30
print(combined.verseNumber);    // 29: starting number, kept for compatibility.
print(combined.endVerseNumber); // 30
print(combined.toReference()); // A VerseRangeRef.

For separate 5a and 5b entries, getVerseByRef('John 1:5a') selects the first part, while getPassage('John 1:5a-5b') and getPassage('John 1:5') return both parts. A single-entry lookup of John 1:5 throws AmbiguousVerseError when multiple parts exist. Programmatic construction uses Verse.labeled(book, chapter, '5a', text); existing numeric constructors remain available.

Ranges return each intersecting source entry once, preserving its complete text. Navigation moves between source entries, and search hits, statistics, and pagination count those entries. SearchOptions(verse: 30) includes a 29-30 entry; SearchOptions(verseLabel: '29-30') selects that declared label.

Locations and edition keys preserve the label for bookmarks and reading progress. Use toReference() for a single or combined entry; toVerseRef() requires a single entry and preserves its subdivision. JSON round-tripping retains the original source label, including Unicode digits and dashes.

References and rich passages

Reference parsing is multilingual by default. The loaded language and custom book names are preferences, while a concrete inputLanguage makes parsing strict:

final parsed = bible.parseReference('Juan 3:16');
if (parsed case ParseSuccess(value: final reference)) {
  print(bible.resolveReference(reference).single.text);
}

final spanish = bible.getVerseByRef(
  'Juan 3:16',
  inputLanguage: BibleLanguageEnum.spanish,
);

The reference grammar supports verse lists, chapter ranges, semicolon-separated sequences, and cross-book ranges:

final selection = bible.getPassage(
  'John 3:16,18-20; Acts 2:1-4; Romans 8',
);
final crossBook = bible.getVerseRangeByRef('John 21:25-Acts 1:2');

final osis = referenceFromOsisIdentifier('John.3.16');
final verse = bible.resolveReference(osis).single;

search() is a fast all-terms search, not an exact phrase search:

final verses = bible.search('faith hope');

Use SearchOptions for explicit modes, scope, normalization, and pagination:

final page = bible.searchWithOptions(
  'creacion',
  SearchOptions(
    mode: SearchMode.any,
    wholeWords: false,
    ignoreDiacritics: true,
    offset: 20,
    maxResults: 20,
  ),
);

print('Showing ${page.count} results; more: ${page.hasMore}');
print('Next offset: ${page.nextOffset}');

Canonical NFC normalization is enabled by default, so equivalent NFC and NFD text matches consistently. Diacritics remain meaningful unless ignoreDiacritics is enabled; that option also removes Hebrew niqqud and Arabic harakat. All/any search can match query substrings in scripts commonly written without spaces, including Chinese, Japanese, Thai, Lao, Khmer, and Myanmar. Set wholeWords: true when exact token boundaries are required.

Search hits are ready for highlighted UI rendering:

for (final hit in page.hits) {
  print('${hit.reference}: ${hit.snippet}');
  print(hit.snippetMatchRanges); // Offsets relative to hit.snippet.
  print(hit.matchRanges);        // Offsets relative to hit.verse.text.
}

snippetStart and snippetEnd locate the snippet within the full verse. Cropping respects grapheme boundaries.

Typo-tolerant search uses bounded Unicode edit distance and the same normalization, multi-term modes, pagination, and unspaced-script substring behavior:

final fuzzy = bible.fuzzySearch(
  'beginnig creatd',
  maxDistance: 1,
  mode: SearchMode.all,
  maxResults: 20,
);

Validation and errors

Decoded content is strict by default: it must contain books, chapters, verses, and non-blank verse text. Malformed data throws BibleDataFormatError with a stable code and JSON path instead of leaking cast or parser errors.

Intentionally partial content can opt into the compatibility policy:

final partial = Bible.fromDecodedJson(
  data,
  options: const BibleLoadOptions(
    validation: BibleDataValidationOptions.permissive,
  ),
);

Permissive validation allows skeletal content; it does not accept malformed types, invalid identifiers, duplicate locations, or non-positive numbers.

For ordinary lookup failures at application boundaries, use result helpers:

final result = bible.getVerseResult(BibleBookEnum.genesis, 1, 1);

switch (result) {
  case Success(value: final verse):
    print(verse.text);
  case Failure(error: final error):
    print(error);
}

Translation catalogs and metadata

BibleCatalog validates unique, non-blank source IDs and supports list- or map-shaped catalogs:

final catalog = BibleCatalog.fromDecodedJson(catalogJson);
final englishSources = catalog.forLanguage('en');
final source = catalog.findById('eng-kjv-1769');

Metadata includes edition identity, language, display name, abbreviation, direction, copyright, content license, canon, and version date. Unknown JSON-compatible metadata and nested source fields are retained at their original levels for round-tripping.

Content schema

Schema version 1 adds explicit bookOrder, edition identity, and optional annotations while accepting the original plain-string verse format. See the content schema for the full contract and migration guidance.

Statistics and diagnostics

print(bible.stats);
print(bible.getBook(BibleBookEnum.genesis).stats);
print(bible.getChapter(BibleBookEnum.genesis, 1).stats);
print(bible.performanceMetrics);

Performance metrics report content size and retained-index state as estimates; they are diagnostics, not heap-profiler measurements.

Licensing

Bible IO source code is intentionally licensed under the GNU Affero General Public License v3 or later (AGPL-3.0-or-later) so improvements to Bible software remain free, including software offered over a network.

Bible translations, study notes, footnotes, and other content loaded by the package are independent works. Their copyright and license are not changed by the package's code license. Applications and data distributors must obtain and honor the rights for each content edition; use metadata copyright and license fields to carry that information with the edition.

Development and release checks

See package structure for module responsibilities and the construction and search boundaries.

dart format --output=none --set-exit-if-changed lib bin example test
dart analyze
dart test
mkdir -p build/web
dart compile js example/bible_io_example.dart -o build/web/example.js
dart pub publish --dry-run

Run the conventional example with:

dart run example/bible_io_example.dart

Publishing a release

Pushing a tag such as Release-1.2.0 runs the publishing workflow. It checks that the tag matches version in pubspec.yaml, runs the complete CI workflow (including minimum-SDK tests), and then publishes to pub.flutter-io.cn using Dart's official reusable workflow and GitHub OIDC authentication. No stored pub.flutter-io.cn token is needed. See Dart's automated publishing documentation.

The pub.flutter-io.cn settings must use repository m0ty/bible-io-package-dart, tag pattern Release-{{version}}, and publishing from push events enabled.

Update the package version, changelog, and installation example, then commit and push those changes before creating the release tag:

git tag Release-1.2.0
git push origin Release-1.2.0

The tag push triggers publication; creating a GitHub Release is optional.

Libraries

bible_example
bible_io
A Dart package for loading and working with structured Bible text data.