Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//! `RIS`, `NBIB`, `EndNote XML`, `BibTeX`, and `BibLaTeX` files.
mod error;
pub mod nbib;
mod parse_util;
mod record;
pub mod ris;

pub use error::Error;
pub use record::{PublicationDate, Record};
35 changes: 6 additions & 29 deletions src/nbib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
//! - [PubMed Help](https://pubmed.ncbi.nlm.nih.gov/help/)
//! - [PubMed XML DTD (field definitions)](https://dtd.nlm.nih.gov/ncbi/pubmed/doc/out/250101/index.html)

use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write as _;

use winnow::Parser;
use winnow::ascii::space0;
use winnow::token::{rest, take_while};

use crate::parse_util::{append_to_last, append_to_opt, check_empty, normalize_line_endings};
use crate::{Error, PublicationDate, Record};

/// Parse NBIB-formatted text into zero or more [`Record`]s.
Expand All @@ -22,17 +22,8 @@ use crate::{Error, PublicationDate, Record};
/// Returns [`Error::EmptyInput`] if the input is empty or whitespace-only.
/// Returns [`Error::MissingRequiredField`] if a record lacks a `TI` (title) field.
pub fn parse(input: &str) -> Result<Vec<Record>, Error> {
if input.trim().is_empty() {
return Err(Error::EmptyInput);
}

// Normalise Windows line endings. Uses Cow to avoid allocation when the
// input already uses Unix endings (the common case).
let input: Cow<'_, str> = if input.contains('\r') {
Cow::Owned(input.replace("\r\n", "\n"))
} else {
Cow::Borrowed(input)
};
check_empty(input)?;
let input = normalize_line_endings(input);

let mut records = Vec::new();
let mut builder = RecordBuilder::default();
Expand Down Expand Up @@ -282,9 +273,9 @@ impl RecordBuilder {
"TI" => append_to_opt(&mut self.title, text),
"AB" => append_to_opt(&mut self.abstract_text, text),
"JT" => append_to_opt(&mut self.journal, text),
"FAU" => append_to_last_vec(&mut self.fau_authors, text),
"AU" => append_to_last_vec(&mut self.au_authors, text),
"LID" | "AID" => append_to_last_vec(&mut self.doi_candidates, text),
"FAU" => append_to_last(&mut self.fau_authors, text),
"AU" => append_to_last(&mut self.au_authors, text),
"LID" | "AID" => append_to_last(&mut self.doi_candidates, text),
_ => {}
}
}
Expand Down Expand Up @@ -318,17 +309,3 @@ impl RecordBuilder {
})
}
}

fn append_to_opt(field: &mut Option<String>, text: &str) {
if let Some(existing) = field {
existing.push(' ');
existing.push_str(text);
}
}

fn append_to_last_vec(vec: &mut [String], text: &str) {
if let Some(last) = vec.last_mut() {
last.push(' ');
last.push_str(text);
}
}
37 changes: 37 additions & 0 deletions src/parse_util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::borrow::Cow;

use crate::Error;

/// Return [`Error::EmptyInput`] if `input` is empty or whitespace-only.
pub(crate) fn check_empty(input: &str) -> Result<(), Error> {
if input.trim().is_empty() {
return Err(Error::EmptyInput);
}
Ok(())
}

/// Normalise `\r\n` → `\n`. Returns a borrowed view when no `\r` is present
/// (the common case), avoiding allocation.
pub(crate) fn normalize_line_endings(input: &str) -> Cow<'_, str> {
if input.contains('\r') {
Cow::Owned(input.replace("\r\n", "\n"))
} else {
Cow::Borrowed(input)
}
}

/// Append `text` (with a space separator) to an existing `Option<String>`.
pub(crate) fn append_to_opt(field: &mut Option<String>, text: &str) {
if let Some(existing) = field {
existing.push(' ');
existing.push_str(text);
}
}

/// Append `text` (with a space separator) to the last element of a slice.
pub(crate) fn append_to_last(vec: &mut [String], text: &str) {
if let Some(last) = vec.last_mut() {
last.push(' ');
last.push_str(text);
}
}
Loading
Loading