use std::sync::LazyLock;
use regex::Regex;
use crate::{Parser, Span, content::Content};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Author {
name: String,
firstname: String,
middlename: Option<String>,
lastname: Option<String>,
email: Option<String>,
}
impl Author {
pub(crate) fn parse(source: &str, parser: &Parser) -> Option<Self> {
let source = source.trim();
if source.is_empty() {
return None;
}
let is_single_attribute = source.trim().starts_with('{')
&& source.trim().ends_with('}')
&& source.matches('{').count() == 1;
if is_single_attribute {
let expanded_source = apply_author_subs(source, parser);
let name_with_spaces = replace_underscores_with_spaces(expanded_source);
Some(Self {
name: name_with_spaces.clone(),
firstname: name_with_spaces,
middlename: None,
lastname: None,
email: None,
})
} else if let Some(captures) = AUTHOR.captures(source) {
let name_without_email = source.split_once('<').unwrap_or((source, "")).0.trim();
let name = replace_underscores_with_spaces(name_without_email.to_string());
let firstname =
replace_underscores_with_spaces(apply_author_subs(&captures[1], parser));
let mut middlename = captures
.get(2)
.map(|m| replace_underscores_with_spaces(apply_author_subs(m.as_str(), parser)));
let mut lastname = captures
.get(3)
.map(|m| replace_underscores_with_spaces(apply_author_subs(m.as_str(), parser)));
let email = captures
.get(4)
.map(|m| apply_author_subs(m.as_str(), parser));
if middlename.is_some() && lastname.is_none() {
lastname = middlename;
middlename = None;
}
Some(Self {
name,
firstname,
middlename,
lastname,
email,
})
} else if source.contains('{') {
let expanded_source = apply_author_subs(source, parser);
if let Some(captures) = AUTHOR.captures(&expanded_source) {
let name_without_email = expanded_source
.split_once('<')
.unwrap_or((&expanded_source, ""))
.0
.trim();
let name = replace_underscores_with_spaces(name_without_email.to_string());
let firstname = replace_underscores_with_spaces(captures[1].to_string());
let mut middlename = captures
.get(2)
.map(|m| replace_underscores_with_spaces(m.as_str().to_string()));
let mut lastname = captures
.get(3)
.map(|m| replace_underscores_with_spaces(m.as_str().to_string()));
let email = captures.get(4).map(|m| m.as_str().to_string());
if middlename.is_some() && lastname.is_none() {
lastname = middlename;
middlename = None;
}
Some(Self {
name,
firstname,
middlename,
lastname,
email,
})
} else {
let mut expanded_name = expanded_source;
if expanded_name.contains('<') && expanded_name.contains('>') {
let span = crate::Span::new(&expanded_name);
let mut content = crate::content::Content::from(span);
crate::content::SubstitutionStep::SpecialCharacters.apply(
&mut content,
parser,
None,
);
expanded_name = content.rendered().to_string();
}
let name_with_spaces = replace_underscores_with_spaces(expanded_name);
Some(Self {
name: name_with_spaces.clone(),
firstname: name_with_spaces,
middlename: None,
lastname: None,
email: None,
})
}
} else {
let mut name = source.to_string();
if name.contains('<') && name.contains('>') {
let span = crate::Span::new(&name);
let mut content = crate::content::Content::from(span);
crate::content::SubstitutionStep::SpecialCharacters.apply(
&mut content,
parser,
None,
);
name = content.rendered().to_string();
}
let name_with_spaces = replace_underscores_with_spaces(name);
Some(Self {
name: name_with_spaces.clone(),
firstname: name_with_spaces,
middlename: None,
lastname: None,
email: None,
})
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn firstname(&self) -> &str {
&self.firstname
}
pub fn middlename(&self) -> Option<&str> {
self.middlename.as_deref()
}
pub fn lastname(&self) -> Option<&str> {
self.lastname.as_deref()
}
pub fn email(&self) -> Option<&str> {
self.email.as_deref()
}
pub fn initials(&self) -> String {
format!(
"{first}{middle}{last}",
first = first_char_or_empty_string(&self.firstname),
middle = opt_first_char_or_empty_string(self.middlename.as_deref()),
last = opt_first_char_or_empty_string(self.lastname.as_deref()),
)
}
}
fn first_char_or_empty_string(s: &str) -> String {
s.chars().next().map_or(String::new(), |c| c.to_string())
}
fn opt_first_char_or_empty_string(s: Option<&str>) -> String {
s.map(first_char_or_empty_string).unwrap_or_default()
}
fn replace_underscores_with_spaces(name: String) -> String {
name.replace('_', " ")
}
static AUTHOR: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::unwrap_used)]
Regex::new(
r#"(?x)
^
# Group 1: First name (required)
([a-zA-Z0-9_\p{L}\p{N}&\#;][a-zA-Z0-9_\p{L}\p{N}\-'.&\#;]*)
# Group 2: Middle name (optional)
(?:\ +([a-zA-Z0-9_\p{L}\p{N}&\#;][a-zA-Z0-9_\p{L}\p{N}\-'.&\#;]*))?
# Group 3: Last name (optional)
(?:\ +([a-zA-Z0-9_\p{L}\p{N}&\#;][a-zA-Z0-9_\p{L}\p{N}\-'.&\#;]*))?
# Group 4: Email address (optional)
(?:\ +<([^>]+)>)?
$
"#,
)
.unwrap()
});
fn apply_author_subs(source: &str, parser: &Parser) -> String {
let span = Span::new(source);
let mut content = Content::from(span);
use crate::content::SubstitutionStep;
SubstitutionStep::AttributeReferences.apply(&mut content, parser, None);
let is_simple_single_attribute = source.trim().starts_with('{')
&& source.trim().ends_with('}')
&& source.matches('{').count() == 1;
let has_multiple_attributes = source.matches('{').count() > 1;
let rendered = content.rendered();
let has_angle_brackets = rendered.contains('<') && rendered.contains('>');
let has_unencoded_ampersand = rendered.contains('&') && !rendered.contains("&");
if !is_simple_single_attribute
&& has_multiple_attributes
&& (has_angle_brackets || has_unencoded_ampersand)
{
SubstitutionStep::SpecialCharacters.apply(&mut content, parser, None);
}
content.rendered().to_string()
}