bible-io-references-dart
A comprehensive Dart library for parsing Bible verse references into structured objects, supporting multiple languages and flexible formatting.
Features
- ✅ Single verse parsing:
John 3:16,jo 3:16(abbreviations) - ✅ Verse range parsing:
John 3:16-17,John 3:16-4:1,John 3:16-Acts 1:2 - ✅ Subdivided and combined verses:
John 1:5a-5band exact source labels such as3-4 - ✅ Flexible formatting: Supports
:,.separators and various dash types (-,–,—) - ✅ Multi-language support: English plus 12 localized language packs
- ✅ Auto language detection: Intelligently handles language precedence and collisions
- ✅ Immutable value objects: Equality, stable hashes, and checked copies
- ✅ Comprehensive error handling: Detailed error codes and diagnostics
- ✅ Non-throwing parsing: Nullable and typed-result APIs for user input
- ✅ Localized formatting: Long or abbreviated book names in supported languages
- ✅ Value semantics and JSON: Equality, checked copies, and serialization
- ✅ Configurable parsing: Custom aliases, language priority, and ambiguity policy
- ✅ Parse metadata: Detected languages, selected matches, and alternatives
- ✅ Rich passage grammar: Whole books, chapters, verse lists, sequences, and single-chapter shorthand
- ✅ Unicode-aware input: Fullwidth forms, Arabic-Indic digits, RTL punctuation, and CJK adjacency
- ✅ Reference extraction: Find and safely linkify passages embedded in arbitrary prose
- ✅ Interoperability: OSIS and USFM identifiers for every supported book
- ✅ Batch CLI: UTF-8 stdin/files, JSON Lines, and meaningful exit codes
- ✅ Zero dependencies: Pure Dart implementation
Installation
dart pub add bible_io_references
Quick Start
import 'package:bible_io_references/bible_io_references.dart';
void main() {
// Parse a single verse
final verse = VerseRef.parse("John 3:16");
print(verse.displayString); // "John 3:16"
print("${verse.book.fullName} ${verse.chapter}:${verse.verse}");
// Parse a verse range
final range = VerseRangeRef.parse("John 3:16-4:1");
print(range.displayString); // "John 3:16-4:1"
// Parse any reference type
final reference = Reference.parse("John 3:16-17");
if (reference is VerseRef) {
print("Single verse: ${reference.displayString}");
} else if (reference is VerseRangeRef) {
print("Range: ${reference.displayString}");
}
}
API Overview
Core Classes
Reference: Sealed class representing either a verse or rangeVerseRef: Single verse reference (book, chapter, verse)VerseRangeRef: Verse range reference (start and end VerseRef)VerseLabel: One source text entry's exact verse label, including combined versesBibleBookEnum: Enumeration of all Bible books with names and abbreviationsBibleLanguageEnum: Supported languages for parsingReferenceParser: Reusable parser with aliases and ambiguity configurationPassage: Sealed value type for books, chapters, verse selections, and sequencesPassageParser: Reusable rich-grammar parser built onReferenceParserReferenceInputNormalizer: Syntax-oriented Unicode normalization with UTF-16 source mappingReferenceExtractor: Parser-driven extraction and span-safe replacement/linkificationParseResult<T>: Non-throwing success/failure result with parse metadata
Parsing Methods
// Direct parsing to specific types
final verse = VerseRef.parse("John 3:16");
final range = VerseRangeRef.parse("John 3:16-17");
// Flexible parsing (returns Reference union)
final single = Reference.parse("John 3:16"); // VerseRef
final passage = Reference.parse("John 3:16-17"); // VerseRangeRef
// Non-throwing alternatives
const userInput = "John 3:16";
final optional = Reference.tryParse(userInput);
final result = Reference.parseResult(userInput);
// Legacy parsing functions (still available)
final legacyVerse = verseRefFromStr("John 3:16");
final legacyRange = verseRangeRefFromStr("John 3:16-17");
Subdivided and Combined Verses
Verse subdivisions use one letter a-z. Parsers accept uppercase letters and
normalize them to lowercase. The existing integer verse field remains intact:
final verse = VerseRef.parse('John 1:5a');
print(verse.verse); // 5
print(verse.subdivision); // a
print(verse.verseLabel); // 5a
final range = Reference.parse('John 1:5a-5b');
final passage = Passage.parse('John 1:5a,5b-6a; Jude 5a');
print(range.displayString); // John 1:5a-5b
print(passage.displayString); // John 1:5a,5b-6a; Jude 1:5a
Subdivisions survive localized formatting, extraction, JSON, and machine
identifiers. John 1:5a serializes to OSIS John.1.5!a and USFM JHN 1:5a.
Plain verse JSON keeps its existing shape; subdivided verses add
"subdivision": "a" while "verse" remains an integer. Checked constructors
require a lowercase suffix. copyWith retains it unless a replacement is
provided or clearSubdivision: true is set. Ordering is by book, chapter,
verse, then suffix (5 < 5a < 5b < 6); this orders identifiers without defining
which text portions they contain.
A translation may store verses 3-4 as one text entry. Use VerseLabel to
preserve that source label, then resolve its location when the book and chapter
are known:
final label = VerseLabel.parse('3–4');
print(label.source); // 3–4 (exact original string)
print(label.displayString); // 3-4 (normalized spelling)
print(label.isCombined); // true
final reference = label.toReference(book: BibleBookEnum.john, chapter: 1);
print(reference.displayString); // John 1:3-4
final restored = VerseLabel.fromJson(label.toJson());
print(restored.source); // 3–4
VerseLabel also accepts 5a and 5a-5b, and offers tryParse and
parseResult. Its JSON stores the original string as {"label": "3–4"};
equality compares that source string, including spelling and whitespace. Keep
the label alongside your text entry: converting it to a reference yields a
normalized verse or range. Reference.parse('John 1:3-4') remains an ordinary
range and does not infer that a translation combines the verses into one entry.
Rich Passage Grammar
Use Passage.parse when an expression can be broader than one verse or one
contiguous verse range. Reference.parse intentionally remains the narrower
verse/range API.
final book = Passage.parse('John');
print(book); // John
final chapter = Passage.parse('John 3');
print(chapter); // John 3
final chapters = Passage.parse('John 3-4');
print(chapters); // John 3-4
final verses = Passage.parse('John 3:16,18-20,4:1');
print(verses); // John 3:16,18-20,4:1
final sequence = Passage.parse('John 3:16; Acts 2:1-4; Romans 8');
print(sequence); // John 3:16; Acts 2:1-4; Romans 8
// Standard single-chapter books interpret a bare number as a verse.
final jude = Passage.parse('Jude 3');
print(jude); // Jude 1:3
final result = Passage.parseResult('John 3:16,18-20');
print(result.valueOrNull);
print(result.metadataOrNull?.detectedLanguage);
print(verses.format(language: BibleLanguageEnum.spanish));
// Juan 3:16,18-20,4:1
print(verses.osisIdentifier); // John.3.16 John.3.18-John.3.20 John.4.1
print(verses.usfmIdentifier); // JHN 3:16,18-20,JHN 4:1
The built-in single-chapter set is Obadiah, Philemon, 2 John, 3 John, and Jude. It can be replaced for a parser instance:
final parser = PassageParser(
referenceParser: ReferenceParser(
aliases: {'favorite': BibleBookEnum.john},
),
singleChapterBooks: {BibleBookEnum.jude},
);
print(parser.parse('favorite 3:16,18')); // John 3:16,18
All passage variants have value equality, immutable collections, toJson,
and Passage.fromJson support.
Unicode Input Normalization
Reference and passage parsing normalize common reference-syntax variants, including fullwidth ASCII, Arabic-Indic and Eastern Arabic/Persian digits, Unicode spaces, dash/comma/semicolon/colon variants, combining marks, and directional controls. CJK book names may be adjacent to their chapter number.
final passage = Passage.parse('John \u0663\uff1a\u0661\u0666');
print(passage); // John 3:16
final normalization = ReferenceInputNormalizer.normalizeDetailed(
'\u200fJohn \u0663\uff1a\u0661\u0666',
);
print(normalization.normalizedText); // John 3:16
final originalSpan = normalization.mapNormalizedSpan(
0,
normalization.normalizedLength,
);
print(originalSpan.start); // UTF-16 offset in the original input
print(normalization.originalTextFor(0, normalization.normalizedLength));
This normalizer is deliberately reference-syntax-oriented, not a general Unicode normalization or transliteration library. It preserves letter case and whitespace runs.
Extracting and Linkifying References
ReferenceExtractor finds the longest deterministic, non-overlapping passage
matches in prose. Every match retains the parsed Passage, parse metadata,
exact source substring, and original UTF-16 offsets.
const text = 'Study John 3:16,18-20; Acts 2:1-4 today.';
final extractor = ReferenceExtractor();
final matches = extractor.extract(text);
for (final match in matches) {
print(match.passage);
print('${match.start}-${match.end}: ${match.sourceText}');
}
final markdown = extractor.linkifyMarkdown(
text,
uriBuilder: (match) => Uri.https(
'example.test',
'/passage',
{'q': match.passage.displayString},
),
);
replaceMatches and linkify apply callbacks in one pass; reference-looking
text returned by a callback is never scanned again. Bare-book extraction is
off by default and can be enabled with
ReferenceExtractor(includeBareBooks: true).
Language Support
// Auto language detection (default)
final detected = VerseRef.parse("Juan 3:16"); // Detects Spanish
// Explicit language specification
final spanish = VerseRef.parse(
"Juan 3:16",
language: BibleLanguageEnum.spanish,
);
// Language from string
final fromCode = VerseRef.parse(
"Juan 3:16",
language: BibleLanguageEnum.fromStr("es"),
);
Configurable Parsing and Detection Metadata
final parser = ReferenceParser(
aliases: {'jn': BibleBookEnum.john},
preferredLanguages: [BibleLanguageEnum.spanish],
ambiguityPolicy: ReferenceAmbiguityPolicy.reject,
);
final result = parser.parseResult('jn 3:16');
if (result case ParseSuccess(value: final reference, metadata: final metadata)) {
print(reference); // John 3:16
print(metadata.detectedLanguage); // null: custom global alias
print(metadata.alternateMatches.length); // bundled matches remain visible
}
final detected = Reference.parseResult('Juan 3:16');
print(detected.metadataOrNull?.detectedLanguage); // Spanish
Localized Formatting
final ref = Reference.parse('Juan 3:16', language: BibleLanguageEnum.spanish);
print(ref.format(language: BibleLanguageEnum.spanish));
// Juan 3:16
print(ref.format(
language: BibleLanguageEnum.spanish,
bookNameStyle: ReferenceBookNameStyle.short,
));
// Jn 3:16
print(BibleLanguageEnum.spanish.isParsingSupported); // true
print(BibleLanguageEnum.greek.isParsingSupported); // false
Value Objects and JSON
final verse = VerseRef.parse('John 3:16');
final nextVerse = verse.copyWith(verse: 17);
final json = nextVerse.toJson();
final restored = Reference.fromJson(json);
print(restored == nextVerse); // true
OSIS and USFM Identifiers
final verse = Reference.parse('John 3:16');
print(verse.osisIdentifier); // John.3.16
print(verse.usfmIdentifier); // JHN 3:16
final range = referenceFromOsisIdentifier('2Cor.6.14-2Cor.7.1');
print(range); // 2 Corinthians 6:14-7:1
print(BibleBookEnum.john.osisIdentifier); // John
print(BibleBookEnum.john.usfmIdentifier); // JHN
OSIS ranges use complete dotted endpoints. USFM identifiers use the official
three-character book codes; ADE is retained as a documented
Paratext-compatible extension for separately modeled Esther additions.
Mappings follow the CrossWire OSIS book vocabulary
and the official USFM book identifier table.
Supported Languages
- Arabic (ar) - Complete support
- English (en) - Complete support
- Spanish (es) - Complete support
- French (fr) - Complete support
- German (de) - Complete support
- Portuguese (pt) - Complete support
- Russian (ru) - Complete support
- Korean (ko) - Complete support
- Chinese (zh) - Complete support
- Hebrew (he) - Complete support
- Hindi (hi) - Complete support
- Indonesian (id) - Complete support
- Tagalog (tl) - Complete support
Use language.isParsingSupported or supportedParsingLanguages before
offering a language in user-facing controls. Some enum identifiers are reserved
for future language packs but do not yet have parsing data.
Advanced Usage
Error Handling
try {
final ref = VerseRef.parse("InvalidBook 3:16");
} on ParseVerseRefError catch (e) {
print("Error code: ${e.code}"); // "unknown_book"
print("Details: ${e.details}"); // "book token 'InvalidBook' did not match known books"
}
Working with Book Enums
// Get all books
for (final book in BibleBookEnum.values) {
print("${book.fullName} (${book.asStr()})");
}
// Parse from abbreviation
final book = BibleBookEnum.fromStr("jn"); // BibleBookEnum.jonah
Auto Language Collisions
The library handles ambiguous abbreviations intelligently:
// "jn" could be Jonah or John - auto mode prefers Jonah due to precedence
final jonahRef = VerseRef.parse("jn 1:1");
print(jonahRef.book); // BibleBookEnum.jonah
// "jud" prefers Judges over Jude
final judgesRef = VerseRef.parse("jud 1:1");
print(judgesRef.book); // BibleBookEnum.judges
Current Limitations
- Chapter and verse numbers receive broad sanity checks only. The package does not apply edition-specific chapter or verse tables.
- Canon membership is not enforced; book availability and numbering vary by Bible tradition and edition.
- Subdivisions are limited to one Latin letter
a-z. Multi-letter suffixes, chapter subdivisions, and abbreviated endpoints such as5a-bare not supported; write5a-5b. Source verse labels cover one chapter only. - Rich passage lists use commas and passage sequences use semicolons; prose words such as “and” are not grammar separators.
- Unicode normalization targets reference syntax. It does not transliterate book names, perform locale-sensitive case folding, or provide general-purpose Unicode normalization.
- Extraction is bounded by configurable look-behind/look-ahead windows
(96/256 UTF-16 code units by default). Bare books are opt-in, and auto mode
suppresses several very short/common aliases to avoid prose false positives;
pass an explicit
language:to extraction when those aliases are intentional.
Command Line Usage
# Parse a verse, range, or richer passage from the command line
dart run bible_io_references "John 3:16"
# Output: John 3:16
dart run bible_io_references "John 3:16,18-20; Acts 2"
# Output: John 3:16,18-20; Acts 2
dart run bible_io_references --language es "Juan 3:16"
# Output: Juan 3:16
dart run bible_io_references --format json "John 3:16"
# Output: {"type":"verse","book":"jo","chapter":3,"verse":16}
dart run bible_io_references --format osis "John 3:16-17"
# Output: John.3.16-John.3.17
dart run bible_io_references --format usfm "John 3:16-17"
# Output: JHN 3:16-17
# UTF-8 batch input, one passage per nonblank line
dart run bible_io_references --input references.txt --format json
# Or read a batch from stdin
dart run bible_io_references --batch --format usfm
Existing verse/range JSON remains unchanged. Rich single-input JSON uses the
book, chapter, verses, or sequence passage shape; batch success records
contain reference for the narrow legacy shapes and passage for rich shapes.
Batch JSON uses JSON Lines and includes a record for every success or failure.
Exit codes are 0 for success, 64 for usage errors, 65 when parsing fails,
and 66 when input cannot be read or decoded.
Performance
- Fast parsing: Cached book lookups keep repeated parsing inexpensive
- Stateless API: Parse operations do not mutate shared parser state
- Small footprint: No runtime package dependencies
- Benchmarked: Performance checks are isolated behind the
performancetag
Testing
The package includes comprehensive test coverage:
# Run all tests
dart test
# Run with coverage
dart test --coverage=coverage
# Run specific test groups
dart test --tags performance
Contributing
Code structure
lib/bible_io_references.dart exports the public API; lib/package.dart keeps
the compatibility import available. lib/references.dart owns the reference
and passage library, with its implementation organized into parts under
lib/src/:
| File | Responsibility |
|---|---|
reference_models.dart |
Verse and range values, numeric limits, and JSON serialization |
verse_label.dart |
Exact source labels for single, subdivided, or combined text entries |
passage_models.dart |
Book, chapter, verse-selection, and sequence values |
parse_result.dart |
Parse results, errors, ambiguity policy, and match metadata |
reference_parser.dart |
Configurable verse and range parsing |
passage_parser.dart |
Rich passage grammar built on the reference parser |
book_alias_index.dart |
Alias indexing and default language priority |
legacy_reference_parser.dart |
Original top-level parsers and collision reporting |
reference_support.dart |
Shared validation, JSON decoding, and value comparison |
These files remain parts of one library so sealed model types and private parser
helpers can work together without changing existing imports. Import the public
entrypoints rather than files under lib/src/. The legacy top-level parsers keep
their original whitespace and book-matching rules while accepting verse
subdivisions; changes to the configurable parser should not silently change
those legacy rules.
Formatting, machine identifiers, extraction, and input normalization each have
their own module under lib/. Language data lives in lib/languages/, the CLI
in bin/, and feature tests in test/.
Development workflow
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
GNU Affero General Public License v3.0 - see LICENSE for details.
Changelog
See CHANGELOG.md for version history.
Libraries
- bible_book_enum
- bible_io_references
- Tools for parsing Bible verse references in multiple languages.
- bible_language_enum
- languages
- languages/arabic
- languages/chinese
- languages/french
- languages/german
- languages/hebrew
- languages/hindi
- languages/indonesian
- languages/korean
- languages/portuguese
- languages/russian
- languages/spanish
- languages/tagalog
- package
- Compatibility entrypoint.
- reference_extractor
- reference_formatter
- reference_identifiers
- reference_input_normalizer
- references
- Bible reference and passage models, parsing APIs, and parse results.