#![deny(missing_docs)]
#![deny(unsafe_code)]
#[cfg(feature = "json")]
pub mod json;
pub mod taxonomy;
mod util;
use std::fmt::{self, Debug};
use std::iter::repeat;
use std::num::{NonZeroI16, NonZeroUsize};
use quick_xml::de::{Deserializer, SliceReader};
use serde::{Deserialize, Serialize};
use taxonomy::{
DateVariable, Kind, Locator, NameVariable, NumberOrPageVariable, NumberVariable,
OtherTerm, Term, Variable,
};
use self::util::*;
pub type XmlResult<T> = Result<T, XmlError>;
pub type XmlError = quick_xml::de::DeError;
const EVENT_BUFFER_SIZE: Option<NonZeroUsize> = NonZeroUsize::new(4096);
pub trait ToFormatting {
fn to_formatting(&self) -> Formatting;
}
macro_rules! to_formatting {
($name:ty, self) => {
impl ToFormatting for $name {
fn to_formatting(&self) -> Formatting {
Formatting {
font_style: self.font_style,
font_variant: self.font_variant,
font_weight: self.font_weight,
text_decoration: self.text_decoration,
vertical_align: self.vertical_align,
}
}
}
};
($name:ty) => {
impl ToFormatting for $name {
fn to_formatting(&self) -> Formatting {
self.formatting.clone()
}
}
};
}
pub trait ToAffixes {
fn to_affixes(&self) -> Affixes;
}
macro_rules! to_affixes {
($name:ty, self) => {
impl ToAffixes for $name {
fn to_affixes(&self) -> Affixes {
Affixes {
prefix: self.prefix.clone(),
suffix: self.suffix.clone(),
}
}
}
};
($name:ty) => {
impl ToAffixes for $name {
fn to_affixes(&self) -> Affixes {
self.affixes.clone()
}
}
};
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
struct RawStyle {
pub info: StyleInfo,
#[serde(rename = "@default-locale")]
#[serde(skip_serializing_if = "Option::is_none")]
pub default_locale: Option<LocaleCode>,
#[serde(rename = "@version")]
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub citation: Option<Citation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bibliography: Option<Bibliography>,
#[serde(flatten)]
pub independent_settings: Option<IndependentStyleSettings>,
#[serde(rename = "macro", default)]
pub macros: Vec<CslMacro>,
#[serde(default)]
pub locale: Vec<Locale>,
}
impl RawStyle {
pub fn parent_link(&self) -> Option<&InfoLink> {
self.info
.link
.iter()
.find(|link| link.rel == InfoLinkRel::IndependentParent)
}
}
impl From<IndependentStyle> for RawStyle {
fn from(value: IndependentStyle) -> Self {
Self {
info: value.info,
default_locale: value.default_locale,
version: value.version,
citation: Some(value.citation),
bibliography: value.bibliography,
independent_settings: Some(value.settings),
macros: value.macros,
locale: value.locale,
}
}
}
impl From<DependentStyle> for RawStyle {
fn from(value: DependentStyle) -> Self {
Self {
info: value.info,
default_locale: value.default_locale,
version: value.version,
citation: None,
bibliography: None,
independent_settings: None,
macros: Vec::new(),
locale: Vec::new(),
}
}
}
impl From<Style> for RawStyle {
fn from(value: Style) -> Self {
match value {
Style::Independent(i) => i.into(),
Style::Dependent(d) => d.into(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct IndependentStyle {
pub info: StyleInfo,
pub default_locale: Option<LocaleCode>,
pub version: String,
pub citation: Citation,
pub bibliography: Option<Bibliography>,
pub settings: IndependentStyleSettings,
pub macros: Vec<CslMacro>,
pub locale: Vec<Locale>,
}
impl IndependentStyle {
pub fn from_xml(xml: &str) -> XmlResult<Self> {
let de = &mut deserializer(xml);
IndependentStyle::deserialize(de)
}
pub fn purge(&mut self, level: PurgeLevel) {
self.info.purge(level);
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum PurgeLevel {
Basic,
Full,
}
impl<'de> Deserialize<'de> for IndependentStyle {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
let raw_style = RawStyle::deserialize(deserializer)?;
let style: Style = raw_style.try_into().map_err(serde::de::Error::custom)?;
match style {
Style::Independent(i) => Ok(i),
Style::Dependent(_) => Err(serde::de::Error::custom(
"expected an independent style but got a dependent style",
)),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct DependentStyle {
pub info: StyleInfo,
pub default_locale: Option<LocaleCode>,
pub version: String,
pub parent_link: InfoLink,
}
impl DependentStyle {
pub fn from_xml(xml: &str) -> XmlResult<Self> {
let de = &mut deserializer(xml);
DependentStyle::deserialize(de)
}
pub fn purge(&mut self, level: PurgeLevel) {
self.info.purge(level);
}
}
impl<'de> Deserialize<'de> for DependentStyle {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
let raw_style = RawStyle::deserialize(deserializer)?;
let style: Style = raw_style.try_into().map_err(serde::de::Error::custom)?;
match style {
Style::Dependent(d) => Ok(d),
Style::Independent(_) => Err(serde::de::Error::custom(
"expected a dependent style but got an independent style",
)),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(clippy::large_enum_variant)]
pub enum Style {
Independent(IndependentStyle),
Dependent(DependentStyle),
}
impl Style {
pub fn from_xml(xml: &str) -> XmlResult<Self> {
let de = &mut deserializer(xml);
Style::deserialize(de)
}
pub fn to_xml(&self) -> XmlResult<String> {
let mut buf = String::new();
let ser = quick_xml::se::Serializer::with_root(&mut buf, Some("style"))?;
self.serialize(ser)?;
Ok(buf)
}
pub fn purge(&mut self, level: PurgeLevel) {
match self {
Self::Independent(i) => i.purge(level),
Self::Dependent(d) => d.purge(level),
}
}
pub fn info(&self) -> &StyleInfo {
match self {
Self::Independent(i) => &i.info,
Self::Dependent(d) => &d.info,
}
}
}
impl<'de> Deserialize<'de> for Style {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
let raw_style = RawStyle::deserialize(deserializer)?;
raw_style.try_into().map_err(serde::de::Error::custom)
}
}
impl Serialize for Style {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
RawStyle::from(self.clone()).serialize(serializer)
}
}
impl TryFrom<RawStyle> for Style {
type Error = StyleValidationError;
fn try_from(value: RawStyle) -> Result<Self, Self::Error> {
let has_bibliography = value.bibliography.is_some();
if let Some(citation) = value.citation {
if let Some(settings) = value.independent_settings {
Ok(Self::Independent(IndependentStyle {
info: value.info,
default_locale: value.default_locale,
version: value.version,
citation,
bibliography: value.bibliography,
settings,
macros: value.macros,
locale: value.locale,
}))
} else {
Err(StyleValidationError::MissingClassAttr)
}
} else if has_bibliography {
Err(StyleValidationError::MissingCitation)
} else if let Some(parent_link) = value.parent_link().cloned() {
Ok(Self::Dependent(DependentStyle {
info: value.info,
default_locale: value.default_locale,
version: value.version,
parent_link,
}))
} else {
Err(StyleValidationError::MissingParent)
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum StyleValidationError {
MissingCitation,
MissingParent,
MissingClassAttr,
}
impl fmt::Display for StyleValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::MissingCitation => "root element is missing `cs:citation` child despite having a `cs:bibliography`",
Self::MissingParent => "`cs:link` tag with `independent-parent` as a `rel` attribute is missing but no `cs:citation` was defined",
Self::MissingClassAttr => "`cs:style` tag is missing the `class` attribute",
})
}
}
fn deserializer(xml: &str) -> Deserializer<SliceReader<'_>> {
let mut style_deserializer = Deserializer::from_str(xml);
style_deserializer.event_buffer_size(EVENT_BUFFER_SIZE);
style_deserializer
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct IndependentStyleSettings {
#[serde(rename = "@class")]
pub class: StyleClass,
#[serde(
rename = "@initialize-with-hyphen",
default = "IndependentStyleSettings::default_initialize_with_hyphen",
deserialize_with = "deserialize_bool"
)]
pub initialize_with_hyphen: bool,
#[serde(rename = "@page-range-format")]
#[serde(skip_serializing_if = "Option::is_none")]
pub page_range_format: Option<PageRangeFormat>,
#[serde(rename = "@demote-non-dropping-particle", default)]
pub demote_non_dropping_particle: DemoteNonDroppingParticle,
#[serde(flatten)]
pub options: InheritableNameOptions,
}
impl IndependentStyleSettings {
pub const fn default_initialize_with_hyphen() -> bool {
true
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct LocaleCode(pub String);
impl<'a> LocaleCode {
pub fn en_us() -> Self {
Self("en-US".to_string())
}
pub fn parse_base(&self) -> Option<BaseLanguage> {
let mut parts = self.0.split('-').take(2);
let first = parts.next()?;
match first {
"i" | "I" => {
let second = parts.next()?;
if second.is_empty() {
return None;
}
Some(BaseLanguage::Iana(second.to_string()))
}
"x" | "X" => {
let second = parts.next()?;
if second.len() > 8 || second.is_empty() {
return None;
}
let mut code = [0; 8];
code[..second.len()].copy_from_slice(second.as_bytes());
Some(BaseLanguage::Unregistered(code))
}
_ if first.len() == 2 => {
let mut code = [0; 2];
code.copy_from_slice(first.as_bytes());
Some(BaseLanguage::Iso639_1(code))
}
_ => None,
}
}
pub fn extensions(&'a self) -> impl Iterator<Item = &'a str> + 'a {
self.0
.split('-')
.enumerate()
.filter_map(|(i, e)| {
if i == 0 && ["x", "X", "i", "I"].contains(&e) {
None
} else {
Some(e)
}
})
.skip(1)
}
pub fn is_english(&self) -> bool {
let en = "en";
let hyphen = "-";
self.0.starts_with(en)
&& (self.0.len() == 2
|| self.0.get(en.len()..en.len() + hyphen.len()) == Some(hyphen))
}
pub fn fallback(&self) -> Option<LocaleCode> {
match self.parse_base()? {
BaseLanguage::Iso639_1(code) => match &code {
b"af" => Some("af-ZA"),
b"bg" => Some("bg-BG"),
b"ca" => Some("ca-AD"),
b"cs" => Some("cs-CZ"),
b"da" => Some("da-DK"),
b"de" => Some("de-DE"),
b"el" => Some("el-GR"),
b"en" => Some("en-US"),
b"es" => Some("es-ES"),
b"et" => Some("et-EE"),
b"fa" => Some("fa-IR"),
b"fi" => Some("fi-FI"),
b"fr" => Some("fr-FR"),
b"he" => Some("he-IL"),
b"hr" => Some("hr-HR"),
b"hu" => Some("hu-HU"),
b"is" => Some("is-IS"),
b"it" => Some("it-IT"),
b"ja" => Some("ja-JP"),
b"km" => Some("km-KH"),
b"ko" => Some("ko-KR"),
b"lt" => Some("lt-LT"),
b"lv" => Some("lv-LV"),
b"mn" => Some("mn-MN"),
b"nb" => Some("nb-NO"),
b"nl" => Some("nl-NL"),
b"nn" => Some("nn-NO"),
b"pl" => Some("pl-PL"),
b"pt" => Some("pt-PT"),
b"ro" => Some("ro-RO"),
b"ru" => Some("ru-RU"),
b"sk" => Some("sk-SK"),
b"sl" => Some("sl-SI"),
b"sr" => Some("sr-RS"),
b"sv" => Some("sv-SE"),
b"th" => Some("th-TH"),
b"tr" => Some("tr-TR"),
b"uk" => Some("uk-UA"),
b"vi" => Some("vi-VN"),
b"zh" => Some("zh-CN"),
_ => None,
}
.map(ToString::to_string)
.map(LocaleCode)
.filter(|f| f != self),
_ => None,
}
}
}
impl fmt::Display for LocaleCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
pub enum BaseLanguage {
Iso639_1([u8; 2]),
Iana(String),
Unregistered([u8; 8]),
}
impl BaseLanguage {
pub fn as_str(&self) -> &str {
match self {
Self::Iso639_1(code) => std::str::from_utf8(code).unwrap(),
Self::Iana(code) => code,
Self::Unregistered(code) => std::str::from_utf8(code).unwrap(),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum StyleClass {
InText,
Note,
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PageRangeFormat {
#[serde(alias = "chicago")]
#[serde(rename = "chicago-15")]
Chicago15,
#[serde(rename = "chicago-16")]
Chicago16,
#[default]
Expanded,
Minimal,
MinimalTwo,
}
impl PageRangeFormat {
pub fn format(
self,
buf: &mut impl fmt::Write,
start: &str,
end: &str,
separator: Option<&str>,
) -> Result<(), fmt::Error> {
let separator = separator.unwrap_or("ā");
let start = start.trim();
let end = end.trim();
let (start_pre, x) = split_max_digit_suffix(start);
let (end_pre, y) = split_max_digit_suffix(end);
if start_pre == end_pre {
let pref = start_pre;
let x_len = x.len();
let y_len = y.len();
let y = if x_len <= y_len {
y.to_string()
} else {
let mut s = x[..(x_len - y_len)].to_string();
s.push_str(y);
s
};
write!(buf, "{pref}{x}{separator}")?;
match self {
PageRangeFormat::Chicago15 | PageRangeFormat::Chicago16
if x_len < 3 || x.ends_with("00") =>
{
write!(buf, "{y}")
}
PageRangeFormat::Chicago15 | PageRangeFormat::Chicago16
if x[x_len - 2..].starts_with('0') =>
{
minimal(buf, 1, x, &y)
}
PageRangeFormat::Chicago15
if x_len == 4 && changed_digits(x, &y) >= 3 =>
{
write!(buf, "{y}")
}
PageRangeFormat::Chicago15 | PageRangeFormat::Chicago16 => {
minimal(buf, 2, x, &y)
}
PageRangeFormat::Expanded => write!(buf, "{pref}{y}"),
PageRangeFormat::Minimal => minimal(buf, 1, x, &y),
PageRangeFormat::MinimalTwo => minimal(buf, 2, x, &y),
}
} else {
write!(buf, "{start}{separator}{end}")
}
}
}
fn changed_digits(x: &str, y: &str) -> usize {
let x = if x.len() < y.len() {
let mut s = String::from_iter(repeat(' ').take(y.len() - x.len()));
s.push_str(x);
s
} else {
x.to_string()
};
debug_assert!(x.len() == y.len());
let xs = x.chars().rev();
let ys = y.chars().rev();
for (i, (c, d)) in xs.zip(ys).enumerate() {
if c == d {
return i;
}
}
x.len()
}
fn minimal(
buf: &mut impl fmt::Write,
thresh: usize,
x: &str,
y: &str,
) -> Result<(), fmt::Error> {
if y.len() > x.len() {
return write!(buf, "{y}");
}
let mut xs = String::new();
let mut ys = String::new();
for (c, d) in x.chars().zip(y.chars()).skip_while(|(c, d)| c == d) {
xs.push(c);
ys.push(d);
}
if ys.len() < thresh && y.len() >= thresh {
write!(buf, "{}", &y[(y.len() - thresh)..])
} else {
write!(buf, "{ys}")
}
}
fn split_max_digit_suffix(s: &str) -> (&str, &str) {
let suffix_len = s.chars().rev().take_while(|c| c.is_ascii_digit()).count();
let idx = s.len() - suffix_len;
(&s[..idx], &s[idx..])
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DemoteNonDroppingParticle {
Never,
SortOnly,
#[default]
DisplayAndSort,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct StyleInfo {
#[serde(rename = "author")]
#[serde(default)]
pub authors: Vec<StyleAttribution>,
#[serde(rename = "contributor")]
#[serde(default)]
pub contibutors: Vec<StyleAttribution>,
#[serde(default)]
pub category: Vec<StyleCategory>,
#[serde(default)]
pub field: Vec<Field>,
pub id: String,
#[serde(default)]
pub issn: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub eissn: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issnl: Option<String>,
#[serde(default)]
pub link: Vec<InfoLink>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rights: Option<License>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<LocalString>,
pub title: LocalString,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_short: Option<LocalString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<Timestamp>,
}
impl StyleInfo {
pub fn purge(&mut self, level: PurgeLevel) {
self.field.clear();
self.issn.clear();
self.eissn = None;
self.issnl = None;
self.published = None;
self.summary = None;
self.updated = None;
match level {
PurgeLevel::Basic => {
for person in self.authors.iter_mut().chain(self.contibutors.iter_mut()) {
person.email = None;
person.uri = None;
}
self.link.retain(|i| {
matches!(i.rel, InfoLinkRel::IndependentParent | InfoLinkRel::Zelf)
});
}
PurgeLevel::Full => {
self.authors.clear();
self.contibutors.clear();
self.link.retain(|i| i.rel == InfoLinkRel::IndependentParent);
self.rights = None;
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct LocalString {
#[serde(rename = "@lang")]
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<LocaleCode>,
#[serde(rename = "$value", default)]
pub value: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct StyleAttribution {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(untagged)]
pub enum StyleCategory {
CitationFormat {
#[serde(rename = "@citation-format")]
format: CitationFormat,
},
Field {
#[serde(rename = "@field")]
field: Field,
},
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CitationFormat {
AuthorDate,
Author,
Numeric,
Label,
Note,
}
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Field {
Anthropology,
Astronomy,
Biology,
Botany,
Chemistry,
Communications,
Engineering,
#[serde(rename = "generic-base")]
GenericBase,
Geography,
Geology,
History,
Humanities,
Law,
Linguistics,
Literature,
Math,
Medicine,
Philosophy,
Physics,
PoliticalScience,
Psychology,
Science,
SocialScience,
Sociology,
Theology,
Zoology,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct InfoLink {
#[serde(rename = "@href")]
pub href: String,
#[serde(rename = "@rel")]
pub rel: InfoLinkRel,
#[serde(rename = "$value")]
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "@xml:lang")]
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<LocaleCode>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum InfoLinkRel {
#[serde(rename = "self")]
Zelf,
Template,
Documentation,
IndependentParent,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Timestamp {
#[serde(rename = "$text")]
pub raw: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct License {
#[serde(rename = "$text")]
pub name: String,
#[serde(rename = "@license")]
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(rename = "@xml:lang")]
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<LocaleCode>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Citation {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
pub layout: Layout,
#[serde(
rename = "@disambiguate-add-givenname",
default,
deserialize_with = "deserialize_bool"
)]
pub disambiguate_add_givenname: bool,
#[serde(rename = "@givenname-disambiguation-rule", default)]
pub givenname_disambiguation_rule: DisambiguationRule,
#[serde(
rename = "@disambiguate-add-names",
default,
deserialize_with = "deserialize_bool"
)]
pub disambiguate_add_names: bool,
#[serde(
rename = "@disambiguate-add-year-suffix",
default,
deserialize_with = "deserialize_bool"
)]
pub disambiguate_add_year_suffix: bool,
#[serde(rename = "@cite-group-delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub cite_group_delimiter: Option<String>,
#[serde(rename = "@collapse")]
#[serde(skip_serializing_if = "Option::is_none")]
pub collapse: Option<Collapse>,
#[serde(rename = "@year-suffix-delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub year_suffix_delimiter: Option<String>,
#[serde(rename = "@after-collapse-delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub after_collapse_delimiter: Option<String>,
#[serde(
rename = "@near-note-distance",
default = "Citation::default_near_note_distance",
deserialize_with = "deserialize_u32"
)]
pub near_note_distance: u32,
#[serde(flatten)]
pub name_options: InheritableNameOptions,
}
impl Citation {
pub const DEFAULT_CITE_GROUP_DELIMITER: &'static str = ", ";
pub fn with_layout(layout: Layout) -> Self {
Self {
sort: None,
layout,
disambiguate_add_givenname: false,
givenname_disambiguation_rule: DisambiguationRule::default(),
disambiguate_add_names: false,
disambiguate_add_year_suffix: false,
cite_group_delimiter: None,
collapse: None,
year_suffix_delimiter: None,
after_collapse_delimiter: None,
near_note_distance: Self::default_near_note_distance(),
name_options: Default::default(),
}
}
pub fn get_year_suffix_delimiter(&self) -> &str {
self.year_suffix_delimiter
.as_deref()
.or(self.layout.delimiter.as_deref())
.unwrap_or_default()
}
pub fn get_after_collapse_delimiter(&self) -> &str {
self.after_collapse_delimiter
.as_deref()
.or(self.layout.delimiter.as_deref())
.unwrap_or_default()
}
pub const fn default_near_note_distance() -> u32 {
5
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DisambiguationRule {
AllNames,
AllNamesWithInitials,
PrimaryName,
PrimaryNameWithInitials,
#[default]
ByCite,
}
impl DisambiguationRule {
pub fn allows_full_first_names(self) -> bool {
match self {
Self::AllNames | Self::PrimaryName | Self::ByCite => true,
Self::AllNamesWithInitials | Self::PrimaryNameWithInitials => false,
}
}
pub fn allows_multiple_names(self) -> bool {
match self {
Self::AllNames | Self::AllNamesWithInitials | Self::ByCite => true,
Self::PrimaryName | Self::PrimaryNameWithInitials => false,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Collapse {
CitationNumber,
Year,
YearSuffix,
YearSuffixRanged,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Bibliography {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<Sort>,
pub layout: Layout,
#[serde(rename = "@hanging-indent", default, deserialize_with = "deserialize_bool")]
pub hanging_indent: bool,
#[serde(rename = "@second-field-align")]
#[serde(skip_serializing_if = "Option::is_none")]
pub second_field_align: Option<SecondFieldAlign>,
#[serde(rename = "@line-spacing", default = "Bibliography::default_line_spacing")]
pub line_spacing: NonZeroI16,
#[serde(rename = "@entry-spacing", default = "Bibliography::default_entry_spacing")]
pub entry_spacing: i16,
#[serde(rename = "@subsequent-author-substitute")]
#[serde(skip_serializing_if = "Option::is_none")]
pub subsequent_author_substitute: Option<String>,
#[serde(rename = "@subsequent-author-substitute-rule", default)]
pub subsequent_author_substitute_rule: SubsequentAuthorSubstituteRule,
#[serde(flatten)]
pub name_options: InheritableNameOptions,
}
impl Bibliography {
pub fn with_layout(layout: Layout) -> Self {
Self {
sort: None,
layout,
hanging_indent: false,
second_field_align: None,
line_spacing: Self::default_line_spacing(),
entry_spacing: Self::default_entry_spacing(),
subsequent_author_substitute: None,
subsequent_author_substitute_rule: Default::default(),
name_options: Default::default(),
}
}
fn default_line_spacing() -> NonZeroI16 {
NonZeroI16::new(1).unwrap()
}
const fn default_entry_spacing() -> i16 {
1
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SecondFieldAlign {
Margin,
Flush,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SubsequentAuthorSubstituteRule {
#[default]
CompleteAll,
CompleteEach,
PartialEach,
PartialFirst,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Sort {
#[serde(rename = "key")]
pub keys: Vec<SortKey>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SortKey {
Variable {
#[serde(rename = "@variable")]
variable: Variable,
#[serde(rename = "@sort", default)]
sort_direction: SortDirection,
},
MacroName {
#[serde(rename = "@macro")]
name: String,
#[serde(
rename = "@names-min",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
names_min: Option<u32>,
#[serde(
rename = "@names-use-first",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
names_use_first: Option<u32>,
#[serde(
rename = "@names-use-last",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
names_use_last: Option<bool>,
#[serde(rename = "@sort", default)]
sort_direction: SortDirection,
},
}
impl From<Variable> for SortKey {
fn from(value: Variable) -> Self {
Self::Variable {
variable: value,
sort_direction: SortDirection::default(),
}
}
}
impl SortKey {
pub const fn sort_direction(&self) -> SortDirection {
match self {
Self::Variable { sort_direction, .. } => *sort_direction,
Self::MacroName { sort_direction, .. } => *sort_direction,
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SortDirection {
#[default]
Ascending,
Descending,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Layout {
#[serde(rename = "$value")]
pub elements: Vec<LayoutRenderingElement>,
#[serde(rename = "@font-style")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
#[serde(rename = "@font-variant")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_variant: Option<FontVariant>,
#[serde(rename = "@font-weight")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_weight: Option<FontWeight>,
#[serde(rename = "@text-decoration")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_decoration: Option<TextDecoration>,
#[serde(rename = "@vertical-align")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vertical_align: Option<VerticalAlign>,
#[serde(rename = "@prefix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(rename = "@suffix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(rename = "@delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter: Option<String>,
}
to_formatting!(Layout, self);
to_affixes!(Layout, self);
impl Layout {
pub fn new(
elements: Vec<LayoutRenderingElement>,
formatting: Formatting,
affixes: Option<Affixes>,
delimiter: Option<String>,
) -> Self {
let (prefix, suffix) = if let Some(affixes) = affixes {
(affixes.prefix, affixes.suffix)
} else {
(None, None)
};
Self {
elements,
font_style: formatting.font_style,
font_variant: formatting.font_variant,
font_weight: formatting.font_weight,
text_decoration: formatting.text_decoration,
vertical_align: formatting.vertical_align,
prefix,
suffix,
delimiter,
}
}
pub fn with_elements(elements: Vec<LayoutRenderingElement>) -> Self {
Self::new(elements, Formatting::default(), None, None)
}
pub fn find_variable_element(
&self,
variable: Variable,
macros: &[CslMacro],
) -> Option<LayoutRenderingElement> {
self.elements
.iter()
.find_map(|e| e.find_variable_element(variable, macros))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum LayoutRenderingElement {
Text(Text),
Date(Date),
Number(Number),
Names(Names),
Label(Label),
Group(Group),
Choose(Choose),
}
impl LayoutRenderingElement {
pub fn find_variable_element(
&self,
variable: Variable,
macros: &[CslMacro],
) -> Option<Self> {
match self {
Self::Text(t) => t.find_variable_element(variable, macros),
Self::Choose(c) => c.find_variable_element(variable, macros),
Self::Date(d) => {
if d.variable.map(Variable::Date) == Some(variable) {
Some(self.clone())
} else {
None
}
}
Self::Number(n) => {
if Variable::Number(n.variable) == variable {
Some(self.clone())
} else {
None
}
}
Self::Names(n) => {
if n.variable.iter().any(|v| Variable::Name(*v) == variable) {
Some(self.clone())
} else {
None
}
}
Self::Group(g) => g
.children
.iter()
.find_map(|e| e.find_variable_element(variable, macros)),
Self::Label(_) => None,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(untagged)]
pub enum RenderingElement {
Layout(Layout),
Other(LayoutRenderingElement),
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Text {
#[serde(flatten)]
pub target: TextTarget,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
#[serde(rename = "@display")]
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(rename = "@quotes", default, deserialize_with = "deserialize_bool")]
pub quotes: bool,
#[serde(rename = "@strip-periods", default, deserialize_with = "deserialize_bool")]
pub strip_periods: bool,
#[serde(rename = "@text-case")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_case: Option<TextCase>,
}
impl Text {
pub fn with_target(target: impl Into<TextTarget>) -> Self {
Self {
target: target.into(),
formatting: Default::default(),
affixes: Default::default(),
display: None,
quotes: false,
strip_periods: false,
text_case: None,
}
}
pub fn find_variable_element(
&self,
variable: Variable,
macros: &[CslMacro],
) -> Option<LayoutRenderingElement> {
match &self.target {
TextTarget::Variable { var, .. } => {
if *var == variable {
Some(LayoutRenderingElement::Text(self.clone()))
} else {
None
}
}
TextTarget::Macro { name } => {
if let Some(m) = macros.iter().find(|m| m.name == *name) {
m.children
.iter()
.find_map(|e| e.find_variable_element(variable, macros))
} else {
None
}
}
TextTarget::Term { .. } => None,
TextTarget::Value { .. } => None,
}
}
}
to_formatting!(Text);
to_affixes!(Text);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(untagged)]
pub enum TextTarget {
Variable {
#[serde(rename = "@variable")]
var: Variable,
#[serde(rename = "@form", default)]
form: LongShortForm,
},
Macro {
#[serde(rename = "@macro")]
name: String,
},
Term {
#[serde(rename = "@term")]
term: Term,
#[serde(rename = "@form", default)]
form: TermForm,
#[serde(rename = "@plural", default, deserialize_with = "deserialize_bool")]
plural: bool,
},
Value {
#[serde(rename = "@value")]
val: String,
},
}
impl From<Variable> for TextTarget {
fn from(value: Variable) -> Self {
Self::Variable { var: value, form: LongShortForm::default() }
}
}
impl From<Term> for TextTarget {
fn from(value: Term) -> Self {
Self::Term {
term: value,
form: TermForm::default(),
plural: bool::default(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Date {
#[serde(rename = "@variable")]
#[serde(skip_serializing_if = "Option::is_none")]
pub variable: Option<DateVariable>,
#[serde(rename = "@form")]
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<DateForm>,
#[serde(rename = "@date-parts")]
#[serde(skip_serializing_if = "Option::is_none")]
pub parts: Option<DateParts>,
#[serde(default)]
pub date_part: Vec<DatePart>,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
#[serde(rename = "@delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter: Option<String>,
#[serde(rename = "@display")]
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(rename = "@text-case")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_case: Option<TextCase>,
}
to_formatting!(Date);
to_affixes!(Date);
impl Date {
pub const fn is_localized(&self) -> bool {
self.form.is_some()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DateForm {
Numeric,
Text,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[allow(missing_docs)]
#[serde(rename_all = "kebab-case")]
pub enum DateParts {
Year,
YearMonth,
#[default]
YearMonthDay,
}
impl DateParts {
pub const fn has_month(self) -> bool {
matches!(self, Self::YearMonth | Self::YearMonthDay)
}
pub const fn has_day(self) -> bool {
matches!(self, Self::YearMonthDay)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct DatePart {
#[serde(rename = "@name")]
pub name: DatePartName,
#[serde(rename = "@form")]
#[serde(skip_serializing_if = "Option::is_none")]
form: Option<DateAnyForm>,
#[serde(rename = "@range-delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub range_delimiter: Option<String>,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
#[serde(rename = "@strip-periods", default, deserialize_with = "deserialize_bool")]
pub strip_periods: bool,
#[serde(rename = "@text-case")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_case: Option<TextCase>,
}
to_formatting!(DatePart);
to_affixes!(DatePart);
impl DatePart {
pub const DEFAULT_DELIMITER: &'static str = "ā";
pub fn form(&self) -> DateStrongAnyForm {
DateStrongAnyForm::for_name(self.name, self.form)
}
}
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DatePartName {
Day,
Month,
Year,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DateAnyForm {
Numeric,
NumericLeadingZeros,
Ordinal,
Long,
Short,
}
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum DateStrongAnyForm {
Day(DateDayForm),
Month(DateMonthForm),
Year(LongShortForm),
}
impl DateStrongAnyForm {
pub fn for_name(name: DatePartName, form: Option<DateAnyForm>) -> Self {
match name {
DatePartName::Day => {
Self::Day(form.map(DateAnyForm::form_for_day).unwrap_or_default())
}
DatePartName::Month => {
Self::Month(form.map(DateAnyForm::form_for_month).unwrap_or_default())
}
DatePartName::Year => {
Self::Year(form.map(DateAnyForm::form_for_year).unwrap_or_default())
}
}
}
}
impl DateAnyForm {
pub fn form_for_day(self) -> DateDayForm {
match self {
Self::NumericLeadingZeros => DateDayForm::NumericLeadingZeros,
Self::Ordinal => DateDayForm::Ordinal,
_ => DateDayForm::default(),
}
}
pub fn form_for_month(self) -> DateMonthForm {
match self {
Self::Short => DateMonthForm::Short,
Self::Numeric => DateMonthForm::Numeric,
Self::NumericLeadingZeros => DateMonthForm::NumericLeadingZeros,
_ => DateMonthForm::default(),
}
}
pub fn form_for_year(self) -> LongShortForm {
match self {
Self::Short => LongShortForm::Short,
_ => LongShortForm::default(),
}
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DateDayForm {
#[default]
Numeric,
NumericLeadingZeros,
Ordinal,
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DateMonthForm {
#[default]
Long,
Short,
Numeric,
NumericLeadingZeros,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub enum LongShortForm {
#[default]
Long,
Short,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Number {
#[serde(rename = "@variable")]
pub variable: NumberVariable,
#[serde(rename = "@form", default)]
pub form: NumberForm,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
#[serde(rename = "@display")]
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(rename = "@text-case")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_case: Option<TextCase>,
}
to_formatting!(Number);
to_affixes!(Number);
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NumberForm {
#[default]
Numeric,
Ordinal,
LongOrdinal,
Roman,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Names {
#[serde(rename = "@variable", default)]
pub variable: Vec<NameVariable>,
#[serde(rename = "$value", default)]
pub children: Vec<NamesChild>,
#[serde(rename = "@delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
delimiter: Option<String>,
#[serde(rename = "@and")]
#[serde(skip_serializing_if = "Option::is_none")]
pub and: Option<NameAnd>,
#[serde(rename = "@delimiter-precedes-et-al")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter_precedes_et_al: Option<DelimiterBehavior>,
#[serde(rename = "@delimiter-precedes-last")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter_precedes_last: Option<DelimiterBehavior>,
#[serde(rename = "@et-al-min", deserialize_with = "deserialize_u32_option", default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_min: Option<u32>,
#[serde(
rename = "@et-al-use-first",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_use_first: Option<u32>,
#[serde(
rename = "@et-al-subsequent-min",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_subsequent_min: Option<u32>,
#[serde(
rename = "@et-al-subsequent-use-first",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_subsequent_use_first: Option<u32>,
#[serde(
rename = "@et-al-use-last",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_use_last: Option<bool>,
#[serde(rename = "@name-form")]
#[serde(skip_serializing_if = "Option::is_none")]
pub name_form: Option<NameForm>,
#[serde(
rename = "@initialize",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub initialize: Option<bool>,
#[serde(rename = "@initialize-with")]
#[serde(skip_serializing_if = "Option::is_none")]
pub initialize_with: Option<String>,
#[serde(rename = "@name-as-sort-order")]
#[serde(skip_serializing_if = "Option::is_none")]
pub name_as_sort_order: Option<NameAsSortOrder>,
#[serde(rename = "@sort-separator")]
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_separator: Option<String>,
#[serde(rename = "@font-style")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
#[serde(rename = "@font-variant")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_variant: Option<FontVariant>,
#[serde(rename = "@font-weight")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_weight: Option<FontWeight>,
#[serde(rename = "@text-decoration")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_decoration: Option<TextDecoration>,
#[serde(rename = "@vertical-align")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vertical_align: Option<VerticalAlign>,
#[serde(rename = "@prefix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(rename = "@suffix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(rename = "@display")]
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
}
impl Names {
pub fn with_variables(variables: Vec<NameVariable>) -> Self {
Self {
variable: variables,
children: Vec::default(),
delimiter: None,
and: None,
delimiter_precedes_et_al: None,
delimiter_precedes_last: None,
et_al_min: None,
et_al_use_first: None,
et_al_subsequent_min: None,
et_al_subsequent_use_first: None,
et_al_use_last: None,
name_form: None,
initialize: None,
initialize_with: None,
name_as_sort_order: None,
sort_separator: None,
font_style: None,
font_variant: None,
font_weight: None,
text_decoration: None,
vertical_align: None,
prefix: None,
suffix: None,
display: None,
}
}
pub fn delimiter<'a>(&'a self, name_options: &'a InheritableNameOptions) -> &'a str {
self.delimiter
.as_deref()
.or(name_options.name_delimiter.as_deref())
.unwrap_or_default()
}
pub fn name(&self) -> Option<&Name> {
self.children.iter().find_map(|c| match c {
NamesChild::Name(n) => Some(n),
_ => None,
})
}
pub fn et_al(&self) -> Option<&EtAl> {
self.children.iter().find_map(|c| match c {
NamesChild::EtAl(e) => Some(e),
_ => None,
})
}
pub fn label(&self) -> Option<(&VariablelessLabel, NameLabelPosition)> {
let mut pos = NameLabelPosition::BeforeName;
self.children.iter().find_map(|c| match c {
NamesChild::Label(l) => Some((l, pos)),
NamesChild::Name(_) => {
pos = NameLabelPosition::AfterName;
None
}
_ => None,
})
}
pub fn substitute(&self) -> Option<&Substitute> {
self.children.iter().find_map(|c| match c {
NamesChild::Substitute(s) => Some(s),
_ => None,
})
}
pub fn options(&self) -> InheritableNameOptions {
InheritableNameOptions {
and: self.and,
delimiter_precedes_et_al: self.delimiter_precedes_et_al,
delimiter_precedes_last: self.delimiter_precedes_last,
et_al_min: self.et_al_min,
et_al_use_first: self.et_al_use_first,
et_al_subsequent_min: self.et_al_subsequent_min,
et_al_subsequent_use_first: self.et_al_subsequent_use_first,
et_al_use_last: self.et_al_use_last,
name_form: self.name_form,
initialize: self.initialize,
initialize_with: self.initialize_with.clone(),
name_as_sort_order: self.name_as_sort_order,
sort_separator: self.sort_separator.clone(),
name_delimiter: None,
names_delimiter: self.delimiter.clone(),
}
}
pub fn from_names_substitute(&self, child: &Self) -> Names {
if child.name().is_some()
|| child.et_al().is_some()
|| child.substitute().is_some()
{
return child.clone();
}
let formatting = child.to_formatting().apply(self.to_formatting());
let options = self.options().apply(&child.options());
Names {
variable: if child.variable.is_empty() {
self.variable.clone()
} else {
child.variable.clone()
},
children: self
.children
.iter()
.filter(|c| !matches!(c, NamesChild::Substitute(_)))
.cloned()
.collect(),
delimiter: child.delimiter.clone().or_else(|| self.delimiter.clone()),
and: options.and,
delimiter_precedes_et_al: options.delimiter_precedes_et_al,
delimiter_precedes_last: options.delimiter_precedes_last,
et_al_min: options.et_al_min,
et_al_use_first: options.et_al_use_first,
et_al_subsequent_min: options.et_al_subsequent_min,
et_al_subsequent_use_first: options.et_al_subsequent_use_first,
et_al_use_last: options.et_al_use_last,
name_form: options.name_form,
initialize: options.initialize,
initialize_with: options.initialize_with,
name_as_sort_order: options.name_as_sort_order,
sort_separator: options.sort_separator,
font_style: formatting.font_style,
font_variant: formatting.font_variant,
font_weight: formatting.font_weight,
text_decoration: formatting.text_decoration,
vertical_align: formatting.vertical_align,
prefix: child.prefix.clone().or_else(|| self.prefix.clone()),
suffix: child.suffix.clone().or_else(|| self.suffix.clone()),
display: child.display.or(self.display),
}
}
}
to_formatting!(Names, self);
to_affixes!(Names, self);
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum NameLabelPosition {
AfterName,
BeforeName,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NamesChild {
Name(Name),
EtAl(EtAl),
Label(VariablelessLabel),
Substitute(Substitute),
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct Name {
#[serde(rename = "@delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
delimiter: Option<String>,
#[serde(rename = "@form")]
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<NameForm>,
#[serde(rename = "name-part")]
parts: Vec<NamePart>,
#[serde(flatten)]
options: InheritableNameOptions,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
}
to_formatting!(Name);
to_affixes!(Name);
impl Name {
pub fn name_part_given(&self) -> Option<&NamePart> {
self.parts.iter().find(|p| p.name == NamePartName::Given)
}
pub fn name_part_family(&self) -> Option<&NamePart> {
self.parts.iter().find(|p| p.name == NamePartName::Family)
}
pub fn options<'s>(&'s self, inherited: &'s InheritableNameOptions) -> NameOptions {
let applied = inherited.apply(&self.options);
NameOptions {
and: applied.and,
delimiter: self
.delimiter
.as_deref()
.or(inherited.name_delimiter.as_deref())
.unwrap_or(", "),
delimiter_precedes_et_al: applied
.delimiter_precedes_et_al
.unwrap_or_default(),
delimiter_precedes_last: applied.delimiter_precedes_last.unwrap_or_default(),
et_al_min: applied.et_al_min,
et_al_use_first: applied.et_al_use_first,
et_al_subsequent_min: applied.et_al_subsequent_min,
et_al_subsequent_use_first: applied.et_al_subsequent_use_first,
et_al_use_last: applied.et_al_use_last.unwrap_or_default(),
form: self.form.or(inherited.name_form).unwrap_or_default(),
initialize: applied.initialize.unwrap_or(true),
initialize_with: self
.options
.initialize_with
.as_deref()
.or(inherited.initialize_with.as_deref()),
name_as_sort_order: applied.name_as_sort_order,
sort_separator: self
.options
.sort_separator
.as_deref()
.or(inherited.sort_separator.as_deref())
.unwrap_or(", "),
}
}
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(default)]
pub struct InheritableNameOptions {
#[serde(rename = "@and")]
#[serde(skip_serializing_if = "Option::is_none")]
pub and: Option<NameAnd>,
#[serde(rename = "@name-delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub name_delimiter: Option<String>,
#[serde(rename = "@names-delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub names_delimiter: Option<String>,
#[serde(rename = "@delimiter-precedes-et-al")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter_precedes_et_al: Option<DelimiterBehavior>,
#[serde(rename = "@delimiter-precedes-last")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter_precedes_last: Option<DelimiterBehavior>,
#[serde(rename = "@et-al-min", deserialize_with = "deserialize_u32_option", default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_min: Option<u32>,
#[serde(
rename = "@et-al-use-first",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_use_first: Option<u32>,
#[serde(
rename = "@et-al-subsequent-min",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_subsequent_min: Option<u32>,
#[serde(
rename = "@et-al-subsequent-use-first",
deserialize_with = "deserialize_u32_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_subsequent_use_first: Option<u32>,
#[serde(
rename = "@et-al-use-last",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub et_al_use_last: Option<bool>,
#[serde(rename = "@name-form")]
#[serde(skip_serializing_if = "Option::is_none")]
pub name_form: Option<NameForm>,
#[serde(
rename = "@initialize",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub initialize: Option<bool>,
#[serde(rename = "@initialize-with")]
#[serde(skip_serializing_if = "Option::is_none")]
pub initialize_with: Option<String>,
#[serde(rename = "@name-as-sort-order")]
#[serde(skip_serializing_if = "Option::is_none")]
pub name_as_sort_order: Option<NameAsSortOrder>,
#[serde(rename = "@sort-separator")]
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_separator: Option<String>,
}
pub struct NameOptions<'s> {
pub and: Option<NameAnd>,
pub delimiter: &'s str,
pub delimiter_precedes_et_al: DelimiterBehavior,
pub delimiter_precedes_last: DelimiterBehavior,
pub et_al_min: Option<u32>,
pub et_al_use_first: Option<u32>,
pub et_al_subsequent_min: Option<u32>,
pub et_al_subsequent_use_first: Option<u32>,
pub et_al_use_last: bool,
pub form: NameForm,
pub initialize: bool,
pub initialize_with: Option<&'s str>,
pub name_as_sort_order: Option<NameAsSortOrder>,
pub sort_separator: &'s str,
}
impl InheritableNameOptions {
pub fn apply(&self, child: &Self) -> Self {
Self {
and: child.and.or(self.and),
name_delimiter: child
.name_delimiter
.clone()
.or_else(|| self.name_delimiter.clone()),
names_delimiter: child
.names_delimiter
.clone()
.or_else(|| self.names_delimiter.clone()),
delimiter_precedes_et_al: child
.delimiter_precedes_et_al
.or(self.delimiter_precedes_et_al),
delimiter_precedes_last: child
.delimiter_precedes_last
.or(self.delimiter_precedes_last),
et_al_min: child.et_al_min.or(self.et_al_min),
et_al_use_first: child.et_al_use_first.or(self.et_al_use_first),
et_al_subsequent_min: child
.et_al_subsequent_min
.or(self.et_al_subsequent_min),
et_al_subsequent_use_first: child
.et_al_subsequent_use_first
.or(self.et_al_subsequent_use_first),
et_al_use_last: child.et_al_use_last.or(self.et_al_use_last),
name_form: child.name_form.or(self.name_form),
initialize: child.initialize.or(self.initialize),
initialize_with: child
.initialize_with
.clone()
.or_else(|| self.initialize_with.clone()),
name_as_sort_order: child.name_as_sort_order.or(self.name_as_sort_order),
sort_separator: child
.sort_separator
.clone()
.or_else(|| self.sort_separator.clone()),
}
}
}
impl NameOptions<'_> {
pub fn is_suppressed(&self, idx: usize, length: usize, is_subsequent: bool) -> bool {
if self.et_al_use_last && idx + 1 >= length {
return false;
}
let (et_al_min, et_al_use_first) = if is_subsequent {
(
self.et_al_subsequent_min.or(self.et_al_min),
self.et_al_subsequent_use_first.or(self.et_al_use_first),
)
} else {
(self.et_al_min, self.et_al_use_first)
};
let et_al_min = et_al_min.map_or(usize::MAX, |u| u as usize);
let et_al_use_first = et_al_use_first.map_or(usize::MAX, |u| u as usize);
length >= et_al_min && idx + 1 > et_al_use_first
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NameAnd {
Text,
Symbol,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DelimiterBehavior {
#[default]
Contextual,
AfterInvertedName,
Always,
Never,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NameForm {
#[default]
Long,
Short,
Count,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NameAsSortOrder {
First,
All,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct NamePart {
#[serde(rename = "@name")]
pub name: NamePartName,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
#[serde(rename = "@text-case")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_case: Option<TextCase>,
}
to_formatting!(NamePart);
to_affixes!(NamePart);
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NamePartName {
Given,
Family,
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct EtAl {
#[serde(rename = "@term", default)]
pub term: EtAlTerm,
#[serde(flatten)]
pub formatting: Formatting,
}
to_formatting!(EtAl);
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub enum EtAlTerm {
#[default]
#[serde(rename = "et al", alias = "et-al")]
EtAl,
#[serde(rename = "and others", alias = "and-others")]
AndOthers,
}
impl From<EtAlTerm> for Term {
fn from(term: EtAlTerm) -> Self {
match term {
EtAlTerm::EtAl => Term::Other(OtherTerm::EtAl),
EtAlTerm::AndOthers => Term::Other(OtherTerm::AndOthers),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Substitute {
#[serde(rename = "$value")]
pub children: Vec<LayoutRenderingElement>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Label {
#[serde(rename = "@variable")]
pub variable: NumberOrPageVariable,
#[serde(flatten)]
pub label: VariablelessLabel,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct VariablelessLabel {
#[serde(rename = "@form", default)]
pub form: TermForm,
#[serde(rename = "@plural", default)]
pub plural: LabelPluralize,
#[serde(flatten)]
pub formatting: Formatting,
#[serde(flatten)]
pub affixes: Affixes,
#[serde(rename = "@text-case")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_case: Option<TextCase>,
#[serde(rename = "@strip-periods", default, deserialize_with = "deserialize_bool")]
pub strip_periods: bool,
}
to_formatting!(VariablelessLabel);
to_affixes!(VariablelessLabel);
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum LabelPluralize {
#[default]
Contextual,
Always,
Never,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Group {
#[serde(rename = "$value")]
pub children: Vec<LayoutRenderingElement>,
#[serde(rename = "@font-style")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
#[serde(rename = "@font-variant")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_variant: Option<FontVariant>,
#[serde(rename = "@font-weight")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_weight: Option<FontWeight>,
#[serde(rename = "@text-decoration")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_decoration: Option<TextDecoration>,
#[serde(rename = "@vertical-align")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vertical_align: Option<VerticalAlign>,
#[serde(rename = "@prefix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(rename = "@suffix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(rename = "@delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter: Option<String>,
#[serde(rename = "@display")]
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
}
to_formatting!(Group, self);
to_affixes!(Group, self);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Choose {
#[serde(rename = "if")]
pub if_: ChooseBranch,
#[serde(rename = "else-if")]
#[serde(default)]
pub else_if: Vec<ChooseBranch>,
#[serde(rename = "else")]
#[serde(skip_serializing_if = "Option::is_none")]
pub otherwise: Option<ElseBranch>,
#[serde(rename = "@delimiter")]
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter: Option<String>,
}
impl Choose {
pub fn branches(&self) -> impl Iterator<Item = &ChooseBranch> {
std::iter::once(&self.if_).chain(self.else_if.iter())
}
pub fn find_variable_element(
&self,
variable: Variable,
macros: &[CslMacro],
) -> Option<LayoutRenderingElement> {
self.branches()
.find_map(|b| {
b.children
.iter()
.find_map(|c| c.find_variable_element(variable, macros))
})
.clone()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct ChooseBranch {
#[serde(
rename = "@disambiguate",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub disambiguate: Option<bool>,
#[serde(rename = "@is-numeric")]
#[serde(skip_serializing_if = "Option::is_none")]
pub is_numeric: Option<Vec<Variable>>,
#[serde(rename = "@is-uncertain-date")]
#[serde(skip_serializing_if = "Option::is_none")]
pub is_uncertain_date: Option<Vec<DateVariable>>,
#[serde(rename = "@locator")]
#[serde(skip_serializing_if = "Option::is_none")]
pub locator: Option<Vec<Locator>>,
#[serde(rename = "@position")]
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<Vec<TestPosition>>,
#[serde(rename = "@type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub type_: Option<Vec<Kind>>,
#[serde(rename = "@variable")]
#[serde(skip_serializing_if = "Option::is_none")]
pub variable: Option<Vec<Variable>>,
#[serde(rename = "@match")]
#[serde(default)]
pub match_: ChooseMatch,
#[serde(rename = "$value", default)]
pub children: Vec<LayoutRenderingElement>,
}
impl ChooseBranch {
pub fn test(&self) -> Option<ChooseTest> {
if let Some(disambiguate) = self.disambiguate {
if !disambiguate {
None
} else {
Some(ChooseTest::Disambiguate)
}
} else if let Some(is_numeric) = &self.is_numeric {
Some(ChooseTest::IsNumeric(is_numeric))
} else if let Some(is_uncertain_date) = &self.is_uncertain_date {
Some(ChooseTest::IsUncertainDate(is_uncertain_date))
} else if let Some(locator) = &self.locator {
Some(ChooseTest::Locator(locator))
} else if let Some(position) = &self.position {
Some(ChooseTest::Position(position))
} else if let Some(type_) = &self.type_ {
Some(ChooseTest::Type(type_))
} else {
self.variable.as_ref().map(|variable| ChooseTest::Variable(variable))
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct ElseBranch {
#[serde(rename = "$value")]
pub children: Vec<LayoutRenderingElement>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum ChooseTest<'a> {
Disambiguate,
IsNumeric(&'a [Variable]),
IsUncertainDate(&'a [DateVariable]),
Locator(&'a [Locator]),
Position(&'a [TestPosition]),
Type(&'a [Kind]),
Variable(&'a [Variable]),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum TestPosition {
First,
Subsequent,
IbidWithLocator,
Ibid,
NearNote,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ChooseMatch {
#[default]
All,
Any,
None,
}
impl ChooseMatch {
pub fn test(self, mut tests: impl Iterator<Item = bool>) -> bool {
match self {
Self::All => tests.all(|t| t),
Self::Any => tests.any(|t| t),
Self::None => tests.all(|t| !t),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct CslMacro {
#[serde(rename = "@name")]
pub name: String,
#[serde(rename = "$value")]
#[serde(default)]
pub children: Vec<LayoutRenderingElement>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct LocaleFile {
#[serde(rename = "@version")]
pub version: String,
#[serde(rename = "@lang")]
pub lang: LocaleCode,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<LocaleInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms: Option<Terms>,
#[serde(default)]
pub date: Vec<Date>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style_options: Option<LocaleOptions>,
}
impl LocaleFile {
pub fn from_xml(xml: &str) -> XmlResult<Self> {
let locale: Self = quick_xml::de::from_str(xml)?;
Ok(locale)
}
pub fn to_xml(&self) -> XmlResult<String> {
let mut buf = String::new();
let ser = quick_xml::se::Serializer::with_root(&mut buf, Some("style"))?;
self.serialize(ser)?;
Ok(buf)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Locale {
#[serde(rename = "@lang")]
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<LocaleCode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<LocaleInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms: Option<Terms>,
#[serde(default)]
pub date: Vec<Date>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style_options: Option<LocaleOptions>,
}
impl Locale {
pub fn term(&self, term: Term, form: TermForm) -> Option<&LocalizedTerm> {
self.terms.as_ref().and_then(|terms| {
terms
.terms
.iter()
.find(|t| t.name.is_lexically_same(term) && t.form == form)
})
}
pub fn ordinals(&self) -> Option<OrdinalLookup<'_>> {
self.terms.as_ref().and_then(|terms| {
terms.terms.iter().any(|t| t.name.is_ordinal()).then(|| {
OrdinalLookup::new(terms.terms.iter().filter(|t| t.name.is_ordinal()))
})
})
}
}
pub struct OrdinalLookup<'a> {
terms: Vec<&'a LocalizedTerm>,
legacy_behavior: bool,
}
impl<'a> OrdinalLookup<'a> {
fn new(ordinal_terms: impl Iterator<Item = &'a LocalizedTerm>) -> Self {
let terms = ordinal_terms.collect::<Vec<_>>();
let mut legacy_behavior = false;
let defines_ordinal =
terms.iter().any(|t| t.name == Term::Other(OtherTerm::Ordinal));
if !defines_ordinal {
legacy_behavior = (1..=4).all(|n| {
terms.iter().any(|t| t.name == Term::Other(OtherTerm::OrdinalN(n)))
})
}
Self { terms, legacy_behavior }
}
pub const fn empty() -> Self {
Self { terms: Vec::new(), legacy_behavior: false }
}
pub fn lookup(&self, n: i32, gender: Option<GrammarGender>) -> Option<&'a str> {
let mut best_match: Option<&'a LocalizedTerm> = None;
let mut change_match = |other_match: &'a LocalizedTerm| {
let Some(current) = best_match else {
best_match = Some(other_match);
return;
};
let Term::Other(OtherTerm::OrdinalN(other_n)) = other_match.name else {
return;
};
let Term::Other(OtherTerm::OrdinalN(curr_n)) = current.name else {
best_match = Some(other_match);
return;
};
best_match = Some(if other_n >= 10 && curr_n < 10 {
other_match
} else if other_n < 10 && curr_n >= 10 {
current
} else {
if gender == current.gender && gender != other_match.gender {
current
} else if gender != current.gender && gender == other_match.gender {
other_match
} else {
let diff_other = (n - other_n as i32).abs();
let diff_curr = (n - curr_n as i32).abs();
if diff_other <= diff_curr {
other_match
} else {
current
}
}
})
};
for term in self.terms.iter().copied() {
let Term::Other(term_name) = term.name else { continue };
let hit = match term_name {
OtherTerm::Ordinal => true,
OtherTerm::OrdinalN(o) if self.legacy_behavior => {
let class = match (n, n % 10) {
(11..=13, _) => 4,
(_, v @ 1..=3) => v as u8,
_ => 4,
};
o == class
}
OtherTerm::OrdinalN(o @ 0..=9) => match term.match_ {
Some(OrdinalMatch::LastDigit) | None => n % 10 == o as i32,
Some(OrdinalMatch::LastTwoDigits) => n % 100 == o as i32,
Some(OrdinalMatch::WholeNumber) => n == o as i32,
},
OtherTerm::OrdinalN(o @ 10..=99) => match term.match_ {
Some(OrdinalMatch::LastTwoDigits) | None => n % 100 == o as i32,
Some(OrdinalMatch::WholeNumber) => n == o as i32,
_ => false,
},
_ => false,
};
if hit {
change_match(term);
}
}
best_match.and_then(|t| t.single().or_else(|| t.multiple()))
}
pub fn lookup_long(&self, n: i32) -> Option<&'a str> {
self.terms
.iter()
.find(|t| {
let Term::Other(OtherTerm::LongOrdinal(o)) = t.name else { return false };
if n > 0 && n <= 10 {
n == o as i32
} else {
match t.match_ {
Some(OrdinalMatch::LastTwoDigits) | None => n % 100 == o as i32,
Some(OrdinalMatch::WholeNumber) => n == o as i32,
_ => false,
}
}
})
.and_then(|t| t.single().or_else(|| t.multiple()))
}
}
impl From<LocaleFile> for Locale {
fn from(file: LocaleFile) -> Self {
Self {
lang: Some(file.lang),
info: file.info,
terms: file.terms,
date: file.date,
style_options: file.style_options,
}
}
}
impl TryFrom<Locale> for LocaleFile {
type Error = ();
fn try_from(value: Locale) -> Result<Self, Self::Error> {
if value.lang.is_some() {
Ok(Self {
version: "1.0".to_string(),
lang: value.lang.unwrap(),
info: value.info,
terms: value.terms,
date: value.date,
style_options: value.style_options,
})
} else {
Err(())
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct LocaleInfo {
#[serde(rename = "translator")]
#[serde(default)]
pub translators: Vec<StyleAttribution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rights: Option<License>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<Timestamp>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Terms {
#[serde(rename = "term")]
pub terms: Vec<LocalizedTerm>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct LocalizedTerm {
#[serde(rename = "@name")]
pub name: Term,
#[serde(rename = "$text")]
#[serde(skip_serializing_if = "Option::is_none")]
localization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
single: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
multiple: Option<String>,
#[serde(rename = "@form", default)]
pub form: TermForm,
#[serde(rename = "@match")]
#[serde(skip_serializing_if = "Option::is_none")]
pub match_: Option<OrdinalMatch>,
#[serde(rename = "@gender")]
#[serde(skip_serializing_if = "Option::is_none")]
pub gender: Option<GrammarGender>,
#[serde(rename = "@gender-form")]
#[serde(skip_serializing_if = "Option::is_none")]
pub gender_form: Option<GrammarGender>,
}
impl LocalizedTerm {
pub fn single(&self) -> Option<&str> {
self.single.as_deref().or(self.localization.as_deref())
}
pub fn multiple(&self) -> Option<&str> {
self.multiple.as_deref().or(self.localization.as_deref())
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum TermForm {
#[default]
Long,
Short,
Verb,
VerbShort,
Symbol,
}
impl TermForm {
pub const fn fallback(self) -> Option<Self> {
match self {
Self::Long => None,
Self::Short => Some(Self::Long),
Self::Verb => Some(Self::Long),
Self::VerbShort => Some(Self::Verb),
Self::Symbol => Some(Self::Short),
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum OrdinalMatch {
#[default]
LastDigit,
LastTwoDigits,
WholeNumber,
}
#[allow(missing_docs)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GrammarGender {
Feminine,
Masculine,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct LocaleOptions {
#[serde(
rename = "@limit-day-ordinals-to-day-1",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub limit_day_ordinals_to_day_1: Option<bool>,
#[serde(
rename = "@punctuation-in-quote",
deserialize_with = "deserialize_bool_option",
default
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub punctuation_in_quote: Option<bool>,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Formatting {
#[serde(rename = "@font-style")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
#[serde(rename = "@font-variant")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_variant: Option<FontVariant>,
#[serde(rename = "@font-weight")]
#[serde(skip_serializing_if = "Option::is_none")]
pub font_weight: Option<FontWeight>,
#[serde(rename = "@text-decoration")]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_decoration: Option<TextDecoration>,
#[serde(rename = "@vertical-align")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vertical_align: Option<VerticalAlign>,
}
impl Formatting {
pub fn is_empty(&self) -> bool {
self.font_style.is_none()
&& self.font_variant.is_none()
&& self.font_weight.is_none()
&& self.text_decoration.is_none()
&& self.vertical_align.is_none()
}
pub fn apply(self, base: Self) -> Self {
Self {
font_style: self.font_style.or(base.font_style),
font_variant: self.font_variant.or(base.font_variant),
font_weight: self.font_weight.or(base.font_weight),
text_decoration: self.text_decoration.or(base.text_decoration),
vertical_align: self.vertical_align.or(base.vertical_align),
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum FontStyle {
#[default]
Normal,
Italic,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum FontVariant {
#[default]
Normal,
SmallCaps,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum FontWeight {
#[default]
Normal,
Bold,
Light,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TextDecoration {
#[default]
None,
Underline,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VerticalAlign {
#[default]
#[serde(rename = "")]
None,
Baseline,
Sup,
Sub,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct Affixes {
#[serde(rename = "@prefix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(rename = "@suffix")]
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Display {
Block,
LeftMargin,
RightInline,
Indent,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum TextCase {
Lowercase,
Uppercase,
CapitalizeFirst,
CapitalizeAll,
#[serde(rename = "sentence")]
SentenceCase,
#[serde(rename = "title")]
TitleCase,
}
impl TextCase {
pub fn is_language_independent(self) -> bool {
match self {
Self::Lowercase
| Self::Uppercase
| Self::CapitalizeFirst
| Self::CapitalizeAll => true,
Self::SentenceCase | Self::TitleCase => false,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use serde::de::DeserializeOwned;
use std::{error::Error, fs};
fn folder<F>(
files: &'static str,
extension: &'static str,
kind: &'static str,
mut check: F,
) where
F: FnMut(&str) -> Option<Box<dyn Error>>,
{
let mut failures = 0;
let mut tests = 0;
for entry in fs::read_dir(files).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map(|os| os.to_str().unwrap()) != Some(extension)
|| !entry.file_type().unwrap().is_file()
{
continue;
}
tests += 1;
let source = fs::read_to_string(&path).unwrap();
let result = check(&source);
if let Some(err) = result {
failures += 1;
println!("ā {:?} failed: \n\n{:#?}", &path, &err);
}
}
if failures == 0 {
print!("\nš")
} else {
print!("\nš¢")
}
println!(
" {} out of {} {} files parsed successfully",
tests - failures,
tests,
kind
);
if failures > 0 {
panic!("{} tests failed", failures);
}
}
fn check_style(csl_files: &'static str, kind: &'static str) {
folder(csl_files, "csl", kind, |source| {
let de = &mut deserializer(source);
let result: Result<RawStyle, _> = serde_path_to_error::deserialize(de);
match result {
Ok(_) => None,
Err(err) => Some(Box::new(err)),
}
})
}
fn check_locale(locale_files: &'static str) {
folder(locale_files, "xml", "Locale", |source| {
let de = &mut deserializer(source);
let result: Result<LocaleFile, _> = serde_path_to_error::deserialize(de);
match result {
Ok(_) => None,
Err(err) => Some(Box::new(err)),
}
})
}
#[track_caller]
fn to_cbor<T: Serialize>(style: &T) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::ser::into_writer(style, &mut buf).unwrap();
buf
}
#[track_caller]
fn from_cbor<T: DeserializeOwned>(reader: &[u8]) -> T {
ciborium::de::from_reader(reader).unwrap()
}
#[test]
fn test_independent() {
check_style("tests/independent", "independent CSL style");
}
#[test]
fn test_dependent() {
check_style("tests/dependent", "dependent CSL style");
}
#[test]
fn test_locale() {
check_locale("tests/locales");
}
#[test]
fn roundtrip_cbor_all() {
fs::create_dir_all("tests/artifacts/styles").unwrap();
for style_thing in
fs::read_dir("../styles/").expect("please check out the CSL styles repo")
{
let thing = style_thing.unwrap();
if thing.file_type().unwrap().is_dir() {
continue;
}
let path = thing.path();
let extension = path.extension();
if let Some(extension) = extension {
if extension.to_str() != Some("csl") {
continue;
}
} else {
continue;
}
eprintln!("Testing {}", path.display());
let source = fs::read_to_string(&path).unwrap();
let style = Style::from_xml(&source).unwrap();
let cbor = to_cbor(&style);
fs::write(
format!(
"tests/artifacts/styles/{}.cbor",
path.file_stem().unwrap().to_str().unwrap()
),
&cbor,
)
.unwrap();
let style2 = from_cbor(&cbor);
assert_eq!(style, style2);
}
}
#[test]
fn roundtrip_cbor_all_locales() {
fs::create_dir_all("tests/artifacts/locales").unwrap();
for style_thing in
fs::read_dir("../locales/").expect("please check out the CSL locales repo")
{
let thing = style_thing.unwrap();
if thing.file_type().unwrap().is_dir() {
continue;
}
let path = thing.path();
let extension = path.extension();
if let Some(extension) = extension {
if extension.to_str() != Some("xml")
|| !path
.file_stem()
.unwrap()
.to_str()
.unwrap()
.starts_with("locales-")
{
continue;
}
} else {
continue;
}
eprintln!("Testing {}", path.display());
let source = fs::read_to_string(&path).unwrap();
let locale = LocaleFile::from_xml(&source).unwrap();
let cbor = to_cbor(&locale);
fs::write(
format!(
"tests/artifacts/locales/{}.cbor",
path.file_stem().unwrap().to_str().unwrap()
),
&cbor,
)
.unwrap();
let locale2 = from_cbor(&cbor);
assert_eq!(locale, locale2);
}
}
#[test]
fn page_range() {
fn run(format: PageRangeFormat, start: &str, end: &str) -> String {
let mut buf = String::new();
format.format(&mut buf, start, end, None).unwrap();
buf
}
let c15 = PageRangeFormat::Chicago15;
let c16 = PageRangeFormat::Chicago16;
let exp = PageRangeFormat::Expanded;
let min = PageRangeFormat::Minimal;
let mi2 = PageRangeFormat::MinimalTwo;
assert_eq!("3ā10", run(c15, "3", "10"));
assert_eq!("71ā72", run(c15, "71", "72"));
assert_eq!("100ā104", run(c15, "100", "4"));
assert_eq!("600ā613", run(c15, "600", "613"));
assert_eq!("1100ā1123", run(c15, "1100", "1123"));
assert_eq!("107ā8", run(c15, "107", "108"));
assert_eq!("505ā17", run(c15, "505", "517"));
assert_eq!("1002ā6", run(c15, "1002", "1006"));
assert_eq!("321ā25", run(c15, "321", "325"));
assert_eq!("415ā532", run(c15, "415", "532"));
assert_eq!("11564ā68", run(c15, "11564", "11568"));
assert_eq!("13792ā803", run(c15, "13792", "13803"));
assert_eq!("1496ā1504", run(c15, "1496", "1504"));
assert_eq!("2787ā2816", run(c15, "2787", "2816"));
assert_eq!("101ā8", run(c15, "101", "108"));
assert_eq!("3ā10", run(c16, "3", "10"));
assert_eq!("71ā72", run(c16, "71", "72"));
assert_eq!("92ā113", run(c16, "92", "113"));
assert_eq!("100ā104", run(c16, "100", "4"));
assert_eq!("600ā613", run(c16, "600", "613"));
assert_eq!("1100ā1123", run(c16, "1100", "1123"));
assert_eq!("107ā8", run(c16, "107", "108"));
assert_eq!("505ā17", run(c16, "505", "517"));
assert_eq!("1002ā6", run(c16, "1002", "1006"));
assert_eq!("321ā25", run(c16, "321", "325"));
assert_eq!("415ā532", run(c16, "415", "532"));
assert_eq!("1087ā89", run(c16, "1087", "1089"));
assert_eq!("1496ā500", run(c16, "1496", "1500"));
assert_eq!("11564ā68", run(c16, "11564", "11568"));
assert_eq!("13792ā803", run(c16, "13792", "13803"));
assert_eq!("12991ā3001", run(c16, "12991", "13001"));
assert_eq!("12991ā123001", run(c16, "12991", "123001"));
assert_eq!("42ā45", run(exp, "42", "45"));
assert_eq!("321ā328", run(exp, "321", "328"));
assert_eq!("2787ā2816", run(exp, "2787", "2816"));
assert_eq!("42ā5", run(min, "42", "45"));
assert_eq!("321ā8", run(min, "321", "328"));
assert_eq!("2787ā816", run(min, "2787", "2816"));
assert_eq!("7ā8", run(mi2, "7", "8"));
assert_eq!("42ā45", run(mi2, "42", "45"));
assert_eq!("321ā28", run(mi2, "321", "328"));
assert_eq!("2787ā816", run(mi2, "2787", "2816"));
}
#[test]
fn test_bug_hayagriva_115() {
fn run(format: PageRangeFormat, start: &str, end: &str) -> String {
let mut buf = String::new();
format.format(&mut buf, start, end, None).unwrap();
buf
}
let c16 = PageRangeFormat::Chicago16;
assert_eq!("12991ā123001", run(c16, "12991", "123001"));
}
#[test]
fn page_range_prefix() {
fn run(format: PageRangeFormat, start: &str, end: &str) -> String {
let mut buf = String::new();
format.format(&mut buf, start, end, None).unwrap();
buf
}
let c15 = PageRangeFormat::Chicago15;
let exp = PageRangeFormat::Expanded;
let min = PageRangeFormat::Minimal;
assert_eq!("8n11564ā68", run(c15, "8n11564", "8n1568"));
assert_eq!("n11564ā68", run(c15, "n11564", "n1568"));
assert_eq!("n11564ā1568", run(c15, "n11564", "1568"));
assert_eq!("N110ā5", run(exp, "N110 ", " 5"));
assert_eq!("N110āN115", run(exp, "N110 ", " N5"));
assert_eq!("110āN6", run(exp, "110 ", " N6"));
assert_eq!("N110āP5", run(exp, "N110 ", " P5"));
assert_eq!("123N110āN5", run(exp, "123N110 ", " N5"));
assert_eq!("456K200ā99", run(exp, "456K200 ", " 99"));
assert_eq!("000c23ā22", run(exp, "000c23 ", " 22"));
assert_eq!("n11564ā8", run(min, "n11564 ", " n1568"));
assert_eq!("n11564ā1568", run(min, "n11564 ", " 1568"));
}
}