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