bibtex-parser 0.2.2

BibTeX parser for Rust
Documentation
use bibtex_parser::{EntryDelimiter, ParsedBlock, Parser, ValueDelimiter};

#[test]
fn raw_preservation_is_opt_in() {
    let input = r#"@article{paper, title = "Fast"}"#;

    let plain = Parser::new().parse_document(input).unwrap();
    assert!(plain.entries()[0].raw.is_none());
    assert!(plain.entries()[0].fields[0].raw.is_none());
    assert!(plain.entries()[0].fields[0].value.raw.is_none());

    let preserved = Parser::new().preserve_raw().parse_document(input).unwrap();
    assert_eq!(preserved.entries()[0].raw.as_deref(), Some(input));
    assert_eq!(
        preserved.entries()[0].fields[0].raw.as_deref(),
        Some("title = \"Fast\"")
    );
    assert_eq!(
        preserved.entries()[0].fields[0].value.raw.as_deref(),
        Some("\"Fast\"")
    );
}

#[test]
fn raw_entry_field_and_value_text_are_exact_source_slices() {
    let input = "@custom(entry-key,\n  quoted = \"A, B\",\n  braced = {A {Nested} Value},\n  bare = jan,\n  number = 2026,\n  concat = \"A\" # mid # {C},\n)";
    let document = Parser::new().preserve_raw().parse_document(input).unwrap();
    let entry = &document.entries()[0];

    assert_eq!(entry.raw.as_deref(), Some(input));
    assert_eq!(entry.delimiter, Some(EntryDelimiter::Parentheses));

    let expected = [
        ("quoted = \"A, B\",", "\"A, B\"", ValueDelimiter::Quotes),
        (
            "braced = {A {Nested} Value},",
            "{A {Nested} Value}",
            ValueDelimiter::Braces,
        ),
        ("bare = jan,", "jan", ValueDelimiter::Bare),
        ("number = 2026,", "2026", ValueDelimiter::Bare),
        (
            "concat = \"A\" # mid # {C},",
            "\"A\" # mid # {C}",
            ValueDelimiter::Concatenation,
        ),
    ];

    for (field, (raw_field, raw_value, delimiter)) in entry.fields.iter().zip(expected) {
        assert_eq!(field.raw.as_deref(), Some(raw_field));
        assert_eq!(field.value.raw.as_deref(), Some(raw_value));
        assert_eq!(field.value.delimiter, Some(delimiter));
    }
}

#[test]
fn non_entry_blocks_preserve_raw_text_and_source_order() {
    let input = "@string{venue = \"VLDB\"}\n@preamble{\"prefix\" # venue}\n% keep me\n@comment{formal}\n@article{paper, title = venue}";
    let document = Parser::new().preserve_raw().parse_document(input).unwrap();

    assert_eq!(
        document.blocks(),
        &[
            ParsedBlock::String(0),
            ParsedBlock::Preamble(0),
            ParsedBlock::Comment(0),
            ParsedBlock::Comment(1),
            ParsedBlock::Entry(0),
        ]
    );
    assert_eq!(
        document.strings()[0].raw.as_deref(),
        Some("@string{venue = \"VLDB\"}")
    );
    assert_eq!(document.strings()[0].value.raw.as_deref(), Some("\"VLDB\""));
    assert_eq!(
        document.preambles()[0].raw.as_deref(),
        Some("@preamble{\"prefix\" # venue}")
    );
    assert_eq!(
        document.preambles()[0].value.raw.as_deref(),
        Some("\"prefix\" # venue")
    );
    assert_eq!(document.comments()[0].raw.as_deref(), Some("% keep me\n"));
    assert_eq!(
        document.comments()[1].raw.as_deref(),
        Some("@comment{formal}")
    );
}

#[test]
fn failed_blocks_always_retain_raw_text() {
    let input = "@article{, title = \"Missing key\"}";
    let document = Parser::new()
        .tolerant()
        .preserve_raw()
        .parse_document(input)
        .unwrap();

    assert_eq!(document.failed_blocks()[0].raw.as_ref(), input);
}