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, andVersemodels with exact source labels - Versioned content schema with explicit canon order and extensible annotations
- Strict validation with path-aware
BibleDataFormatErrorfailures - 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_references1.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.
Navigation
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
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.