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::{
53 inherited_variant_context, resolve_style_template_variants,
54 resolve_style_template_variants_with_overlay,
55};
56
57pub fn resolve_local_template_variants(
71 style: &mut crate::Style,
72) -> Result<(), crate::ResolutionError> {
73 resolution::resolve_style_template_variants(style, None)
74}
75
76pub type Template = Vec<TemplateComponent>;
78
79pub type TemplateVariants = IndexMap<TypeSelector, TemplateVariant>;
81
82pub type LocalizedTemplateVariants = IndexMap<TypeSelector, Template>;
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[cfg_attr(feature = "schema", derive(JsonSchema))]
91#[serde(rename_all = "kebab-case")]
92pub enum VerticalAlign {
93 Baseline,
95 Superscript,
97 Subscript,
99}
100
101#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
111#[cfg_attr(feature = "schema", derive(JsonSchema))]
112#[serde(rename_all = "kebab-case", default)]
113pub struct Rendering {
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub text_case: Option<crate::options::titles::TextCase>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub emph: Option<bool>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub quote: Option<bool>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub strong: Option<bool>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub small_caps: Option<bool>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub vertical_align: Option<VerticalAlign>,
132 #[serde(skip_serializing_if = "Option::is_none")]
135 pub prefix: Option<DelimiterPunctuation>,
136 #[serde(skip_serializing_if = "Option::is_none")]
139 pub suffix: Option<DelimiterPunctuation>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub wrap: Option<WrapConfig>,
143 #[serde(skip_serializing_if = "Option::is_none")]
146 pub suppress: Option<bool>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub initialize_with: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
152 pub name_form: Option<crate::options::contributors::NameForm>,
153 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
155 pub strip_periods: Option<bool>,
156}
157
158impl Rendering {
159 pub fn merge(&mut self, other: &Rendering) {
163 crate::merge_options!(
164 self,
165 other,
166 text_case,
167 emph,
168 quote,
169 strong,
170 small_caps,
171 vertical_align,
172 prefix,
173 suffix,
174 wrap,
175 suppress,
176 initialize_with,
177 name_form,
178 strip_periods,
179 );
180 }
181}
182
183#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
185#[cfg_attr(feature = "schema", derive(JsonSchema))]
186#[serde(rename_all = "kebab-case")]
187pub enum WrapPunctuation {
188 #[default]
189 Parentheses,
190 Brackets,
191 Quotes,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize)]
199#[serde(rename_all = "kebab-case")]
200pub struct WrapConfig {
201 pub punctuation: WrapPunctuation,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub inner_prefix: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub inner_suffix: Option<String>,
209}
210
211#[cfg(feature = "schema")]
217impl JsonSchema for WrapConfig {
218 fn schema_name() -> std::borrow::Cow<'static, str> {
219 "WrapConfig".into()
220 }
221
222 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
223 let punctuation = generator.subschema_for::<WrapPunctuation>();
224 schemars::json_schema!({
225 "description": "Wrapping punctuation, as a bare punctuation name or \
226 a mapping with optional inner affixes.",
227 "oneOf": [
228 punctuation,
229 {
230 "type": "object",
231 "properties": {
232 "punctuation": generator.subschema_for::<WrapPunctuation>(),
233 "inner-prefix": { "type": ["string", "null"] },
234 "inner-suffix": { "type": ["string", "null"] },
235 },
236 "required": ["punctuation"],
237 "additionalProperties": false,
238 },
239 ],
240 })
241 }
242}
243
244impl<'de> serde::Deserialize<'de> for WrapConfig {
245 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
246 struct WrapConfigVisitor;
247
248 impl<'de> serde::de::Visitor<'de> for WrapConfigVisitor {
249 type Value = WrapConfig;
250
251 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
252 write!(
253 f,
254 "a wrap punctuation string or a mapping with a 'punctuation' key"
255 )
256 }
257
258 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<WrapConfig, E> {
259 let punctuation = match v {
260 "parentheses" => WrapPunctuation::Parentheses,
261 "brackets" => WrapPunctuation::Brackets,
262 "quotes" => WrapPunctuation::Quotes,
263 other => {
264 return Err(E::unknown_variant(
265 other,
266 &["parentheses", "brackets", "quotes"],
267 ));
268 }
269 };
270 Ok(WrapConfig {
271 punctuation,
272 inner_prefix: None,
273 inner_suffix: None,
274 })
275 }
276
277 fn visit_map<A: serde::de::MapAccess<'de>>(
278 self,
279 mut map: A,
280 ) -> Result<WrapConfig, A::Error> {
281 let mut punctuation: Option<WrapPunctuation> = None;
282 let mut inner_prefix: Option<String> = None;
283 let mut inner_suffix: Option<String> = None;
284
285 while let Some(key) = map.next_key::<String>()? {
286 match key.as_str() {
287 "punctuation" => {
288 punctuation = Some(map.next_value()?);
289 }
290 "inner-prefix" => {
291 inner_prefix = Some(map.next_value()?);
292 }
293 "inner-suffix" => {
294 inner_suffix = Some(map.next_value()?);
295 }
296 other => {
297 return Err(serde::de::Error::unknown_field(
298 other,
299 &["punctuation", "inner-prefix", "inner-suffix"],
300 ));
301 }
302 }
303 }
304
305 let punctuation =
306 punctuation.ok_or_else(|| serde::de::Error::missing_field("punctuation"))?;
307 Ok(WrapConfig {
308 punctuation,
309 inner_prefix,
310 inner_suffix,
311 })
312 }
313 }
314
315 deserializer.deserialize_any(WrapConfigVisitor)
316 }
317}
318
319impl From<WrapPunctuation> for WrapConfig {
320 fn from(punctuation: WrapPunctuation) -> Self {
321 WrapConfig {
322 punctuation,
323 inner_prefix: None,
324 inner_suffix: None,
325 }
326 }
327}
328
329pub use crate::options::KNOWN_REFERENCE_TYPE_NAMES as VALID_TYPE_NAMES;
335
336pub fn validate_type_name(s: &str) -> bool {
346 crate::options::ReferenceTypeName::is_known_canonical(s) || s == "default"
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Hash)]
352pub enum TypeSelector {
353 Single(String),
354 Multiple(Vec<String>),
355}
356
357#[cfg(feature = "schema")]
363fn selector_atom_names() -> Vec<serde_json::Value> {
364 VALID_TYPE_NAMES
365 .iter()
366 .copied()
367 .chain(std::iter::once("default"))
368 .map(|name| serde_json::Value::String(name.to_string()))
369 .collect()
370}
371
372#[cfg(feature = "schema")]
377#[must_use]
378pub fn reference_type_name_schema() -> schemars::Schema {
379 let names: Vec<serde_json::Value> = VALID_TYPE_NAMES
380 .iter()
381 .map(|name| serde_json::Value::String((*name).to_string()))
382 .collect();
383 schemars::json_schema!({
384 "type": "string",
385 "enum": names,
386 })
387}
388
389#[cfg(feature = "schema")]
396#[must_use]
397pub fn type_selector_name_schema() -> schemars::Schema {
398 let alternation = VALID_TYPE_NAMES
399 .iter()
400 .copied()
401 .chain(std::iter::once("default"))
402 .map(regex_escape)
403 .collect::<Vec<_>>()
404 .join("|");
405 let atom = format!("(?:{alternation})");
406 schemars::json_schema!({
407 "type": "string",
408 "pattern": format!("^\\s*{atom}(?:\\s*,\\s*{atom})*\\s*$"),
409 })
410}
411
412#[cfg(feature = "schema")]
417fn regex_escape(value: &str) -> String {
418 value
419 .chars()
420 .flat_map(|c| {
421 let escape = matches!(
422 c,
423 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
424 );
425 escape.then_some('\\').into_iter().chain(std::iter::once(c))
426 })
427 .collect()
428}
429
430#[cfg(feature = "schema")]
437pub fn type_keyed_map_schema<V: JsonSchema>(
438 generator: &mut schemars::SchemaGenerator,
439) -> schemars::Schema {
440 let value = generator.subschema_for::<V>();
441 schemars::json_schema!({
442 "type": ["object", "null"],
443 "propertyNames": type_selector_name_schema(),
444 "additionalProperties": value,
445 })
446}
447
448#[cfg(feature = "schema")]
454pub fn reference_type_keyed_map_schema<V: JsonSchema>(
455 generator: &mut schemars::SchemaGenerator,
456) -> schemars::Schema {
457 let value = generator.subschema_for::<V>();
458 schemars::json_schema!({
459 "type": ["object", "null"],
460 "propertyNames": reference_type_name_schema(),
461 "additionalProperties": value,
462 })
463}
464
465#[cfg(feature = "schema")]
466impl JsonSchema for TypeSelector {
467 fn schema_name() -> std::borrow::Cow<'static, str> {
468 "TypeSelector".into()
469 }
470
471 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
472 schemars::json_schema!({
473 "description": "A reference type name, a comma-joined list of them, \
474 or a sequence of them.",
475 "oneOf": [
476 type_selector_name_schema(),
477 {
478 "type": "array",
479 "items": { "type": "string", "enum": selector_atom_names() },
480 "minItems": 1,
481 },
482 ],
483 })
484 }
485}
486
487impl Serialize for TypeSelector {
488 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
489 where
490 S: serde::Serializer,
491 {
492 serializer.serialize_str(&self.to_string())
493 }
494}
495
496impl<'de> Deserialize<'de> for TypeSelector {
497 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
498 where
499 D: serde::Deserializer<'de>,
500 {
501 struct Visitor;
502 impl<'de> serde::de::Visitor<'de> for Visitor {
503 type Value = TypeSelector;
504
505 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
506 formatter.write_str("a string or a sequence of strings")
507 }
508
509 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
510 where
511 E: serde::de::Error,
512 {
513 v.parse().map_err(E::custom)
514 }
515
516 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
517 where
518 A: serde::de::SeqAccess<'de>,
519 {
520 let mut types = Vec::new();
521 while let Some(t) = seq.next_element::<String>()? {
522 types.push(t);
523 }
524 if types.len() == 1 {
525 Ok(TypeSelector::Single(types.remove(0)))
526 } else {
527 Ok(TypeSelector::Multiple(types))
528 }
529 }
530 }
531 deserializer.deserialize_any(Visitor)
532 }
533}
534
535impl std::fmt::Display for TypeSelector {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 match self {
538 TypeSelector::Single(s) => write!(f, "{s}"),
539 TypeSelector::Multiple(types) => write!(f, "{}", types.join(",")),
540 }
541 }
542}
543
544impl std::str::FromStr for TypeSelector {
545 type Err = std::convert::Infallible;
546
547 fn from_str(s: &str) -> Result<Self, Self::Err> {
548 if s.contains(',') {
549 Ok(TypeSelector::Multiple(
550 s.split(',').map(|t| t.trim().to_string()).collect(),
551 ))
552 } else {
553 Ok(TypeSelector::Single(s.to_string()))
554 }
555 }
556}
557
558impl TypeSelector {
559 #[must_use]
564 pub fn is_default(&self) -> bool {
565 matches!(self, Self::Single(value) if value == "default")
566 }
567
568 pub fn matches(&self, ref_type: &str) -> bool {
579 let normalized_ref = ref_type.replace('_', "-");
580 let base_ref = normalized_ref
581 .split_once('+')
582 .map(|(base, _)| base)
583 .unwrap_or(&normalized_ref);
584 let eq = |s: &str| -> bool {
585 s == ref_type
586 || s == normalized_ref
587 || s == base_ref
588 || (s == "default" && ref_type == "default")
589 };
590 match self {
591 TypeSelector::Single(s) => eq(s),
592 TypeSelector::Multiple(types) => types.iter().any(|t| eq(t)),
593 }
594 }
595
596 pub fn unknown_type_names(&self) -> Vec<&str> {
601 match self {
602 TypeSelector::Single(s) => {
603 if validate_type_name(s) {
604 vec![]
605 } else {
606 vec![s.as_str()]
607 }
608 }
609 TypeSelector::Multiple(types) => types
610 .iter()
611 .filter(|s| !validate_type_name(s))
612 .map(|s| s.as_str())
613 .collect(),
614 }
615 }
616}
617
618#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
622#[cfg_attr(feature = "schema", derive(JsonSchema))]
623#[serde(untagged)]
624#[non_exhaustive]
625pub enum TemplateComponent {
626 Contributor(TemplateContributor),
627 Date(TemplateDate),
628 Title(TemplateTitle),
629 Number(TemplateNumber),
630 Identifier(TemplateIdentifier),
631 Variable(TemplateVariable),
632 Message(TemplateMessage),
633 Group(TemplateGroup),
634 Term(TemplateTerm),
635 TypeLabel(TemplateTypeLabel),
636}
637
638impl Default for TemplateComponent {
639 fn default() -> Self {
640 TemplateComponent::Variable(TemplateVariable::default())
641 }
642}
643
644impl TemplateComponent {
645 pub fn rendering(&self) -> &Rendering {
649 crate::dispatch_component!(self, |inner| &inner.rendering)
650 }
651
652 pub fn rendering_mut(&mut self) -> &mut Rendering {
657 crate::dispatch_component!(self, |inner| &mut inner.rendering)
658 }
659}
660
661#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
663#[cfg_attr(feature = "schema", derive(JsonSchema))]
664#[serde(untagged)]
665pub enum TemplateVariant {
666 Full(Vec<TemplateComponent>),
668 Diff(TemplateVariantDiff),
670}
671
672impl TemplateVariant {
673 #[must_use]
675 pub fn as_template(&self) -> Option<&[TemplateComponent]> {
676 match self {
677 Self::Full(template) => Some(template.as_slice()),
678 Self::Diff(_) => None,
679 }
680 }
681
682 pub fn as_template_mut(&mut self) -> Option<&mut Vec<TemplateComponent>> {
684 match self {
685 Self::Full(template) => Some(template),
686 Self::Diff(_) => None,
687 }
688 }
689
690 #[must_use]
692 pub fn into_template(self) -> Option<Vec<TemplateComponent>> {
693 match self {
694 Self::Full(template) => Some(template),
695 Self::Diff(_) => None,
696 }
697 }
698}
699
700impl From<Vec<TemplateComponent>> for TemplateVariant {
701 fn from(template: Vec<TemplateComponent>) -> Self {
702 Self::Full(template)
703 }
704}
705
706#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
708#[cfg_attr(feature = "schema", derive(JsonSchema))]
709#[serde(rename_all = "kebab-case", deny_unknown_fields)]
710pub struct TemplateVariantDiff {
711 #[serde(skip_serializing_if = "Option::is_none")]
714 pub extends: Option<TypeSelector>,
715 #[serde(skip_serializing_if = "Vec::is_empty", default)]
717 pub modify: Vec<TemplateModifyOperation>,
718 #[serde(skip_serializing_if = "Vec::is_empty", default)]
720 pub remove: Vec<TemplateRemoveOperation>,
721 #[serde(skip_serializing_if = "Vec::is_empty", default)]
723 pub add: Vec<TemplateAddOperation>,
724}
725
726#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
728#[cfg_attr(feature = "schema", derive(JsonSchema))]
729#[serde(transparent)]
730pub struct TemplateComponentSelector {
731 pub fields: BTreeMap<String, serde_json::Value>,
733}
734
735impl TemplateComponentSelector {
736 #[must_use]
738 pub fn is_empty(&self) -> bool {
739 self.fields.is_empty()
740 }
741
742 #[must_use]
744 pub fn matches(&self, component: &TemplateComponent) -> bool {
745 let Ok(serde_json::Value::Object(component_fields)) = serde_json::to_value(component)
746 else {
747 return false;
748 };
749
750 self.fields.iter().all(|(key, expected)| {
751 component_fields
752 .get(key)
753 .is_some_and(|actual| selector_value_matches(expected, actual))
754 })
755 }
756}
757
758fn selector_value_matches(expected: &serde_json::Value, actual: &serde_json::Value) -> bool {
759 match (expected, actual) {
760 (serde_json::Value::Object(expected_fields), serde_json::Value::Object(actual_fields)) => {
761 expected_fields.iter().all(|(key, expected_value)| {
762 actual_fields.get(key).is_some_and(|actual_value| {
763 selector_value_matches(expected_value, actual_value)
764 })
765 })
766 }
767 (serde_json::Value::Array(expected_items), serde_json::Value::Array(actual_items)) => {
768 expected_items.len() == actual_items.len()
769 && expected_items.iter().zip(actual_items.iter()).all(
770 |(expected_item, actual_item)| {
771 selector_value_matches(expected_item, actual_item)
772 },
773 )
774 }
775 _ => expected == actual,
776 }
777}
778
779#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
781#[cfg_attr(feature = "schema", derive(JsonSchema))]
782#[serde(rename_all = "kebab-case", deny_unknown_fields)]
783pub struct TemplateModifyOperation {
784 #[serde(rename = "match")]
786 pub match_selector: TemplateComponentSelector,
787 #[serde(skip_serializing_if = "Option::is_none")]
789 pub label_form: Option<LabelForm>,
790 #[serde(flatten, default)]
792 pub rendering: Rendering,
793}
794
795#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
797#[cfg_attr(feature = "schema", derive(JsonSchema))]
798#[serde(rename_all = "kebab-case", deny_unknown_fields)]
799pub struct TemplateRemoveOperation {
800 #[serde(rename = "match")]
802 pub match_selector: TemplateComponentSelector,
803}
804
805#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
807#[cfg_attr(feature = "schema", derive(JsonSchema))]
808#[serde(rename_all = "kebab-case", deny_unknown_fields)]
809pub struct TemplateAddOperation {
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub before: Option<TemplateComponentSelector>,
813 #[serde(skip_serializing_if = "Option::is_none")]
815 pub after: Option<TemplateComponentSelector>,
816 pub component: TemplateComponent,
818}
819
820#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
822#[cfg_attr(feature = "schema", derive(JsonSchema))]
823#[serde(rename_all = "kebab-case")]
824pub struct RoleLabel {
825 pub term: String,
827 #[serde(default)]
829 pub form: RoleLabelForm,
830 #[serde(default)]
832 pub placement: LabelPlacement,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
837 pub text_case: Option<crate::options::titles::TextCase>,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
842 pub wrap: Option<Box<WrapConfig>>,
843 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub prefix: Option<DelimiterPunctuation>,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub suffix: Option<DelimiterPunctuation>,
854}
855
856#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
858#[cfg_attr(feature = "schema", derive(JsonSchema))]
859#[serde(rename_all = "kebab-case")]
860pub enum RoleLabelForm {
861 #[default]
862 Short,
863 Long,
864}
865
866#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
868#[cfg_attr(feature = "schema", derive(JsonSchema))]
869#[serde(rename_all = "kebab-case")]
870pub enum LabelPlacement {
871 Prefix,
872 #[default]
873 Suffix,
874}
875
876#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
878#[cfg_attr(feature = "schema", derive(JsonSchema))]
879#[serde(untagged)]
880pub enum ContributorRoles {
881 Single(ContributorRole),
883 Multiple(#[cfg_attr(feature = "schema", schemars(length(min = 2)))] Vec<ContributorRole>),
885}
886
887impl Default for ContributorRoles {
888 fn default() -> Self {
889 Self::Single(ContributorRole::Author)
890 }
891}
892
893impl ContributorRoles {
894 #[must_use]
896 pub fn as_slice(&self) -> &[ContributorRole] {
897 match self {
898 Self::Single(role) => std::slice::from_ref(role),
899 Self::Multiple(roles) => roles,
900 }
901 }
902
903 #[must_use]
905 pub fn as_single(&self) -> Option<&ContributorRole> {
906 match self {
907 Self::Single(role) => Some(role),
908 Self::Multiple(_) => None,
909 }
910 }
911
912 #[must_use]
914 pub fn is_multiple(&self) -> bool {
915 matches!(self, Self::Multiple(_))
916 }
917
918 #[must_use]
920 pub fn contains(&self, role: &ContributorRole) -> bool {
921 self.as_slice().contains(role)
922 }
923}
924
925impl From<ContributorRole> for ContributorRoles {
926 fn from(role: ContributorRole) -> Self {
927 Self::Single(role)
928 }
929}
930
931impl From<Vec<ContributorRole>> for ContributorRoles {
932 fn from(roles: Vec<ContributorRole>) -> Self {
933 Self::Multiple(roles)
934 }
935}
936
937impl PartialEq<ContributorRole> for ContributorRoles {
938 fn eq(&self, other: &ContributorRole) -> bool {
939 self.as_single() == Some(other)
940 }
941}
942
943#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
945#[cfg_attr(feature = "schema", derive(JsonSchema))]
946#[serde(rename_all = "kebab-case")]
947pub enum ContributorMergeOrder {
948 #[default]
950 Document,
951 Role,
953}
954
955#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
957#[cfg_attr(feature = "schema", derive(JsonSchema))]
958#[serde(rename_all = "kebab-case")]
959pub enum ContributorLabelMode {
960 #[default]
962 Individual,
963 Collective,
965 None,
967}
968
969#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
971#[cfg_attr(feature = "schema", derive(JsonSchema))]
972#[serde(rename_all = "kebab-case", deny_unknown_fields)]
973pub struct ContributorMergeRole {
974 #[serde(skip_serializing_if = "Option::is_none")]
976 pub labels: Option<ContributorLabelMode>,
977 #[serde(skip_serializing_if = "Option::is_none")]
979 pub label: Option<RoleLabel>,
980}
981
982#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
984#[cfg_attr(feature = "schema", derive(JsonSchema))]
985#[serde(rename_all = "kebab-case", deny_unknown_fields)]
986pub struct ContributorMerge {
987 #[serde(default)]
989 pub order: ContributorMergeOrder,
990 #[serde(default)]
992 pub labels: ContributorLabelMode,
993 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
995 pub roles: HashMap<ContributorRole, ContributorMergeRole>,
996 #[serde(default = "default_combine_same_person")]
998 pub combine_same_person: bool,
999 #[serde(skip_serializing_if = "Option::is_none")]
1001 pub role_conjunction: Option<String>,
1002}
1003
1004fn default_combine_same_person() -> bool {
1005 true
1006}
1007
1008impl Default for ContributorMerge {
1009 fn default() -> Self {
1010 Self {
1011 order: ContributorMergeOrder::Document,
1012 labels: ContributorLabelMode::Individual,
1013 roles: HashMap::new(),
1014 combine_same_person: true,
1015 role_conjunction: None,
1016 }
1017 }
1018}
1019
1020#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1022#[cfg_attr(feature = "schema", derive(JsonSchema))]
1023#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1024pub struct TemplateContributor {
1025 pub contributor: ContributorRoles,
1027 pub form: ContributorForm,
1029 #[serde(skip_serializing_if = "Option::is_none")]
1037 pub fallback: Option<Vec<TemplateComponent>>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub label: Option<RoleLabel>,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub merge: Option<ContributorMerge>,
1044 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub name_order: Option<NameOrder>,
1048 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
1050 pub name_form: Option<crate::options::contributors::NameForm>,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub delimiter: Option<DelimiterPunctuation>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub sort_separator: Option<String>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub shorten: Option<crate::options::ShortenListOptions>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub and: Option<crate::options::AndOptions>,
1064 #[serde(flatten, default)]
1065 pub rendering: Rendering,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1068 pub links: Option<crate::options::LinksConfig>,
1069 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub gender: Option<GrammaticalGender>,
1072
1073 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub custom: Option<HashMap<String, serde_json::Value>>,
1076}
1077
1078#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1080#[cfg_attr(feature = "schema", derive(JsonSchema))]
1081#[serde(rename_all = "kebab-case")]
1082pub enum NameOrder {
1083 GivenFirst,
1085 #[default]
1087 FamilyFirst,
1088 FamilyFirstOnly,
1090 FamilyFirstExceptLast,
1095}
1096
1097#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1099#[cfg_attr(feature = "schema", derive(JsonSchema))]
1100#[serde(rename_all = "kebab-case")]
1101pub enum ContributorForm {
1102 #[default]
1103 Long,
1104 Short,
1105 FamilyOnly,
1106 Verb,
1107 VerbShort,
1108}
1109
1110crate::str_enum! {
1111 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
1113 pub enum ContributorRole {
1114 #[default] Author = "author",
1115 Chair = "chair",
1116 Editor = "editor",
1117 Translator = "translator",
1118 Annotator = "annotator",
1120 Commentator = "commentator",
1122 ForewordAuthor = "foreword-author",
1124 IntroductionAuthor = "introduction-author",
1126 AfterwordAuthor = "afterword-author",
1128 Director = "director",
1129 Publisher = "publisher",
1130 Recipient = "recipient",
1131 Interviewer = "interviewer",
1132 Interviewee = "interviewee",
1133 Guest = "guest",
1134 Performer = "performer",
1135 Inventor = "inventor",
1136 Counsel = "counsel",
1137 Composer = "composer",
1138 Writer = "writer",
1139 Producer = "producer",
1140 CollectionEditor = "collection-editor",
1141 ContainerAuthor = "container-author",
1142 EditorialDirector = "editorial-director",
1143 TextualEditor = "textual-editor",
1144 Illustrator = "illustrator",
1145 Narrator = "narrator",
1146 OriginalAuthor = "original-author",
1147 ReviewedAuthor = "reviewed-author"
1148 }
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 TemplateDate {
1156 pub date: DateVariable,
1157 pub form: DateForm,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub fallback: Option<Vec<TemplateComponent>>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1172 pub suppress_note: Option<bool>,
1173 #[serde(skip_serializing_if = "Option::is_none")]
1181 pub suppress_disamb_suffix: Option<bool>,
1182 #[serde(flatten, default)]
1183 pub rendering: Rendering,
1184 #[serde(skip_serializing_if = "Option::is_none")]
1186 pub links: Option<crate::options::LinksConfig>,
1187
1188 #[serde(skip_serializing_if = "Option::is_none")]
1190 pub custom: Option<HashMap<String, serde_json::Value>>,
1191}
1192
1193#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1195#[cfg_attr(feature = "schema", derive(JsonSchema))]
1196#[serde(rename_all = "kebab-case")]
1197pub enum DateVariable {
1198 #[default]
1199 Issued,
1200 Accessed,
1201 OriginalPublished,
1202 Submitted,
1203 EventDate,
1204 Copyright,
1207 Printing,
1210}
1211
1212crate::str_enum! {
1213 #[derive(Debug, Default, Clone, PartialEq)]
1215 pub enum DateForm {
1216 #[default]
1217 Year = "year",
1218 YearMonth = "year-month",
1219 Month = "month",
1222 Full = "full",
1223 MonthDay = "month-day",
1224 YearMonthDay = "year-month-day",
1225 DayMonthAbbrYear = "day-month-abbr-year",
1226 MonthAbbrDayYear = "month-abbr-day-year"
1228 }
1229}
1230
1231#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1233#[cfg_attr(feature = "schema", derive(JsonSchema))]
1234#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1235pub struct TemplateTitle {
1236 pub title: TitleType,
1237 #[serde(skip_serializing_if = "Option::is_none")]
1238 pub form: Option<TitleForm>,
1239 #[serde(skip_serializing_if = "Option::is_none")]
1244 pub disambiguate_only: Option<bool>,
1245 #[serde(skip_serializing_if = "Option::is_none")]
1255 pub strip_periods_all: Option<bool>,
1256 #[serde(flatten, default)]
1257 pub rendering: Rendering,
1258 #[serde(skip_serializing_if = "Option::is_none")]
1260 pub links: Option<crate::options::LinksConfig>,
1261
1262 #[serde(skip_serializing_if = "Option::is_none")]
1264 pub custom: Option<HashMap<String, serde_json::Value>>,
1265}
1266
1267#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1269#[cfg_attr(feature = "schema", derive(JsonSchema))]
1270#[serde(rename_all = "kebab-case")]
1271#[non_exhaustive]
1272pub enum TitleType {
1273 #[default]
1275 Primary,
1276 ContainerTitle,
1278 ParentMonograph,
1280 ParentSerial,
1282 CollectionTitle,
1284 Original,
1286}
1287
1288#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1290#[cfg_attr(feature = "schema", derive(JsonSchema))]
1291#[serde(rename_all = "kebab-case")]
1292pub enum TitleForm {
1293 Short,
1294 #[default]
1295 Long,
1296}
1297
1298#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1300#[cfg_attr(feature = "schema", derive(JsonSchema))]
1301#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1302pub struct TemplateNumber {
1303 pub number: NumberVariable,
1304 #[serde(skip_serializing_if = "Option::is_none")]
1305 pub form: Option<NumberForm>,
1306 #[serde(skip_serializing_if = "Option::is_none")]
1307 pub label_form: Option<LabelForm>,
1308 #[serde(skip_serializing_if = "Option::is_none")]
1311 pub show_with_locator: Option<bool>,
1312 #[serde(flatten)]
1313 pub rendering: Rendering,
1314 #[serde(skip_serializing_if = "Option::is_none")]
1316 pub links: Option<crate::options::LinksConfig>,
1317 #[serde(skip_serializing_if = "Option::is_none")]
1319 pub gender: Option<GrammaticalGender>,
1320 #[serde(skip_serializing_if = "Option::is_none")]
1333 pub when_numeric: Option<LabelForm>,
1334
1335 #[serde(skip_serializing_if = "Option::is_none")]
1337 pub custom: Option<HashMap<String, serde_json::Value>>,
1338}
1339
1340#[derive(Debug, Default, Clone)]
1347#[non_exhaustive]
1348pub enum NumberVariable {
1349 #[default]
1350 Volume,
1351 Issue,
1352 Pages,
1353 Edition,
1354 ChapterNumber,
1355 CollectionNumber,
1356 NumberOfPages,
1357 NumberOfVolumes,
1358 FirstReferenceNoteNumber,
1362 Number,
1363 DocketNumber,
1364 PatentNumber,
1365 StandardNumber,
1366 ReportNumber,
1367 PartNumber,
1368 SupplementNumber,
1369 PrintingNumber,
1370 Custom(String),
1372}
1373
1374impl NumberVariable {
1375 #[must_use]
1377 pub fn as_key(&self) -> Cow<'_, str> {
1378 match self {
1379 Self::Volume => Cow::Borrowed("volume"),
1380 Self::Issue => Cow::Borrowed("issue"),
1381 Self::Pages => Cow::Borrowed("pages"),
1382 Self::Edition => Cow::Borrowed("edition"),
1383 Self::ChapterNumber => Cow::Borrowed("chapter-number"),
1384 Self::CollectionNumber => Cow::Borrowed("collection-number"),
1385 Self::NumberOfPages => Cow::Borrowed("number-of-pages"),
1386 Self::NumberOfVolumes => Cow::Borrowed("number-of-volumes"),
1387 Self::FirstReferenceNoteNumber => Cow::Borrowed("first-reference-note-number"),
1388 Self::Number => Cow::Borrowed("number"),
1389 Self::DocketNumber => Cow::Borrowed("docket-number"),
1390 Self::PatentNumber => Cow::Borrowed("patent-number"),
1391 Self::StandardNumber => Cow::Borrowed("standard-number"),
1392 Self::ReportNumber => Cow::Borrowed("report-number"),
1393 Self::PartNumber => Cow::Borrowed("part-number"),
1394 Self::SupplementNumber => Cow::Borrowed("supplement-number"),
1395 Self::PrintingNumber => Cow::Borrowed("printing-number"),
1396 Self::Custom(value) => normalize_kind_key(value)
1397 .map(Cow::Owned)
1398 .unwrap_or_else(|| Cow::Borrowed(value.as_str())),
1399 }
1400 }
1401
1402 fn from_key(value: &str) -> Result<Self, String> {
1403 let canonical = normalize_kind_key(value)
1404 .ok_or_else(|| "number variable must not be empty".to_string())?;
1405 Ok(match canonical.as_str() {
1406 "volume" => Self::Volume,
1407 "issue" => Self::Issue,
1408 "pages" => Self::Pages,
1409 "edition" => Self::Edition,
1410 "chapter-number" => Self::ChapterNumber,
1411 "collection-number" => Self::CollectionNumber,
1412 "number-of-pages" => Self::NumberOfPages,
1413 "number-of-volumes" => Self::NumberOfVolumes,
1414 "citation-number" => {
1420 return Err(
1421 "`citation-number` is a processor-owned reference marker, not a \
1422 number variable: declare `label-mode: numeric` on citation.options \
1423 or bibliography.options"
1424 .to_string(),
1425 );
1426 }
1427 "citation-label" => {
1428 return Err(
1429 "`citation-label` is a processor-owned reference marker, not a \
1430 number variable: declare `label-mode: alphabetic` on \
1431 citation.options or bibliography.options"
1432 .to_string(),
1433 );
1434 }
1435 "first-reference-note-number" => Self::FirstReferenceNoteNumber,
1436 "number" => Self::Number,
1437 "docket-number" => Self::DocketNumber,
1438 "patent-number" => Self::PatentNumber,
1439 "standard-number" => Self::StandardNumber,
1440 "report-number" => Self::ReportNumber,
1441 "part-number" => Self::PartNumber,
1442 "supplement-number" => Self::SupplementNumber,
1443 "printing-number" => Self::PrintingNumber,
1444 _ => Self::Custom(canonical),
1445 })
1446 }
1447}
1448
1449impl PartialEq for NumberVariable {
1450 fn eq(&self, other: &Self) -> bool {
1451 self.as_key().as_ref() == other.as_key().as_ref()
1452 }
1453}
1454
1455impl Eq for NumberVariable {}
1456
1457impl Hash for NumberVariable {
1458 fn hash<H: Hasher>(&self, state: &mut H) {
1459 self.as_key().as_ref().hash(state);
1460 }
1461}
1462
1463impl Serialize for NumberVariable {
1464 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1465 where
1466 S: Serializer,
1467 {
1468 serializer.serialize_str(self.as_key().as_ref())
1469 }
1470}
1471
1472impl<'de> Deserialize<'de> for NumberVariable {
1473 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1474 where
1475 D: Deserializer<'de>,
1476 {
1477 let value = String::deserialize(deserializer)?;
1478 Self::from_key(&value).map_err(serde::de::Error::custom)
1479 }
1480}
1481
1482#[cfg(feature = "schema")]
1483impl JsonSchema for NumberVariable {
1484 fn schema_name() -> std::borrow::Cow<'static, str> {
1485 "NumberVariable".into()
1486 }
1487
1488 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1489 schemars::json_schema!({
1490 "type": "string",
1491 "description": "Known number variable keyword or custom kebab-case identifier."
1492 })
1493 }
1494}
1495
1496#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1498#[cfg_attr(feature = "schema", derive(JsonSchema))]
1499#[serde(rename_all = "lowercase")]
1500pub enum NumberForm {
1501 #[default]
1502 Numeric,
1503 Ordinal,
1504 Roman,
1505}
1506
1507fn normalize_kind_key(value: &str) -> Option<String> {
1508 let mut normalized = String::new();
1509 let mut pending_dash = false;
1510
1511 for ch in value.trim().chars() {
1512 if ch.is_ascii_alphanumeric() {
1513 if pending_dash && !normalized.is_empty() {
1514 normalized.push('-');
1515 }
1516 normalized.push(ch.to_ascii_lowercase());
1517 pending_dash = false;
1518 } else if !normalized.is_empty() {
1519 pending_dash = true;
1520 }
1521 }
1522
1523 if normalized.is_empty() {
1524 None
1525 } else {
1526 Some(normalized)
1527 }
1528}
1529
1530#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1532#[cfg_attr(feature = "schema", derive(JsonSchema))]
1533#[serde(rename_all = "kebab-case")]
1534pub enum LabelForm {
1535 Long,
1536 #[default]
1537 Short,
1538 Symbol,
1539}
1540
1541#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1543#[cfg_attr(feature = "schema", derive(JsonSchema))]
1544#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1545pub struct TemplateVariable {
1546 pub variable: SimpleVariable,
1547 #[serde(flatten)]
1548 pub rendering: Rendering,
1549 #[serde(skip_serializing_if = "Option::is_none")]
1551 pub links: Option<crate::options::LinksConfig>,
1552
1553 #[serde(skip_serializing_if = "Option::is_none")]
1555 pub custom: Option<HashMap<String, serde_json::Value>>,
1556}
1557
1558#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1560#[cfg_attr(feature = "schema", derive(JsonSchema))]
1561#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1562pub struct TemplateIdentifier {
1563 pub identifier: crate::reference::IdentifierName,
1565 #[serde(flatten, default)]
1566 pub rendering: Rendering,
1567}
1568
1569#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1574#[cfg_attr(feature = "schema", derive(JsonSchema))]
1575#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1576pub struct TemplateMessage {
1577 pub message: String,
1579 #[serde(skip_serializing_if = "Option::is_none")]
1581 pub form: Option<TermForm>,
1582 #[serde(skip_serializing_if = "Option::is_none")]
1584 pub gender: Option<GrammaticalGender>,
1585 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1587 pub args: HashMap<String, MessageArgSource>,
1588 #[serde(flatten, default)]
1589 pub rendering: Rendering,
1590
1591 #[serde(skip_serializing_if = "Option::is_none")]
1593 pub custom: Option<HashMap<String, serde_json::Value>>,
1594}
1595
1596#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1598#[cfg_attr(feature = "schema", derive(JsonSchema))]
1599#[serde(untagged)]
1600pub enum MessageArgSource {
1601 Literal { literal: String },
1603 ReferenceType {
1605 #[serde(rename = "reference-type")]
1606 reference_type: MessageReferenceTypeSource,
1607 },
1608 Carrier { carrier: MessageCarrierSource },
1610 Contributor(Box<TemplateContributor>),
1612 Date(TemplateDate),
1614 Group(TemplateGroup),
1616 Title(TemplateTitle),
1618 Number(TemplateNumber),
1620 Variable(TemplateVariable),
1622 Term(TemplateTerm),
1624}
1625
1626impl MessageArgSource {
1627 #[must_use]
1630 pub fn as_template_component(&self) -> Option<TemplateComponent> {
1631 match self {
1632 Self::Literal { .. } | Self::ReferenceType { .. } | Self::Carrier { .. } => None,
1633 Self::Contributor(component) => {
1634 Some(TemplateComponent::Contributor(component.as_ref().clone()))
1635 }
1636 Self::Date(component) => Some(TemplateComponent::Date(component.clone())),
1637 Self::Group(component) => Some(TemplateComponent::Group(component.clone())),
1638 Self::Title(component) => Some(TemplateComponent::Title(component.clone())),
1639 Self::Number(component) => Some(TemplateComponent::Number(component.clone())),
1640 Self::Variable(component) => Some(TemplateComponent::Variable(component.clone())),
1641 Self::Term(component) => Some(TemplateComponent::Term(component.clone())),
1642 }
1643 }
1644}
1645
1646#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
1648#[cfg_attr(feature = "schema", derive(JsonSchema))]
1649#[serde(rename_all = "kebab-case")]
1650pub enum MessageReferenceTypeSource {
1651 Key,
1653}
1654
1655#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1657#[cfg_attr(feature = "schema", derive(JsonSchema))]
1658#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1659pub struct MessageCarrierSource {
1660 pub online: String,
1662 pub absent: String,
1664}
1665
1666#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1673#[cfg_attr(feature = "schema", derive(JsonSchema))]
1674#[serde(rename_all = "kebab-case")]
1675#[non_exhaustive]
1676pub enum SimpleVariable {
1677 #[default]
1678 Doi,
1679 Isbn,
1680 Issn,
1681 Url,
1682 Pmid,
1683 Pmcid,
1684 Abstract,
1685 Note,
1686 Annote,
1687 Keyword,
1688 Genre,
1689 RawGenre,
1690 Medium,
1691 RawMedium,
1692 Source,
1693 Status,
1694 Archive,
1695 ArchiveLocation,
1696 ArchiveName,
1697 ArchivePlace,
1698 ArchiveCollection,
1699 ArchiveCollectionId,
1700 ArchiveSeries,
1701 ArchiveBox,
1702 ArchiveFolder,
1703 ArchiveItem,
1704 ArchiveUrl,
1705 EprintId,
1706 EprintServer,
1707 EprintClass,
1708 Publisher,
1709 PublisherPlace,
1710 OriginalPublisher,
1711 OriginalPublisherPlace,
1712 EventTitle,
1713 EventPlace,
1714 Dimensions,
1715 References,
1716 Scale,
1717 Version,
1718 VolumeTitle,
1719 Locator,
1720 ContainerTitleShort,
1721 Authority,
1722 Code,
1723 Reporter,
1724 Page,
1725 Section,
1726 Volume,
1727 Number,
1728 DocketNumber,
1729 PatentNumber,
1730 StandardNumber,
1731 ReportNumber,
1732 AdsBibcode,
1733}
1734
1735#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1737#[cfg_attr(feature = "schema", derive(JsonSchema))]
1738#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1739pub struct TemplateTerm {
1740 pub term: GeneralTerm,
1742 #[serde(skip_serializing_if = "Option::is_none")]
1744 pub form: Option<TermForm>,
1745 #[serde(skip_serializing_if = "Option::is_none")]
1747 pub gender: Option<GrammaticalGender>,
1748 #[serde(flatten, default)]
1749 pub rendering: Rendering,
1750
1751 #[serde(skip_serializing_if = "Option::is_none")]
1753 pub custom: Option<HashMap<String, serde_json::Value>>,
1754}
1755
1756#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
1763#[cfg_attr(feature = "schema", derive(JsonSchema))]
1764#[serde(rename_all = "kebab-case")]
1765#[non_exhaustive]
1766pub enum TypeLabelSource {
1767 #[default]
1770 ReferenceType,
1771}
1772
1773#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1783#[cfg_attr(feature = "schema", derive(JsonSchema))]
1784#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1785pub struct TemplateTypeLabel {
1786 #[serde(rename = "type-label")]
1788 pub type_label: TypeLabelSource,
1789 #[serde(flatten, default)]
1790 pub rendering: Rendering,
1791
1792 #[serde(skip_serializing_if = "Option::is_none")]
1794 pub custom: Option<HashMap<String, serde_json::Value>>,
1795}
1796
1797#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1800#[cfg_attr(feature = "schema", derive(JsonSchema))]
1801#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1802pub struct TemplateGroup {
1803 pub group: Vec<TemplateComponent>,
1804 #[serde(skip_serializing_if = "Option::is_none")]
1806 pub render_when: Option<TemplateGroupCondition>,
1807 #[serde(skip_serializing_if = "Option::is_none")]
1808 pub delimiter: Option<DelimiterPunctuation>,
1809 #[serde(flatten, default)]
1810 pub rendering: Rendering,
1811
1812 #[serde(skip_serializing_if = "Option::is_none")]
1814 pub custom: Option<HashMap<String, serde_json::Value>>,
1815}
1816
1817#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1819#[cfg_attr(feature = "schema", derive(JsonSchema))]
1820#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1821pub struct TemplateGroupCondition {
1822 #[serde(skip_serializing_if = "Option::is_none")]
1824 pub field_present: Option<TemplateConditionField>,
1825 #[serde(skip_serializing_if = "Option::is_none")]
1827 pub field_absent: Option<TemplateConditionField>,
1828}
1829
1830#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
1832#[cfg_attr(feature = "schema", derive(JsonSchema))]
1833#[serde(rename_all = "kebab-case")]
1834pub enum TemplateConditionField {
1835 Author,
1837 Editor,
1839 Recipient,
1841 Translator,
1843 Title,
1845 CollectionTitle,
1847 Issued,
1849 OriginalPublished,
1851 Publisher,
1853 OriginalPublisher,
1855 OriginalPublisherPlace,
1857 OriginalTitle,
1859 Doi,
1861 Genre,
1863 Archive,
1865 ArchiveLocation,
1867 VolumeOrIssue,
1873}
1874
1875#[derive(Debug, Default, Clone, PartialEq)]
1881pub enum DelimiterPunctuation {
1882 #[default]
1884 Comma,
1885 Semicolon,
1887 Period,
1889 Colon,
1891 Parentheses,
1893 Brackets,
1895 Ampersand,
1897 VerticalLine,
1899 Slash,
1901 Hyphen,
1903 Space,
1905 None,
1907 Custom(String),
1909}
1910
1911#[cfg(feature = "schema")]
1912impl JsonSchema for DelimiterPunctuation {
1913 fn schema_name() -> std::borrow::Cow<'static, str> {
1914 "DelimiterPunctuation".into()
1915 }
1916
1917 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1918 schemars::json_schema!({
1919 "oneOf": [
1920 {
1921 "type": "string",
1922 "description": "Literal punctuation or text."
1923 },
1924 {
1925 "type": "object",
1926 "additionalProperties": false,
1927 "required": ["mark"],
1928 "properties": {
1929 "mark": {
1930 "type": "string",
1931 "enum": [
1932 "comma",
1933 "colon",
1934 "semicolon",
1935 "period",
1936 "parentheses",
1937 "brackets"
1938 ]
1939 }
1940 }
1941 }
1942 ],
1943 "description": "Literal text or an explicit semantic punctuation mark."
1944 })
1945 }
1946}
1947
1948impl Serialize for DelimiterPunctuation {
1949 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1950 use serde::ser::SerializeMap as _;
1951
1952 let mark = match self {
1953 Self::Comma => Some("comma"),
1954 Self::Semicolon => Some("semicolon"),
1955 Self::Period => Some("period"),
1956 Self::Colon => Some("colon"),
1957 Self::Parentheses => Some("parentheses"),
1958 Self::Brackets => Some("brackets"),
1959 Self::Ampersand
1960 | Self::VerticalLine
1961 | Self::Slash
1962 | Self::Hyphen
1963 | Self::Space
1964 | Self::None
1965 | Self::Custom(_) => None,
1966 };
1967
1968 if let Some(mark) = mark {
1969 let mut map = serializer.serialize_map(Some(1))?;
1970 map.serialize_entry("mark", mark)?;
1971 map.end()
1972 } else {
1973 serializer.serialize_str(self.as_default_str())
1974 }
1975 }
1976}
1977
1978impl<'de> Deserialize<'de> for DelimiterPunctuation {
1979 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1980 #[derive(Deserialize)]
1981 #[serde(deny_unknown_fields)]
1982 struct MarkReference {
1983 mark: String,
1984 }
1985
1986 #[derive(Deserialize)]
1987 #[serde(untagged)]
1988 enum LiteralOrMark {
1989 Literal(String),
1990 Mark(MarkReference),
1991 }
1992
1993 match LiteralOrMark::deserialize(deserializer)? {
1994 LiteralOrMark::Literal(value) => Ok(Self::Custom(value)),
1995 LiteralOrMark::Mark(reference) => match reference.mark.as_str() {
1996 "comma" => Ok(Self::Comma),
1997 "colon" => Ok(Self::Colon),
1998 "semicolon" => Ok(Self::Semicolon),
1999 "period" => Ok(Self::Period),
2000 "parentheses" => Ok(Self::Parentheses),
2001 "brackets" => Ok(Self::Brackets),
2002 other => Err(serde::de::Error::unknown_variant(
2003 other,
2004 &[
2005 "comma",
2006 "colon",
2007 "semicolon",
2008 "period",
2009 "parentheses",
2010 "brackets",
2011 ],
2012 )),
2013 },
2014 }
2015 }
2016}
2017
2018impl DelimiterPunctuation {
2019 #[must_use]
2022 pub fn is_semantic(&self) -> bool {
2023 matches!(
2024 self,
2025 Self::Comma
2026 | Self::Semicolon
2027 | Self::Period
2028 | Self::Colon
2029 | Self::Parentheses
2030 | Self::Brackets
2031 )
2032 }
2033
2034 #[must_use]
2036 pub fn as_default_str(&self) -> &str {
2037 match self {
2038 Self::Comma => ", ",
2039 Self::Semicolon => "; ",
2040 Self::Period => ". ",
2041 Self::Colon => ": ",
2042 Self::Parentheses => "()",
2043 Self::Brackets => "[]",
2044 Self::Ampersand => " & ",
2045 Self::VerticalLine => " | ",
2046 Self::Slash => "/",
2047 Self::Hyphen => "-",
2048 Self::Space => " ",
2049 Self::None => "",
2050 Self::Custom(value) => value,
2051 }
2052 }
2053
2054 pub fn to_string_with_space(&self) -> String {
2058 self.as_default_str().to_string()
2059 }
2060
2061 pub fn from_csl_string(s: &str) -> Self {
2066 if s == " " {
2067 return Self::Space;
2068 }
2069
2070 let trimmed = s.trim();
2071 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
2072 return Self::None;
2073 }
2074
2075 match trimmed {
2076 "," => Self::Comma,
2077 ";" => Self::Semicolon,
2078 "." => Self::Period,
2079 ":" => Self::Colon,
2080 "&" => Self::Ampersand,
2081 "|" => Self::VerticalLine,
2082 "/" => Self::Slash,
2083 "-" => Self::Hyphen,
2084 _ => Self::Custom(s.to_string()),
2085 }
2086 }
2087}
2088
2089impl std::ops::Deref for DelimiterPunctuation {
2090 type Target = str;
2091
2092 fn deref(&self) -> &Self::Target {
2093 self.as_default_str()
2094 }
2095}
2096
2097impl std::fmt::Display for DelimiterPunctuation {
2098 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2099 formatter.write_str(self.as_default_str())
2100 }
2101}
2102
2103impl From<String> for DelimiterPunctuation {
2104 fn from(value: String) -> Self {
2105 Self::Custom(value)
2106 }
2107}
2108
2109impl From<&str> for DelimiterPunctuation {
2110 fn from(value: &str) -> Self {
2111 Self::Custom(value.to_string())
2112 }
2113}
2114
2115#[cfg(test)]
2116#[allow(
2117 clippy::unwrap_used,
2118 clippy::expect_used,
2119 clippy::panic,
2120 clippy::indexing_slicing,
2121 clippy::todo,
2122 clippy::unimplemented,
2123 clippy::unreachable,
2124 clippy::get_unwrap,
2125 reason = "Panicking is acceptable and often desired in tests."
2126)]
2127mod tests {
2128 use super::*;
2129
2130 #[test]
2131 fn test_contributor_deserialization() {
2132 let yaml = r#"
2133contributor: author
2134form: long
2135"#;
2136 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
2137 assert_eq!(comp.contributor, ContributorRole::Author);
2138 assert_eq!(comp.form, ContributorForm::Long);
2139 }
2140
2141 #[test]
2142 fn test_contributor_name_order_family_first_except_last_deserialization() {
2143 let yaml = r#"
2144contributor: author
2145form: long
2146name-order: family-first-except-last
2147"#;
2148 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
2149 assert_eq!(comp.name_order, Some(NameOrder::FamilyFirstExceptLast));
2150 }
2151
2152 #[test]
2153 fn test_template_component_untagged() {
2154 let yaml = r#"
2155- contributor: author
2156 form: short
2157- date: issued
2158 form: year
2159- title: primary
2160"#;
2161 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
2162 assert_eq!(components.len(), 3);
2163
2164 match &components[0] {
2165 TemplateComponent::Contributor(c) => {
2166 assert_eq!(c.contributor, ContributorRole::Author);
2167 }
2168 _ => panic!("Expected Contributor"),
2169 }
2170
2171 match &components[1] {
2172 TemplateComponent::Date(d) => {
2173 assert_eq!(d.date, DateVariable::Issued);
2174 }
2175 _ => panic!("Expected Date"),
2176 }
2177 }
2178
2179 #[test]
2180 fn test_flattened_rendering() {
2181 let yaml = r#"
2183- title: parent-monograph
2184 prefix: "In "
2185 emph: true
2186- date: issued
2187 form: year
2188 wrap: parentheses
2189"#;
2190 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
2191 assert_eq!(components.len(), 2);
2192
2193 match &components[0] {
2194 TemplateComponent::Title(t) => {
2195 assert_eq!(t.rendering.prefix.as_deref(), Some("In "));
2196 assert_eq!(t.rendering.emph, Some(true));
2197 }
2198 _ => panic!("Expected Title"),
2199 }
2200
2201 match &components[1] {
2202 TemplateComponent::Date(d) => {
2203 assert_eq!(
2204 d.rendering.wrap,
2205 Some(WrapConfig {
2206 punctuation: WrapPunctuation::Parentheses,
2207 inner_prefix: None,
2208 inner_suffix: None,
2209 })
2210 );
2211 }
2212 _ => panic!("Expected Date"),
2213 }
2214 }
2215
2216 #[test]
2217 fn test_number_variable_custom_normalizes_manual_construction() {
2218 let number = NumberVariable::Custom("Reel Label".to_string());
2219
2220 assert_eq!(number.as_key(), "reel-label");
2221 assert_eq!(
2222 number,
2223 serde_yaml::from_str::<NumberVariable>("reel-label")
2224 .expect("custom number variable should parse")
2225 );
2226 assert_eq!(
2227 serde_json::to_string(&number).expect("custom number variable should serialize"),
2228 "\"reel-label\""
2229 );
2230 }
2231
2232 #[test]
2233 fn test_contributor_with_wrap() {
2234 let yaml = r#"
2235contributor: publisher
2236form: short
2237wrap: parentheses
2238"#;
2239 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
2240 assert_eq!(comp.contributor, ContributorRole::Publisher);
2241 assert_eq!(
2242 comp.rendering.wrap,
2243 Some(WrapConfig {
2244 punctuation: WrapPunctuation::Parentheses,
2245 inner_prefix: None,
2246 inner_suffix: None,
2247 })
2248 );
2249 }
2250
2251 #[test]
2252 fn test_variable_deserialization() {
2253 let yaml = "variable: publisher\n";
2255 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2256 match comp {
2257 TemplateComponent::Variable(v) => {
2258 assert_eq!(v.variable, SimpleVariable::Publisher);
2259 }
2260 _ => panic!("Expected Variable(Publisher), got {:?}", comp),
2261 }
2262 }
2263
2264 #[test]
2265 fn test_message_component_deserialization() {
2266 let yaml = r#"
2267message: pattern.in-container
2268args:
2269 container:
2270 group:
2271 - title: parent-monograph
2272 emph: true
2273text-case: capitalize-first
2274"#;
2275 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2276
2277 match comp {
2278 TemplateComponent::Message(message) => {
2279 assert_eq!(message.message, "pattern.in-container");
2280 assert!(matches!(
2281 message.args.get("container"),
2282 Some(MessageArgSource::Group(group)) if group.group.len() == 1
2283 && matches!(
2284 group.group.first(),
2285 Some(TemplateComponent::Title(title))
2286 if title.title == TitleType::ParentMonograph
2287 && title.rendering.emph == Some(true)
2288 )
2289 ));
2290 assert_eq!(
2291 message.rendering.text_case,
2292 Some(crate::options::titles::TextCase::CapitalizeFirst)
2293 );
2294 }
2295 _ => panic!("Expected Message component, got {comp:?}"),
2296 }
2297 }
2298
2299 #[test]
2300 fn test_term_backed_message_component_deserializes_form() {
2301 let yaml = r#"
2302message: term.in
2303form: long
2304suffix: ":"
2305"#;
2306 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2307
2308 match comp {
2309 TemplateComponent::Message(message) => {
2310 assert_eq!(message.message, "term.in");
2311 assert_eq!(message.form, Some(TermForm::Long));
2312 assert_eq!(message.rendering.suffix.as_deref(), Some(":"));
2313 }
2314 _ => panic!("Expected Message component, got {comp:?}"),
2315 }
2316 }
2317
2318 #[test]
2319 fn test_group_deserializes_term_backed_message_component_with_form() {
2320 let yaml = r#"
2321group:
2322- message: term.in
2323 form: long
2324 suffix: ":"
2325- title: parent-monograph
2326"#;
2327 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
2328
2329 match comp {
2330 TemplateComponent::Group(group) => {
2331 assert!(matches!(
2332 group.group.first(),
2333 Some(TemplateComponent::Message(message))
2334 if message.message == "term.in"
2335 && message.form == Some(TermForm::Long)
2336 && message.rendering.suffix.as_deref() == Some(":")
2337 ));
2338 }
2339 _ => panic!("Expected Group component, got {comp:?}"),
2340 }
2341 }
2342
2343 #[test]
2344 fn test_variable_array_parsing() {
2345 let yaml = r#"
2346- variable: doi
2347 prefix: "https://doi.org/"
2348- variable: publisher
2349"#;
2350 let comps: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
2351 assert_eq!(comps.len(), 2);
2352 match &comps[0] {
2353 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Doi),
2354 _ => panic!("Expected Variable for doi, got {:?}", comps[0]),
2355 }
2356 match &comps[1] {
2357 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Publisher),
2358 _ => panic!("Expected Variable for publisher, got {:?}", comps[1]),
2359 }
2360 }
2361
2362 #[test]
2363 fn test_type_selector_default_only_matches_default_context() {
2364 let selector = TypeSelector::Single("default".to_string());
2365 assert!(selector.matches("default"));
2366 assert!(!selector.matches("article-journal"));
2367
2368 let mixed = TypeSelector::Multiple(vec!["default".to_string(), "chapter".to_string()]);
2369 assert!(mixed.matches("default"));
2370 assert!(mixed.matches("chapter"));
2371 assert!(!mixed.matches("book"));
2372 }
2373
2374 #[test]
2375 fn test_template_component_selector_matches_nested_partial_group() {
2376 let component: TemplateComponent = serde_yaml::from_str(
2377 r#"
2378delimiter: ""
2379group:
2380- number: volume
2381 wrap:
2382 punctuation: brackets
2383- contributor: author
2384 form: long
2385"#,
2386 )
2387 .unwrap();
2388 let selector = TemplateComponentSelector {
2389 fields: BTreeMap::from([(
2390 "group".to_string(),
2391 serde_json::json!([
2392 { "number": "volume" },
2393 { "contributor": "author" }
2394 ]),
2395 )]),
2396 };
2397
2398 assert!(selector.matches(&component));
2399 }
2400
2401 #[test]
2402 fn test_delimiter_from_csl_string_normalizes_none_and_trimmed_values() {
2403 assert_eq!(
2404 DelimiterPunctuation::from_csl_string("none"),
2405 DelimiterPunctuation::None
2406 );
2407 assert_eq!(
2408 DelimiterPunctuation::from_csl_string(" none "),
2409 DelimiterPunctuation::None
2410 );
2411 assert_eq!(
2412 DelimiterPunctuation::from_csl_string(" "),
2413 DelimiterPunctuation::Space
2414 );
2415 assert_eq!(
2416 DelimiterPunctuation::from_csl_string(" : "),
2417 DelimiterPunctuation::Colon
2418 );
2419 }
2420}