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::matched_localized_template;
49pub use reference::{
50 LocalizedTemplateSpec, ResolvedLocalizedTemplate, TemplatePreset, TemplateReference,
51};
52pub(crate) use resolution::{inherited_variant_context, resolve_style_template_variants};
53
54pub fn resolve_local_template_variants(
68 style: &mut crate::Style,
69) -> Result<(), crate::ResolutionError> {
70 resolution::resolve_style_template_variants(style, None)
71}
72
73pub type Template = Vec<TemplateComponent>;
75
76pub type TemplateVariants = IndexMap<TypeSelector, TemplateVariant>;
78
79pub type LocalizedTemplateVariants = IndexMap<TypeSelector, Template>;
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "schema", derive(JsonSchema))]
88#[serde(rename_all = "kebab-case")]
89pub enum VerticalAlign {
90 Baseline,
92 Superscript,
94 Subscript,
96}
97
98#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
108#[cfg_attr(feature = "schema", derive(JsonSchema))]
109#[serde(rename_all = "kebab-case", default)]
110pub struct Rendering {
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub text_case: Option<crate::options::titles::TextCase>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub emph: Option<bool>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub quote: Option<bool>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub strong: Option<bool>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub small_caps: Option<bool>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub vertical_align: Option<VerticalAlign>,
129 #[serde(skip_serializing_if = "Option::is_none")]
132 pub prefix: Option<DelimiterPunctuation>,
133 #[serde(skip_serializing_if = "Option::is_none")]
136 pub suffix: Option<DelimiterPunctuation>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub wrap: Option<WrapConfig>,
140 #[serde(skip_serializing_if = "Option::is_none")]
143 pub suppress: Option<bool>,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub initialize_with: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
149 pub name_form: Option<crate::options::contributors::NameForm>,
150 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
152 pub strip_periods: Option<bool>,
153}
154
155impl Rendering {
156 pub fn merge(&mut self, other: &Rendering) {
160 crate::merge_options!(
161 self,
162 other,
163 text_case,
164 emph,
165 quote,
166 strong,
167 small_caps,
168 vertical_align,
169 prefix,
170 suffix,
171 wrap,
172 suppress,
173 initialize_with,
174 name_form,
175 strip_periods,
176 );
177 }
178}
179
180#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
182#[cfg_attr(feature = "schema", derive(JsonSchema))]
183#[serde(rename_all = "kebab-case")]
184pub enum WrapPunctuation {
185 #[default]
186 Parentheses,
187 Brackets,
188 Quotes,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize)]
196#[cfg_attr(feature = "schema", derive(JsonSchema))]
197#[serde(rename_all = "kebab-case")]
198pub struct WrapConfig {
199 pub punctuation: WrapPunctuation,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub inner_prefix: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub inner_suffix: Option<String>,
207}
208
209impl<'de> serde::Deserialize<'de> for WrapConfig {
210 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
211 struct WrapConfigVisitor;
212
213 impl<'de> serde::de::Visitor<'de> for WrapConfigVisitor {
214 type Value = WrapConfig;
215
216 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
217 write!(
218 f,
219 "a wrap punctuation string or a mapping with a 'punctuation' key"
220 )
221 }
222
223 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<WrapConfig, E> {
224 let punctuation = match v {
225 "parentheses" => WrapPunctuation::Parentheses,
226 "brackets" => WrapPunctuation::Brackets,
227 "quotes" => WrapPunctuation::Quotes,
228 other => {
229 return Err(E::unknown_variant(
230 other,
231 &["parentheses", "brackets", "quotes"],
232 ));
233 }
234 };
235 Ok(WrapConfig {
236 punctuation,
237 inner_prefix: None,
238 inner_suffix: None,
239 })
240 }
241
242 fn visit_map<A: serde::de::MapAccess<'de>>(
243 self,
244 mut map: A,
245 ) -> Result<WrapConfig, A::Error> {
246 let mut punctuation: Option<WrapPunctuation> = None;
247 let mut inner_prefix: Option<String> = None;
248 let mut inner_suffix: Option<String> = None;
249
250 while let Some(key) = map.next_key::<String>()? {
251 match key.as_str() {
252 "punctuation" => {
253 punctuation = Some(map.next_value()?);
254 }
255 "inner-prefix" => {
256 inner_prefix = Some(map.next_value()?);
257 }
258 "inner-suffix" => {
259 inner_suffix = Some(map.next_value()?);
260 }
261 other => {
262 return Err(serde::de::Error::unknown_field(
263 other,
264 &["punctuation", "inner-prefix", "inner-suffix"],
265 ));
266 }
267 }
268 }
269
270 let punctuation =
271 punctuation.ok_or_else(|| serde::de::Error::missing_field("punctuation"))?;
272 Ok(WrapConfig {
273 punctuation,
274 inner_prefix,
275 inner_suffix,
276 })
277 }
278 }
279
280 deserializer.deserialize_any(WrapConfigVisitor)
281 }
282}
283
284impl From<WrapPunctuation> for WrapConfig {
285 fn from(punctuation: WrapPunctuation) -> Self {
286 WrapConfig {
287 punctuation,
288 inner_prefix: None,
289 inner_suffix: None,
290 }
291 }
292}
293
294pub const VALID_TYPE_NAMES: &[&str] = &[
298 "book",
299 "manual",
300 "report",
301 "thesis",
302 "webpage",
303 "map",
304 "post",
305 "interview",
306 "manuscript",
307 "personal-communication",
308 "document",
309 "chapter",
310 "entry-dictionary",
311 "paper-conference",
312 "article-journal",
313 "article-magazine",
314 "article-newspaper",
315 "broadcast",
316 "motion-picture",
317 "collection",
318 "legal-case",
319 "statute",
320 "treaty",
321 "hearing",
322 "regulation",
323 "brief",
324 "classic",
325 "patent",
326 "dataset",
327 "standard",
328 "software",
329 "all",
331 "default",
332];
333
334pub fn validate_type_name(s: &str) -> bool {
340 let normalized = s.replace('_', "-");
341 VALID_TYPE_NAMES.iter().any(|&known| known == normalized)
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Hash)]
347#[cfg_attr(feature = "schema", derive(JsonSchema))]
348pub enum TypeSelector {
349 Single(String),
350 Multiple(Vec<String>),
351}
352
353impl Serialize for TypeSelector {
354 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
355 where
356 S: serde::Serializer,
357 {
358 serializer.serialize_str(&self.to_string())
359 }
360}
361
362impl<'de> Deserialize<'de> for TypeSelector {
363 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
364 where
365 D: serde::Deserializer<'de>,
366 {
367 struct Visitor;
368 impl<'de> serde::de::Visitor<'de> for Visitor {
369 type Value = TypeSelector;
370
371 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
372 formatter.write_str("a string or a sequence of strings")
373 }
374
375 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
376 where
377 E: serde::de::Error,
378 {
379 v.parse().map_err(E::custom)
380 }
381
382 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
383 where
384 A: serde::de::SeqAccess<'de>,
385 {
386 let mut types = Vec::new();
387 while let Some(t) = seq.next_element::<String>()? {
388 types.push(t);
389 }
390 if types.len() == 1 {
391 Ok(TypeSelector::Single(types.remove(0)))
392 } else {
393 Ok(TypeSelector::Multiple(types))
394 }
395 }
396 }
397 deserializer.deserialize_any(Visitor)
398 }
399}
400
401impl std::fmt::Display for TypeSelector {
402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 match self {
404 TypeSelector::Single(s) => write!(f, "{s}"),
405 TypeSelector::Multiple(types) => write!(f, "{}", types.join(",")),
406 }
407 }
408}
409
410impl std::str::FromStr for TypeSelector {
411 type Err = std::convert::Infallible;
412
413 fn from_str(s: &str) -> Result<Self, Self::Err> {
414 if s.contains(',') {
415 Ok(TypeSelector::Multiple(
416 s.split(',').map(|t| t.trim().to_string()).collect(),
417 ))
418 } else {
419 Ok(TypeSelector::Single(s.to_string()))
420 }
421 }
422}
423
424impl TypeSelector {
425 pub fn matches(&self, ref_type: &str) -> bool {
433 let normalized_ref = ref_type.replace('_', "-");
434 let base_ref = normalized_ref
435 .split_once('+')
436 .map(|(base, _)| base)
437 .unwrap_or(&normalized_ref);
438 let eq = |s: &str| -> bool {
439 s == ref_type
440 || s.replace('_', "-") == normalized_ref
441 || s.replace('_', "-") == base_ref
442 || s == "all"
443 || (s == "default" && ref_type == "default")
444 };
445 match self {
446 TypeSelector::Single(s) => eq(s),
447 TypeSelector::Multiple(types) => types.iter().any(|t| eq(t)),
448 }
449 }
450
451 pub fn unknown_type_names(&self) -> Vec<&str> {
456 match self {
457 TypeSelector::Single(s) => {
458 if validate_type_name(s) {
459 vec![]
460 } else {
461 vec![s.as_str()]
462 }
463 }
464 TypeSelector::Multiple(types) => types
465 .iter()
466 .filter(|s| !validate_type_name(s))
467 .map(|s| s.as_str())
468 .collect(),
469 }
470 }
471}
472
473#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
477#[cfg_attr(feature = "schema", derive(JsonSchema))]
478#[serde(untagged)]
479#[non_exhaustive]
480pub enum TemplateComponent {
481 Contributor(TemplateContributor),
482 Date(TemplateDate),
483 Title(TemplateTitle),
484 Number(TemplateNumber),
485 Identifier(TemplateIdentifier),
486 Variable(TemplateVariable),
487 Message(TemplateMessage),
488 Group(TemplateGroup),
489 Term(TemplateTerm),
490 TypeLabel(TemplateTypeLabel),
491}
492
493impl Default for TemplateComponent {
494 fn default() -> Self {
495 TemplateComponent::Variable(TemplateVariable::default())
496 }
497}
498
499impl TemplateComponent {
500 pub fn rendering(&self) -> &Rendering {
504 crate::dispatch_component!(self, |inner| &inner.rendering)
505 }
506
507 pub fn rendering_mut(&mut self) -> &mut Rendering {
512 crate::dispatch_component!(self, |inner| &mut inner.rendering)
513 }
514}
515
516#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
518#[cfg_attr(feature = "schema", derive(JsonSchema))]
519#[serde(untagged)]
520pub enum TemplateVariant {
521 Full(Vec<TemplateComponent>),
523 Diff(TemplateVariantDiff),
525}
526
527impl TemplateVariant {
528 #[must_use]
530 pub fn as_template(&self) -> Option<&[TemplateComponent]> {
531 match self {
532 Self::Full(template) => Some(template.as_slice()),
533 Self::Diff(_) => None,
534 }
535 }
536
537 pub fn as_template_mut(&mut self) -> Option<&mut Vec<TemplateComponent>> {
539 match self {
540 Self::Full(template) => Some(template),
541 Self::Diff(_) => None,
542 }
543 }
544
545 #[must_use]
547 pub fn into_template(self) -> Option<Vec<TemplateComponent>> {
548 match self {
549 Self::Full(template) => Some(template),
550 Self::Diff(_) => None,
551 }
552 }
553}
554
555impl From<Vec<TemplateComponent>> for TemplateVariant {
556 fn from(template: Vec<TemplateComponent>) -> Self {
557 Self::Full(template)
558 }
559}
560
561#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
563#[cfg_attr(feature = "schema", derive(JsonSchema))]
564#[serde(rename_all = "kebab-case", deny_unknown_fields)]
565pub struct TemplateVariantDiff {
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub extends: Option<TypeSelector>,
569 #[serde(skip_serializing_if = "Vec::is_empty", default)]
571 pub modify: Vec<TemplateModifyOperation>,
572 #[serde(skip_serializing_if = "Vec::is_empty", default)]
574 pub remove: Vec<TemplateRemoveOperation>,
575 #[serde(skip_serializing_if = "Vec::is_empty", default)]
577 pub add: Vec<TemplateAddOperation>,
578}
579
580#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
582#[cfg_attr(feature = "schema", derive(JsonSchema))]
583#[serde(transparent)]
584pub struct TemplateComponentSelector {
585 pub fields: BTreeMap<String, serde_json::Value>,
587}
588
589impl TemplateComponentSelector {
590 #[must_use]
592 pub fn is_empty(&self) -> bool {
593 self.fields.is_empty()
594 }
595
596 #[must_use]
598 pub fn matches(&self, component: &TemplateComponent) -> bool {
599 let Ok(serde_json::Value::Object(component_fields)) = serde_json::to_value(component)
600 else {
601 return false;
602 };
603
604 self.fields.iter().all(|(key, expected)| {
605 component_fields
606 .get(key)
607 .is_some_and(|actual| selector_value_matches(expected, actual))
608 })
609 }
610}
611
612fn selector_value_matches(expected: &serde_json::Value, actual: &serde_json::Value) -> bool {
613 match (expected, actual) {
614 (serde_json::Value::Object(expected_fields), serde_json::Value::Object(actual_fields)) => {
615 expected_fields.iter().all(|(key, expected_value)| {
616 actual_fields.get(key).is_some_and(|actual_value| {
617 selector_value_matches(expected_value, actual_value)
618 })
619 })
620 }
621 (serde_json::Value::Array(expected_items), serde_json::Value::Array(actual_items)) => {
622 expected_items.len() == actual_items.len()
623 && expected_items.iter().zip(actual_items.iter()).all(
624 |(expected_item, actual_item)| {
625 selector_value_matches(expected_item, actual_item)
626 },
627 )
628 }
629 _ => expected == actual,
630 }
631}
632
633#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
635#[cfg_attr(feature = "schema", derive(JsonSchema))]
636#[serde(rename_all = "kebab-case", deny_unknown_fields)]
637pub struct TemplateModifyOperation {
638 #[serde(rename = "match")]
640 pub match_selector: TemplateComponentSelector,
641 #[serde(skip_serializing_if = "Option::is_none")]
643 pub label_form: Option<LabelForm>,
644 #[serde(flatten, default)]
646 pub rendering: Rendering,
647}
648
649#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
651#[cfg_attr(feature = "schema", derive(JsonSchema))]
652#[serde(rename_all = "kebab-case", deny_unknown_fields)]
653pub struct TemplateRemoveOperation {
654 #[serde(rename = "match")]
656 pub match_selector: TemplateComponentSelector,
657}
658
659#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
661#[cfg_attr(feature = "schema", derive(JsonSchema))]
662#[serde(rename_all = "kebab-case", deny_unknown_fields)]
663pub struct TemplateAddOperation {
664 #[serde(skip_serializing_if = "Option::is_none")]
666 pub before: Option<TemplateComponentSelector>,
667 #[serde(skip_serializing_if = "Option::is_none")]
669 pub after: Option<TemplateComponentSelector>,
670 pub component: TemplateComponent,
672}
673
674#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
676#[cfg_attr(feature = "schema", derive(JsonSchema))]
677#[serde(rename_all = "kebab-case")]
678pub struct RoleLabel {
679 pub term: String,
681 #[serde(default)]
683 pub form: RoleLabelForm,
684 #[serde(default)]
686 pub placement: LabelPlacement,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub text_case: Option<crate::options::titles::TextCase>,
692 #[serde(default, skip_serializing_if = "Option::is_none")]
696 pub wrap: Option<Box<WrapConfig>>,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub prefix: Option<DelimiterPunctuation>,
703 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub suffix: Option<DelimiterPunctuation>,
708}
709
710#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
712#[cfg_attr(feature = "schema", derive(JsonSchema))]
713#[serde(rename_all = "kebab-case")]
714pub enum RoleLabelForm {
715 #[default]
716 Short,
717 Long,
718}
719
720#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
722#[cfg_attr(feature = "schema", derive(JsonSchema))]
723#[serde(rename_all = "kebab-case")]
724pub enum LabelPlacement {
725 Prefix,
726 #[default]
727 Suffix,
728}
729
730#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
732#[cfg_attr(feature = "schema", derive(JsonSchema))]
733#[serde(untagged)]
734pub enum ContributorRoles {
735 Single(ContributorRole),
737 Multiple(#[cfg_attr(feature = "schema", schemars(length(min = 2)))] Vec<ContributorRole>),
739}
740
741impl Default for ContributorRoles {
742 fn default() -> Self {
743 Self::Single(ContributorRole::Author)
744 }
745}
746
747impl ContributorRoles {
748 #[must_use]
750 pub fn as_slice(&self) -> &[ContributorRole] {
751 match self {
752 Self::Single(role) => std::slice::from_ref(role),
753 Self::Multiple(roles) => roles,
754 }
755 }
756
757 #[must_use]
759 pub fn as_single(&self) -> Option<&ContributorRole> {
760 match self {
761 Self::Single(role) => Some(role),
762 Self::Multiple(_) => None,
763 }
764 }
765
766 #[must_use]
768 pub fn is_multiple(&self) -> bool {
769 matches!(self, Self::Multiple(_))
770 }
771
772 #[must_use]
774 pub fn contains(&self, role: &ContributorRole) -> bool {
775 self.as_slice().contains(role)
776 }
777}
778
779impl From<ContributorRole> for ContributorRoles {
780 fn from(role: ContributorRole) -> Self {
781 Self::Single(role)
782 }
783}
784
785impl From<Vec<ContributorRole>> for ContributorRoles {
786 fn from(roles: Vec<ContributorRole>) -> Self {
787 Self::Multiple(roles)
788 }
789}
790
791impl PartialEq<ContributorRole> for ContributorRoles {
792 fn eq(&self, other: &ContributorRole) -> bool {
793 self.as_single() == Some(other)
794 }
795}
796
797#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
799#[cfg_attr(feature = "schema", derive(JsonSchema))]
800#[serde(rename_all = "kebab-case")]
801pub enum ContributorMergeOrder {
802 #[default]
804 Document,
805 Role,
807}
808
809#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
811#[cfg_attr(feature = "schema", derive(JsonSchema))]
812#[serde(rename_all = "kebab-case")]
813pub enum ContributorLabelMode {
814 #[default]
816 Individual,
817 Collective,
819 None,
821}
822
823#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
825#[cfg_attr(feature = "schema", derive(JsonSchema))]
826#[serde(rename_all = "kebab-case", deny_unknown_fields)]
827pub struct ContributorMergeRole {
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub labels: Option<ContributorLabelMode>,
831 #[serde(skip_serializing_if = "Option::is_none")]
833 pub label: Option<RoleLabel>,
834}
835
836#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
838#[cfg_attr(feature = "schema", derive(JsonSchema))]
839#[serde(rename_all = "kebab-case", deny_unknown_fields)]
840pub struct ContributorMerge {
841 #[serde(default)]
843 pub order: ContributorMergeOrder,
844 #[serde(default)]
846 pub labels: ContributorLabelMode,
847 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
849 pub roles: HashMap<ContributorRole, ContributorMergeRole>,
850 #[serde(default = "default_combine_same_person")]
852 pub combine_same_person: bool,
853 #[serde(skip_serializing_if = "Option::is_none")]
855 pub role_conjunction: Option<String>,
856}
857
858fn default_combine_same_person() -> bool {
859 true
860}
861
862impl Default for ContributorMerge {
863 fn default() -> Self {
864 Self {
865 order: ContributorMergeOrder::Document,
866 labels: ContributorLabelMode::Individual,
867 roles: HashMap::new(),
868 combine_same_person: true,
869 role_conjunction: None,
870 }
871 }
872}
873
874#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
876#[cfg_attr(feature = "schema", derive(JsonSchema))]
877#[serde(rename_all = "kebab-case", deny_unknown_fields)]
878pub struct TemplateContributor {
879 pub contributor: ContributorRoles,
881 pub form: ContributorForm,
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub label: Option<RoleLabel>,
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub merge: Option<ContributorMerge>,
889 #[serde(skip_serializing_if = "Option::is_none")]
892 pub name_order: Option<NameOrder>,
893 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
895 pub name_form: Option<crate::options::contributors::NameForm>,
896 #[serde(skip_serializing_if = "Option::is_none")]
898 pub delimiter: Option<DelimiterPunctuation>,
899 #[serde(skip_serializing_if = "Option::is_none")]
901 pub sort_separator: Option<String>,
902 #[serde(skip_serializing_if = "Option::is_none")]
904 pub shorten: Option<crate::options::ShortenListOptions>,
905 #[serde(skip_serializing_if = "Option::is_none")]
908 pub and: Option<crate::options::AndOptions>,
909 #[serde(flatten, default)]
910 pub rendering: Rendering,
911 #[serde(skip_serializing_if = "Option::is_none")]
913 pub links: Option<crate::options::LinksConfig>,
914 #[serde(skip_serializing_if = "Option::is_none")]
916 pub gender: Option<GrammaticalGender>,
917
918 #[serde(skip_serializing_if = "Option::is_none")]
920 pub custom: Option<HashMap<String, serde_json::Value>>,
921}
922
923#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
925#[cfg_attr(feature = "schema", derive(JsonSchema))]
926#[serde(rename_all = "kebab-case")]
927pub enum NameOrder {
928 GivenFirst,
930 #[default]
932 FamilyFirst,
933 FamilyFirstOnly,
935 FamilyFirstExceptLast,
940}
941
942#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
944#[cfg_attr(feature = "schema", derive(JsonSchema))]
945#[serde(rename_all = "kebab-case")]
946pub enum ContributorForm {
947 #[default]
948 Long,
949 Short,
950 FamilyOnly,
951 Verb,
952 VerbShort,
953}
954
955crate::str_enum! {
956 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
958 pub enum ContributorRole {
959 #[default] Author = "author",
960 Chair = "chair",
961 Editor = "editor",
962 Translator = "translator",
963 Director = "director",
964 Publisher = "publisher",
965 Recipient = "recipient",
966 Interviewer = "interviewer",
967 Interviewee = "interviewee",
968 Guest = "guest",
969 Performer = "performer",
970 Inventor = "inventor",
971 Counsel = "counsel",
972 Composer = "composer",
973 Writer = "writer",
974 Producer = "producer",
975 CollectionEditor = "collection-editor",
976 ContainerAuthor = "container-author",
977 EditorialDirector = "editorial-director",
978 TextualEditor = "textual-editor",
979 Illustrator = "illustrator",
980 OriginalAuthor = "original-author",
981 ReviewedAuthor = "reviewed-author"
982 }
983}
984
985#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
987#[cfg_attr(feature = "schema", derive(JsonSchema))]
988#[serde(rename_all = "kebab-case", deny_unknown_fields)]
989pub struct TemplateDate {
990 pub date: DateVariable,
991 pub form: DateForm,
992 #[serde(skip_serializing_if = "Option::is_none")]
996 pub fallback: Option<Vec<TemplateComponent>>,
997 #[serde(flatten, default)]
998 pub rendering: Rendering,
999 #[serde(skip_serializing_if = "Option::is_none")]
1001 pub links: Option<crate::options::LinksConfig>,
1002
1003 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub custom: Option<HashMap<String, serde_json::Value>>,
1006}
1007
1008#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1010#[cfg_attr(feature = "schema", derive(JsonSchema))]
1011#[serde(rename_all = "kebab-case")]
1012pub enum DateVariable {
1013 #[default]
1014 Issued,
1015 Accessed,
1016 OriginalPublished,
1017 Submitted,
1018 EventDate,
1019 Copyright,
1022 Printing,
1025}
1026
1027crate::str_enum! {
1028 #[derive(Debug, Default, Clone, PartialEq)]
1030 pub enum DateForm {
1031 #[default]
1032 Year = "year",
1033 YearMonth = "year-month",
1034 Month = "month",
1037 Full = "full",
1038 MonthDay = "month-day",
1039 YearMonthDay = "year-month-day",
1040 DayMonthAbbrYear = "day-month-abbr-year",
1041 MonthAbbrDayYear = "month-abbr-day-year"
1043 }
1044}
1045
1046#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1048#[cfg_attr(feature = "schema", derive(JsonSchema))]
1049#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1050pub struct TemplateTitle {
1051 pub title: TitleType,
1052 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub form: Option<TitleForm>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub disambiguate_only: Option<bool>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1070 pub strip_periods_all: Option<bool>,
1071 #[serde(flatten, default)]
1072 pub rendering: Rendering,
1073 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub links: Option<crate::options::LinksConfig>,
1076
1077 #[serde(skip_serializing_if = "Option::is_none")]
1079 pub custom: Option<HashMap<String, serde_json::Value>>,
1080}
1081
1082#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1084#[cfg_attr(feature = "schema", derive(JsonSchema))]
1085#[serde(rename_all = "kebab-case")]
1086#[non_exhaustive]
1087pub enum TitleType {
1088 #[default]
1090 Primary,
1091 ContainerTitle,
1093 ParentMonograph,
1095 ParentSerial,
1097 CollectionTitle,
1099 Original,
1101}
1102
1103#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1105#[cfg_attr(feature = "schema", derive(JsonSchema))]
1106#[serde(rename_all = "kebab-case")]
1107pub enum TitleForm {
1108 Short,
1109 #[default]
1110 Long,
1111}
1112
1113#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1115#[cfg_attr(feature = "schema", derive(JsonSchema))]
1116#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1117pub struct TemplateNumber {
1118 pub number: NumberVariable,
1119 #[serde(skip_serializing_if = "Option::is_none")]
1120 pub form: Option<NumberForm>,
1121 #[serde(skip_serializing_if = "Option::is_none")]
1122 pub label_form: Option<LabelForm>,
1123 #[serde(skip_serializing_if = "Option::is_none")]
1126 pub show_with_locator: Option<bool>,
1127 #[serde(flatten)]
1128 pub rendering: Rendering,
1129 #[serde(skip_serializing_if = "Option::is_none")]
1131 pub links: Option<crate::options::LinksConfig>,
1132 #[serde(skip_serializing_if = "Option::is_none")]
1134 pub gender: Option<GrammaticalGender>,
1135 #[serde(skip_serializing_if = "Option::is_none")]
1148 pub when_numeric: Option<LabelForm>,
1149
1150 #[serde(skip_serializing_if = "Option::is_none")]
1152 pub custom: Option<HashMap<String, serde_json::Value>>,
1153}
1154
1155#[derive(Debug, Default, Clone)]
1162#[non_exhaustive]
1163pub enum NumberVariable {
1164 #[default]
1165 Volume,
1166 Issue,
1167 Pages,
1168 Edition,
1169 ChapterNumber,
1170 CollectionNumber,
1171 NumberOfPages,
1172 NumberOfVolumes,
1173 CitationNumber,
1174 FirstReferenceNoteNumber,
1178 CitationLabel,
1179 Number,
1180 DocketNumber,
1181 PatentNumber,
1182 StandardNumber,
1183 ReportNumber,
1184 PartNumber,
1185 SupplementNumber,
1186 PrintingNumber,
1187 Custom(String),
1189}
1190
1191impl NumberVariable {
1192 #[must_use]
1194 pub fn as_key(&self) -> Cow<'_, str> {
1195 match self {
1196 Self::Volume => Cow::Borrowed("volume"),
1197 Self::Issue => Cow::Borrowed("issue"),
1198 Self::Pages => Cow::Borrowed("pages"),
1199 Self::Edition => Cow::Borrowed("edition"),
1200 Self::ChapterNumber => Cow::Borrowed("chapter-number"),
1201 Self::CollectionNumber => Cow::Borrowed("collection-number"),
1202 Self::NumberOfPages => Cow::Borrowed("number-of-pages"),
1203 Self::NumberOfVolumes => Cow::Borrowed("number-of-volumes"),
1204 Self::CitationNumber => Cow::Borrowed("citation-number"),
1205 Self::FirstReferenceNoteNumber => Cow::Borrowed("first-reference-note-number"),
1206 Self::CitationLabel => Cow::Borrowed("citation-label"),
1207 Self::Number => Cow::Borrowed("number"),
1208 Self::DocketNumber => Cow::Borrowed("docket-number"),
1209 Self::PatentNumber => Cow::Borrowed("patent-number"),
1210 Self::StandardNumber => Cow::Borrowed("standard-number"),
1211 Self::ReportNumber => Cow::Borrowed("report-number"),
1212 Self::PartNumber => Cow::Borrowed("part-number"),
1213 Self::SupplementNumber => Cow::Borrowed("supplement-number"),
1214 Self::PrintingNumber => Cow::Borrowed("printing-number"),
1215 Self::Custom(value) => normalize_kind_key(value)
1216 .map(Cow::Owned)
1217 .unwrap_or_else(|| Cow::Borrowed(value.as_str())),
1218 }
1219 }
1220
1221 fn from_key(value: &str) -> Result<Self, String> {
1222 let canonical = normalize_kind_key(value)
1223 .ok_or_else(|| "number variable must not be empty".to_string())?;
1224 Ok(match canonical.as_str() {
1225 "volume" => Self::Volume,
1226 "issue" => Self::Issue,
1227 "pages" => Self::Pages,
1228 "edition" => Self::Edition,
1229 "chapter-number" => Self::ChapterNumber,
1230 "collection-number" => Self::CollectionNumber,
1231 "number-of-pages" => Self::NumberOfPages,
1232 "number-of-volumes" => Self::NumberOfVolumes,
1233 "citation-number" => Self::CitationNumber,
1234 "first-reference-note-number" => Self::FirstReferenceNoteNumber,
1235 "citation-label" => Self::CitationLabel,
1236 "number" => Self::Number,
1237 "docket-number" => Self::DocketNumber,
1238 "patent-number" => Self::PatentNumber,
1239 "standard-number" => Self::StandardNumber,
1240 "report-number" => Self::ReportNumber,
1241 "part-number" => Self::PartNumber,
1242 "supplement-number" => Self::SupplementNumber,
1243 "printing-number" => Self::PrintingNumber,
1244 _ => Self::Custom(canonical),
1245 })
1246 }
1247}
1248
1249impl PartialEq for NumberVariable {
1250 fn eq(&self, other: &Self) -> bool {
1251 self.as_key().as_ref() == other.as_key().as_ref()
1252 }
1253}
1254
1255impl Eq for NumberVariable {}
1256
1257impl Hash for NumberVariable {
1258 fn hash<H: Hasher>(&self, state: &mut H) {
1259 self.as_key().as_ref().hash(state);
1260 }
1261}
1262
1263impl Serialize for NumberVariable {
1264 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1265 where
1266 S: Serializer,
1267 {
1268 serializer.serialize_str(self.as_key().as_ref())
1269 }
1270}
1271
1272impl<'de> Deserialize<'de> for NumberVariable {
1273 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1274 where
1275 D: Deserializer<'de>,
1276 {
1277 let value = String::deserialize(deserializer)?;
1278 Self::from_key(&value).map_err(serde::de::Error::custom)
1279 }
1280}
1281
1282#[cfg(feature = "schema")]
1283impl JsonSchema for NumberVariable {
1284 fn schema_name() -> std::borrow::Cow<'static, str> {
1285 "NumberVariable".into()
1286 }
1287
1288 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1289 schemars::json_schema!({
1290 "type": "string",
1291 "description": "Known number variable keyword or custom kebab-case identifier."
1292 })
1293 }
1294}
1295
1296#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1298#[cfg_attr(feature = "schema", derive(JsonSchema))]
1299#[serde(rename_all = "lowercase")]
1300pub enum NumberForm {
1301 #[default]
1302 Numeric,
1303 Ordinal,
1304 Roman,
1305}
1306
1307fn normalize_kind_key(value: &str) -> Option<String> {
1308 let mut normalized = String::new();
1309 let mut pending_dash = false;
1310
1311 for ch in value.trim().chars() {
1312 if ch.is_ascii_alphanumeric() {
1313 if pending_dash && !normalized.is_empty() {
1314 normalized.push('-');
1315 }
1316 normalized.push(ch.to_ascii_lowercase());
1317 pending_dash = false;
1318 } else if !normalized.is_empty() {
1319 pending_dash = true;
1320 }
1321 }
1322
1323 if normalized.is_empty() {
1324 None
1325 } else {
1326 Some(normalized)
1327 }
1328}
1329
1330#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1332#[cfg_attr(feature = "schema", derive(JsonSchema))]
1333#[serde(rename_all = "kebab-case")]
1334pub enum LabelForm {
1335 Long,
1336 #[default]
1337 Short,
1338 Symbol,
1339}
1340
1341#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1343#[cfg_attr(feature = "schema", derive(JsonSchema))]
1344#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1345pub struct TemplateVariable {
1346 pub variable: SimpleVariable,
1347 #[serde(flatten)]
1348 pub rendering: Rendering,
1349 #[serde(skip_serializing_if = "Option::is_none")]
1351 pub links: Option<crate::options::LinksConfig>,
1352
1353 #[serde(skip_serializing_if = "Option::is_none")]
1355 pub custom: Option<HashMap<String, serde_json::Value>>,
1356}
1357
1358#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1360#[cfg_attr(feature = "schema", derive(JsonSchema))]
1361#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1362pub struct TemplateIdentifier {
1363 pub identifier: crate::reference::IdentifierName,
1365 #[serde(flatten, default)]
1366 pub rendering: Rendering,
1367}
1368
1369#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1374#[cfg_attr(feature = "schema", derive(JsonSchema))]
1375#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1376pub struct TemplateMessage {
1377 pub message: String,
1379 #[serde(skip_serializing_if = "Option::is_none")]
1381 pub form: Option<TermForm>,
1382 #[serde(skip_serializing_if = "Option::is_none")]
1384 pub gender: Option<GrammaticalGender>,
1385 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1387 pub args: HashMap<String, MessageArgSource>,
1388 #[serde(flatten, default)]
1389 pub rendering: Rendering,
1390
1391 #[serde(skip_serializing_if = "Option::is_none")]
1393 pub custom: Option<HashMap<String, serde_json::Value>>,
1394}
1395
1396#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1398#[cfg_attr(feature = "schema", derive(JsonSchema))]
1399#[serde(untagged)]
1400pub enum MessageArgSource {
1401 Literal { literal: String },
1403 ReferenceType {
1405 #[serde(rename = "reference-type")]
1406 reference_type: MessageReferenceTypeSource,
1407 },
1408 Carrier { carrier: MessageCarrierSource },
1410 Contributor(Box<TemplateContributor>),
1412 Date(TemplateDate),
1414 Group(TemplateGroup),
1416 Title(TemplateTitle),
1418 Number(TemplateNumber),
1420 Variable(TemplateVariable),
1422 Term(TemplateTerm),
1424}
1425
1426impl MessageArgSource {
1427 #[must_use]
1430 pub fn as_template_component(&self) -> Option<TemplateComponent> {
1431 match self {
1432 Self::Literal { .. } | Self::ReferenceType { .. } | Self::Carrier { .. } => None,
1433 Self::Contributor(component) => {
1434 Some(TemplateComponent::Contributor(component.as_ref().clone()))
1435 }
1436 Self::Date(component) => Some(TemplateComponent::Date(component.clone())),
1437 Self::Group(component) => Some(TemplateComponent::Group(component.clone())),
1438 Self::Title(component) => Some(TemplateComponent::Title(component.clone())),
1439 Self::Number(component) => Some(TemplateComponent::Number(component.clone())),
1440 Self::Variable(component) => Some(TemplateComponent::Variable(component.clone())),
1441 Self::Term(component) => Some(TemplateComponent::Term(component.clone())),
1442 }
1443 }
1444}
1445
1446#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
1448#[cfg_attr(feature = "schema", derive(JsonSchema))]
1449#[serde(rename_all = "kebab-case")]
1450pub enum MessageReferenceTypeSource {
1451 Key,
1453}
1454
1455#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1457#[cfg_attr(feature = "schema", derive(JsonSchema))]
1458#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1459pub struct MessageCarrierSource {
1460 pub online: String,
1462 pub absent: String,
1464}
1465
1466#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1473#[cfg_attr(feature = "schema", derive(JsonSchema))]
1474#[serde(rename_all = "kebab-case")]
1475#[non_exhaustive]
1476pub enum SimpleVariable {
1477 #[default]
1478 Doi,
1479 Isbn,
1480 Issn,
1481 Url,
1482 Pmid,
1483 Pmcid,
1484 Abstract,
1485 Note,
1486 Annote,
1487 Keyword,
1488 Genre,
1489 RawGenre,
1490 Medium,
1491 RawMedium,
1492 Source,
1493 Status,
1494 Archive,
1495 ArchiveLocation,
1496 ArchiveName,
1497 ArchivePlace,
1498 ArchiveCollection,
1499 ArchiveCollectionId,
1500 ArchiveSeries,
1501 ArchiveBox,
1502 ArchiveFolder,
1503 ArchiveItem,
1504 ArchiveUrl,
1505 EprintId,
1506 EprintServer,
1507 EprintClass,
1508 Publisher,
1509 PublisherPlace,
1510 OriginalPublisher,
1511 OriginalPublisherPlace,
1512 EventTitle,
1513 EventPlace,
1514 Dimensions,
1515 References,
1516 Scale,
1517 Version,
1518 VolumeTitle,
1519 Locator,
1520 ContainerTitleShort,
1521 Authority,
1522 Code,
1523 Reporter,
1524 Page,
1525 Section,
1526 Volume,
1527 Number,
1528 DocketNumber,
1529 PatentNumber,
1530 StandardNumber,
1531 ReportNumber,
1532 AdsBibcode,
1533}
1534
1535#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1537#[cfg_attr(feature = "schema", derive(JsonSchema))]
1538#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1539pub struct TemplateTerm {
1540 pub term: GeneralTerm,
1542 #[serde(skip_serializing_if = "Option::is_none")]
1544 pub form: Option<TermForm>,
1545 #[serde(skip_serializing_if = "Option::is_none")]
1547 pub gender: Option<GrammaticalGender>,
1548 #[serde(flatten, default)]
1549 pub rendering: Rendering,
1550
1551 #[serde(skip_serializing_if = "Option::is_none")]
1553 pub custom: Option<HashMap<String, serde_json::Value>>,
1554}
1555
1556#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
1563#[cfg_attr(feature = "schema", derive(JsonSchema))]
1564#[serde(rename_all = "kebab-case")]
1565#[non_exhaustive]
1566pub enum TypeLabelSource {
1567 #[default]
1570 ReferenceType,
1571}
1572
1573#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1583#[cfg_attr(feature = "schema", derive(JsonSchema))]
1584#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1585pub struct TemplateTypeLabel {
1586 #[serde(rename = "type-label")]
1588 pub type_label: TypeLabelSource,
1589 #[serde(flatten, default)]
1590 pub rendering: Rendering,
1591
1592 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub custom: Option<HashMap<String, serde_json::Value>>,
1595}
1596
1597#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1600#[cfg_attr(feature = "schema", derive(JsonSchema))]
1601#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1602pub struct TemplateGroup {
1603 pub group: Vec<TemplateComponent>,
1604 #[serde(skip_serializing_if = "Option::is_none")]
1606 pub render_when: Option<TemplateGroupCondition>,
1607 #[serde(skip_serializing_if = "Option::is_none")]
1608 pub delimiter: Option<DelimiterPunctuation>,
1609 #[serde(flatten, default)]
1610 pub rendering: Rendering,
1611
1612 #[serde(skip_serializing_if = "Option::is_none")]
1614 pub custom: Option<HashMap<String, serde_json::Value>>,
1615}
1616
1617#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1619#[cfg_attr(feature = "schema", derive(JsonSchema))]
1620#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1621pub struct TemplateGroupCondition {
1622 #[serde(skip_serializing_if = "Option::is_none")]
1624 pub field_present: Option<TemplateConditionField>,
1625 #[serde(skip_serializing_if = "Option::is_none")]
1627 pub field_absent: Option<TemplateConditionField>,
1628}
1629
1630#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1632#[cfg_attr(feature = "schema", derive(JsonSchema))]
1633#[serde(rename_all = "kebab-case")]
1634pub enum TemplateConditionField {
1635 Author,
1637 Editor,
1639 Recipient,
1641 Translator,
1643 Title,
1645 CollectionTitle,
1647 Issued,
1649 OriginalPublished,
1651 Publisher,
1653 OriginalPublisher,
1655 OriginalPublisherPlace,
1657 OriginalTitle,
1659 Doi,
1661 Genre,
1663 Archive,
1665 ArchiveLocation,
1667 VolumeOrIssue,
1673}
1674
1675#[derive(Debug, Default, Clone, PartialEq)]
1681pub enum DelimiterPunctuation {
1682 #[default]
1684 Comma,
1685 Semicolon,
1687 Period,
1689 Colon,
1691 Parentheses,
1693 Brackets,
1695 Ampersand,
1697 VerticalLine,
1699 Slash,
1701 Hyphen,
1703 Space,
1705 None,
1707 Custom(String),
1709}
1710
1711#[cfg(feature = "schema")]
1712impl JsonSchema for DelimiterPunctuation {
1713 fn schema_name() -> std::borrow::Cow<'static, str> {
1714 "DelimiterPunctuation".into()
1715 }
1716
1717 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1718 schemars::json_schema!({
1719 "oneOf": [
1720 {
1721 "type": "string",
1722 "description": "Literal punctuation or text."
1723 },
1724 {
1725 "type": "object",
1726 "additionalProperties": false,
1727 "required": ["mark"],
1728 "properties": {
1729 "mark": {
1730 "type": "string",
1731 "enum": [
1732 "comma",
1733 "colon",
1734 "semicolon",
1735 "period",
1736 "parentheses",
1737 "brackets"
1738 ]
1739 }
1740 }
1741 }
1742 ],
1743 "description": "Literal text or an explicit semantic punctuation mark."
1744 })
1745 }
1746}
1747
1748impl Serialize for DelimiterPunctuation {
1749 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1750 use serde::ser::SerializeMap as _;
1751
1752 let mark = match self {
1753 Self::Comma => Some("comma"),
1754 Self::Semicolon => Some("semicolon"),
1755 Self::Period => Some("period"),
1756 Self::Colon => Some("colon"),
1757 Self::Parentheses => Some("parentheses"),
1758 Self::Brackets => Some("brackets"),
1759 Self::Ampersand
1760 | Self::VerticalLine
1761 | Self::Slash
1762 | Self::Hyphen
1763 | Self::Space
1764 | Self::None
1765 | Self::Custom(_) => None,
1766 };
1767
1768 if let Some(mark) = mark {
1769 let mut map = serializer.serialize_map(Some(1))?;
1770 map.serialize_entry("mark", mark)?;
1771 map.end()
1772 } else {
1773 serializer.serialize_str(self.as_default_str())
1774 }
1775 }
1776}
1777
1778impl<'de> Deserialize<'de> for DelimiterPunctuation {
1779 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1780 #[derive(Deserialize)]
1781 #[serde(deny_unknown_fields)]
1782 struct MarkReference {
1783 mark: String,
1784 }
1785
1786 #[derive(Deserialize)]
1787 #[serde(untagged)]
1788 enum LiteralOrMark {
1789 Literal(String),
1790 Mark(MarkReference),
1791 }
1792
1793 match LiteralOrMark::deserialize(deserializer)? {
1794 LiteralOrMark::Literal(value) => Ok(Self::Custom(value)),
1795 LiteralOrMark::Mark(reference) => match reference.mark.as_str() {
1796 "comma" => Ok(Self::Comma),
1797 "colon" => Ok(Self::Colon),
1798 "semicolon" => Ok(Self::Semicolon),
1799 "period" => Ok(Self::Period),
1800 "parentheses" => Ok(Self::Parentheses),
1801 "brackets" => Ok(Self::Brackets),
1802 other => Err(serde::de::Error::unknown_variant(
1803 other,
1804 &[
1805 "comma",
1806 "colon",
1807 "semicolon",
1808 "period",
1809 "parentheses",
1810 "brackets",
1811 ],
1812 )),
1813 },
1814 }
1815 }
1816}
1817
1818impl DelimiterPunctuation {
1819 #[must_use]
1822 pub fn is_semantic(&self) -> bool {
1823 matches!(
1824 self,
1825 Self::Comma
1826 | Self::Semicolon
1827 | Self::Period
1828 | Self::Colon
1829 | Self::Parentheses
1830 | Self::Brackets
1831 )
1832 }
1833
1834 #[must_use]
1836 pub fn as_default_str(&self) -> &str {
1837 match self {
1838 Self::Comma => ", ",
1839 Self::Semicolon => "; ",
1840 Self::Period => ". ",
1841 Self::Colon => ": ",
1842 Self::Parentheses => "()",
1843 Self::Brackets => "[]",
1844 Self::Ampersand => " & ",
1845 Self::VerticalLine => " | ",
1846 Self::Slash => "/",
1847 Self::Hyphen => "-",
1848 Self::Space => " ",
1849 Self::None => "",
1850 Self::Custom(value) => value,
1851 }
1852 }
1853
1854 pub fn to_string_with_space(&self) -> String {
1858 self.as_default_str().to_string()
1859 }
1860
1861 pub fn from_csl_string(s: &str) -> Self {
1866 if s == " " {
1867 return Self::Space;
1868 }
1869
1870 let trimmed = s.trim();
1871 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
1872 return Self::None;
1873 }
1874
1875 match trimmed {
1876 "," => Self::Comma,
1877 ";" => Self::Semicolon,
1878 "." => Self::Period,
1879 ":" => Self::Colon,
1880 "&" => Self::Ampersand,
1881 "|" => Self::VerticalLine,
1882 "/" => Self::Slash,
1883 "-" => Self::Hyphen,
1884 _ => Self::Custom(s.to_string()),
1885 }
1886 }
1887}
1888
1889impl std::ops::Deref for DelimiterPunctuation {
1890 type Target = str;
1891
1892 fn deref(&self) -> &Self::Target {
1893 self.as_default_str()
1894 }
1895}
1896
1897impl std::fmt::Display for DelimiterPunctuation {
1898 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1899 formatter.write_str(self.as_default_str())
1900 }
1901}
1902
1903impl From<String> for DelimiterPunctuation {
1904 fn from(value: String) -> Self {
1905 Self::Custom(value)
1906 }
1907}
1908
1909impl From<&str> for DelimiterPunctuation {
1910 fn from(value: &str) -> Self {
1911 Self::Custom(value.to_string())
1912 }
1913}
1914
1915#[cfg(test)]
1916#[allow(
1917 clippy::unwrap_used,
1918 clippy::expect_used,
1919 clippy::panic,
1920 clippy::indexing_slicing,
1921 clippy::todo,
1922 clippy::unimplemented,
1923 clippy::unreachable,
1924 clippy::get_unwrap,
1925 reason = "Panicking is acceptable and often desired in tests."
1926)]
1927mod tests {
1928 use super::*;
1929
1930 #[test]
1931 fn test_contributor_deserialization() {
1932 let yaml = r#"
1933contributor: author
1934form: long
1935"#;
1936 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1937 assert_eq!(comp.contributor, ContributorRole::Author);
1938 assert_eq!(comp.form, ContributorForm::Long);
1939 }
1940
1941 #[test]
1942 fn test_contributor_name_order_family_first_except_last_deserialization() {
1943 let yaml = r#"
1944contributor: author
1945form: long
1946name-order: family-first-except-last
1947"#;
1948 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1949 assert_eq!(comp.name_order, Some(NameOrder::FamilyFirstExceptLast));
1950 }
1951
1952 #[test]
1953 fn test_template_component_untagged() {
1954 let yaml = r#"
1955- contributor: author
1956 form: short
1957- date: issued
1958 form: year
1959- title: primary
1960"#;
1961 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1962 assert_eq!(components.len(), 3);
1963
1964 match &components[0] {
1965 TemplateComponent::Contributor(c) => {
1966 assert_eq!(c.contributor, ContributorRole::Author);
1967 }
1968 _ => panic!("Expected Contributor"),
1969 }
1970
1971 match &components[1] {
1972 TemplateComponent::Date(d) => {
1973 assert_eq!(d.date, DateVariable::Issued);
1974 }
1975 _ => panic!("Expected Date"),
1976 }
1977 }
1978
1979 #[test]
1980 fn test_flattened_rendering() {
1981 let yaml = r#"
1983- title: parent-monograph
1984 prefix: "In "
1985 emph: true
1986- date: issued
1987 form: year
1988 wrap: parentheses
1989"#;
1990 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1991 assert_eq!(components.len(), 2);
1992
1993 match &components[0] {
1994 TemplateComponent::Title(t) => {
1995 assert_eq!(t.rendering.prefix.as_deref(), Some("In "));
1996 assert_eq!(t.rendering.emph, Some(true));
1997 }
1998 _ => panic!("Expected Title"),
1999 }
2000
2001 match &components[1] {
2002 TemplateComponent::Date(d) => {
2003 assert_eq!(
2004 d.rendering.wrap,
2005 Some(WrapConfig {
2006 punctuation: WrapPunctuation::Parentheses,
2007 inner_prefix: None,
2008 inner_suffix: None,
2009 })
2010 );
2011 }
2012 _ => panic!("Expected Date"),
2013 }
2014 }
2015
2016 #[test]
2017 fn test_number_variable_custom_normalizes_manual_construction() {
2018 let number = NumberVariable::Custom("Reel Label".to_string());
2019
2020 assert_eq!(number.as_key(), "reel-label");
2021 assert_eq!(
2022 number,
2023 serde_yaml::from_str::<NumberVariable>("reel-label")
2024 .expect("custom number variable should parse")
2025 );
2026 assert_eq!(
2027 serde_json::to_string(&number).expect("custom number variable should serialize"),
2028 "\"reel-label\""
2029 );
2030 }
2031
2032 #[test]
2033 fn test_contributor_with_wrap() {
2034 let yaml = r#"
2035contributor: publisher
2036form: short
2037wrap: parentheses
2038"#;
2039 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
2040 assert_eq!(comp.contributor, ContributorRole::Publisher);
2041 assert_eq!(
2042 comp.rendering.wrap,
2043 Some(WrapConfig {
2044 punctuation: WrapPunctuation::Parentheses,
2045 inner_prefix: None,
2046 inner_suffix: None,
2047 })
2048 );
2049 }
2050
2051 #[test]
2052 fn test_variable_deserialization() {
2053 let yaml = "variable: publisher\n";
2055 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2056 match comp {
2057 TemplateComponent::Variable(v) => {
2058 assert_eq!(v.variable, SimpleVariable::Publisher);
2059 }
2060 _ => panic!("Expected Variable(Publisher), got {:?}", comp),
2061 }
2062 }
2063
2064 #[test]
2065 fn test_message_component_deserialization() {
2066 let yaml = r#"
2067message: pattern.in-container
2068args:
2069 container:
2070 group:
2071 - title: parent-monograph
2072 emph: true
2073text-case: capitalize-first
2074"#;
2075 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2076
2077 match comp {
2078 TemplateComponent::Message(message) => {
2079 assert_eq!(message.message, "pattern.in-container");
2080 assert!(matches!(
2081 message.args.get("container"),
2082 Some(MessageArgSource::Group(group)) if group.group.len() == 1
2083 && matches!(
2084 group.group.first(),
2085 Some(TemplateComponent::Title(title))
2086 if title.title == TitleType::ParentMonograph
2087 && title.rendering.emph == Some(true)
2088 )
2089 ));
2090 assert_eq!(
2091 message.rendering.text_case,
2092 Some(crate::options::titles::TextCase::CapitalizeFirst)
2093 );
2094 }
2095 _ => panic!("Expected Message component, got {comp:?}"),
2096 }
2097 }
2098
2099 #[test]
2100 fn test_term_backed_message_component_deserializes_form() {
2101 let yaml = r#"
2102message: term.in
2103form: long
2104suffix: ":"
2105"#;
2106 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2107
2108 match comp {
2109 TemplateComponent::Message(message) => {
2110 assert_eq!(message.message, "term.in");
2111 assert_eq!(message.form, Some(TermForm::Long));
2112 assert_eq!(message.rendering.suffix.as_deref(), Some(":"));
2113 }
2114 _ => panic!("Expected Message component, got {comp:?}"),
2115 }
2116 }
2117
2118 #[test]
2119 fn test_group_deserializes_term_backed_message_component_with_form() {
2120 let yaml = r#"
2121group:
2122- message: term.in
2123 form: long
2124 suffix: ":"
2125- title: parent-monograph
2126"#;
2127 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2128
2129 match comp {
2130 TemplateComponent::Group(group) => {
2131 assert!(matches!(
2132 group.group.first(),
2133 Some(TemplateComponent::Message(message))
2134 if message.message == "term.in"
2135 && message.form == Some(TermForm::Long)
2136 && message.rendering.suffix.as_deref() == Some(":")
2137 ));
2138 }
2139 _ => panic!("Expected Group component, got {comp:?}"),
2140 }
2141 }
2142
2143 #[test]
2144 fn test_variable_array_parsing() {
2145 let yaml = r#"
2146- variable: doi
2147 prefix: "https://doi.org/"
2148- variable: publisher
2149"#;
2150 let comps: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
2151 assert_eq!(comps.len(), 2);
2152 match &comps[0] {
2153 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Doi),
2154 _ => panic!("Expected Variable for doi, got {:?}", comps[0]),
2155 }
2156 match &comps[1] {
2157 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Publisher),
2158 _ => panic!("Expected Variable for publisher, got {:?}", comps[1]),
2159 }
2160 }
2161
2162 #[test]
2163 fn test_type_selector_default_only_matches_default_context() {
2164 let selector = TypeSelector::Single("default".to_string());
2165 assert!(selector.matches("default"));
2166 assert!(!selector.matches("article-journal"));
2167
2168 let mixed = TypeSelector::Multiple(vec!["default".to_string(), "chapter".to_string()]);
2169 assert!(mixed.matches("default"));
2170 assert!(mixed.matches("chapter"));
2171 assert!(!mixed.matches("book"));
2172 }
2173
2174 #[test]
2175 fn test_template_component_selector_matches_nested_partial_group() {
2176 let component: TemplateComponent = serde_yaml::from_str(
2177 r#"
2178delimiter: ""
2179group:
2180- number: citation-number
2181 wrap:
2182 punctuation: brackets
2183- contributor: author
2184 form: long
2185"#,
2186 )
2187 .unwrap();
2188 let selector = TemplateComponentSelector {
2189 fields: BTreeMap::from([(
2190 "group".to_string(),
2191 serde_json::json!([
2192 { "number": "citation-number" },
2193 { "contributor": "author" }
2194 ]),
2195 )]),
2196 };
2197
2198 assert!(selector.matches(&component));
2199 }
2200
2201 #[test]
2202 fn test_delimiter_from_csl_string_normalizes_none_and_trimmed_values() {
2203 assert_eq!(
2204 DelimiterPunctuation::from_csl_string("none"),
2205 DelimiterPunctuation::None
2206 );
2207 assert_eq!(
2208 DelimiterPunctuation::from_csl_string(" none "),
2209 DelimiterPunctuation::None
2210 );
2211 assert_eq!(
2212 DelimiterPunctuation::from_csl_string(" "),
2213 DelimiterPunctuation::Space
2214 );
2215 assert_eq!(
2216 DelimiterPunctuation::from_csl_string(" : "),
2217 DelimiterPunctuation::Colon
2218 );
2219 }
2220}