1use crate::locale::{GeneralTerm, GrammaticalGender, TermForm};
37use indexmap::IndexMap;
38#[cfg(feature = "schema")]
39use schemars::JsonSchema;
40use serde::{Deserialize, Deserializer, Serialize, Serializer};
41use std::borrow::Cow;
42use std::collections::{BTreeMap, HashMap};
43use std::hash::{Hash, Hasher};
44
45mod reference;
46pub(crate) mod resolution;
47
48pub(crate) use reference::locale_matches;
49pub use reference::{LocalizedTemplateSpec, TemplatePreset, TemplateReference};
50pub(crate) use resolution::{inherited_variant_context, resolve_style_template_variants};
51
52pub type Template = Vec<TemplateComponent>;
54
55pub type TemplateVariants = IndexMap<TypeSelector, TemplateVariant>;
57
58#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
68#[cfg_attr(feature = "schema", derive(JsonSchema))]
69#[serde(rename_all = "kebab-case", default)]
70pub struct Rendering {
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub text_case: Option<crate::options::titles::TextCase>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub emph: Option<bool>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub quote: Option<bool>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub strong: Option<bool>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub small_caps: Option<bool>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub vertical_align: Option<crate::VerticalAlign>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub prefix: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub suffix: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub wrap: Option<WrapConfig>,
98 #[serde(skip_serializing_if = "Option::is_none")]
101 pub suppress: Option<bool>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub initialize_with: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
107 pub name_form: Option<crate::options::contributors::NameForm>,
108 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
110 pub strip_periods: Option<bool>,
111}
112
113impl Rendering {
114 pub fn merge(&mut self, other: &Rendering) {
118 crate::merge_options!(
119 self,
120 other,
121 text_case,
122 emph,
123 quote,
124 strong,
125 small_caps,
126 vertical_align,
127 prefix,
128 suffix,
129 wrap,
130 suppress,
131 initialize_with,
132 name_form,
133 strip_periods,
134 );
135 }
136}
137
138#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
140#[cfg_attr(feature = "schema", derive(JsonSchema))]
141#[serde(rename_all = "kebab-case")]
142pub enum WrapPunctuation {
143 #[default]
144 Parentheses,
145 Brackets,
146 Quotes,
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize)]
154#[cfg_attr(feature = "schema", derive(JsonSchema))]
155#[serde(rename_all = "kebab-case")]
156pub struct WrapConfig {
157 pub punctuation: WrapPunctuation,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub inner_prefix: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub inner_suffix: Option<String>,
165}
166
167impl<'de> serde::Deserialize<'de> for WrapConfig {
168 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
169 struct WrapConfigVisitor;
170
171 impl<'de> serde::de::Visitor<'de> for WrapConfigVisitor {
172 type Value = WrapConfig;
173
174 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175 write!(
176 f,
177 "a wrap punctuation string or a mapping with a 'punctuation' key"
178 )
179 }
180
181 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<WrapConfig, E> {
182 let punctuation = match v {
183 "parentheses" => WrapPunctuation::Parentheses,
184 "brackets" => WrapPunctuation::Brackets,
185 "quotes" => WrapPunctuation::Quotes,
186 other => {
187 return Err(E::unknown_variant(
188 other,
189 &["parentheses", "brackets", "quotes"],
190 ));
191 }
192 };
193 Ok(WrapConfig {
194 punctuation,
195 inner_prefix: None,
196 inner_suffix: None,
197 })
198 }
199
200 fn visit_map<A: serde::de::MapAccess<'de>>(
201 self,
202 mut map: A,
203 ) -> Result<WrapConfig, A::Error> {
204 let mut punctuation: Option<WrapPunctuation> = None;
205 let mut inner_prefix: Option<String> = None;
206 let mut inner_suffix: Option<String> = None;
207
208 while let Some(key) = map.next_key::<String>()? {
209 match key.as_str() {
210 "punctuation" => {
211 punctuation = Some(map.next_value()?);
212 }
213 "inner-prefix" => {
214 inner_prefix = Some(map.next_value()?);
215 }
216 "inner-suffix" => {
217 inner_suffix = Some(map.next_value()?);
218 }
219 other => {
220 return Err(serde::de::Error::unknown_field(
221 other,
222 &["punctuation", "inner-prefix", "inner-suffix"],
223 ));
224 }
225 }
226 }
227
228 let punctuation =
229 punctuation.ok_or_else(|| serde::de::Error::missing_field("punctuation"))?;
230 Ok(WrapConfig {
231 punctuation,
232 inner_prefix,
233 inner_suffix,
234 })
235 }
236 }
237
238 deserializer.deserialize_any(WrapConfigVisitor)
239 }
240}
241
242impl From<WrapPunctuation> for WrapConfig {
243 fn from(punctuation: WrapPunctuation) -> Self {
244 WrapConfig {
245 punctuation,
246 inner_prefix: None,
247 inner_suffix: None,
248 }
249 }
250}
251
252pub const VALID_TYPE_NAMES: &[&str] = &[
256 "book",
257 "manual",
258 "report",
259 "thesis",
260 "webpage",
261 "post",
262 "interview",
263 "manuscript",
264 "personal-communication",
265 "document",
266 "chapter",
267 "paper-conference",
268 "article-journal",
269 "article-magazine",
270 "article-newspaper",
271 "broadcast",
272 "motion-picture",
273 "collection",
274 "legal-case",
275 "statute",
276 "treaty",
277 "hearing",
278 "regulation",
279 "brief",
280 "classic",
281 "patent",
282 "dataset",
283 "standard",
284 "software",
285 "all",
287 "default",
288];
289
290pub fn validate_type_name(s: &str) -> bool {
296 let normalized = s.replace('_', "-");
297 VALID_TYPE_NAMES.iter().any(|&known| known == normalized)
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Hash)]
303#[cfg_attr(feature = "schema", derive(JsonSchema))]
304pub enum TypeSelector {
305 Single(String),
306 Multiple(Vec<String>),
307}
308
309impl Serialize for TypeSelector {
310 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
311 where
312 S: serde::Serializer,
313 {
314 serializer.serialize_str(&self.to_string())
315 }
316}
317
318impl<'de> Deserialize<'de> for TypeSelector {
319 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
320 where
321 D: serde::Deserializer<'de>,
322 {
323 struct Visitor;
324 impl<'de> serde::de::Visitor<'de> for Visitor {
325 type Value = TypeSelector;
326
327 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
328 formatter.write_str("a string or a sequence of strings")
329 }
330
331 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
332 where
333 E: serde::de::Error,
334 {
335 v.parse().map_err(E::custom)
336 }
337
338 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
339 where
340 A: serde::de::SeqAccess<'de>,
341 {
342 let mut types = Vec::new();
343 while let Some(t) = seq.next_element::<String>()? {
344 types.push(t);
345 }
346 if types.len() == 1 {
347 Ok(TypeSelector::Single(types.remove(0)))
348 } else {
349 Ok(TypeSelector::Multiple(types))
350 }
351 }
352 }
353 deserializer.deserialize_any(Visitor)
354 }
355}
356
357impl std::fmt::Display for TypeSelector {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 match self {
360 TypeSelector::Single(s) => write!(f, "{s}"),
361 TypeSelector::Multiple(types) => write!(f, "{}", types.join(",")),
362 }
363 }
364}
365
366impl std::str::FromStr for TypeSelector {
367 type Err = std::convert::Infallible;
368
369 fn from_str(s: &str) -> Result<Self, Self::Err> {
370 if s.contains(',') {
371 Ok(TypeSelector::Multiple(
372 s.split(',').map(|t| t.trim().to_string()).collect(),
373 ))
374 } else {
375 Ok(TypeSelector::Single(s.to_string()))
376 }
377 }
378}
379
380impl TypeSelector {
381 pub fn matches(&self, ref_type: &str) -> bool {
389 let normalized_ref = ref_type.replace('_', "-");
390 let base_ref = normalized_ref
391 .split_once('+')
392 .map(|(base, _)| base)
393 .unwrap_or(&normalized_ref);
394 let eq = |s: &str| -> bool {
395 s == ref_type
396 || s.replace('_', "-") == normalized_ref
397 || s.replace('_', "-") == base_ref
398 || s == "all"
399 || (s == "default" && ref_type == "default")
400 };
401 match self {
402 TypeSelector::Single(s) => eq(s),
403 TypeSelector::Multiple(types) => types.iter().any(|t| eq(t)),
404 }
405 }
406
407 pub fn unknown_type_names(&self) -> Vec<&str> {
412 match self {
413 TypeSelector::Single(s) => {
414 if validate_type_name(s) {
415 vec![]
416 } else {
417 vec![s.as_str()]
418 }
419 }
420 TypeSelector::Multiple(types) => types
421 .iter()
422 .filter(|s| !validate_type_name(s))
423 .map(|s| s.as_str())
424 .collect(),
425 }
426 }
427}
428
429#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
433#[cfg_attr(feature = "schema", derive(JsonSchema))]
434#[serde(untagged)]
435#[non_exhaustive]
436pub enum TemplateComponent {
437 Contributor(TemplateContributor),
438 Date(TemplateDate),
439 Title(TemplateTitle),
440 Number(TemplateNumber),
441 Variable(TemplateVariable),
442 Group(TemplateGroup),
443 Term(TemplateTerm),
444}
445
446impl Default for TemplateComponent {
447 fn default() -> Self {
448 TemplateComponent::Variable(TemplateVariable::default())
449 }
450}
451
452impl TemplateComponent {
453 pub fn rendering(&self) -> &Rendering {
457 crate::dispatch_component!(self, |inner| &inner.rendering)
458 }
459
460 pub fn rendering_mut(&mut self) -> &mut Rendering {
465 crate::dispatch_component!(self, |inner| &mut inner.rendering)
466 }
467}
468
469#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
471#[cfg_attr(feature = "schema", derive(JsonSchema))]
472#[serde(untagged)]
473pub enum TemplateVariant {
474 Full(Vec<TemplateComponent>),
476 Diff(TemplateVariantDiff),
478}
479
480impl TemplateVariant {
481 #[must_use]
483 pub fn as_template(&self) -> Option<&[TemplateComponent]> {
484 match self {
485 Self::Full(template) => Some(template.as_slice()),
486 Self::Diff(_) => None,
487 }
488 }
489
490 pub fn as_template_mut(&mut self) -> Option<&mut Vec<TemplateComponent>> {
492 match self {
493 Self::Full(template) => Some(template),
494 Self::Diff(_) => None,
495 }
496 }
497
498 #[must_use]
500 pub fn into_template(self) -> Option<Vec<TemplateComponent>> {
501 match self {
502 Self::Full(template) => Some(template),
503 Self::Diff(_) => None,
504 }
505 }
506}
507
508impl From<Vec<TemplateComponent>> for TemplateVariant {
509 fn from(template: Vec<TemplateComponent>) -> Self {
510 Self::Full(template)
511 }
512}
513
514#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
516#[cfg_attr(feature = "schema", derive(JsonSchema))]
517#[serde(rename_all = "kebab-case", deny_unknown_fields)]
518pub struct TemplateVariantDiff {
519 #[serde(skip_serializing_if = "Option::is_none")]
521 pub extends: Option<TypeSelector>,
522 #[serde(skip_serializing_if = "Vec::is_empty", default)]
524 pub modify: Vec<TemplateModifyOperation>,
525 #[serde(skip_serializing_if = "Vec::is_empty", default)]
527 pub remove: Vec<TemplateRemoveOperation>,
528 #[serde(skip_serializing_if = "Vec::is_empty", default)]
530 pub add: Vec<TemplateAddOperation>,
531}
532
533#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
535#[cfg_attr(feature = "schema", derive(JsonSchema))]
536#[serde(transparent)]
537pub struct TemplateComponentSelector {
538 pub fields: BTreeMap<String, serde_json::Value>,
540}
541
542impl TemplateComponentSelector {
543 #[must_use]
545 pub fn is_empty(&self) -> bool {
546 self.fields.is_empty()
547 }
548
549 #[must_use]
551 pub fn matches(&self, component: &TemplateComponent) -> bool {
552 let Ok(serde_json::Value::Object(component_fields)) = serde_json::to_value(component)
553 else {
554 return false;
555 };
556
557 self.fields.iter().all(|(key, expected)| {
558 component_fields
559 .get(key)
560 .is_some_and(|actual| selector_value_matches(expected, actual))
561 })
562 }
563}
564
565fn selector_value_matches(expected: &serde_json::Value, actual: &serde_json::Value) -> bool {
566 match (expected, actual) {
567 (serde_json::Value::Object(expected_fields), serde_json::Value::Object(actual_fields)) => {
568 expected_fields.iter().all(|(key, expected_value)| {
569 actual_fields.get(key).is_some_and(|actual_value| {
570 selector_value_matches(expected_value, actual_value)
571 })
572 })
573 }
574 (serde_json::Value::Array(expected_items), serde_json::Value::Array(actual_items)) => {
575 expected_items.len() == actual_items.len()
576 && expected_items.iter().zip(actual_items.iter()).all(
577 |(expected_item, actual_item)| {
578 selector_value_matches(expected_item, actual_item)
579 },
580 )
581 }
582 _ => expected == actual,
583 }
584}
585
586#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
588#[cfg_attr(feature = "schema", derive(JsonSchema))]
589#[serde(rename_all = "kebab-case", deny_unknown_fields)]
590pub struct TemplateModifyOperation {
591 #[serde(rename = "match")]
593 pub match_selector: TemplateComponentSelector,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub label_form: Option<LabelForm>,
597 #[serde(flatten, default)]
599 pub rendering: Rendering,
600}
601
602#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
604#[cfg_attr(feature = "schema", derive(JsonSchema))]
605#[serde(rename_all = "kebab-case", deny_unknown_fields)]
606pub struct TemplateRemoveOperation {
607 #[serde(rename = "match")]
609 pub match_selector: TemplateComponentSelector,
610}
611
612#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
614#[cfg_attr(feature = "schema", derive(JsonSchema))]
615#[serde(rename_all = "kebab-case", deny_unknown_fields)]
616pub struct TemplateAddOperation {
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub before: Option<TemplateComponentSelector>,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub after: Option<TemplateComponentSelector>,
623 pub component: TemplateComponent,
625}
626
627#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
629#[cfg_attr(feature = "schema", derive(JsonSchema))]
630#[serde(rename_all = "kebab-case")]
631pub struct RoleLabel {
632 pub term: String,
634 #[serde(default)]
636 pub form: RoleLabelForm,
637 #[serde(default)]
639 pub placement: LabelPlacement,
640}
641
642#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
644#[cfg_attr(feature = "schema", derive(JsonSchema))]
645#[serde(rename_all = "kebab-case")]
646pub enum RoleLabelForm {
647 #[default]
648 Short,
649 Long,
650}
651
652#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
654#[cfg_attr(feature = "schema", derive(JsonSchema))]
655#[serde(rename_all = "kebab-case")]
656pub enum LabelPlacement {
657 Prefix,
658 #[default]
659 Suffix,
660}
661
662#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
664#[cfg_attr(feature = "schema", derive(JsonSchema))]
665#[serde(rename_all = "kebab-case", deny_unknown_fields)]
666pub struct TemplateContributor {
667 pub contributor: ContributorRole,
669 pub form: ContributorForm,
671 #[serde(skip_serializing_if = "Option::is_none")]
673 pub label: Option<RoleLabel>,
674 #[serde(skip_serializing_if = "Option::is_none")]
677 pub name_order: Option<NameOrder>,
678 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
680 pub name_form: Option<crate::options::contributors::NameForm>,
681 #[serde(skip_serializing_if = "Option::is_none")]
683 pub delimiter: Option<String>,
684 #[serde(skip_serializing_if = "Option::is_none")]
686 pub sort_separator: Option<String>,
687 #[serde(skip_serializing_if = "Option::is_none")]
689 pub shorten: Option<crate::options::ShortenListOptions>,
690 #[serde(skip_serializing_if = "Option::is_none")]
693 pub and: Option<crate::options::AndOptions>,
694 #[serde(flatten, default)]
695 pub rendering: Rendering,
696 #[serde(skip_serializing_if = "Option::is_none")]
698 pub links: Option<crate::options::LinksConfig>,
699 #[serde(skip_serializing_if = "Option::is_none")]
701 pub gender: Option<GrammaticalGender>,
702
703 #[serde(skip_serializing_if = "Option::is_none")]
705 pub custom: Option<HashMap<String, serde_json::Value>>,
706}
707
708#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
710#[cfg_attr(feature = "schema", derive(JsonSchema))]
711#[serde(rename_all = "kebab-case")]
712pub enum NameOrder {
713 GivenFirst,
715 #[default]
717 FamilyFirst,
718 FamilyFirstOnly,
720}
721
722#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
724#[cfg_attr(feature = "schema", derive(JsonSchema))]
725#[serde(rename_all = "kebab-case")]
726pub enum ContributorForm {
727 #[default]
728 Long,
729 Short,
730 FamilyOnly,
731 Verb,
732 VerbShort,
733}
734
735crate::str_enum! {
736 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
738 pub enum ContributorRole {
739 #[default] Author = "author",
740 Chair = "chair",
741 Editor = "editor",
742 Translator = "translator",
743 Director = "director",
744 Publisher = "publisher",
745 Recipient = "recipient",
746 Interviewer = "interviewer",
747 Interviewee = "interviewee",
748 Guest = "guest",
749 Inventor = "inventor",
750 Counsel = "counsel",
751 Composer = "composer",
752 CollectionEditor = "collection-editor",
753 ContainerAuthor = "container-author",
754 EditorialDirector = "editorial-director",
755 TextualEditor = "textual-editor",
756 Illustrator = "illustrator",
757 OriginalAuthor = "original-author",
758 ReviewedAuthor = "reviewed-author"
759 }
760}
761
762#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
764#[cfg_attr(feature = "schema", derive(JsonSchema))]
765#[serde(rename_all = "kebab-case", deny_unknown_fields)]
766pub struct TemplateDate {
767 pub date: DateVariable,
768 pub form: DateForm,
769 #[serde(skip_serializing_if = "Option::is_none")]
771 pub fallback: Option<Vec<TemplateComponent>>,
772 #[serde(flatten, default)]
773 pub rendering: Rendering,
774 #[serde(skip_serializing_if = "Option::is_none")]
776 pub links: Option<crate::options::LinksConfig>,
777
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub custom: Option<HashMap<String, serde_json::Value>>,
781}
782
783#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
785#[cfg_attr(feature = "schema", derive(JsonSchema))]
786#[serde(rename_all = "kebab-case")]
787pub enum DateVariable {
788 #[default]
789 Issued,
790 Accessed,
791 OriginalPublished,
792 Submitted,
793 EventDate,
794}
795
796crate::str_enum! {
797 #[derive(Debug, Default, Clone, PartialEq)]
799 pub enum DateForm {
800 #[default]
801 Year = "year",
802 YearMonth = "year-month",
803 Full = "full",
804 MonthDay = "month-day",
805 YearMonthDay = "year-month-day",
806 DayMonthAbbrYear = "day-month-abbr-year",
807 MonthAbbrDayYear = "month-abbr-day-year"
809 }
810}
811
812#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
814#[cfg_attr(feature = "schema", derive(JsonSchema))]
815#[serde(rename_all = "kebab-case", deny_unknown_fields)]
816pub struct TemplateTitle {
817 pub title: TitleType,
818 #[serde(skip_serializing_if = "Option::is_none")]
819 pub form: Option<TitleForm>,
820 #[serde(skip_serializing_if = "Option::is_none")]
825 pub disambiguate_only: Option<bool>,
826 #[serde(flatten, default)]
827 pub rendering: Rendering,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub links: Option<crate::options::LinksConfig>,
831
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub custom: Option<HashMap<String, serde_json::Value>>,
835}
836
837#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
839#[cfg_attr(feature = "schema", derive(JsonSchema))]
840#[serde(rename_all = "kebab-case")]
841#[non_exhaustive]
842pub enum TitleType {
843 #[default]
845 Primary,
846 ParentMonograph,
848 ParentSerial,
850}
851
852#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
854#[cfg_attr(feature = "schema", derive(JsonSchema))]
855#[serde(rename_all = "kebab-case")]
856pub enum TitleForm {
857 Short,
858 #[default]
859 Long,
860}
861
862#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
864#[cfg_attr(feature = "schema", derive(JsonSchema))]
865#[serde(rename_all = "kebab-case", deny_unknown_fields)]
866pub struct TemplateNumber {
867 pub number: NumberVariable,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 pub form: Option<NumberForm>,
870 #[serde(skip_serializing_if = "Option::is_none")]
871 pub label_form: Option<LabelForm>,
872 #[serde(skip_serializing_if = "Option::is_none")]
875 pub show_with_locator: Option<bool>,
876 #[serde(flatten)]
877 pub rendering: Rendering,
878 #[serde(skip_serializing_if = "Option::is_none")]
880 pub links: Option<crate::options::LinksConfig>,
881 #[serde(skip_serializing_if = "Option::is_none")]
883 pub gender: Option<GrammaticalGender>,
884
885 #[serde(skip_serializing_if = "Option::is_none")]
887 pub custom: Option<HashMap<String, serde_json::Value>>,
888}
889
890#[derive(Debug, Default, Clone)]
897#[non_exhaustive]
898pub enum NumberVariable {
899 #[default]
900 Volume,
901 Issue,
902 Pages,
903 Edition,
904 ChapterNumber,
905 CollectionNumber,
906 NumberOfPages,
907 NumberOfVolumes,
908 CitationNumber,
909 CitationLabel,
910 Number,
911 DocketNumber,
912 PatentNumber,
913 StandardNumber,
914 ReportNumber,
915 PartNumber,
916 SupplementNumber,
917 PrintingNumber,
918 Custom(String),
920}
921
922impl NumberVariable {
923 #[must_use]
925 pub fn as_key(&self) -> Cow<'_, str> {
926 match self {
927 Self::Volume => Cow::Borrowed("volume"),
928 Self::Issue => Cow::Borrowed("issue"),
929 Self::Pages => Cow::Borrowed("pages"),
930 Self::Edition => Cow::Borrowed("edition"),
931 Self::ChapterNumber => Cow::Borrowed("chapter-number"),
932 Self::CollectionNumber => Cow::Borrowed("collection-number"),
933 Self::NumberOfPages => Cow::Borrowed("number-of-pages"),
934 Self::NumberOfVolumes => Cow::Borrowed("number-of-volumes"),
935 Self::CitationNumber => Cow::Borrowed("citation-number"),
936 Self::CitationLabel => Cow::Borrowed("citation-label"),
937 Self::Number => Cow::Borrowed("number"),
938 Self::DocketNumber => Cow::Borrowed("docket-number"),
939 Self::PatentNumber => Cow::Borrowed("patent-number"),
940 Self::StandardNumber => Cow::Borrowed("standard-number"),
941 Self::ReportNumber => Cow::Borrowed("report-number"),
942 Self::PartNumber => Cow::Borrowed("part-number"),
943 Self::SupplementNumber => Cow::Borrowed("supplement-number"),
944 Self::PrintingNumber => Cow::Borrowed("printing-number"),
945 Self::Custom(value) => normalize_kind_key(value)
946 .map(Cow::Owned)
947 .unwrap_or_else(|| Cow::Borrowed(value.as_str())),
948 }
949 }
950
951 fn from_key(value: &str) -> Result<Self, String> {
952 let canonical = normalize_kind_key(value)
953 .ok_or_else(|| "number variable must not be empty".to_string())?;
954 Ok(match canonical.as_str() {
955 "volume" => Self::Volume,
956 "issue" => Self::Issue,
957 "pages" => Self::Pages,
958 "edition" => Self::Edition,
959 "chapter-number" => Self::ChapterNumber,
960 "collection-number" => Self::CollectionNumber,
961 "number-of-pages" => Self::NumberOfPages,
962 "number-of-volumes" => Self::NumberOfVolumes,
963 "citation-number" => Self::CitationNumber,
964 "citation-label" => Self::CitationLabel,
965 "number" => Self::Number,
966 "docket-number" => Self::DocketNumber,
967 "patent-number" => Self::PatentNumber,
968 "standard-number" => Self::StandardNumber,
969 "report-number" => Self::ReportNumber,
970 "part-number" => Self::PartNumber,
971 "supplement-number" => Self::SupplementNumber,
972 "printing-number" => Self::PrintingNumber,
973 _ => Self::Custom(canonical),
974 })
975 }
976}
977
978impl PartialEq for NumberVariable {
979 fn eq(&self, other: &Self) -> bool {
980 self.as_key().as_ref() == other.as_key().as_ref()
981 }
982}
983
984impl Eq for NumberVariable {}
985
986impl Hash for NumberVariable {
987 fn hash<H: Hasher>(&self, state: &mut H) {
988 self.as_key().as_ref().hash(state);
989 }
990}
991
992impl Serialize for NumberVariable {
993 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
994 where
995 S: Serializer,
996 {
997 serializer.serialize_str(self.as_key().as_ref())
998 }
999}
1000
1001impl<'de> Deserialize<'de> for NumberVariable {
1002 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1003 where
1004 D: Deserializer<'de>,
1005 {
1006 let value = String::deserialize(deserializer)?;
1007 Self::from_key(&value).map_err(serde::de::Error::custom)
1008 }
1009}
1010
1011#[cfg(feature = "schema")]
1012impl JsonSchema for NumberVariable {
1013 fn schema_name() -> std::borrow::Cow<'static, str> {
1014 "NumberVariable".into()
1015 }
1016
1017 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1018 schemars::json_schema!({
1019 "type": "string",
1020 "description": "Known number variable keyword or custom kebab-case identifier."
1021 })
1022 }
1023}
1024
1025#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1027#[cfg_attr(feature = "schema", derive(JsonSchema))]
1028#[serde(rename_all = "lowercase")]
1029pub enum NumberForm {
1030 #[default]
1031 Numeric,
1032 Ordinal,
1033 Roman,
1034}
1035
1036fn normalize_kind_key(value: &str) -> Option<String> {
1037 let mut normalized = String::new();
1038 let mut pending_dash = false;
1039
1040 for ch in value.trim().chars() {
1041 if ch.is_ascii_alphanumeric() {
1042 if pending_dash && !normalized.is_empty() {
1043 normalized.push('-');
1044 }
1045 normalized.push(ch.to_ascii_lowercase());
1046 pending_dash = false;
1047 } else if !normalized.is_empty() {
1048 pending_dash = true;
1049 }
1050 }
1051
1052 if normalized.is_empty() {
1053 None
1054 } else {
1055 Some(normalized)
1056 }
1057}
1058
1059#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1061#[cfg_attr(feature = "schema", derive(JsonSchema))]
1062#[serde(rename_all = "kebab-case")]
1063pub enum LabelForm {
1064 Long,
1065 #[default]
1066 Short,
1067 Symbol,
1068}
1069
1070#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1072#[cfg_attr(feature = "schema", derive(JsonSchema))]
1073#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1074pub struct TemplateVariable {
1075 pub variable: SimpleVariable,
1076 #[serde(flatten)]
1077 pub rendering: Rendering,
1078 #[serde(skip_serializing_if = "Option::is_none")]
1080 pub links: Option<crate::options::LinksConfig>,
1081
1082 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub custom: Option<HashMap<String, serde_json::Value>>,
1085}
1086
1087#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1094#[cfg_attr(feature = "schema", derive(JsonSchema))]
1095#[serde(rename_all = "kebab-case")]
1096#[non_exhaustive]
1097pub enum SimpleVariable {
1098 #[default]
1099 Doi,
1100 Isbn,
1101 Issn,
1102 Url,
1103 Pmid,
1104 Pmcid,
1105 Abstract,
1106 Note,
1107 Annote,
1108 Keyword,
1109 Genre,
1110 Medium,
1111 Source,
1112 Status,
1113 Archive,
1114 ArchiveLocation,
1115 ArchiveName,
1116 ArchivePlace,
1117 ArchiveCollection,
1118 ArchiveCollectionId,
1119 ArchiveSeries,
1120 ArchiveBox,
1121 ArchiveFolder,
1122 ArchiveItem,
1123 ArchiveUrl,
1124 EprintId,
1125 EprintServer,
1126 EprintClass,
1127 Publisher,
1128 PublisherPlace,
1129 OriginalPublisher,
1130 OriginalPublisherPlace,
1131 EventPlace,
1132 Dimensions,
1133 Scale,
1134 Version,
1135 Locator,
1136 ContainerTitleShort,
1137 Authority,
1138 Code,
1139 Reporter,
1140 Page,
1141 Section,
1142 Volume,
1143 Number,
1144 DocketNumber,
1145 PatentNumber,
1146 StandardNumber,
1147 ReportNumber,
1148 AdsBibcode,
1149}
1150
1151#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1153#[cfg_attr(feature = "schema", derive(JsonSchema))]
1154#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1155pub struct TemplateTerm {
1156 pub term: GeneralTerm,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1160 pub form: Option<TermForm>,
1161 #[serde(skip_serializing_if = "Option::is_none")]
1163 pub gender: Option<GrammaticalGender>,
1164 #[serde(flatten, default)]
1165 pub rendering: Rendering,
1166
1167 #[serde(skip_serializing_if = "Option::is_none")]
1169 pub custom: Option<HashMap<String, serde_json::Value>>,
1170}
1171
1172#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1175#[cfg_attr(feature = "schema", derive(JsonSchema))]
1176#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1177pub struct TemplateGroup {
1178 pub group: Vec<TemplateComponent>,
1179 #[serde(skip_serializing_if = "Option::is_none")]
1180 pub delimiter: Option<DelimiterPunctuation>,
1181 #[serde(flatten, default)]
1182 pub rendering: Rendering,
1183
1184 #[serde(skip_serializing_if = "Option::is_none")]
1186 pub custom: Option<HashMap<String, serde_json::Value>>,
1187}
1188
1189#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1191#[serde(rename_all = "kebab-case")]
1192pub enum DelimiterPunctuation {
1193 #[default]
1194 Comma,
1195 Semicolon,
1196 Period,
1197 Colon,
1198 Ampersand,
1199 VerticalLine,
1200 Slash,
1201 Hyphen,
1202 Space,
1203 None,
1204 #[serde(untagged)]
1206 Custom(String),
1207}
1208
1209#[cfg(feature = "schema")]
1210impl JsonSchema for DelimiterPunctuation {
1211 fn schema_name() -> std::borrow::Cow<'static, str> {
1212 "DelimiterPunctuation".into()
1213 }
1214
1215 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1216 schemars::json_schema!({"type": "string", "description": "Delimiter punctuation options."})
1217 }
1218}
1219
1220impl DelimiterPunctuation {
1221 pub fn to_string_with_space(&self) -> String {
1225 match self {
1226 Self::Comma => ", ".to_string(),
1227 Self::Semicolon => "; ".to_string(),
1228 Self::Period => ". ".to_string(),
1229 Self::Colon => ": ".to_string(),
1230 Self::Ampersand => " & ".to_string(),
1231 Self::VerticalLine => " | ".to_string(),
1232 Self::Slash => "/".to_string(),
1233 Self::Hyphen => "-".to_string(),
1234 Self::Space => " ".to_string(),
1235 Self::None => "".to_string(),
1236 Self::Custom(s) => s.clone(),
1237 }
1238 }
1239
1240 pub fn from_csl_string(s: &str) -> Self {
1245 if s == " " {
1246 return Self::Space;
1247 }
1248
1249 let trimmed = s.trim();
1250 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
1251 return Self::None;
1252 }
1253
1254 match trimmed {
1255 "," => Self::Comma,
1256 ";" => Self::Semicolon,
1257 "." => Self::Period,
1258 ":" => Self::Colon,
1259 "&" => Self::Ampersand,
1260 "|" => Self::VerticalLine,
1261 "/" => Self::Slash,
1262 "-" => Self::Hyphen,
1263 _ => Self::Custom(s.to_string()),
1264 }
1265 }
1266}
1267
1268#[cfg(test)]
1269#[allow(
1270 clippy::unwrap_used,
1271 clippy::expect_used,
1272 clippy::panic,
1273 clippy::indexing_slicing,
1274 clippy::todo,
1275 clippy::unimplemented,
1276 clippy::unreachable,
1277 clippy::get_unwrap,
1278 reason = "Panicking is acceptable and often desired in tests."
1279)]
1280mod tests {
1281 use super::*;
1282
1283 #[test]
1284 fn test_contributor_deserialization() {
1285 let yaml = r#"
1286contributor: author
1287form: long
1288"#;
1289 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1290 assert_eq!(comp.contributor, ContributorRole::Author);
1291 assert_eq!(comp.form, ContributorForm::Long);
1292 }
1293
1294 #[test]
1295 fn test_template_component_untagged() {
1296 let yaml = r#"
1297- contributor: author
1298 form: short
1299- date: issued
1300 form: year
1301- title: primary
1302"#;
1303 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1304 assert_eq!(components.len(), 3);
1305
1306 match &components[0] {
1307 TemplateComponent::Contributor(c) => {
1308 assert_eq!(c.contributor, ContributorRole::Author);
1309 }
1310 _ => panic!("Expected Contributor"),
1311 }
1312
1313 match &components[1] {
1314 TemplateComponent::Date(d) => {
1315 assert_eq!(d.date, DateVariable::Issued);
1316 }
1317 _ => panic!("Expected Date"),
1318 }
1319 }
1320
1321 #[test]
1322 fn test_flattened_rendering() {
1323 let yaml = r#"
1325- title: parent-monograph
1326 prefix: "In "
1327 emph: true
1328- date: issued
1329 form: year
1330 wrap: parentheses
1331"#;
1332 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1333 assert_eq!(components.len(), 2);
1334
1335 match &components[0] {
1336 TemplateComponent::Title(t) => {
1337 assert_eq!(t.rendering.prefix, Some("In ".to_string()));
1338 assert_eq!(t.rendering.emph, Some(true));
1339 }
1340 _ => panic!("Expected Title"),
1341 }
1342
1343 match &components[1] {
1344 TemplateComponent::Date(d) => {
1345 assert_eq!(
1346 d.rendering.wrap,
1347 Some(WrapConfig {
1348 punctuation: WrapPunctuation::Parentheses,
1349 inner_prefix: None,
1350 inner_suffix: None,
1351 })
1352 );
1353 }
1354 _ => panic!("Expected Date"),
1355 }
1356 }
1357
1358 #[test]
1359 fn test_number_variable_custom_normalizes_manual_construction() {
1360 let number = NumberVariable::Custom("Reel Label".to_string());
1361
1362 assert_eq!(number.as_key(), "reel-label");
1363 assert_eq!(
1364 number,
1365 serde_yaml::from_str::<NumberVariable>("reel-label")
1366 .expect("custom number variable should parse")
1367 );
1368 assert_eq!(
1369 serde_json::to_string(&number).expect("custom number variable should serialize"),
1370 "\"reel-label\""
1371 );
1372 }
1373
1374 #[test]
1375 fn test_contributor_with_wrap() {
1376 let yaml = r#"
1377contributor: publisher
1378form: short
1379wrap: parentheses
1380"#;
1381 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1382 assert_eq!(comp.contributor, ContributorRole::Publisher);
1383 assert_eq!(
1384 comp.rendering.wrap,
1385 Some(WrapConfig {
1386 punctuation: WrapPunctuation::Parentheses,
1387 inner_prefix: None,
1388 inner_suffix: None,
1389 })
1390 );
1391 }
1392
1393 #[test]
1394 fn test_variable_deserialization() {
1395 let yaml = "variable: publisher\n";
1397 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
1398 match comp {
1399 TemplateComponent::Variable(v) => {
1400 assert_eq!(v.variable, SimpleVariable::Publisher);
1401 }
1402 _ => panic!("Expected Variable(Publisher), got {:?}", comp),
1403 }
1404 }
1405
1406 #[test]
1407 fn test_variable_array_parsing() {
1408 let yaml = r#"
1409- variable: doi
1410 prefix: "https://doi.org/"
1411- variable: publisher
1412"#;
1413 let comps: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1414 assert_eq!(comps.len(), 2);
1415 match &comps[0] {
1416 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Doi),
1417 _ => panic!("Expected Variable for doi, got {:?}", comps[0]),
1418 }
1419 match &comps[1] {
1420 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Publisher),
1421 _ => panic!("Expected Variable for publisher, got {:?}", comps[1]),
1422 }
1423 }
1424
1425 #[test]
1426 fn test_type_selector_default_only_matches_default_context() {
1427 let selector = TypeSelector::Single("default".to_string());
1428 assert!(selector.matches("default"));
1429 assert!(!selector.matches("article-journal"));
1430
1431 let mixed = TypeSelector::Multiple(vec!["default".to_string(), "chapter".to_string()]);
1432 assert!(mixed.matches("default"));
1433 assert!(mixed.matches("chapter"));
1434 assert!(!mixed.matches("book"));
1435 }
1436
1437 #[test]
1438 fn test_template_component_selector_matches_nested_partial_group() {
1439 let component: TemplateComponent = serde_yaml::from_str(
1440 r#"
1441delimiter: ""
1442group:
1443- number: citation-number
1444 wrap:
1445 punctuation: brackets
1446- contributor: author
1447 form: long
1448"#,
1449 )
1450 .unwrap();
1451 let selector = TemplateComponentSelector {
1452 fields: BTreeMap::from([(
1453 "group".to_string(),
1454 serde_json::json!([
1455 { "number": "citation-number" },
1456 { "contributor": "author" }
1457 ]),
1458 )]),
1459 };
1460
1461 assert!(selector.matches(&component));
1462 }
1463
1464 #[test]
1465 fn test_delimiter_from_csl_string_normalizes_none_and_trimmed_values() {
1466 assert_eq!(
1467 DelimiterPunctuation::from_csl_string("none"),
1468 DelimiterPunctuation::None
1469 );
1470 assert_eq!(
1471 DelimiterPunctuation::from_csl_string(" none "),
1472 DelimiterPunctuation::None
1473 );
1474 assert_eq!(
1475 DelimiterPunctuation::from_csl_string(" "),
1476 DelimiterPunctuation::Space
1477 );
1478 assert_eq!(
1479 DelimiterPunctuation::from_csl_string(" : "),
1480 DelimiterPunctuation::Colon
1481 );
1482 }
1483}