1 // This Source Code Form is subject to the terms of the Mozilla Public
2 // License, v. 2.0. If a copy of the MPL was not distributed with this
3 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 //
5 // Copyright © 2018 Corporation for Digital Scholarship
6 
7 use super::MonthForm;
8 use crate::error::*;
9 use crate::version::Features;
10 #[cfg(feature = "serde")]
11 use serde::{Deserialize, Serialize};
12 use std::str::FromStr;
13 
14 use super::attr::{EnumGetAttribute, GetAttribute};
15 use super::variables::{NameVariable, NumberVariable};
16 
17 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
18 pub enum TextTermSelector {
19     Simple(SimpleTermSelector),
20     Gendered(GenderedTermSelector),
21     Role(RoleTermSelector),
22     // You can't render ordinals using a <text> node, only using <number>
23 }
24 
25 pub enum AnyTermName {
26     Number(NumberVariable),
27     Month(MonthTerm),
28     Loc(LocatorType),
29 
30     Misc(MiscTerm),
31     Category(Category),
32     Season(SeasonTerm),
33     Quote(QuoteTerm),
34 
35     Role(RoleTerm),
36 
37     Ordinal(OrdinalTerm),
38 }
39 
40 impl EnumGetAttribute for MonthTerm {}
41 impl EnumGetAttribute for LocatorType {}
42 impl EnumGetAttribute for MiscTerm {}
43 impl EnumGetAttribute for Category {}
44 impl EnumGetAttribute for SeasonTerm {}
45 impl EnumGetAttribute for QuoteTerm {}
46 impl EnumGetAttribute for RoleTerm {}
47 impl EnumGetAttribute for OrdinalTerm {}
48 impl EnumGetAttribute for OrdinalMatch {}
49 impl EnumGetAttribute for Gender {}
50 impl EnumGetAttribute for TermForm {}
51 impl EnumGetAttribute for TermFormExtended {}
52 impl EnumGetAttribute for TermPlurality {}
53 
54 impl GetAttribute for AnyTermName {
get_attr(s: &str, features: &Features) -> Result<Self, UnknownAttributeValue>55     fn get_attr(s: &str, features: &Features) -> Result<Self, UnknownAttributeValue> {
56         if let Ok(v) = MiscTerm::get_attr(s, features) {
57             return Ok(AnyTermName::Misc(v));
58         } else if let Ok(v) = MonthTerm::get_attr(s, features) {
59             return Ok(AnyTermName::Month(v));
60         } else if let Ok(v) = NumberVariable::get_attr(s, features) {
61             return Ok(AnyTermName::Number(v));
62         } else if let Ok(v) = LocatorType::get_attr(s, features) {
63             return Ok(AnyTermName::Loc(v));
64         } else if let Ok(v) = SeasonTerm::get_attr(s, features) {
65             return Ok(AnyTermName::Season(v));
66         } else if let Ok(v) = QuoteTerm::get_attr(s, features) {
67             return Ok(AnyTermName::Quote(v));
68         } else if let Ok(v) = RoleTerm::get_attr(s, features) {
69             return Ok(AnyTermName::Role(v));
70         } else if let Ok(v) = OrdinalTerm::get_attr(s, features) {
71             return Ok(AnyTermName::Ordinal(v));
72         } else if let Ok(v) = Category::get_attr(s, features) {
73             return Ok(AnyTermName::Category(v));
74         }
75         Err(UnknownAttributeValue::new(s))
76     }
77 }
78 
79 /// TermSelector is used
80 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
81 pub enum SimpleTermSelector {
82     Misc(MiscTerm, TermFormExtended),
83     Category(Category, TermForm),
84     Quote(QuoteTerm),
85 }
86 
87 impl SimpleTermSelector {
fallback(self) -> Box<dyn Iterator<Item = Self>>88     pub fn fallback(self) -> Box<dyn Iterator<Item = Self>> {
89         match self {
90             SimpleTermSelector::Misc(t, form) => {
91                 Box::new(form.fallback().map(move |x| SimpleTermSelector::Misc(t, x)))
92             }
93             SimpleTermSelector::Category(t, form) => Box::new(
94                 form.fallback()
95                     .map(move |x| SimpleTermSelector::Category(t, x)),
96             ),
97             SimpleTermSelector::Quote(t) => Box::new(
98                 // Quotes don't do fallback. Not spec'd, but what on earth is the long form of a
99                 // close quote mark? "', she said sarcastically."?
100                 std::iter::once(SimpleTermSelector::Quote(t)),
101             ),
102         }
103     }
104 }
105 
106 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
107 pub struct OrdinalTermSelector(pub OrdinalTerm, pub Gender);
108 
109 struct OrdinalTermIter(Option<OrdinalTerm>);
110 
111 impl Iterator for OrdinalTermIter {
112     type Item = OrdinalTerm;
next(&mut self) -> Option<Self::Item>113     fn next(&mut self) -> Option<Self::Item> {
114         let next = if let Some(x) = self.0 {
115             match x {
116                 // The end
117                 OrdinalTerm::Ordinal => None,
118                 // Second last
119                 OrdinalTerm::Mod100(_, OrdinalMatch::LastDigit) => Some(OrdinalTerm::Ordinal),
120                 OrdinalTerm::Mod100(n, OrdinalMatch::LastTwoDigits) => {
121                     Some(OrdinalTerm::Mod100(n % 10, OrdinalMatch::LastDigit))
122                 }
123                 OrdinalTerm::Mod100(n, OrdinalMatch::WholeNumber) => {
124                     Some(OrdinalTerm::Mod100(n, OrdinalMatch::LastTwoDigits))
125                 }
126                 // The rest are LongOrdinal*, which should fall back with LongOrdinal01 -> Mod100(1), etc.
127                 t => Some(OrdinalTerm::Mod100(
128                     t.to_number(),
129                     OrdinalMatch::WholeNumber,
130                 )),
131             }
132         } else {
133             None
134         };
135         mem::replace(&mut self.0, next)
136     }
137 }
138 
139 impl OrdinalTerm {
fallback(self) -> impl Iterator<Item = Self>140     pub fn fallback(self) -> impl Iterator<Item = Self> {
141         OrdinalTermIter(Some(self))
142     }
143 }
144 
145 impl OrdinalTermSelector {
fallback(self) -> impl Iterator<Item = OrdinalTermSelector>146     pub fn fallback(self) -> impl Iterator<Item = OrdinalTermSelector> {
147         use std::iter::once;
148         let OrdinalTermSelector(term, gender) = self;
149         term.fallback().flat_map(move |term| {
150             once(OrdinalTermSelector(term, gender))
151                 .chain(once(OrdinalTermSelector(term, Gender::Neuter)))
152         })
153     }
154 }
155 
156 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
157 pub enum GenderedTermSelector {
158     /// Edition is the only MiscTerm that can have a gender, so it's here instead
159     Number(NumberVariable, TermForm),
160     Locator(LocatorType, TermForm),
161     Month(MonthTerm, TermForm),
162     Season(SeasonTerm, TermForm),
163 }
164 
165 impl GenderedTermSelector {
from_number_variable( loc_type: Option<LocatorType>, var: NumberVariable, form: TermForm, ) -> Option<GenderedTermSelector>166     pub fn from_number_variable(
167         loc_type: Option<LocatorType>,
168         var: NumberVariable,
169         form: TermForm,
170     ) -> Option<GenderedTermSelector> {
171         match var {
172             NumberVariable::Locator => match loc_type {
173                 None => None,
174                 Some(l) => Some(GenderedTermSelector::Locator(l, form)),
175             },
176             v => Some(GenderedTermSelector::Number(v, form)),
177         }
178     }
179 
from_month_u32(month_or_season: u32, form: MonthForm) -> Option<Self>180     pub fn from_month_u32(month_or_season: u32, form: MonthForm) -> Option<Self> {
181         let term_form = match form {
182             MonthForm::Long => TermForm::Long,
183             MonthForm::Short => TermForm::Short,
184             // Not going to be using the terms anyway
185             _ => return None,
186         };
187         if month_or_season == 0 || month_or_season > 16 {
188             return None;
189         }
190         let sel = if month_or_season > 12 {
191             // it's a season; 1 -> Spring, etc
192             let season = month_or_season - 12;
193             GenderedTermSelector::Season(
194                 match season {
195                     1 => SeasonTerm::Season01,
196                     2 => SeasonTerm::Season02,
197                     3 => SeasonTerm::Season03,
198                     4 => SeasonTerm::Season04,
199                     _ => return None,
200                 },
201                 term_form,
202             )
203         } else {
204             GenderedTermSelector::Month(
205                 MonthTerm::from_u32(month_or_season).expect("we know it's a month now"),
206                 term_form,
207             )
208         };
209         Some(sel)
210     }
211 
normalise(self) -> Self212     pub fn normalise(self) -> Self {
213         use GenderedTermSelector::*;
214         match self {
215             Number(NumberVariable::Page, x) => Locator(LocatorType::Page, x),
216             Number(NumberVariable::Issue, x) => Locator(LocatorType::Issue, x),
217             Number(NumberVariable::Volume, x) => Locator(LocatorType::Volume, x),
218             g => g,
219         }
220     }
221 
fallback(self) -> Box<dyn Iterator<Item = Self>>222     pub fn fallback(self) -> Box<dyn Iterator<Item = Self>> {
223         match self {
224             GenderedTermSelector::Number(t, form) => Box::new(
225                 form.fallback()
226                     .map(move |x| GenderedTermSelector::Number(t, x)),
227             ),
228             GenderedTermSelector::Locator(t, form) => Box::new(
229                 form.fallback()
230                     .map(move |x| GenderedTermSelector::Locator(t, x)),
231             ),
232             GenderedTermSelector::Month(t, form) => Box::new(
233                 form.fallback()
234                     .map(move |x| GenderedTermSelector::Month(t, x)),
235             ),
236             GenderedTermSelector::Season(t, form) => Box::new(
237                 form.fallback()
238                     .map(move |x| GenderedTermSelector::Season(t, x)),
239             ),
240         }
241     }
242 }
243 
244 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
245 pub struct RoleTermSelector(pub RoleTerm, pub TermFormExtended);
246 
247 impl RoleTermSelector {
fallback(self) -> Box<dyn Iterator<Item = Self>>248     pub fn fallback(self) -> Box<dyn Iterator<Item = Self>> {
249         Box::new(self.1.fallback().map(move |x| RoleTermSelector(self.0, x)))
250     }
from_name_variable(var: NameVariable, form: TermFormExtended) -> Option<Self>251     pub fn from_name_variable(var: NameVariable, form: TermFormExtended) -> Option<Self> {
252         let term = RoleTerm::from_name_var(var);
253         term.map(|t| RoleTermSelector(t, form))
254     }
255 }
256 
257 #[derive(Debug, Clone, PartialEq, Eq)]
258 pub struct GenderedTerm(pub TermPlurality, pub Gender);
259 
260 #[derive(AsRefStr, EnumString, EnumProperty, Debug, Copy, Clone, PartialEq, Eq, Hash)]
261 #[strum(serialize_all = "kebab_case")]
262 pub enum TermForm {
263     Long,
264     Short,
265     Symbol,
266     // XXX form="static", e.g. in jm-oscola, for CSL-M only
267 }
268 
269 impl Default for TermForm {
default() -> Self270     fn default() -> Self {
271         TermForm::Long
272     }
273 }
274 
275 pub struct TermFallbackIter(Option<TermForm>);
276 
277 impl TermForm {
fallback(self) -> TermFallbackIter278     pub fn fallback(self) -> TermFallbackIter {
279         TermFallbackIter(Some(self))
280     }
281 }
282 
283 impl Iterator for TermFallbackIter {
284     type Item = TermForm;
next(&mut self) -> Option<TermForm>285     fn next(&mut self) -> Option<TermForm> {
286         use self::TermForm::*;
287         let next = match self.0 {
288             Some(Symbol) => Some(Short),
289             Some(Short) => Some(Long),
290             _ => None,
291         };
292         mem::replace(&mut self.0, next)
293     }
294 }
295 /// Includes the extra Verb and VerbShort variants
296 #[derive(AsRefStr, EnumString, EnumProperty, Debug, Copy, Clone, PartialEq, Eq, Hash)]
297 #[strum(serialize_all = "kebab_case")]
298 pub enum TermFormExtended {
299     Long,
300     Short,
301     Symbol,
302     Verb,
303     VerbShort,
304 }
305 
306 impl Default for TermFormExtended {
default() -> Self307     fn default() -> Self {
308         TermFormExtended::Long
309     }
310 }
311 
312 pub struct TermFallbackExtendedIter(Option<TermFormExtended>);
313 
314 impl TermFormExtended {
fallback(self) -> TermFallbackExtendedIter315     pub fn fallback(self) -> TermFallbackExtendedIter {
316         TermFallbackExtendedIter(Some(self))
317     }
318 }
319 
320 use std::mem;
321 
322 impl Iterator for TermFallbackExtendedIter {
323     type Item = TermFormExtended;
next(&mut self) -> Option<TermFormExtended>324     fn next(&mut self) -> Option<TermFormExtended> {
325         use self::TermFormExtended::*;
326         let next = match self.0 {
327             Some(VerbShort) => Some(Verb),
328             Some(Symbol) => Some(Short),
329             Some(Verb) => Some(Long),
330             Some(Short) => Some(Long),
331             _ => None,
332         };
333         mem::replace(&mut self.0, next)
334     }
335 }
336 
337 #[derive(AsRefStr, EnumString, EnumProperty, Debug, Clone, PartialEq, Eq, Hash)]
338 #[strum(serialize_all = "kebab_case")]
339 pub enum TermPlurality {
340     Pluralized { single: String, multiple: String },
341     Invariant(String),
342 }
343 
344 impl TermPlurality {
get(&self, plural: bool) -> &str345     pub fn get(&self, plural: bool) -> &str {
346         if plural {
347             self.plural()
348         } else {
349             self.singular()
350         }
351     }
plural(&self) -> &str352     pub fn plural(&self) -> &str {
353         match self {
354             TermPlurality::Invariant(s) => &s,
355             TermPlurality::Pluralized { multiple, .. } => &multiple,
356         }
357     }
singular(&self) -> &str358     pub fn singular(&self) -> &str {
359         match self {
360             TermPlurality::Invariant(s) => &s,
361             TermPlurality::Pluralized { single, .. } => &single,
362         }
363     }
no_plural_allowed(&self) -> Option<&str>364     pub fn no_plural_allowed(&self) -> Option<&str> {
365         match self {
366             TermPlurality::Invariant(s) => Some(&s),
367             _ => None,
368         }
369     }
370 }
371 
372 /// Represents a gender for the purpose of *defining* or *selecting* a term.
373 ///
374 /// `gender="feminine"` is an output property of a term:
375 ///
376 ///   * you only define `<term name="edition">` once per locale, and that localization has a
377 ///   specific gender.
378 ///
379 /// `gender-form="feminine"` is part of the selector:
380 ///
381 ///   * you can define multiple `<term name="ordinal-01">`s, each with a different `gender-form`,
382 ///   such that when the style needs an ordinal with a specific gender, it can fetch one.
383 ///
384 /// So, for `{ "issue": 1 }`:
385 ///
386 /// ```xml
387 /// <term name="issue" gender="feminine">issue</term>
388 /// <term name="ordinal-01" gender-form="feminine">FFF</term>
389 /// <term name="ordinal-01" gender-form="masculine">MMM</term>
390 /// ...
391 /// <number variable="issue" form="ordinal" suffix=" " />
392 /// <label variable="issue" />
393 /// ```
394 
395 /// Produces `1FFF issue`, because:
396 ///
397 /// 1. `cs:number` wants to render the number `1` as an ordinal, so it needs to know the underlying
398 ///    gender of the variable's associated noun.
399 /// 2. `cs:number` looks up `GenderedTermSelector::Locator(LocatorType::Issue, TermForm::Long)` and
400 ///    gets back `GenderedTerm(TermPlurality::Invariant("issue"), Gender::Feminine)`
401 /// 3. It then needs an ordinal to match `Gender::Feminine`, so it looks up, in order:
402 ///
403 ///    1. `OrdinalTermSelector(Mod100(1), Feminine, LastDigit)` and finds a match with content
404 ///       `FFF`.
405 ///    2. Would also look up OridnalMatch::LastTwoDigits Neuter
406 ///
407 #[derive(AsStaticStr, EnumString, EnumProperty, Debug, Copy, Clone, PartialEq, Eq, Hash)]
408 #[strum(serialize_all = "kebab_case")]
409 pub enum Gender {
410     Masculine,
411     Feminine,
412     /// (Neuter is the default if left unspecified)
413     Neuter,
414 }
415 
416 impl Default for Gender {
default() -> Self417     fn default() -> Self {
418         Gender::Neuter
419     }
420 }
421 
422 /// [Spec](https://docs.citationstyles.org/en/stable/specification.html#ordinal-suffixes)
423 /// LastTwoDigits is the default
424 #[derive(AsStaticStr, EnumString, EnumProperty, Debug, Copy, Clone, PartialEq, Eq, Hash)]
425 #[strum(serialize_all = "kebab_case")]
426 pub enum OrdinalMatch {
427     /// Default for `Mod100(n) if n < 10`. Matches 9, 29, 109, 129.
428     LastDigit,
429     /// Default for `Mod100(n) if n >= 10`. Matches 9, 109.
430     LastTwoDigits,
431     /// Not a default. Matches 9.
432     WholeNumber,
433 }
434 
435 impl Default for OrdinalMatch {
default() -> Self436     fn default() -> Self {
437         OrdinalMatch::LastDigit
438     }
439 }
440 
441 /// [Spec](https://docs.citationstyles.org/en/stable/specification.html#locators)
442 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
443 #[cfg_attr(feature = "serde", derive(Deserialize))]
444 #[strum(serialize_all = "kebab_case")]
445 #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
446 #[non_exhaustive]
447 pub enum LocatorType {
448     Book,
449     Chapter,
450     Column,
451     Figure,
452     Folio,
453     Issue,
454     Line,
455     Note,
456     Opus,
457     Page,
458     Paragraph,
459     Part,
460     Section,
461     // hyphenated is when it's a variable matcher, spaced is as a term name
462     #[strum(serialize = "sub-verbo", serialize = "sub verbo")]
463     #[cfg_attr(feature = "serde", serde(rename = "sub-verbo", alias = "sub verbo"))]
464     SubVerbo,
465     Verse,
466     Volume,
467 
468     #[strum(props(csl = "0", cslM = "1"))]
469     Article,
470     #[strum(props(csl = "0", cslM = "1"))]
471     Subparagraph,
472     #[strum(props(csl = "0", cslM = "1"))]
473     Rule,
474     #[strum(props(csl = "0", cslM = "1"))]
475     Subsection,
476     #[strum(props(csl = "0", cslM = "1"))]
477     Schedule,
478     #[strum(props(csl = "0", cslM = "1"))]
479     Title,
480 
481     // Not documented but in use?
482     #[strum(props(csl = "0", cslM = "1"))]
483     Supplement,
484 }
485 
486 impl Default for LocatorType {
default() -> Self487     fn default() -> Self {
488         LocatorType::Page
489     }
490 }
491 
492 /// [Spec](https://docs.citationstyles.org/en/stable/specification.html#quotes)
493 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
494 #[strum(serialize_all = "kebab_case")]
495 pub enum QuoteTerm {
496     OpenQuote,
497     CloseQuote,
498     OpenInnerQuote,
499     CloseInnerQuote,
500 }
501 
502 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
503 // Strum's auto kebab_case doesn't hyphenate to "season-01", so manual it is
504 pub enum SeasonTerm {
505     #[strum(serialize = "season-01")]
506     Season01,
507     #[strum(serialize = "season-02")]
508     Season02,
509     #[strum(serialize = "season-03")]
510     Season03,
511     #[strum(serialize = "season-04")]
512     Season04,
513 }
514 
515 /// Yes, this differs slightly from NameVariable.
516 /// It includes "editortranslator" for the names special case.
517 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
518 #[strum(serialize_all = "kebab_case")]
519 #[non_exhaustive]
520 pub enum RoleTerm {
521     Author,
522     CollectionEditor,
523     Composer,
524     ContainerAuthor,
525     Director,
526     Editor,
527     EditorialDirector,
528     #[strum(serialize = "editortranslator")]
529     EditorTranslator,
530     Illustrator,
531     Interviewer,
532     OriginalAuthor,
533     Recipient,
534     ReviewedAuthor,
535     Translator,
536 
537     // From the CSL-JSON schema
538     Curator,
539     ScriptWriter,
540     Performer,
541     Producer,
542     ExecutiveProducer,
543     Guest,
544     Narrator,
545     Chair,
546     Compiler,
547     Contributor,
548     SeriesCreator,
549     Organizer,
550     Host,
551 }
552 
553 impl RoleTerm {
from_name_var(var: NameVariable) -> Option<Self>554     pub fn from_name_var(var: NameVariable) -> Option<Self> {
555         // TODO: how do we get RoleTerm::EditorTranslator?
556         Some(match var {
557             NameVariable::Author => RoleTerm::Author,
558             NameVariable::CollectionEditor => RoleTerm::CollectionEditor,
559             NameVariable::Composer => RoleTerm::Composer,
560             NameVariable::ContainerAuthor => RoleTerm::ContainerAuthor,
561             NameVariable::Director => RoleTerm::Director,
562             NameVariable::Editor => RoleTerm::Editor,
563             NameVariable::EditorialDirector => RoleTerm::EditorialDirector,
564             NameVariable::EditorTranslator => RoleTerm::EditorTranslator,
565             NameVariable::Illustrator => RoleTerm::Illustrator,
566             NameVariable::Interviewer => RoleTerm::Interviewer,
567             NameVariable::OriginalAuthor => RoleTerm::OriginalAuthor,
568             NameVariable::Recipient => RoleTerm::Recipient,
569             NameVariable::ReviewedAuthor => RoleTerm::ReviewedAuthor,
570             NameVariable::Translator => RoleTerm::Translator,
571             NameVariable::Curator => RoleTerm::Curator,
572             NameVariable::ScriptWriter => RoleTerm::ScriptWriter,
573             NameVariable::Performer => RoleTerm::Performer,
574             NameVariable::Producer => RoleTerm::Producer,
575             NameVariable::ExecutiveProducer => RoleTerm::ExecutiveProducer,
576             NameVariable::Guest => RoleTerm::Guest,
577             NameVariable::Narrator => RoleTerm::Narrator,
578             NameVariable::Chair => RoleTerm::Chair,
579             NameVariable::Compiler => RoleTerm::Compiler,
580             NameVariable::Contributor => RoleTerm::Contributor,
581             NameVariable::SeriesCreator => RoleTerm::SeriesCreator,
582             NameVariable::Organizer => RoleTerm::Organizer,
583             NameVariable::Host => RoleTerm::Host,
584             // CSL-M only
585             NameVariable::Authority => {
586                 warn!("unimplemented: CSL-M authority role term");
587                 return None;
588             }
589             // CSL-M only
590             NameVariable::Dummy => return None,
591         })
592     }
593 }
594 
595 /// This is all the "miscellaneous" terms from the spec, EXCEPT `edition`. Edition is the only one
596 /// that matches "terms accompanying the number variables" in [option (a)
597 /// here](https://docs.citationstyles.org/en/stable/specification.html#gender-specific-ordinals)
598 
599 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
600 #[cfg_attr(feature = "serde", derive(Serialize))]
601 #[strum(serialize_all = "kebab_case")]
602 #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
603 #[non_exhaustive]
604 pub enum Category {
605     Anthropology,
606     Astronomy,
607     Biology,
608     Botany,
609     Chemistry,
610     Communications,
611     Engineering,
612     /// Used for generic styles like Harvard and APA
613     GenericBase,
614     Geography,
615     Geology,
616     History,
617     Humanities,
618     Law,
619     Linguistics,
620     Literature,
621     Math,
622     Medicine,
623     Philosophy,
624     Physics,
625     /// Accepts both kebab-case and snake_case as the snake_case is anomalous
626     #[strum(serialize = "political-science", serialize = "political_science")]
627     PoliticalScience,
628     Psychology,
629     Science,
630     /// Accepts both kebab-case and snake_case as the snake_case is anomalous
631     #[strum(serialize = "social-science", serialize = "social_science")]
632     SocialScience,
633     Sociology,
634     Theology,
635     Zoology,
636 }
637 
638 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
639 #[strum(serialize_all = "kebab_case")]
640 #[non_exhaustive]
641 pub enum MiscTerm {
642     Accessed,
643     Ad,
644     And,
645     #[strum(serialize = "and others")]
646     AndOthers,
647     Anonymous,
648     At,
649     #[strum(serialize = "available at")]
650     AvailableAt,
651     Bc,
652     By,
653     Circa,
654     Cited,
655     // Edition,
656     EtAl,
657     Forthcoming,
658     From,
659     Ibid,
660     In,
661     #[strum(serialize = "in press")]
662     InPress,
663     Internet,
664     Interview,
665     Letter,
666     #[strum(serialize = "no date")]
667     NoDate,
668     Online,
669     #[strum(serialize = "presented at")]
670     PresentedAt,
671     Reference,
672     Retrieved,
673     Scale,
674     Version,
675 
676     // not technically in the list in either spec:
677 
678     // https://github.com/Juris-M/citeproc-js/blob/30ceaf50a0ef86517a9a8cd46362e450133c7f91/src/util_number.js#L522-L545
679     // https://github.com/Juris-M/citeproc-js/blob/30ceaf50a0ef86517a9a8cd46362e450133c7f91/src/node_datepart.js#L164-L176
680     PageRangeDelimiter,
681     YearRangeDelimiter,
682 }
683 
684 /// [Spec](https://docs.citationstyles.org/en/stable/specification.html#months)
685 #[derive(AsRefStr, EnumProperty, EnumString, Debug, Copy, Clone, PartialEq, Eq, Hash)]
686 #[non_exhaustive]
687 pub enum MonthTerm {
688     #[strum(serialize = "month-01")]
689     Month01,
690     #[strum(serialize = "month-02")]
691     Month02,
692     #[strum(serialize = "month-03")]
693     Month03,
694     #[strum(serialize = "month-04")]
695     Month04,
696     #[strum(serialize = "month-05")]
697     Month05,
698     #[strum(serialize = "month-06")]
699     Month06,
700     #[strum(serialize = "month-07")]
701     Month07,
702     #[strum(serialize = "month-08")]
703     Month08,
704     #[strum(serialize = "month-09")]
705     Month09,
706     #[strum(serialize = "month-10")]
707     Month10,
708     #[strum(serialize = "month-11")]
709     Month11,
710     #[strum(serialize = "month-12")]
711     Month12,
712 }
713 
714 impl MonthTerm {
from_u32(m: u32) -> Option<Self>715     pub fn from_u32(m: u32) -> Option<Self> {
716         use self::MonthTerm::*;
717         match m {
718             1 => Some(Month01),
719             2 => Some(Month02),
720             3 => Some(Month03),
721             4 => Some(Month04),
722             5 => Some(Month05),
723             6 => Some(Month06),
724             7 => Some(Month07),
725             8 => Some(Month08),
726             9 => Some(Month09),
727             10 => Some(Month10),
728             11 => Some(Month11),
729             12 => Some(Month12),
730             _ => None,
731         }
732     }
733 }
734 
735 /// [Spec](https://docs.citationstyles.org/en/stable/specification.html#quotes)
736 #[derive(EnumProperty, Debug, Copy, Clone, PartialEq, Eq, Hash)]
737 pub enum OrdinalTerm {
738     Ordinal,
739     Mod100(u32, OrdinalMatch),
740     LongOrdinal01,
741     LongOrdinal02,
742     LongOrdinal03,
743     LongOrdinal04,
744     LongOrdinal05,
745     LongOrdinal06,
746     LongOrdinal07,
747     LongOrdinal08,
748     LongOrdinal09,
749     LongOrdinal10,
750 }
751 
752 impl OrdinalTerm {
753     /// Returns 0 for OrdinalTerm::Ordinal, and clipped by the match property for Mod100. Useful for fallback to ordinal terms.
to_number(&self) -> u32754     pub fn to_number(&self) -> u32 {
755         match self {
756             OrdinalTerm::Ordinal => 0,
757             OrdinalTerm::Mod100(n, m) => match m {
758                 OrdinalMatch::LastDigit => n % 10,
759                 OrdinalMatch::LastTwoDigits | OrdinalMatch::WholeNumber => n % 100,
760             },
761             OrdinalTerm::LongOrdinal01 => 1,
762             OrdinalTerm::LongOrdinal02 => 2,
763             OrdinalTerm::LongOrdinal03 => 3,
764             OrdinalTerm::LongOrdinal04 => 4,
765             OrdinalTerm::LongOrdinal05 => 5,
766             OrdinalTerm::LongOrdinal06 => 6,
767             OrdinalTerm::LongOrdinal07 => 7,
768             OrdinalTerm::LongOrdinal08 => 8,
769             OrdinalTerm::LongOrdinal09 => 9,
770             OrdinalTerm::LongOrdinal10 => 10,
771         }
772     }
from_number_long(n: u32) -> Self773     pub fn from_number_long(n: u32) -> Self {
774         match n {
775             1 => OrdinalTerm::LongOrdinal01,
776             2 => OrdinalTerm::LongOrdinal02,
777             3 => OrdinalTerm::LongOrdinal03,
778             4 => OrdinalTerm::LongOrdinal04,
779             5 => OrdinalTerm::LongOrdinal05,
780             6 => OrdinalTerm::LongOrdinal06,
781             7 => OrdinalTerm::LongOrdinal07,
782             8 => OrdinalTerm::LongOrdinal08,
783             9 => OrdinalTerm::LongOrdinal09,
784             10 => OrdinalTerm::LongOrdinal10,
785             // Less code than panicking
786             _ => OrdinalTerm::Ordinal,
787         }
788     }
from_number_for_selector(n: u32, long: bool) -> Self789     pub fn from_number_for_selector(n: u32, long: bool) -> Self {
790         if long && n > 0 && n <= 10 {
791             OrdinalTerm::from_number_long(n)
792         } else {
793             OrdinalTerm::Mod100(n % 100, OrdinalMatch::WholeNumber)
794         }
795     }
796 }
797 
798 // impl std::convert::AsRef<str> for OrdinalTerm {
799 //     fn as_ref(&self) -> &str {
800 //         use self::OrdinalTerm::*;
801 //         match *self {
802 //             Ordinal => "ordinal",
803 //             LongOrdinal01 => "long-ordinal-01",
804 //             LongOrdinal02 => "long-ordinal-02",
805 //             LongOrdinal03 => "long-ordinal-03",
806 //             LongOrdinal04 => "long-ordinal-04",
807 //             LongOrdinal05 => "long-ordinal-05",
808 //             LongOrdinal06 => "long-ordinal-06",
809 //             LongOrdinal07 => "long-ordinal-07",
810 //             LongOrdinal08 => "long-ordinal-08",
811 //             LongOrdinal09 => "long-ordinal-09",
812 //             LongOrdinal10 => "long-ordinal-10",
813 //             Mod100(u) => {
814 //                 format!("ordinal-{:02}", u).as_ref()
815 //             },
816 //         }
817 //     }
818 // }
819 
820 /// For Mod100, gives the default OrdinalMatch only; that must be parsed separately and overwritten
821 /// if present.
822 impl FromStr for OrdinalTerm {
823     type Err = UnknownAttributeValue;
from_str(s: &str) -> Result<Self, Self::Err>824     fn from_str(s: &str) -> Result<Self, Self::Err> {
825         use self::OrdinalTerm::*;
826         match s {
827             "ordinal" => Ok(Ordinal),
828             "long-ordinal-01" => Ok(LongOrdinal01),
829             "long-ordinal-02" => Ok(LongOrdinal02),
830             "long-ordinal-03" => Ok(LongOrdinal03),
831             "long-ordinal-04" => Ok(LongOrdinal04),
832             "long-ordinal-05" => Ok(LongOrdinal05),
833             "long-ordinal-06" => Ok(LongOrdinal06),
834             "long-ordinal-07" => Ok(LongOrdinal07),
835             "long-ordinal-08" => Ok(LongOrdinal08),
836             "long-ordinal-09" => Ok(LongOrdinal09),
837             "long-ordinal-10" => Ok(LongOrdinal10),
838             _ => {
839                 if let Ok(("", n)) = zero_through_99(s) {
840                     Ok(OrdinalTerm::Mod100(n, OrdinalMatch::default_for(n)))
841                 } else {
842                     Err(UnknownAttributeValue::new(s))
843                 }
844             }
845         }
846     }
847 }
848 
849 use nom::{
850     bytes::complete::{tag, take_while_m_n},
851     combinator::map_res,
852     sequence::preceded,
853     IResult,
854 };
855 
is_digit(chr: char) -> bool856 fn is_digit(chr: char) -> bool {
857     chr as u8 >= 0x30 && (chr as u8) <= 0x39
858 }
859 
two_digit_num(inp: &str) -> IResult<&str, u32>860 fn two_digit_num(inp: &str) -> IResult<&str, u32> {
861     map_res(take_while_m_n(2, 2, is_digit), |s: &str| s.parse())(inp)
862 }
863 
zero_through_99(inp: &str) -> IResult<&str, u32>864 fn zero_through_99(inp: &str) -> IResult<&str, u32> {
865     preceded(tag("ordinal-"), two_digit_num)(inp)
866 }
867 
868 #[cfg(test)]
869 #[test]
test_ordinals()870 fn test_ordinals() {
871     assert_eq!(
872         Ok(OrdinalTerm::Mod100(34, OrdinalMatch::default_for(34))),
873         OrdinalTerm::from_str("ordinal-34")
874     );
875     assert_eq!(
876         OrdinalTerm::from_str("long-ordinal-08"),
877         Ok(OrdinalTerm::LongOrdinal08)
878     );
879     assert!(OrdinalTerm::from_str("ordinal-129").is_err());
880 }
881