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}