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")]
891 pub fallback: Option<Vec<TemplateComponent>>,
892 #[serde(skip_serializing_if = "Option::is_none")]
894 pub label: Option<RoleLabel>,
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub merge: Option<ContributorMerge>,
898 #[serde(skip_serializing_if = "Option::is_none")]
901 pub name_order: Option<NameOrder>,
902 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
904 pub name_form: Option<crate::options::contributors::NameForm>,
905 #[serde(skip_serializing_if = "Option::is_none")]
907 pub delimiter: Option<DelimiterPunctuation>,
908 #[serde(skip_serializing_if = "Option::is_none")]
910 pub sort_separator: Option<String>,
911 #[serde(skip_serializing_if = "Option::is_none")]
913 pub shorten: Option<crate::options::ShortenListOptions>,
914 #[serde(skip_serializing_if = "Option::is_none")]
917 pub and: Option<crate::options::AndOptions>,
918 #[serde(flatten, default)]
919 pub rendering: Rendering,
920 #[serde(skip_serializing_if = "Option::is_none")]
922 pub links: Option<crate::options::LinksConfig>,
923 #[serde(skip_serializing_if = "Option::is_none")]
925 pub gender: Option<GrammaticalGender>,
926
927 #[serde(skip_serializing_if = "Option::is_none")]
929 pub custom: Option<HashMap<String, serde_json::Value>>,
930}
931
932#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
934#[cfg_attr(feature = "schema", derive(JsonSchema))]
935#[serde(rename_all = "kebab-case")]
936pub enum NameOrder {
937 GivenFirst,
939 #[default]
941 FamilyFirst,
942 FamilyFirstOnly,
944 FamilyFirstExceptLast,
949}
950
951#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
953#[cfg_attr(feature = "schema", derive(JsonSchema))]
954#[serde(rename_all = "kebab-case")]
955pub enum ContributorForm {
956 #[default]
957 Long,
958 Short,
959 FamilyOnly,
960 Verb,
961 VerbShort,
962}
963
964crate::str_enum! {
965 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
967 pub enum ContributorRole {
968 #[default] Author = "author",
969 Chair = "chair",
970 Editor = "editor",
971 Translator = "translator",
972 Director = "director",
973 Publisher = "publisher",
974 Recipient = "recipient",
975 Interviewer = "interviewer",
976 Interviewee = "interviewee",
977 Guest = "guest",
978 Performer = "performer",
979 Inventor = "inventor",
980 Counsel = "counsel",
981 Composer = "composer",
982 Writer = "writer",
983 Producer = "producer",
984 CollectionEditor = "collection-editor",
985 ContainerAuthor = "container-author",
986 EditorialDirector = "editorial-director",
987 TextualEditor = "textual-editor",
988 Illustrator = "illustrator",
989 OriginalAuthor = "original-author",
990 ReviewedAuthor = "reviewed-author"
991 }
992}
993
994#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
996#[cfg_attr(feature = "schema", derive(JsonSchema))]
997#[serde(rename_all = "kebab-case", deny_unknown_fields)]
998pub struct TemplateDate {
999 pub date: DateVariable,
1000 pub form: DateForm,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub fallback: Option<Vec<TemplateComponent>>,
1006 #[serde(skip_serializing_if = "Option::is_none")]
1015 pub suppress_note: Option<bool>,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1024 pub suppress_disamb_suffix: Option<bool>,
1025 #[serde(flatten, default)]
1026 pub rendering: Rendering,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub links: Option<crate::options::LinksConfig>,
1030
1031 #[serde(skip_serializing_if = "Option::is_none")]
1033 pub custom: Option<HashMap<String, serde_json::Value>>,
1034}
1035
1036#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1038#[cfg_attr(feature = "schema", derive(JsonSchema))]
1039#[serde(rename_all = "kebab-case")]
1040pub enum DateVariable {
1041 #[default]
1042 Issued,
1043 Accessed,
1044 OriginalPublished,
1045 Submitted,
1046 EventDate,
1047 Copyright,
1050 Printing,
1053}
1054
1055crate::str_enum! {
1056 #[derive(Debug, Default, Clone, PartialEq)]
1058 pub enum DateForm {
1059 #[default]
1060 Year = "year",
1061 YearMonth = "year-month",
1062 Month = "month",
1065 Full = "full",
1066 MonthDay = "month-day",
1067 YearMonthDay = "year-month-day",
1068 DayMonthAbbrYear = "day-month-abbr-year",
1069 MonthAbbrDayYear = "month-abbr-day-year"
1071 }
1072}
1073
1074#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1076#[cfg_attr(feature = "schema", derive(JsonSchema))]
1077#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1078pub struct TemplateTitle {
1079 pub title: TitleType,
1080 #[serde(skip_serializing_if = "Option::is_none")]
1081 pub form: Option<TitleForm>,
1082 #[serde(skip_serializing_if = "Option::is_none")]
1087 pub disambiguate_only: Option<bool>,
1088 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub strip_periods_all: Option<bool>,
1099 #[serde(flatten, default)]
1100 pub rendering: Rendering,
1101 #[serde(skip_serializing_if = "Option::is_none")]
1103 pub links: Option<crate::options::LinksConfig>,
1104
1105 #[serde(skip_serializing_if = "Option::is_none")]
1107 pub custom: Option<HashMap<String, serde_json::Value>>,
1108}
1109
1110#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1112#[cfg_attr(feature = "schema", derive(JsonSchema))]
1113#[serde(rename_all = "kebab-case")]
1114#[non_exhaustive]
1115pub enum TitleType {
1116 #[default]
1118 Primary,
1119 ContainerTitle,
1121 ParentMonograph,
1123 ParentSerial,
1125 CollectionTitle,
1127 Original,
1129}
1130
1131#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1133#[cfg_attr(feature = "schema", derive(JsonSchema))]
1134#[serde(rename_all = "kebab-case")]
1135pub enum TitleForm {
1136 Short,
1137 #[default]
1138 Long,
1139}
1140
1141#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1143#[cfg_attr(feature = "schema", derive(JsonSchema))]
1144#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1145pub struct TemplateNumber {
1146 pub number: NumberVariable,
1147 #[serde(skip_serializing_if = "Option::is_none")]
1148 pub form: Option<NumberForm>,
1149 #[serde(skip_serializing_if = "Option::is_none")]
1150 pub label_form: Option<LabelForm>,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1154 pub show_with_locator: Option<bool>,
1155 #[serde(flatten)]
1156 pub rendering: Rendering,
1157 #[serde(skip_serializing_if = "Option::is_none")]
1159 pub links: Option<crate::options::LinksConfig>,
1160 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub gender: Option<GrammaticalGender>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1176 pub when_numeric: Option<LabelForm>,
1177
1178 #[serde(skip_serializing_if = "Option::is_none")]
1180 pub custom: Option<HashMap<String, serde_json::Value>>,
1181}
1182
1183#[derive(Debug, Default, Clone)]
1190#[non_exhaustive]
1191pub enum NumberVariable {
1192 #[default]
1193 Volume,
1194 Issue,
1195 Pages,
1196 Edition,
1197 ChapterNumber,
1198 CollectionNumber,
1199 NumberOfPages,
1200 NumberOfVolumes,
1201 CitationNumber,
1202 FirstReferenceNoteNumber,
1206 CitationLabel,
1207 Number,
1208 DocketNumber,
1209 PatentNumber,
1210 StandardNumber,
1211 ReportNumber,
1212 PartNumber,
1213 SupplementNumber,
1214 PrintingNumber,
1215 Custom(String),
1217}
1218
1219impl NumberVariable {
1220 #[must_use]
1222 pub fn as_key(&self) -> Cow<'_, str> {
1223 match self {
1224 Self::Volume => Cow::Borrowed("volume"),
1225 Self::Issue => Cow::Borrowed("issue"),
1226 Self::Pages => Cow::Borrowed("pages"),
1227 Self::Edition => Cow::Borrowed("edition"),
1228 Self::ChapterNumber => Cow::Borrowed("chapter-number"),
1229 Self::CollectionNumber => Cow::Borrowed("collection-number"),
1230 Self::NumberOfPages => Cow::Borrowed("number-of-pages"),
1231 Self::NumberOfVolumes => Cow::Borrowed("number-of-volumes"),
1232 Self::CitationNumber => Cow::Borrowed("citation-number"),
1233 Self::FirstReferenceNoteNumber => Cow::Borrowed("first-reference-note-number"),
1234 Self::CitationLabel => Cow::Borrowed("citation-label"),
1235 Self::Number => Cow::Borrowed("number"),
1236 Self::DocketNumber => Cow::Borrowed("docket-number"),
1237 Self::PatentNumber => Cow::Borrowed("patent-number"),
1238 Self::StandardNumber => Cow::Borrowed("standard-number"),
1239 Self::ReportNumber => Cow::Borrowed("report-number"),
1240 Self::PartNumber => Cow::Borrowed("part-number"),
1241 Self::SupplementNumber => Cow::Borrowed("supplement-number"),
1242 Self::PrintingNumber => Cow::Borrowed("printing-number"),
1243 Self::Custom(value) => normalize_kind_key(value)
1244 .map(Cow::Owned)
1245 .unwrap_or_else(|| Cow::Borrowed(value.as_str())),
1246 }
1247 }
1248
1249 fn from_key(value: &str) -> Result<Self, String> {
1250 let canonical = normalize_kind_key(value)
1251 .ok_or_else(|| "number variable must not be empty".to_string())?;
1252 Ok(match canonical.as_str() {
1253 "volume" => Self::Volume,
1254 "issue" => Self::Issue,
1255 "pages" => Self::Pages,
1256 "edition" => Self::Edition,
1257 "chapter-number" => Self::ChapterNumber,
1258 "collection-number" => Self::CollectionNumber,
1259 "number-of-pages" => Self::NumberOfPages,
1260 "number-of-volumes" => Self::NumberOfVolumes,
1261 "citation-number" => Self::CitationNumber,
1262 "first-reference-note-number" => Self::FirstReferenceNoteNumber,
1263 "citation-label" => Self::CitationLabel,
1264 "number" => Self::Number,
1265 "docket-number" => Self::DocketNumber,
1266 "patent-number" => Self::PatentNumber,
1267 "standard-number" => Self::StandardNumber,
1268 "report-number" => Self::ReportNumber,
1269 "part-number" => Self::PartNumber,
1270 "supplement-number" => Self::SupplementNumber,
1271 "printing-number" => Self::PrintingNumber,
1272 _ => Self::Custom(canonical),
1273 })
1274 }
1275}
1276
1277impl PartialEq for NumberVariable {
1278 fn eq(&self, other: &Self) -> bool {
1279 self.as_key().as_ref() == other.as_key().as_ref()
1280 }
1281}
1282
1283impl Eq for NumberVariable {}
1284
1285impl Hash for NumberVariable {
1286 fn hash<H: Hasher>(&self, state: &mut H) {
1287 self.as_key().as_ref().hash(state);
1288 }
1289}
1290
1291impl Serialize for NumberVariable {
1292 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1293 where
1294 S: Serializer,
1295 {
1296 serializer.serialize_str(self.as_key().as_ref())
1297 }
1298}
1299
1300impl<'de> Deserialize<'de> for NumberVariable {
1301 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1302 where
1303 D: Deserializer<'de>,
1304 {
1305 let value = String::deserialize(deserializer)?;
1306 Self::from_key(&value).map_err(serde::de::Error::custom)
1307 }
1308}
1309
1310#[cfg(feature = "schema")]
1311impl JsonSchema for NumberVariable {
1312 fn schema_name() -> std::borrow::Cow<'static, str> {
1313 "NumberVariable".into()
1314 }
1315
1316 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1317 schemars::json_schema!({
1318 "type": "string",
1319 "description": "Known number variable keyword or custom kebab-case identifier."
1320 })
1321 }
1322}
1323
1324#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1326#[cfg_attr(feature = "schema", derive(JsonSchema))]
1327#[serde(rename_all = "lowercase")]
1328pub enum NumberForm {
1329 #[default]
1330 Numeric,
1331 Ordinal,
1332 Roman,
1333}
1334
1335fn normalize_kind_key(value: &str) -> Option<String> {
1336 let mut normalized = String::new();
1337 let mut pending_dash = false;
1338
1339 for ch in value.trim().chars() {
1340 if ch.is_ascii_alphanumeric() {
1341 if pending_dash && !normalized.is_empty() {
1342 normalized.push('-');
1343 }
1344 normalized.push(ch.to_ascii_lowercase());
1345 pending_dash = false;
1346 } else if !normalized.is_empty() {
1347 pending_dash = true;
1348 }
1349 }
1350
1351 if normalized.is_empty() {
1352 None
1353 } else {
1354 Some(normalized)
1355 }
1356}
1357
1358#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1360#[cfg_attr(feature = "schema", derive(JsonSchema))]
1361#[serde(rename_all = "kebab-case")]
1362pub enum LabelForm {
1363 Long,
1364 #[default]
1365 Short,
1366 Symbol,
1367}
1368
1369#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1371#[cfg_attr(feature = "schema", derive(JsonSchema))]
1372#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1373pub struct TemplateVariable {
1374 pub variable: SimpleVariable,
1375 #[serde(flatten)]
1376 pub rendering: Rendering,
1377 #[serde(skip_serializing_if = "Option::is_none")]
1379 pub links: Option<crate::options::LinksConfig>,
1380
1381 #[serde(skip_serializing_if = "Option::is_none")]
1383 pub custom: Option<HashMap<String, serde_json::Value>>,
1384}
1385
1386#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1388#[cfg_attr(feature = "schema", derive(JsonSchema))]
1389#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1390pub struct TemplateIdentifier {
1391 pub identifier: crate::reference::IdentifierName,
1393 #[serde(flatten, default)]
1394 pub rendering: Rendering,
1395}
1396
1397#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1402#[cfg_attr(feature = "schema", derive(JsonSchema))]
1403#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1404pub struct TemplateMessage {
1405 pub message: String,
1407 #[serde(skip_serializing_if = "Option::is_none")]
1409 pub form: Option<TermForm>,
1410 #[serde(skip_serializing_if = "Option::is_none")]
1412 pub gender: Option<GrammaticalGender>,
1413 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1415 pub args: HashMap<String, MessageArgSource>,
1416 #[serde(flatten, default)]
1417 pub rendering: Rendering,
1418
1419 #[serde(skip_serializing_if = "Option::is_none")]
1421 pub custom: Option<HashMap<String, serde_json::Value>>,
1422}
1423
1424#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1426#[cfg_attr(feature = "schema", derive(JsonSchema))]
1427#[serde(untagged)]
1428pub enum MessageArgSource {
1429 Literal { literal: String },
1431 ReferenceType {
1433 #[serde(rename = "reference-type")]
1434 reference_type: MessageReferenceTypeSource,
1435 },
1436 Carrier { carrier: MessageCarrierSource },
1438 Contributor(Box<TemplateContributor>),
1440 Date(TemplateDate),
1442 Group(TemplateGroup),
1444 Title(TemplateTitle),
1446 Number(TemplateNumber),
1448 Variable(TemplateVariable),
1450 Term(TemplateTerm),
1452}
1453
1454impl MessageArgSource {
1455 #[must_use]
1458 pub fn as_template_component(&self) -> Option<TemplateComponent> {
1459 match self {
1460 Self::Literal { .. } | Self::ReferenceType { .. } | Self::Carrier { .. } => None,
1461 Self::Contributor(component) => {
1462 Some(TemplateComponent::Contributor(component.as_ref().clone()))
1463 }
1464 Self::Date(component) => Some(TemplateComponent::Date(component.clone())),
1465 Self::Group(component) => Some(TemplateComponent::Group(component.clone())),
1466 Self::Title(component) => Some(TemplateComponent::Title(component.clone())),
1467 Self::Number(component) => Some(TemplateComponent::Number(component.clone())),
1468 Self::Variable(component) => Some(TemplateComponent::Variable(component.clone())),
1469 Self::Term(component) => Some(TemplateComponent::Term(component.clone())),
1470 }
1471 }
1472}
1473
1474#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
1476#[cfg_attr(feature = "schema", derive(JsonSchema))]
1477#[serde(rename_all = "kebab-case")]
1478pub enum MessageReferenceTypeSource {
1479 Key,
1481}
1482
1483#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1485#[cfg_attr(feature = "schema", derive(JsonSchema))]
1486#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1487pub struct MessageCarrierSource {
1488 pub online: String,
1490 pub absent: String,
1492}
1493
1494#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1501#[cfg_attr(feature = "schema", derive(JsonSchema))]
1502#[serde(rename_all = "kebab-case")]
1503#[non_exhaustive]
1504pub enum SimpleVariable {
1505 #[default]
1506 Doi,
1507 Isbn,
1508 Issn,
1509 Url,
1510 Pmid,
1511 Pmcid,
1512 Abstract,
1513 Note,
1514 Annote,
1515 Keyword,
1516 Genre,
1517 RawGenre,
1518 Medium,
1519 RawMedium,
1520 Source,
1521 Status,
1522 Archive,
1523 ArchiveLocation,
1524 ArchiveName,
1525 ArchivePlace,
1526 ArchiveCollection,
1527 ArchiveCollectionId,
1528 ArchiveSeries,
1529 ArchiveBox,
1530 ArchiveFolder,
1531 ArchiveItem,
1532 ArchiveUrl,
1533 EprintId,
1534 EprintServer,
1535 EprintClass,
1536 Publisher,
1537 PublisherPlace,
1538 OriginalPublisher,
1539 OriginalPublisherPlace,
1540 EventTitle,
1541 EventPlace,
1542 Dimensions,
1543 References,
1544 Scale,
1545 Version,
1546 VolumeTitle,
1547 Locator,
1548 ContainerTitleShort,
1549 Authority,
1550 Code,
1551 Reporter,
1552 Page,
1553 Section,
1554 Volume,
1555 Number,
1556 DocketNumber,
1557 PatentNumber,
1558 StandardNumber,
1559 ReportNumber,
1560 AdsBibcode,
1561}
1562
1563#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1565#[cfg_attr(feature = "schema", derive(JsonSchema))]
1566#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1567pub struct TemplateTerm {
1568 pub term: GeneralTerm,
1570 #[serde(skip_serializing_if = "Option::is_none")]
1572 pub form: Option<TermForm>,
1573 #[serde(skip_serializing_if = "Option::is_none")]
1575 pub gender: Option<GrammaticalGender>,
1576 #[serde(flatten, default)]
1577 pub rendering: Rendering,
1578
1579 #[serde(skip_serializing_if = "Option::is_none")]
1581 pub custom: Option<HashMap<String, serde_json::Value>>,
1582}
1583
1584#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
1591#[cfg_attr(feature = "schema", derive(JsonSchema))]
1592#[serde(rename_all = "kebab-case")]
1593#[non_exhaustive]
1594pub enum TypeLabelSource {
1595 #[default]
1598 ReferenceType,
1599}
1600
1601#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1611#[cfg_attr(feature = "schema", derive(JsonSchema))]
1612#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1613pub struct TemplateTypeLabel {
1614 #[serde(rename = "type-label")]
1616 pub type_label: TypeLabelSource,
1617 #[serde(flatten, default)]
1618 pub rendering: Rendering,
1619
1620 #[serde(skip_serializing_if = "Option::is_none")]
1622 pub custom: Option<HashMap<String, serde_json::Value>>,
1623}
1624
1625#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1628#[cfg_attr(feature = "schema", derive(JsonSchema))]
1629#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1630pub struct TemplateGroup {
1631 pub group: Vec<TemplateComponent>,
1632 #[serde(skip_serializing_if = "Option::is_none")]
1634 pub render_when: Option<TemplateGroupCondition>,
1635 #[serde(skip_serializing_if = "Option::is_none")]
1636 pub delimiter: Option<DelimiterPunctuation>,
1637 #[serde(flatten, default)]
1638 pub rendering: Rendering,
1639
1640 #[serde(skip_serializing_if = "Option::is_none")]
1642 pub custom: Option<HashMap<String, serde_json::Value>>,
1643}
1644
1645#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1647#[cfg_attr(feature = "schema", derive(JsonSchema))]
1648#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1649pub struct TemplateGroupCondition {
1650 #[serde(skip_serializing_if = "Option::is_none")]
1652 pub field_present: Option<TemplateConditionField>,
1653 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub field_absent: Option<TemplateConditionField>,
1656}
1657
1658#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1660#[cfg_attr(feature = "schema", derive(JsonSchema))]
1661#[serde(rename_all = "kebab-case")]
1662pub enum TemplateConditionField {
1663 Author,
1665 Editor,
1667 Recipient,
1669 Translator,
1671 Title,
1673 CollectionTitle,
1675 Issued,
1677 OriginalPublished,
1679 Publisher,
1681 OriginalPublisher,
1683 OriginalPublisherPlace,
1685 OriginalTitle,
1687 Doi,
1689 Genre,
1691 Archive,
1693 ArchiveLocation,
1695 VolumeOrIssue,
1701}
1702
1703#[derive(Debug, Default, Clone, PartialEq)]
1709pub enum DelimiterPunctuation {
1710 #[default]
1712 Comma,
1713 Semicolon,
1715 Period,
1717 Colon,
1719 Parentheses,
1721 Brackets,
1723 Ampersand,
1725 VerticalLine,
1727 Slash,
1729 Hyphen,
1731 Space,
1733 None,
1735 Custom(String),
1737}
1738
1739#[cfg(feature = "schema")]
1740impl JsonSchema for DelimiterPunctuation {
1741 fn schema_name() -> std::borrow::Cow<'static, str> {
1742 "DelimiterPunctuation".into()
1743 }
1744
1745 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1746 schemars::json_schema!({
1747 "oneOf": [
1748 {
1749 "type": "string",
1750 "description": "Literal punctuation or text."
1751 },
1752 {
1753 "type": "object",
1754 "additionalProperties": false,
1755 "required": ["mark"],
1756 "properties": {
1757 "mark": {
1758 "type": "string",
1759 "enum": [
1760 "comma",
1761 "colon",
1762 "semicolon",
1763 "period",
1764 "parentheses",
1765 "brackets"
1766 ]
1767 }
1768 }
1769 }
1770 ],
1771 "description": "Literal text or an explicit semantic punctuation mark."
1772 })
1773 }
1774}
1775
1776impl Serialize for DelimiterPunctuation {
1777 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1778 use serde::ser::SerializeMap as _;
1779
1780 let mark = match self {
1781 Self::Comma => Some("comma"),
1782 Self::Semicolon => Some("semicolon"),
1783 Self::Period => Some("period"),
1784 Self::Colon => Some("colon"),
1785 Self::Parentheses => Some("parentheses"),
1786 Self::Brackets => Some("brackets"),
1787 Self::Ampersand
1788 | Self::VerticalLine
1789 | Self::Slash
1790 | Self::Hyphen
1791 | Self::Space
1792 | Self::None
1793 | Self::Custom(_) => None,
1794 };
1795
1796 if let Some(mark) = mark {
1797 let mut map = serializer.serialize_map(Some(1))?;
1798 map.serialize_entry("mark", mark)?;
1799 map.end()
1800 } else {
1801 serializer.serialize_str(self.as_default_str())
1802 }
1803 }
1804}
1805
1806impl<'de> Deserialize<'de> for DelimiterPunctuation {
1807 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1808 #[derive(Deserialize)]
1809 #[serde(deny_unknown_fields)]
1810 struct MarkReference {
1811 mark: String,
1812 }
1813
1814 #[derive(Deserialize)]
1815 #[serde(untagged)]
1816 enum LiteralOrMark {
1817 Literal(String),
1818 Mark(MarkReference),
1819 }
1820
1821 match LiteralOrMark::deserialize(deserializer)? {
1822 LiteralOrMark::Literal(value) => Ok(Self::Custom(value)),
1823 LiteralOrMark::Mark(reference) => match reference.mark.as_str() {
1824 "comma" => Ok(Self::Comma),
1825 "colon" => Ok(Self::Colon),
1826 "semicolon" => Ok(Self::Semicolon),
1827 "period" => Ok(Self::Period),
1828 "parentheses" => Ok(Self::Parentheses),
1829 "brackets" => Ok(Self::Brackets),
1830 other => Err(serde::de::Error::unknown_variant(
1831 other,
1832 &[
1833 "comma",
1834 "colon",
1835 "semicolon",
1836 "period",
1837 "parentheses",
1838 "brackets",
1839 ],
1840 )),
1841 },
1842 }
1843 }
1844}
1845
1846impl DelimiterPunctuation {
1847 #[must_use]
1850 pub fn is_semantic(&self) -> bool {
1851 matches!(
1852 self,
1853 Self::Comma
1854 | Self::Semicolon
1855 | Self::Period
1856 | Self::Colon
1857 | Self::Parentheses
1858 | Self::Brackets
1859 )
1860 }
1861
1862 #[must_use]
1864 pub fn as_default_str(&self) -> &str {
1865 match self {
1866 Self::Comma => ", ",
1867 Self::Semicolon => "; ",
1868 Self::Period => ". ",
1869 Self::Colon => ": ",
1870 Self::Parentheses => "()",
1871 Self::Brackets => "[]",
1872 Self::Ampersand => " & ",
1873 Self::VerticalLine => " | ",
1874 Self::Slash => "/",
1875 Self::Hyphen => "-",
1876 Self::Space => " ",
1877 Self::None => "",
1878 Self::Custom(value) => value,
1879 }
1880 }
1881
1882 pub fn to_string_with_space(&self) -> String {
1886 self.as_default_str().to_string()
1887 }
1888
1889 pub fn from_csl_string(s: &str) -> Self {
1894 if s == " " {
1895 return Self::Space;
1896 }
1897
1898 let trimmed = s.trim();
1899 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
1900 return Self::None;
1901 }
1902
1903 match trimmed {
1904 "," => Self::Comma,
1905 ";" => Self::Semicolon,
1906 "." => Self::Period,
1907 ":" => Self::Colon,
1908 "&" => Self::Ampersand,
1909 "|" => Self::VerticalLine,
1910 "/" => Self::Slash,
1911 "-" => Self::Hyphen,
1912 _ => Self::Custom(s.to_string()),
1913 }
1914 }
1915}
1916
1917impl std::ops::Deref for DelimiterPunctuation {
1918 type Target = str;
1919
1920 fn deref(&self) -> &Self::Target {
1921 self.as_default_str()
1922 }
1923}
1924
1925impl std::fmt::Display for DelimiterPunctuation {
1926 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1927 formatter.write_str(self.as_default_str())
1928 }
1929}
1930
1931impl From<String> for DelimiterPunctuation {
1932 fn from(value: String) -> Self {
1933 Self::Custom(value)
1934 }
1935}
1936
1937impl From<&str> for DelimiterPunctuation {
1938 fn from(value: &str) -> Self {
1939 Self::Custom(value.to_string())
1940 }
1941}
1942
1943#[cfg(test)]
1944#[allow(
1945 clippy::unwrap_used,
1946 clippy::expect_used,
1947 clippy::panic,
1948 clippy::indexing_slicing,
1949 clippy::todo,
1950 clippy::unimplemented,
1951 clippy::unreachable,
1952 clippy::get_unwrap,
1953 reason = "Panicking is acceptable and often desired in tests."
1954)]
1955mod tests {
1956 use super::*;
1957
1958 #[test]
1959 fn test_contributor_deserialization() {
1960 let yaml = r#"
1961contributor: author
1962form: long
1963"#;
1964 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1965 assert_eq!(comp.contributor, ContributorRole::Author);
1966 assert_eq!(comp.form, ContributorForm::Long);
1967 }
1968
1969 #[test]
1970 fn test_contributor_name_order_family_first_except_last_deserialization() {
1971 let yaml = r#"
1972contributor: author
1973form: long
1974name-order: family-first-except-last
1975"#;
1976 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1977 assert_eq!(comp.name_order, Some(NameOrder::FamilyFirstExceptLast));
1978 }
1979
1980 #[test]
1981 fn test_template_component_untagged() {
1982 let yaml = r#"
1983- contributor: author
1984 form: short
1985- date: issued
1986 form: year
1987- title: primary
1988"#;
1989 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1990 assert_eq!(components.len(), 3);
1991
1992 match &components[0] {
1993 TemplateComponent::Contributor(c) => {
1994 assert_eq!(c.contributor, ContributorRole::Author);
1995 }
1996 _ => panic!("Expected Contributor"),
1997 }
1998
1999 match &components[1] {
2000 TemplateComponent::Date(d) => {
2001 assert_eq!(d.date, DateVariable::Issued);
2002 }
2003 _ => panic!("Expected Date"),
2004 }
2005 }
2006
2007 #[test]
2008 fn test_flattened_rendering() {
2009 let yaml = r#"
2011- title: parent-monograph
2012 prefix: "In "
2013 emph: true
2014- date: issued
2015 form: year
2016 wrap: parentheses
2017"#;
2018 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
2019 assert_eq!(components.len(), 2);
2020
2021 match &components[0] {
2022 TemplateComponent::Title(t) => {
2023 assert_eq!(t.rendering.prefix.as_deref(), Some("In "));
2024 assert_eq!(t.rendering.emph, Some(true));
2025 }
2026 _ => panic!("Expected Title"),
2027 }
2028
2029 match &components[1] {
2030 TemplateComponent::Date(d) => {
2031 assert_eq!(
2032 d.rendering.wrap,
2033 Some(WrapConfig {
2034 punctuation: WrapPunctuation::Parentheses,
2035 inner_prefix: None,
2036 inner_suffix: None,
2037 })
2038 );
2039 }
2040 _ => panic!("Expected Date"),
2041 }
2042 }
2043
2044 #[test]
2045 fn test_number_variable_custom_normalizes_manual_construction() {
2046 let number = NumberVariable::Custom("Reel Label".to_string());
2047
2048 assert_eq!(number.as_key(), "reel-label");
2049 assert_eq!(
2050 number,
2051 serde_yaml::from_str::<NumberVariable>("reel-label")
2052 .expect("custom number variable should parse")
2053 );
2054 assert_eq!(
2055 serde_json::to_string(&number).expect("custom number variable should serialize"),
2056 "\"reel-label\""
2057 );
2058 }
2059
2060 #[test]
2061 fn test_contributor_with_wrap() {
2062 let yaml = r#"
2063contributor: publisher
2064form: short
2065wrap: parentheses
2066"#;
2067 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
2068 assert_eq!(comp.contributor, ContributorRole::Publisher);
2069 assert_eq!(
2070 comp.rendering.wrap,
2071 Some(WrapConfig {
2072 punctuation: WrapPunctuation::Parentheses,
2073 inner_prefix: None,
2074 inner_suffix: None,
2075 })
2076 );
2077 }
2078
2079 #[test]
2080 fn test_variable_deserialization() {
2081 let yaml = "variable: publisher\n";
2083 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2084 match comp {
2085 TemplateComponent::Variable(v) => {
2086 assert_eq!(v.variable, SimpleVariable::Publisher);
2087 }
2088 _ => panic!("Expected Variable(Publisher), got {:?}", comp),
2089 }
2090 }
2091
2092 #[test]
2093 fn test_message_component_deserialization() {
2094 let yaml = r#"
2095message: pattern.in-container
2096args:
2097 container:
2098 group:
2099 - title: parent-monograph
2100 emph: true
2101text-case: capitalize-first
2102"#;
2103 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2104
2105 match comp {
2106 TemplateComponent::Message(message) => {
2107 assert_eq!(message.message, "pattern.in-container");
2108 assert!(matches!(
2109 message.args.get("container"),
2110 Some(MessageArgSource::Group(group)) if group.group.len() == 1
2111 && matches!(
2112 group.group.first(),
2113 Some(TemplateComponent::Title(title))
2114 if title.title == TitleType::ParentMonograph
2115 && title.rendering.emph == Some(true)
2116 )
2117 ));
2118 assert_eq!(
2119 message.rendering.text_case,
2120 Some(crate::options::titles::TextCase::CapitalizeFirst)
2121 );
2122 }
2123 _ => panic!("Expected Message component, got {comp:?}"),
2124 }
2125 }
2126
2127 #[test]
2128 fn test_term_backed_message_component_deserializes_form() {
2129 let yaml = r#"
2130message: term.in
2131form: long
2132suffix: ":"
2133"#;
2134 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2135
2136 match comp {
2137 TemplateComponent::Message(message) => {
2138 assert_eq!(message.message, "term.in");
2139 assert_eq!(message.form, Some(TermForm::Long));
2140 assert_eq!(message.rendering.suffix.as_deref(), Some(":"));
2141 }
2142 _ => panic!("Expected Message component, got {comp:?}"),
2143 }
2144 }
2145
2146 #[test]
2147 fn test_group_deserializes_term_backed_message_component_with_form() {
2148 let yaml = r#"
2149group:
2150- message: term.in
2151 form: long
2152 suffix: ":"
2153- title: parent-monograph
2154"#;
2155 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2156
2157 match comp {
2158 TemplateComponent::Group(group) => {
2159 assert!(matches!(
2160 group.group.first(),
2161 Some(TemplateComponent::Message(message))
2162 if message.message == "term.in"
2163 && message.form == Some(TermForm::Long)
2164 && message.rendering.suffix.as_deref() == Some(":")
2165 ));
2166 }
2167 _ => panic!("Expected Group component, got {comp:?}"),
2168 }
2169 }
2170
2171 #[test]
2172 fn test_variable_array_parsing() {
2173 let yaml = r#"
2174- variable: doi
2175 prefix: "https://doi.org/"
2176- variable: publisher
2177"#;
2178 let comps: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
2179 assert_eq!(comps.len(), 2);
2180 match &comps[0] {
2181 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Doi),
2182 _ => panic!("Expected Variable for doi, got {:?}", comps[0]),
2183 }
2184 match &comps[1] {
2185 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Publisher),
2186 _ => panic!("Expected Variable for publisher, got {:?}", comps[1]),
2187 }
2188 }
2189
2190 #[test]
2191 fn test_type_selector_default_only_matches_default_context() {
2192 let selector = TypeSelector::Single("default".to_string());
2193 assert!(selector.matches("default"));
2194 assert!(!selector.matches("article-journal"));
2195
2196 let mixed = TypeSelector::Multiple(vec!["default".to_string(), "chapter".to_string()]);
2197 assert!(mixed.matches("default"));
2198 assert!(mixed.matches("chapter"));
2199 assert!(!mixed.matches("book"));
2200 }
2201
2202 #[test]
2203 fn test_template_component_selector_matches_nested_partial_group() {
2204 let component: TemplateComponent = serde_yaml::from_str(
2205 r#"
2206delimiter: ""
2207group:
2208- number: citation-number
2209 wrap:
2210 punctuation: brackets
2211- contributor: author
2212 form: long
2213"#,
2214 )
2215 .unwrap();
2216 let selector = TemplateComponentSelector {
2217 fields: BTreeMap::from([(
2218 "group".to_string(),
2219 serde_json::json!([
2220 { "number": "citation-number" },
2221 { "contributor": "author" }
2222 ]),
2223 )]),
2224 };
2225
2226 assert!(selector.matches(&component));
2227 }
2228
2229 #[test]
2230 fn test_delimiter_from_csl_string_normalizes_none_and_trimmed_values() {
2231 assert_eq!(
2232 DelimiterPunctuation::from_csl_string("none"),
2233 DelimiterPunctuation::None
2234 );
2235 assert_eq!(
2236 DelimiterPunctuation::from_csl_string(" none "),
2237 DelimiterPunctuation::None
2238 );
2239 assert_eq!(
2240 DelimiterPunctuation::from_csl_string(" "),
2241 DelimiterPunctuation::Space
2242 );
2243 assert_eq!(
2244 DelimiterPunctuation::from_csl_string(" : "),
2245 DelimiterPunctuation::Colon
2246 );
2247 }
2248}