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