diff --git a/src/csl/mod.rs b/src/csl/mod.rs index 0b5532cd..08a04af6 100644 --- a/src/csl/mod.rs +++ b/src/csl/mod.rs @@ -6,25 +6,25 @@ use std::num::NonZeroUsize; use citationberg::taxonomy::{Locator, OtherTerm, Term, Variable}; use citationberg::{ - taxonomy as csl_taxonomy, Affixes, CslMacro, Display, FontStyle, FontVariant, - FontWeight, InheritableNameOptions, Locale, LocaleCode, Style, TermForm, - TextDecoration, VerticalAlign, + taxonomy as csl_taxonomy, Affixes, Bibliography, Citation, CslMacro, Display, + FontStyle, FontVariant, FontWeight, InheritableNameOptions, Locale, LocaleCode, + Style, TermForm, TextDecoration, ToFormatting, VerticalAlign, }; use citationberg::{ DateForm, IndependentStyleSettings, LongShortForm, OrdinalLookup, TextCase, }; +use crate::csl::rendering::RenderCsl; use crate::csl::taxonomy::resolve_name_variable; use crate::lang::CaseFolder; use crate::types::{ChunkKind, ChunkedString, Date, MaybeTyped, Numeric, Person}; use crate::Entry; mod rendering; +mod sort; mod taxonomy; -use taxonomy::{ - resolve_date_variable, resolve_number_variable, resolve_standard_variable, -}; +use taxonomy::{resolve_date_variable, resolve_standard_variable}; /// A context that contains all information related to rendering a single entry. #[derive(Debug, Clone, Copy, PartialEq)] @@ -62,6 +62,10 @@ pub(crate) struct StyleContext<'a> { locale_file: &'a [Locale], /// Which locale we're using. locale: LocaleCode, + /// Citation style. + citation: &'a Citation, + /// Bibliography layout. + bibliography: Option<&'a Bibliography>, } impl<'a> StyleContext<'a> { @@ -79,6 +83,8 @@ impl<'a> StyleContext<'a> { locale, style_locales: style.locale.as_slice(), locale_file, + citation: style.citation.as_ref()?, + bibliography: style.bibliography.as_ref(), }) } @@ -101,8 +107,43 @@ impl<'a> StyleContext<'a> { writing: WritingContext::new(), } } + + pub fn citation(&self, items: &mut [CitationItem<'_>]) -> Vec { + self.sort(items, self.citation.sort.as_ref()); + let mut res = Vec::new(); + + for CitationItem(entry, locator) in items.iter() { + let mut ctx = self.ctx(entry, CiteProperties::with_locator(*locator)); + ctx.writing.push_name_options(&self.citation.name_options); + self.citation.layout.render(&mut ctx); + res.extend(ctx.flush()); + } + + if res.iter().all(|c| !c.has_content()) { + return Vec::new(); + } + + let root_fmt = Formatting::default().apply(self.citation.layout.to_formatting()); + if let Some(prefix) = &self.citation.layout.prefix { + res.insert( + 0, + ElemChild::Text(Formatted { text: prefix.clone(), formatting: root_fmt }), + ); + } + + if let Some(suffix) = &self.citation.layout.suffix { + res.push(ElemChild::Text(Formatted { + text: suffix.clone(), + formatting: root_fmt, + })); + } + + simplify_children(res) + } } +pub struct CitationItem<'a>(&'a Entry, Option>); + impl<'a> StyleContext<'a> { /// Retrieve a macro. fn get_macro(&self, name: &str) -> Option<&'a CslMacro> { @@ -335,7 +376,7 @@ impl WritingContext { self.format_stack.len() ); - simplify_children(self.elem_stack.finish()) + self.elem_stack.finish() } /// Note that we have used a macro that had non-empty content. @@ -464,7 +505,7 @@ pub(crate) struct Context<'a> { writing: WritingContext, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] pub struct CiteProperties<'a> { /// Whether this citation is in a note and within `near-note-distance` to /// the previous citation of the same item. @@ -480,9 +521,18 @@ pub struct CiteProperties<'a> { /// if not disambiguated by `choose`. pub is_disambiguation: bool, /// Locator with its type. - pub locator: Option<(Locator, &'a str)>, + pub locator: Option>, +} + +impl<'a> CiteProperties<'a> { + fn with_locator(locator: Option>) -> Option { + Some(Self { locator, ..Self::default() }) + } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpecificLocator<'a>(Locator, &'a str); + impl<'a> Context<'a> { /// Push a format on top of the stack if it is not empty. fn push_format(&mut self, format: citationberg::Formatting) -> FormatIdx { @@ -712,7 +762,7 @@ impl<'a> Context<'a> { variable: csl_taxonomy::NumberVariable, ) -> Option>> { self.writing.prepare_variable_query(variable)?; - let res = resolve_number_variable(self.instance.entry, variable); + let res = self.instance.resolve_number_variable(variable); if res.is_some() { self.writing.usage_info.borrow_mut().last_mut().has_non_empty_vars = true; @@ -1279,14 +1329,11 @@ mod tests { let style_ctx = StyleContext::new(&style, locale, &en_locale).unwrap(); for entry in &bib { - let mut ctx = style_ctx.ctx(entry, None); - let cit_style = style.citation.as_ref().unwrap(); - ctx.writing.push_name_options(&cit_style.name_options); - - cit_style.layout.render(&mut ctx); + let mut item = [CitationItem(entry, None)]; + let citation = style_ctx.citation(&mut item); let mut buf = String::new(); - for e in dbg!(ctx.flush()) { + for e in citation { e.write_buf(&mut buf, BufWriteFormat::VT100).unwrap(); } diff --git a/src/csl/rendering.rs b/src/csl/rendering.rs index 8f9050a5..96b28595 100644 --- a/src/csl/rendering.rs +++ b/src/csl/rendering.rs @@ -84,13 +84,18 @@ impl RenderCsl for citationberg::Text { impl RenderCsl for citationberg::Number { fn render(&self, ctx: &mut Context) { - let depth = ctx.push_elem(self.formatting); + let value = ctx.resolve_number_variable(self.variable); + if ctx.instance.sorting { + if let Some(MaybeTyped::Typed(n)) = value { + n.fmt_value(ctx, true).unwrap(); + return; + } + } + let depth = ctx.push_elem(self.formatting); let affix_loc = ctx.apply_prefix(&self.affixes); - let cidx = ctx.push_case(self.text_case); - let value = ctx.resolve_number_variable(self.variable); match value { Some(MaybeTyped::Typed(num)) if num.will_transform() => { let normal_num = if self.form == NumberForm::Numeric @@ -189,6 +194,51 @@ impl RenderCsl for citationberg::Date { let Some(variable) = self.variable else { return }; let Some(date) = ctx.resolve_date_variable(variable) else { return }; + if ctx.instance.sorting { + let year; + let mut month = false; + let mut day = false; + + if self.is_localized() { + match self.parts { + Some(DateParts::Year) => year = true, + Some(DateParts::YearMonth) => { + year = true; + month = true; + } + Some(DateParts::YearMonthDay) | None => { + year = true; + month = true; + day = true; + } + } + } else { + year = self.date_part.iter().any(|i| i.name == DatePartName::Year); + month = self.date_part.iter().any(|i| i.name == DatePartName::Month); + day = self.date_part.iter().any(|i| i.name == DatePartName::Day); + }; + + if year { + write!(ctx, "{:04}", date.year).unwrap(); + } + + if month { + write!( + ctx, + "{:02}", + date.month.map(|m| m as i32 + 1).unwrap_or_default() + ) + .unwrap(); + } + + if day { + write!(ctx, "{:02}", date.day.map(|d| d as i32 + 1).unwrap_or_default()) + .unwrap(); + } + + return; + } + let base = if let Some(form) = self.form { let Some(base) = ctx.localized_date(form) else { return }; Some(base) @@ -401,12 +451,14 @@ impl RenderCsl for Names { let plural = persons.len() != 1; add_names(self, ctx, persons); - if let Some(label) = &self.label { - render_label_with_var( - label, - ctx, - ctx.term(term, label.form, plural).unwrap_or_default(), - ) + if !ctx.instance.sorting { + if let Some(label) = &self.label { + render_label_with_var( + label, + ctx, + ctx.term(term, label.form, plural).unwrap_or_default(), + ) + } } if i > 0 { @@ -445,10 +497,13 @@ fn add_names(names: &citationberg::Names, ctx: &mut Context, persons: Vec<&Perso let et_al_use_last = names.options.et_al_use_last.unwrap_or_default() && take + 2 <= persons.len(); let mut last_inverted = false; - let demote_non_dropping = matches!( - ctx.style.settings.demote_non_dropping_particle, - DemoteNonDroppingParticle::DisplayAndSort - ); + + let demote_non_dropping = match ctx.style.settings.demote_non_dropping_particle { + DemoteNonDroppingParticle::Never => false, + DemoteNonDroppingParticle::SortOnly => ctx.instance.sorting, + DemoteNonDroppingParticle::DisplayAndSort => true, + }; + let name_opts = names.name.options(&names.options); for (i, name) in persons.iter().take(take).enumerate() { @@ -506,7 +561,6 @@ fn add_names(names: &citationberg::Names, ctx: &mut Context, persons: Vec<&Perso write_name( name, ctx, - false, name_opts.form == NameForm::Long, reverse, demote_non_dropping, @@ -523,7 +577,6 @@ fn add_names(names: &citationberg::Names, ctx: &mut Context, persons: Vec<&Perso write_name( name, ctx, - false, name_opts.form == NameForm::Long, matches!(names.options.name_as_sort_order, Some(NameAsSortOrder::All)), demote_non_dropping, @@ -554,7 +607,6 @@ fn add_names(names: &citationberg::Names, ctx: &mut Context, persons: Vec<&Perso fn write_name( name: &Person, ctx: &mut Context, - sorting: bool, long: bool, reverse: bool, demote_non_dropping: bool, @@ -718,7 +770,7 @@ fn write_name( }; match (long, reverse, demote_non_dropping) { - _ if name.is_institutional() && sorting => { + _ if name.is_institutional() && ctx.instance.sorting => { let idx = ctx.push_format(family_format); let cidx = ctx.push_case(family_case); // TODO make locale aware @@ -752,8 +804,9 @@ fn write_name( } } } - (true, _, false) if sorting => reverse_keep_particle(ctx), - (true, _, true) if sorting => reverse_demote_particle(ctx), + // Always reverse when sorting. + (true, _, false) if ctx.instance.sorting => reverse_keep_particle(ctx), + (true, _, true) if ctx.instance.sorting => reverse_demote_particle(ctx), (true, true, false) => reverse_keep_particle(ctx), (true, true, true) => reverse_demote_particle(ctx), (true, false, _) => { @@ -1151,13 +1204,7 @@ impl RenderCsl for citationberg::LayoutRenderingElement { impl RenderCsl for citationberg::Layout { fn render(&self, ctx: &mut Context) { let fidx = ctx.push_format(self.to_formatting()); - - let affixes = self.to_affixes(); - let affix_pos = ctx.apply_prefix(&affixes); - render_with_delimiter(&self.elements, self.delimiter.as_deref(), ctx); - - ctx.apply_suffix(&affixes, affix_pos); ctx.pop_format(fidx); } } diff --git a/src/csl/sort.rs b/src/csl/sort.rs new file mode 100644 index 00000000..a4c91908 --- /dev/null +++ b/src/csl/sort.rs @@ -0,0 +1,128 @@ +use std::borrow::Cow; +use std::cmp::Ordering; +use std::task::Context; + +use citationberg::taxonomy::Variable; +use citationberg::{InheritableNameOptions, LongShortForm, Sort, SortDirection, SortKey}; + +use crate::csl::rendering::RenderCsl; +use crate::csl::taxonomy::{ + resolve_date_variable, resolve_name_variable, resolve_standard_variable, +}; +use crate::csl::BufWriteFormat; +use crate::types::{ChunkedString, Date}; +use crate::Entry; + +use super::{CitationItem, StyleContext}; + +impl<'a> StyleContext<'a> { + /// Retrieve the ordering of two entries according to the given sort key. + fn cmp_entries(&self, a: &Entry, b: &Entry, key: &SortKey) -> Ordering { + let ordering = match key { + SortKey::Variable { variable: Variable::Standard(s), .. } => { + let a = resolve_standard_variable(a, LongShortForm::default(), *s) + .map(|s| s.to_string()); + let b = resolve_standard_variable(b, LongShortForm::default(), *s) + .map(|s| s.to_string()); + a.cmp(&b) + } + SortKey::Variable { variable: Variable::Date(d), .. } => { + let a = resolve_date_variable(a, *d); + let b = resolve_date_variable(b, *d); + + match (a, b) { + (Some(a), Some(b)) => a.csl_cmp(b), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + } + SortKey::Variable { variable: Variable::Name(n), .. } => { + let a = resolve_name_variable(a, *n); + let b = resolve_name_variable(b, *n); + + for (a_pers, b_pers) in a.iter().zip(b.iter()) { + let ord = a_pers.cmp(b_pers); + if ord != Ordering::Equal { + return ord; + } + } + + if a.len() < b.len() { + Ordering::Less + } else if a.len() > b.len() { + Ordering::Greater + } else { + Ordering::Equal + } + } + SortKey::Variable { variable: Variable::Number(n), .. } => { + let a = self.sorting_ctx(a).instance.resolve_number_variable(*n); + let b = self.sorting_ctx(b).instance.resolve_number_variable(*n); + + match (a, b) { + (Some(a), Some(b)) => a.csl_cmp(&b), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + } + SortKey::MacroName { + name, + names_min, + names_use_first, + names_use_last, + .. + } => { + let render = |entry: &Entry| { + let mut ctx = self.sorting_ctx(entry); + ctx.writing.name_options.push(InheritableNameOptions { + et_al_min: *names_min, + et_al_subsequent_min: *names_min, + et_al_use_first: *names_use_first, + et_al_subsequent_use_first: *names_use_first, + et_al_use_last: *names_use_last, + ..Default::default() + }); + + self.get_macro(name).map(|m| { + for child in &m.children { + child.render(&mut ctx) + } + ctx.flush().into_iter().fold(String::new(), |mut s, f| { + f.write_buf(&mut s, BufWriteFormat::Plain).unwrap(); + s + }) + }) + }; + + let a_rendered = render(a); + let b_rendered = render(b); + + a_rendered.cmp(&b_rendered) + } + }; + + if key.sort_direction() == SortDirection::Descending { + ordering.reverse() + } else { + ordering + } + } + + /// Sorts the given citation items by the style's sort keys. + pub fn sort(&self, cites: &mut [CitationItem], sort: Option<&Sort>) { + if let Some(sort) = sort { + cites.sort_by(|a, b| { + let mut ordering = Ordering::Equal; + for key in &sort.keys { + ordering = self.cmp_entries(a.0, b.0, key); + if ordering != Ordering::Equal { + break; + } + } + ordering + }); + } + } +} diff --git a/src/csl/taxonomy.rs b/src/csl/taxonomy.rs index a7383776..18c2a045 100644 --- a/src/csl/taxonomy.rs +++ b/src/csl/taxonomy.rs @@ -11,83 +11,103 @@ use citationberg::taxonomy::{ use citationberg::{taxonomy, LongShortForm}; use unic_langid::LanguageIdentifier; -pub(crate) fn resolve_number_variable( - entry: &Entry, - variable: NumberVariable, -) -> Option>> { - match variable { - NumberVariable::ChapterNumber => entry - .bound_select( - &select!( - (("e":Anthos) > ("p":Anthology)) | - (("e":*) > ("p":Reference)) | - (("e":Article) > ("p":Proceedings)) | - (("e":*) > ("p":Book)) - ), - "e", - ) - .and_then(Entry::volume) - .map(MaybeTyped::to_cow), - NumberVariable::CitationNumber => todo!("that's for us to do baby"), - NumberVariable::CollectionNumber => entry - .bound_select( - &select!( - (* > ("p":(Book | Anthology | Proceedings))) - ), - "p", - ) - .and_then(Entry::volume) - .map(MaybeTyped::to_cow), - NumberVariable::Edition => entry.map(|e| e.edition()).map(MaybeTyped::to_cow), - NumberVariable::FirstReferenceNoteNumber => todo!("you guessed it, baybee"), - NumberVariable::Issue => entry.map(|e| e.issue()).map(MaybeTyped::to_cow), - NumberVariable::Locator => todo!("boy oh boy"), - NumberVariable::Number => { - return entry.serial_number().map(|s| { - Numeric::from_str(s) - .map(|n| MaybeTyped::Typed(Cow::Owned(n))) - .unwrap_or_else(|_| MaybeTyped::String(s.to_owned())) - }) - } - NumberVariable::NumberOfPages => { - entry.page_total().map(|n| MaybeTyped::Typed(Cow::Borrowed(n))) - } - NumberVariable::NumberOfVolumes => { - entry.volume_total().map(|n| MaybeTyped::Typed(Cow::Borrowed(n))) - } - NumberVariable::Page => { - entry.page_range().map(|n| MaybeTyped::Typed(Cow::Borrowed(n))) - } - NumberVariable::PageFirst => entry - .page_range() - .and_then(|r| r.range()) - .map(|r| MaybeTyped::Typed(Cow::Owned(Numeric::from(r.start)))), - NumberVariable::PartNumber => entry - .bound_select( - &select!( - (("e":*) > (Article | Blog | Book | Legislation)) - ), - "e", - ) - .and_then(Entry::volume) - .map(MaybeTyped::to_cow), - NumberVariable::PrintingNumber => None, - NumberVariable::Section => None, - NumberVariable::SupplementNumber => None, - NumberVariable::Version => entry - .bound_select(&select!(("e":Repository)), "e") - .and_then(Entry::serial_number) - .map(|s| { - Numeric::from_str(s) - .map(|n| MaybeTyped::Typed(Cow::Owned(n))) - .unwrap_or_else(|_| MaybeTyped::String(s.to_owned())) - }), - NumberVariable::Volume => entry.volume().map(MaybeTyped::to_cow), +use super::InstanceContext; + +impl<'a> InstanceContext<'a> { + pub(super) fn resolve_number_variable( + &self, + variable: NumberVariable, + ) -> Option>> { + match variable { + NumberVariable::ChapterNumber => self + .entry + .bound_select( + &select!( + (("e":Anthos) > ("p":Anthology)) | + (("e":*) > ("p":Reference)) | + (("e":Article) > ("p":Proceedings)) | + (("e":*) > ("p":Book)) + ), + "e", + ) + .and_then(Entry::volume) + .map(MaybeTyped::to_cow), + NumberVariable::CitationNumber => todo!("that's for us to do baby"), + NumberVariable::CollectionNumber => self + .entry + .bound_select( + &select!( + (* > ("p":(Book | Anthology | Proceedings))) + ), + "p", + ) + .and_then(Entry::volume) + .map(MaybeTyped::to_cow), + NumberVariable::Edition => { + self.entry.map(|e| e.edition()).map(MaybeTyped::to_cow) + } + NumberVariable::FirstReferenceNoteNumber => todo!("you guessed it, baybee"), + NumberVariable::Issue => { + self.entry.map(|e| e.issue()).map(MaybeTyped::to_cow) + } + NumberVariable::Locator => { + let l = self.cite_props?.locator?.1; + Some( + Numeric::from_str(l) + .map(|n| MaybeTyped::Typed(Cow::Owned(n))) + .unwrap_or_else(|_| MaybeTyped::String(l.to_owned())), + ) + } + NumberVariable::Number => { + return self.entry.serial_number().map(|s| { + Numeric::from_str(s) + .map(|n| MaybeTyped::Typed(Cow::Owned(n))) + .unwrap_or_else(|_| MaybeTyped::String(s.to_owned())) + }) + } + NumberVariable::NumberOfPages => { + self.entry.page_total().map(|n| MaybeTyped::Typed(Cow::Borrowed(n))) + } + NumberVariable::NumberOfVolumes => { + self.entry.volume_total().map(|n| MaybeTyped::Typed(Cow::Borrowed(n))) + } + NumberVariable::Page => { + self.entry.page_range().map(|n| MaybeTyped::Typed(Cow::Borrowed(n))) + } + NumberVariable::PageFirst => self + .entry + .page_range() + .and_then(|r| r.range()) + .map(|r| MaybeTyped::Typed(Cow::Owned(Numeric::from(r.start)))), + NumberVariable::PartNumber => self + .entry + .bound_select( + &select!( + (("e":*) > (Article | Blog | Book | Legislation)) + ), + "e", + ) + .and_then(Entry::volume) + .map(MaybeTyped::to_cow), + NumberVariable::PrintingNumber => None, + NumberVariable::Section => None, + NumberVariable::SupplementNumber => None, + NumberVariable::Version => self + .entry + .bound_select(&select!(("e":Repository)), "e") + .and_then(Entry::serial_number) + .map(|s| { + Numeric::from_str(s) + .map(|n| MaybeTyped::Typed(Cow::Owned(n))) + .unwrap_or_else(|_| MaybeTyped::String(s.to_owned())) + }), + NumberVariable::Volume => self.entry.volume().map(MaybeTyped::to_cow), + } } } // Number variables are standard variables. -pub(crate) fn resolve_standard_variable( +pub(super) fn resolve_standard_variable( entry: &Entry, form: LongShortForm, variable: StandardVariable, @@ -212,7 +232,7 @@ pub(crate) fn resolve_standard_variable( } } -pub(crate) fn resolve_date_variable( +pub(super) fn resolve_date_variable( entry: &Entry, variable: DateVariable, ) -> Option<&Date> { @@ -229,7 +249,7 @@ pub(crate) fn resolve_date_variable( } } -pub(crate) fn resolve_name_variable( +pub(super) fn resolve_name_variable( entry: &Entry, variable: taxonomy::NameVariable, ) -> Vec<&Person> { @@ -336,7 +356,7 @@ pub(crate) fn resolve_name_variable( .unwrap_or_default() } -pub(crate) fn matches_entry_type( +pub(super) fn matches_entry_type( entry: &Entry, kind: citationberg::taxonomy::Kind, ) -> bool { @@ -512,7 +532,7 @@ pub(crate) fn matches_entry_type( } } -pub(crate) fn csl_language(lang_id: &LanguageIdentifier) -> String { +pub(super) fn csl_language(lang_id: &LanguageIdentifier) -> String { let mut buf = String::with_capacity(if lang_id.region.is_some() { 5 } else { 2 }); buf.push_str(lang_id.language.as_str()); if let Some(region) = lang_id.region { diff --git a/src/types/numeric.rs b/src/types/numeric.rs index d20472da..8df03bc5 100644 --- a/src/types/numeric.rs +++ b/src/types/numeric.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::convert::{TryFrom, TryInto}; use std::fmt::Write; use std::fmt::{self, Display}; @@ -228,7 +229,7 @@ impl Numeric { } /// Order the values according to CSL rules. - pub(crate) fn csl_ord(&self, other: &Self) -> std::cmp::Ordering { + pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { let mut i = 0; loop { let a = self.nth(i); @@ -251,11 +252,11 @@ impl Numeric { } } -impl MaybeTyped { +impl<'a> MaybeTyped> { /// Order the values according to CSL rules. - pub(crate) fn csl_ord(&self, other: &Self) -> std::cmp::Ordering { + pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { match (self, other) { - (MaybeTyped::Typed(a), MaybeTyped::Typed(b)) => a.csl_ord(b), + (MaybeTyped::Typed(a), MaybeTyped::Typed(b)) => a.csl_cmp(b), _ => self.to_string().cmp(&other.to_string()), } } diff --git a/tests/art-history.csl b/tests/art-history.csl index 3d436d0a..41920f2e 100644 --- a/tests/art-history.csl +++ b/tests/art-history.csl @@ -82,7 +82,7 @@ - +