Skip to main content

bibtex_parser/
lib.rs

1#![deny(clippy::all)]
2//! # bibtex-parser
3//!
4//! BibTeX parsing with a Rust [`Library`] API.
5//!
6//! `bibtex-parser` supports strict parsing by default, explicit tolerant
7//! recovery for malformed input, string and month expansion, comments and
8//! preambles, validation, query/edit helpers, and configurable writing.
9//!
10//! ## Features
11//!
12//! - Borrowed values where possible for low-allocation parsing.
13//! - String variables, concatenation, and standard month constants.
14//! - Entries, strings, preambles, comments, and tolerant failures in source order.
15//! - Opt-in source-span capture.
16//! - DOI normalization, duplicate detection, validation, sorting, and field normalization.
17//! - Configurable writer for formatting and file output.
18//! - Optional `parallel` feature for parsing multiple files concurrently.
19//! - Optional `latex_to_unicode` feature for LaTeX accent conversion helpers.
20//!
21//! ## Parse
22//!
23//! ```
24//! use bibtex_parser::Library;
25//!
26//! let input = r#"
27//!     @string{venue = "VLDB"}
28//!     @article{paper,
29//!         author = "Jane Doe and John Smith",
30//!         title = "Example Paper",
31//!         journal = venue,
32//!         year = 2026
33//!     }
34//! "#;
35//!
36//! let library = Library::parse(input)?;
37//! let entry = library.find_by_key("paper").unwrap();
38//!
39//! assert_eq!(entry.get("journal"), Some("VLDB"));
40//! assert_eq!(entry.year(), Some("2026".to_string()));
41//! assert_eq!(entry.authors().len(), 2);
42//! # Ok::<(), bibtex_parser::Error>(())
43//! ```
44//!
45//! ## Tolerant Recovery
46//!
47//! ```
48//! use bibtex_parser::{Block, Library};
49//!
50//! let library = Library::parser()
51//!     .tolerant()
52//!     .capture_source()
53//!     .parse(r#"
54//!         @article{ok, title = "Good"}
55//!         @article{bad, title = "Missing close"
56//!         @book{recovered, title = "Recovered"}
57//!     "#)?;
58//!
59//! assert_eq!(library.entries().len(), 2);
60//! assert_eq!(library.failed_blocks().len(), 1);
61//!
62//! let has_failure_span = library.blocks().iter().any(|block| {
63//!     matches!(block, Block::Failed(failed) if failed.source.is_some())
64//! });
65//! assert!(has_failure_span);
66//! # Ok::<(), bibtex_parser::Error>(())
67//! ```
68//!
69//! ## Write
70//!
71//! ```
72//! use bibtex_parser::{Library, Writer, WriterConfig};
73//!
74//! let library = Library::parse(r#"@article{paper, title = "Example Paper"}"#)?;
75//! let mut output = Vec::new();
76//! let config = WriterConfig {
77//!     align_values: true,
78//!     ..Default::default()
79//! };
80//!
81//! Writer::with_config(&mut output, config).write_library(&library)?;
82//! assert!(String::from_utf8(output).unwrap().contains("@article{paper"));
83//! # Ok::<(), bibtex_parser::Error>(())
84//! ```
85//!
86//! ## `Library` Versus `ParsedDocument`
87//!
88//! Use [`Library`] when application code wants structured bibliography data.
89//! Use [`ParsedDocument`] when tooling needs source-order blocks, diagnostics,
90//! partial results, or source-preserving metadata.
91//!
92//! ```
93//! use bibtex_parser::{ParsedBlock, Parser};
94//!
95//! let input = r#"
96//!     % retained comment
97//!     @article{paper, title = "Example Paper"}
98//! "#;
99//!
100//! let document = Parser::new()
101//!     .capture_source()
102//!     .parse_document(input)?;
103//!
104//! assert_eq!(document.library().entries().len(), 1);
105//! assert_eq!(document.entries()[0].key(), "paper");
106//! assert!(matches!(document.blocks()[0], ParsedBlock::Comment(0)));
107//! assert!(document.entries()[0].source.is_some());
108//! # Ok::<(), bibtex_parser::Error>(())
109//! ```
110
111#![forbid(unsafe_code)]
112#![warn(
113    clippy::all,
114    clippy::pedantic,
115    clippy::nursery,
116    clippy::cargo,
117    missing_docs,
118    missing_debug_implementations
119)]
120#![allow(
121    clippy::module_name_repetitions,
122    clippy::missing_errors_doc,
123    clippy::missing_panics_doc,
124    clippy::multiple_crate_versions
125)]
126
127pub mod corpus;
128pub mod document;
129pub mod error;
130pub mod model;
131pub mod parser;
132#[cfg(feature = "python")]
133mod python;
134pub mod source;
135
136#[cfg(feature = "latex_to_unicode")]
137pub mod latex_unicode;
138
139mod library;
140mod writer;
141
142pub use corpus::{
143    CorpusEvent, CorpusSource, DuplicateKeyGroup, DuplicateKeyOccurrence, ParsedCorpus,
144};
145pub use document::{
146    Diagnostic, DiagnosticCode, DiagnosticSeverity, DiagnosticTarget, EntryDelimiter,
147    ExpansionOptions, ParseEvent, ParseFlow, ParseStatus, ParseSummary, ParsedBlock, ParsedComment,
148    ParsedDocument, ParsedEntry, ParsedEntryStatus, ParsedFailedBlock, ParsedField, ParsedPreamble,
149    ParsedSource, ParsedString, ParsedValue, StreamingSummary, UnresolvedVariablePolicy,
150    ValueDelimiter,
151};
152pub use error::{Error, Result, SourceId, SourceSpan};
153pub use library::{
154    Block, Comment, FailedBlock, FieldNameCase, FieldNormalizeOptions, IssueSummary, Library,
155    LibraryBuilder, LibraryStats, MonthStyle, Parser, Preamble, SortOptions, StringDefinition,
156    ValidationReport,
157};
158pub use model::{
159    canonical_biblatex_field_alias, classify_resource_field, normalize_biblatex_field_name,
160    normalize_doi, normalize_field_name_ascii, parse_date_parts, parse_names, DateParseError,
161    DateParts, Entry, EntryType, Field, PersonName, ResourceField, ResourceKind, ValidationError,
162    ValidationLevel, ValidationSeverity, Value,
163};
164pub use parser::{parse_bibtex, ParsedItem};
165pub use source::SourceMap;
166pub use writer::{
167    document_to_string, selected_entries_to_string, to_file, to_string, RawWriteMode,
168    TrailingComma, Writer, WriterConfig,
169};
170
171/// Re-export of common parser functions
172pub mod prelude {
173    pub use crate::{
174        canonical_biblatex_field_alias, classify_resource_field, document_to_string,
175        normalize_biblatex_field_name, normalize_doi, normalize_field_name_ascii, parse_bibtex,
176        parse_date_parts, parse_names, selected_entries_to_string, Block, Comment, CorpusEvent,
177        CorpusSource, DateParseError, DateParts, Diagnostic, DiagnosticCode, DiagnosticSeverity,
178        DiagnosticTarget, DuplicateKeyGroup, DuplicateKeyOccurrence, Entry, EntryDelimiter,
179        EntryType, Error, ExpansionOptions, FailedBlock, Field, FieldNameCase,
180        FieldNormalizeOptions, IssueSummary, Library, LibraryBuilder, LibraryStats, MonthStyle,
181        ParseEvent, ParseFlow, ParseStatus, ParseSummary, ParsedBlock, ParsedComment, ParsedCorpus,
182        ParsedDocument, ParsedEntry, ParsedEntryStatus, ParsedFailedBlock, ParsedField, ParsedItem,
183        ParsedPreamble, ParsedSource, ParsedString, ParsedValue, Parser, PersonName, Preamble,
184        RawWriteMode, ResourceField, ResourceKind, Result, SortOptions, SourceId, SourceMap,
185        SourceSpan, StreamingSummary, StringDefinition, TrailingComma, UnresolvedVariablePolicy,
186        ValidationError, ValidationLevel, ValidationReport, ValidationSeverity, Value,
187        ValueDelimiter, Writer, WriterConfig,
188    };
189}
190
191/// Parse a BibTeX library from a string.
192pub fn parse(input: &str) -> Result<Library<'_>> {
193    Library::parser().parse(input)
194}
195
196/// Parse a BibTeX library from a file.
197pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<Library<'static>> {
198    let content = std::fs::read_to_string(path)?;
199    parse(&content).map(Library::into_owned)
200}
201
202#[cfg(feature = "python")]
203#[pyo3::pymodule]
204fn _native(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
205    python::register(m)
206}