1use crate::locale::{GeneralTerm, GrammaticalGender, TermForm};
37use indexmap::IndexMap;
38#[cfg(feature = "schema")]
39use schemars::JsonSchema;
40use serde::{Deserialize, Deserializer, Serialize, Serializer};
41use std::borrow::Cow;
42use std::collections::{BTreeMap, HashMap};
43use std::hash::{Hash, Hasher};
44
45mod reference;
46pub(crate) mod resolution;
47
48pub(crate) use reference::locale_matches;
49pub use reference::{LocalizedTemplateSpec, TemplatePreset, TemplateReference};
50pub(crate) use resolution::{inherited_variant_context, resolve_style_template_variants};
51
52pub fn resolve_local_template_variants(
66 style: &mut crate::Style,
67) -> Result<(), crate::ResolutionError> {
68 resolution::resolve_style_template_variants(style, None)
69}
70
71pub type Template = Vec<TemplateComponent>;
73
74pub type TemplateVariants = IndexMap<TypeSelector, TemplateVariant>;
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[cfg_attr(feature = "schema", derive(JsonSchema))]
80#[serde(rename_all = "kebab-case")]
81pub enum VerticalAlign {
82 Baseline,
84 Superscript,
86 Subscript,
88}
89
90#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
100#[cfg_attr(feature = "schema", derive(JsonSchema))]
101#[serde(rename_all = "kebab-case", default)]
102pub struct Rendering {
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub text_case: Option<crate::options::titles::TextCase>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub emph: Option<bool>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub quote: Option<bool>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub strong: Option<bool>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub small_caps: Option<bool>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub vertical_align: Option<VerticalAlign>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub prefix: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub suffix: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub wrap: Option<WrapConfig>,
130 #[serde(skip_serializing_if = "Option::is_none")]
133 pub suppress: Option<bool>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub initialize_with: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
139 pub name_form: Option<crate::options::contributors::NameForm>,
140 #[serde(skip_serializing_if = "Option::is_none", rename = "strip-periods")]
142 pub strip_periods: Option<bool>,
143}
144
145impl Rendering {
146 pub fn merge(&mut self, other: &Rendering) {
150 crate::merge_options!(
151 self,
152 other,
153 text_case,
154 emph,
155 quote,
156 strong,
157 small_caps,
158 vertical_align,
159 prefix,
160 suffix,
161 wrap,
162 suppress,
163 initialize_with,
164 name_form,
165 strip_periods,
166 );
167 }
168}
169
170#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
172#[cfg_attr(feature = "schema", derive(JsonSchema))]
173#[serde(rename_all = "kebab-case")]
174pub enum WrapPunctuation {
175 #[default]
176 Parentheses,
177 Brackets,
178 Quotes,
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize)]
186#[cfg_attr(feature = "schema", derive(JsonSchema))]
187#[serde(rename_all = "kebab-case")]
188pub struct WrapConfig {
189 pub punctuation: WrapPunctuation,
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub inner_prefix: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub inner_suffix: Option<String>,
197}
198
199impl<'de> serde::Deserialize<'de> for WrapConfig {
200 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
201 struct WrapConfigVisitor;
202
203 impl<'de> serde::de::Visitor<'de> for WrapConfigVisitor {
204 type Value = WrapConfig;
205
206 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
207 write!(
208 f,
209 "a wrap punctuation string or a mapping with a 'punctuation' key"
210 )
211 }
212
213 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<WrapConfig, E> {
214 let punctuation = match v {
215 "parentheses" => WrapPunctuation::Parentheses,
216 "brackets" => WrapPunctuation::Brackets,
217 "quotes" => WrapPunctuation::Quotes,
218 other => {
219 return Err(E::unknown_variant(
220 other,
221 &["parentheses", "brackets", "quotes"],
222 ));
223 }
224 };
225 Ok(WrapConfig {
226 punctuation,
227 inner_prefix: None,
228 inner_suffix: None,
229 })
230 }
231
232 fn visit_map<A: serde::de::MapAccess<'de>>(
233 self,
234 mut map: A,
235 ) -> Result<WrapConfig, A::Error> {
236 let mut punctuation: Option<WrapPunctuation> = None;
237 let mut inner_prefix: Option<String> = None;
238 let mut inner_suffix: Option<String> = None;
239
240 while let Some(key) = map.next_key::<String>()? {
241 match key.as_str() {
242 "punctuation" => {
243 punctuation = Some(map.next_value()?);
244 }
245 "inner-prefix" => {
246 inner_prefix = Some(map.next_value()?);
247 }
248 "inner-suffix" => {
249 inner_suffix = Some(map.next_value()?);
250 }
251 other => {
252 return Err(serde::de::Error::unknown_field(
253 other,
254 &["punctuation", "inner-prefix", "inner-suffix"],
255 ));
256 }
257 }
258 }
259
260 let punctuation =
261 punctuation.ok_or_else(|| serde::de::Error::missing_field("punctuation"))?;
262 Ok(WrapConfig {
263 punctuation,
264 inner_prefix,
265 inner_suffix,
266 })
267 }
268 }
269
270 deserializer.deserialize_any(WrapConfigVisitor)
271 }
272}
273
274impl From<WrapPunctuation> for WrapConfig {
275 fn from(punctuation: WrapPunctuation) -> Self {
276 WrapConfig {
277 punctuation,
278 inner_prefix: None,
279 inner_suffix: None,
280 }
281 }
282}
283
284pub const VALID_TYPE_NAMES: &[&str] = &[
288 "book",
289 "manual",
290 "report",
291 "thesis",
292 "webpage",
293 "map",
294 "post",
295 "interview",
296 "manuscript",
297 "personal-communication",
298 "document",
299 "chapter",
300 "entry-dictionary",
301 "paper-conference",
302 "article-journal",
303 "article-magazine",
304 "article-newspaper",
305 "broadcast",
306 "motion-picture",
307 "collection",
308 "legal-case",
309 "statute",
310 "treaty",
311 "hearing",
312 "regulation",
313 "brief",
314 "classic",
315 "patent",
316 "dataset",
317 "standard",
318 "software",
319 "all",
321 "default",
322];
323
324pub fn validate_type_name(s: &str) -> bool {
330 let normalized = s.replace('_', "-");
331 VALID_TYPE_NAMES.iter().any(|&known| known == normalized)
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Hash)]
337#[cfg_attr(feature = "schema", derive(JsonSchema))]
338pub enum TypeSelector {
339 Single(String),
340 Multiple(Vec<String>),
341}
342
343impl Serialize for TypeSelector {
344 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
345 where
346 S: serde::Serializer,
347 {
348 serializer.serialize_str(&self.to_string())
349 }
350}
351
352impl<'de> Deserialize<'de> for TypeSelector {
353 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
354 where
355 D: serde::Deserializer<'de>,
356 {
357 struct Visitor;
358 impl<'de> serde::de::Visitor<'de> for Visitor {
359 type Value = TypeSelector;
360
361 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
362 formatter.write_str("a string or a sequence of strings")
363 }
364
365 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
366 where
367 E: serde::de::Error,
368 {
369 v.parse().map_err(E::custom)
370 }
371
372 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
373 where
374 A: serde::de::SeqAccess<'de>,
375 {
376 let mut types = Vec::new();
377 while let Some(t) = seq.next_element::<String>()? {
378 types.push(t);
379 }
380 if types.len() == 1 {
381 Ok(TypeSelector::Single(types.remove(0)))
382 } else {
383 Ok(TypeSelector::Multiple(types))
384 }
385 }
386 }
387 deserializer.deserialize_any(Visitor)
388 }
389}
390
391impl std::fmt::Display for TypeSelector {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 match self {
394 TypeSelector::Single(s) => write!(f, "{s}"),
395 TypeSelector::Multiple(types) => write!(f, "{}", types.join(",")),
396 }
397 }
398}
399
400impl std::str::FromStr for TypeSelector {
401 type Err = std::convert::Infallible;
402
403 fn from_str(s: &str) -> Result<Self, Self::Err> {
404 if s.contains(',') {
405 Ok(TypeSelector::Multiple(
406 s.split(',').map(|t| t.trim().to_string()).collect(),
407 ))
408 } else {
409 Ok(TypeSelector::Single(s.to_string()))
410 }
411 }
412}
413
414impl TypeSelector {
415 pub fn matches(&self, ref_type: &str) -> bool {
423 let normalized_ref = ref_type.replace('_', "-");
424 let base_ref = normalized_ref
425 .split_once('+')
426 .map(|(base, _)| base)
427 .unwrap_or(&normalized_ref);
428 let eq = |s: &str| -> bool {
429 s == ref_type
430 || s.replace('_', "-") == normalized_ref
431 || s.replace('_', "-") == base_ref
432 || s == "all"
433 || (s == "default" && ref_type == "default")
434 };
435 match self {
436 TypeSelector::Single(s) => eq(s),
437 TypeSelector::Multiple(types) => types.iter().any(|t| eq(t)),
438 }
439 }
440
441 pub fn unknown_type_names(&self) -> Vec<&str> {
446 match self {
447 TypeSelector::Single(s) => {
448 if validate_type_name(s) {
449 vec![]
450 } else {
451 vec![s.as_str()]
452 }
453 }
454 TypeSelector::Multiple(types) => types
455 .iter()
456 .filter(|s| !validate_type_name(s))
457 .map(|s| s.as_str())
458 .collect(),
459 }
460 }
461}
462
463#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
467#[cfg_attr(feature = "schema", derive(JsonSchema))]
468#[serde(untagged)]
469#[non_exhaustive]
470pub enum TemplateComponent {
471 Contributor(TemplateContributor),
472 Date(TemplateDate),
473 Title(TemplateTitle),
474 Number(TemplateNumber),
475 Variable(TemplateVariable),
476 Message(TemplateMessage),
477 Group(TemplateGroup),
478 Term(TemplateTerm),
479}
480
481impl Default for TemplateComponent {
482 fn default() -> Self {
483 TemplateComponent::Variable(TemplateVariable::default())
484 }
485}
486
487impl TemplateComponent {
488 pub fn rendering(&self) -> &Rendering {
492 crate::dispatch_component!(self, |inner| &inner.rendering)
493 }
494
495 pub fn rendering_mut(&mut self) -> &mut Rendering {
500 crate::dispatch_component!(self, |inner| &mut inner.rendering)
501 }
502}
503
504#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
506#[cfg_attr(feature = "schema", derive(JsonSchema))]
507#[serde(untagged)]
508pub enum TemplateVariant {
509 Full(Vec<TemplateComponent>),
511 Diff(TemplateVariantDiff),
513}
514
515impl TemplateVariant {
516 #[must_use]
518 pub fn as_template(&self) -> Option<&[TemplateComponent]> {
519 match self {
520 Self::Full(template) => Some(template.as_slice()),
521 Self::Diff(_) => None,
522 }
523 }
524
525 pub fn as_template_mut(&mut self) -> Option<&mut Vec<TemplateComponent>> {
527 match self {
528 Self::Full(template) => Some(template),
529 Self::Diff(_) => None,
530 }
531 }
532
533 #[must_use]
535 pub fn into_template(self) -> Option<Vec<TemplateComponent>> {
536 match self {
537 Self::Full(template) => Some(template),
538 Self::Diff(_) => None,
539 }
540 }
541}
542
543impl From<Vec<TemplateComponent>> for TemplateVariant {
544 fn from(template: Vec<TemplateComponent>) -> Self {
545 Self::Full(template)
546 }
547}
548
549#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
551#[cfg_attr(feature = "schema", derive(JsonSchema))]
552#[serde(rename_all = "kebab-case", deny_unknown_fields)]
553pub struct TemplateVariantDiff {
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub extends: Option<TypeSelector>,
557 #[serde(skip_serializing_if = "Vec::is_empty", default)]
559 pub modify: Vec<TemplateModifyOperation>,
560 #[serde(skip_serializing_if = "Vec::is_empty", default)]
562 pub remove: Vec<TemplateRemoveOperation>,
563 #[serde(skip_serializing_if = "Vec::is_empty", default)]
565 pub add: Vec<TemplateAddOperation>,
566}
567
568#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
570#[cfg_attr(feature = "schema", derive(JsonSchema))]
571#[serde(transparent)]
572pub struct TemplateComponentSelector {
573 pub fields: BTreeMap<String, serde_json::Value>,
575}
576
577impl TemplateComponentSelector {
578 #[must_use]
580 pub fn is_empty(&self) -> bool {
581 self.fields.is_empty()
582 }
583
584 #[must_use]
586 pub fn matches(&self, component: &TemplateComponent) -> bool {
587 let Ok(serde_json::Value::Object(component_fields)) = serde_json::to_value(component)
588 else {
589 return false;
590 };
591
592 self.fields.iter().all(|(key, expected)| {
593 component_fields
594 .get(key)
595 .is_some_and(|actual| selector_value_matches(expected, actual))
596 })
597 }
598}
599
600fn selector_value_matches(expected: &serde_json::Value, actual: &serde_json::Value) -> bool {
601 match (expected, actual) {
602 (serde_json::Value::Object(expected_fields), serde_json::Value::Object(actual_fields)) => {
603 expected_fields.iter().all(|(key, expected_value)| {
604 actual_fields.get(key).is_some_and(|actual_value| {
605 selector_value_matches(expected_value, actual_value)
606 })
607 })
608 }
609 (serde_json::Value::Array(expected_items), serde_json::Value::Array(actual_items)) => {
610 expected_items.len() == actual_items.len()
611 && expected_items.iter().zip(actual_items.iter()).all(
612 |(expected_item, actual_item)| {
613 selector_value_matches(expected_item, actual_item)
614 },
615 )
616 }
617 _ => expected == actual,
618 }
619}
620
621#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
623#[cfg_attr(feature = "schema", derive(JsonSchema))]
624#[serde(rename_all = "kebab-case", deny_unknown_fields)]
625pub struct TemplateModifyOperation {
626 #[serde(rename = "match")]
628 pub match_selector: TemplateComponentSelector,
629 #[serde(skip_serializing_if = "Option::is_none")]
631 pub label_form: Option<LabelForm>,
632 #[serde(flatten, default)]
634 pub rendering: Rendering,
635}
636
637#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
639#[cfg_attr(feature = "schema", derive(JsonSchema))]
640#[serde(rename_all = "kebab-case", deny_unknown_fields)]
641pub struct TemplateRemoveOperation {
642 #[serde(rename = "match")]
644 pub match_selector: TemplateComponentSelector,
645}
646
647#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
649#[cfg_attr(feature = "schema", derive(JsonSchema))]
650#[serde(rename_all = "kebab-case", deny_unknown_fields)]
651pub struct TemplateAddOperation {
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub before: Option<TemplateComponentSelector>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub after: Option<TemplateComponentSelector>,
658 pub component: TemplateComponent,
660}
661
662#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
664#[cfg_attr(feature = "schema", derive(JsonSchema))]
665#[serde(rename_all = "kebab-case")]
666pub struct RoleLabel {
667 pub term: String,
669 #[serde(default)]
671 pub form: RoleLabelForm,
672 #[serde(default)]
674 pub placement: LabelPlacement,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
679 pub text_case: Option<crate::options::titles::TextCase>,
680}
681
682#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
684#[cfg_attr(feature = "schema", derive(JsonSchema))]
685#[serde(rename_all = "kebab-case")]
686pub enum RoleLabelForm {
687 #[default]
688 Short,
689 Long,
690}
691
692#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
694#[cfg_attr(feature = "schema", derive(JsonSchema))]
695#[serde(rename_all = "kebab-case")]
696pub enum LabelPlacement {
697 Prefix,
698 #[default]
699 Suffix,
700}
701
702#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
704#[cfg_attr(feature = "schema", derive(JsonSchema))]
705#[serde(rename_all = "kebab-case", deny_unknown_fields)]
706pub struct TemplateContributor {
707 pub contributor: ContributorRole,
709 pub form: ContributorForm,
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub label: Option<RoleLabel>,
714 #[serde(skip_serializing_if = "Option::is_none")]
717 pub name_order: Option<NameOrder>,
718 #[serde(skip_serializing_if = "Option::is_none", rename = "name-form")]
720 pub name_form: Option<crate::options::contributors::NameForm>,
721 #[serde(skip_serializing_if = "Option::is_none")]
723 pub delimiter: Option<String>,
724 #[serde(skip_serializing_if = "Option::is_none")]
726 pub sort_separator: Option<String>,
727 #[serde(skip_serializing_if = "Option::is_none")]
729 pub shorten: Option<crate::options::ShortenListOptions>,
730 #[serde(skip_serializing_if = "Option::is_none")]
733 pub and: Option<crate::options::AndOptions>,
734 #[serde(flatten, default)]
735 pub rendering: Rendering,
736 #[serde(skip_serializing_if = "Option::is_none")]
738 pub links: Option<crate::options::LinksConfig>,
739 #[serde(skip_serializing_if = "Option::is_none")]
741 pub gender: Option<GrammaticalGender>,
742
743 #[serde(skip_serializing_if = "Option::is_none")]
745 pub custom: Option<HashMap<String, serde_json::Value>>,
746}
747
748#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
750#[cfg_attr(feature = "schema", derive(JsonSchema))]
751#[serde(rename_all = "kebab-case")]
752pub enum NameOrder {
753 GivenFirst,
755 #[default]
757 FamilyFirst,
758 FamilyFirstOnly,
760}
761
762#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
764#[cfg_attr(feature = "schema", derive(JsonSchema))]
765#[serde(rename_all = "kebab-case")]
766pub enum ContributorForm {
767 #[default]
768 Long,
769 Short,
770 FamilyOnly,
771 Verb,
772 VerbShort,
773}
774
775crate::str_enum! {
776 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
778 pub enum ContributorRole {
779 #[default] Author = "author",
780 Chair = "chair",
781 Editor = "editor",
782 Translator = "translator",
783 Director = "director",
784 Publisher = "publisher",
785 Recipient = "recipient",
786 Interviewer = "interviewer",
787 Interviewee = "interviewee",
788 Guest = "guest",
789 Inventor = "inventor",
790 Counsel = "counsel",
791 Composer = "composer",
792 CollectionEditor = "collection-editor",
793 ContainerAuthor = "container-author",
794 EditorialDirector = "editorial-director",
795 TextualEditor = "textual-editor",
796 Illustrator = "illustrator",
797 OriginalAuthor = "original-author",
798 ReviewedAuthor = "reviewed-author"
799 }
800}
801
802#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
804#[cfg_attr(feature = "schema", derive(JsonSchema))]
805#[serde(rename_all = "kebab-case", deny_unknown_fields)]
806pub struct TemplateDate {
807 pub date: DateVariable,
808 pub form: DateForm,
809 #[serde(skip_serializing_if = "Option::is_none")]
811 pub fallback: Option<Vec<TemplateComponent>>,
812 #[serde(flatten, default)]
813 pub rendering: Rendering,
814 #[serde(skip_serializing_if = "Option::is_none")]
816 pub links: Option<crate::options::LinksConfig>,
817
818 #[serde(skip_serializing_if = "Option::is_none")]
820 pub custom: Option<HashMap<String, serde_json::Value>>,
821}
822
823#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
825#[cfg_attr(feature = "schema", derive(JsonSchema))]
826#[serde(rename_all = "kebab-case")]
827pub enum DateVariable {
828 #[default]
829 Issued,
830 Accessed,
831 OriginalPublished,
832 Submitted,
833 EventDate,
834}
835
836crate::str_enum! {
837 #[derive(Debug, Default, Clone, PartialEq)]
839 pub enum DateForm {
840 #[default]
841 Year = "year",
842 YearMonth = "year-month",
843 Month = "month",
846 Full = "full",
847 MonthDay = "month-day",
848 YearMonthDay = "year-month-day",
849 DayMonthAbbrYear = "day-month-abbr-year",
850 MonthAbbrDayYear = "month-abbr-day-year"
852 }
853}
854
855#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
857#[cfg_attr(feature = "schema", derive(JsonSchema))]
858#[serde(rename_all = "kebab-case", deny_unknown_fields)]
859pub struct TemplateTitle {
860 pub title: TitleType,
861 #[serde(skip_serializing_if = "Option::is_none")]
862 pub form: Option<TitleForm>,
863 #[serde(skip_serializing_if = "Option::is_none")]
868 pub disambiguate_only: Option<bool>,
869 #[serde(flatten, default)]
870 pub rendering: Rendering,
871 #[serde(skip_serializing_if = "Option::is_none")]
873 pub links: Option<crate::options::LinksConfig>,
874
875 #[serde(skip_serializing_if = "Option::is_none")]
877 pub custom: Option<HashMap<String, serde_json::Value>>,
878}
879
880#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
882#[cfg_attr(feature = "schema", derive(JsonSchema))]
883#[serde(rename_all = "kebab-case")]
884#[non_exhaustive]
885pub enum TitleType {
886 #[default]
888 Primary,
889 ContainerTitle,
891 ParentMonograph,
893 ParentSerial,
895 CollectionTitle,
897}
898
899#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
901#[cfg_attr(feature = "schema", derive(JsonSchema))]
902#[serde(rename_all = "kebab-case")]
903pub enum TitleForm {
904 Short,
905 #[default]
906 Long,
907}
908
909#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
911#[cfg_attr(feature = "schema", derive(JsonSchema))]
912#[serde(rename_all = "kebab-case", deny_unknown_fields)]
913pub struct TemplateNumber {
914 pub number: NumberVariable,
915 #[serde(skip_serializing_if = "Option::is_none")]
916 pub form: Option<NumberForm>,
917 #[serde(skip_serializing_if = "Option::is_none")]
918 pub label_form: Option<LabelForm>,
919 #[serde(skip_serializing_if = "Option::is_none")]
922 pub show_with_locator: Option<bool>,
923 #[serde(flatten)]
924 pub rendering: Rendering,
925 #[serde(skip_serializing_if = "Option::is_none")]
927 pub links: Option<crate::options::LinksConfig>,
928 #[serde(skip_serializing_if = "Option::is_none")]
930 pub gender: Option<GrammaticalGender>,
931
932 #[serde(skip_serializing_if = "Option::is_none")]
934 pub custom: Option<HashMap<String, serde_json::Value>>,
935}
936
937#[derive(Debug, Default, Clone)]
944#[non_exhaustive]
945pub enum NumberVariable {
946 #[default]
947 Volume,
948 Issue,
949 Pages,
950 Edition,
951 ChapterNumber,
952 CollectionNumber,
953 NumberOfPages,
954 NumberOfVolumes,
955 CitationNumber,
956 FirstReferenceNoteNumber,
960 CitationLabel,
961 Number,
962 DocketNumber,
963 PatentNumber,
964 StandardNumber,
965 ReportNumber,
966 PartNumber,
967 SupplementNumber,
968 PrintingNumber,
969 Custom(String),
971}
972
973impl NumberVariable {
974 #[must_use]
976 pub fn as_key(&self) -> Cow<'_, str> {
977 match self {
978 Self::Volume => Cow::Borrowed("volume"),
979 Self::Issue => Cow::Borrowed("issue"),
980 Self::Pages => Cow::Borrowed("pages"),
981 Self::Edition => Cow::Borrowed("edition"),
982 Self::ChapterNumber => Cow::Borrowed("chapter-number"),
983 Self::CollectionNumber => Cow::Borrowed("collection-number"),
984 Self::NumberOfPages => Cow::Borrowed("number-of-pages"),
985 Self::NumberOfVolumes => Cow::Borrowed("number-of-volumes"),
986 Self::CitationNumber => Cow::Borrowed("citation-number"),
987 Self::FirstReferenceNoteNumber => Cow::Borrowed("first-reference-note-number"),
988 Self::CitationLabel => Cow::Borrowed("citation-label"),
989 Self::Number => Cow::Borrowed("number"),
990 Self::DocketNumber => Cow::Borrowed("docket-number"),
991 Self::PatentNumber => Cow::Borrowed("patent-number"),
992 Self::StandardNumber => Cow::Borrowed("standard-number"),
993 Self::ReportNumber => Cow::Borrowed("report-number"),
994 Self::PartNumber => Cow::Borrowed("part-number"),
995 Self::SupplementNumber => Cow::Borrowed("supplement-number"),
996 Self::PrintingNumber => Cow::Borrowed("printing-number"),
997 Self::Custom(value) => normalize_kind_key(value)
998 .map(Cow::Owned)
999 .unwrap_or_else(|| Cow::Borrowed(value.as_str())),
1000 }
1001 }
1002
1003 fn from_key(value: &str) -> Result<Self, String> {
1004 let canonical = normalize_kind_key(value)
1005 .ok_or_else(|| "number variable must not be empty".to_string())?;
1006 Ok(match canonical.as_str() {
1007 "volume" => Self::Volume,
1008 "issue" => Self::Issue,
1009 "pages" => Self::Pages,
1010 "edition" => Self::Edition,
1011 "chapter-number" => Self::ChapterNumber,
1012 "collection-number" => Self::CollectionNumber,
1013 "number-of-pages" => Self::NumberOfPages,
1014 "number-of-volumes" => Self::NumberOfVolumes,
1015 "citation-number" => Self::CitationNumber,
1016 "first-reference-note-number" => Self::FirstReferenceNoteNumber,
1017 "citation-label" => Self::CitationLabel,
1018 "number" => Self::Number,
1019 "docket-number" => Self::DocketNumber,
1020 "patent-number" => Self::PatentNumber,
1021 "standard-number" => Self::StandardNumber,
1022 "report-number" => Self::ReportNumber,
1023 "part-number" => Self::PartNumber,
1024 "supplement-number" => Self::SupplementNumber,
1025 "printing-number" => Self::PrintingNumber,
1026 _ => Self::Custom(canonical),
1027 })
1028 }
1029}
1030
1031impl PartialEq for NumberVariable {
1032 fn eq(&self, other: &Self) -> bool {
1033 self.as_key().as_ref() == other.as_key().as_ref()
1034 }
1035}
1036
1037impl Eq for NumberVariable {}
1038
1039impl Hash for NumberVariable {
1040 fn hash<H: Hasher>(&self, state: &mut H) {
1041 self.as_key().as_ref().hash(state);
1042 }
1043}
1044
1045impl Serialize for NumberVariable {
1046 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1047 where
1048 S: Serializer,
1049 {
1050 serializer.serialize_str(self.as_key().as_ref())
1051 }
1052}
1053
1054impl<'de> Deserialize<'de> for NumberVariable {
1055 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1056 where
1057 D: Deserializer<'de>,
1058 {
1059 let value = String::deserialize(deserializer)?;
1060 Self::from_key(&value).map_err(serde::de::Error::custom)
1061 }
1062}
1063
1064#[cfg(feature = "schema")]
1065impl JsonSchema for NumberVariable {
1066 fn schema_name() -> std::borrow::Cow<'static, str> {
1067 "NumberVariable".into()
1068 }
1069
1070 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1071 schemars::json_schema!({
1072 "type": "string",
1073 "description": "Known number variable keyword or custom kebab-case identifier."
1074 })
1075 }
1076}
1077
1078#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1080#[cfg_attr(feature = "schema", derive(JsonSchema))]
1081#[serde(rename_all = "lowercase")]
1082pub enum NumberForm {
1083 #[default]
1084 Numeric,
1085 Ordinal,
1086 Roman,
1087}
1088
1089fn normalize_kind_key(value: &str) -> Option<String> {
1090 let mut normalized = String::new();
1091 let mut pending_dash = false;
1092
1093 for ch in value.trim().chars() {
1094 if ch.is_ascii_alphanumeric() {
1095 if pending_dash && !normalized.is_empty() {
1096 normalized.push('-');
1097 }
1098 normalized.push(ch.to_ascii_lowercase());
1099 pending_dash = false;
1100 } else if !normalized.is_empty() {
1101 pending_dash = true;
1102 }
1103 }
1104
1105 if normalized.is_empty() {
1106 None
1107 } else {
1108 Some(normalized)
1109 }
1110}
1111
1112#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
1114#[cfg_attr(feature = "schema", derive(JsonSchema))]
1115#[serde(rename_all = "kebab-case")]
1116pub enum LabelForm {
1117 Long,
1118 #[default]
1119 Short,
1120 Symbol,
1121}
1122
1123#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1125#[cfg_attr(feature = "schema", derive(JsonSchema))]
1126#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1127pub struct TemplateVariable {
1128 pub variable: SimpleVariable,
1129 #[serde(flatten)]
1130 pub rendering: Rendering,
1131 #[serde(skip_serializing_if = "Option::is_none")]
1133 pub links: Option<crate::options::LinksConfig>,
1134
1135 #[serde(skip_serializing_if = "Option::is_none")]
1137 pub custom: Option<HashMap<String, serde_json::Value>>,
1138}
1139
1140#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1146#[cfg_attr(feature = "schema", derive(JsonSchema))]
1147#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1148pub struct TemplateMessage {
1149 pub message: String,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1153 pub form: Option<TermForm>,
1154 #[serde(skip_serializing_if = "Option::is_none")]
1156 pub gender: Option<GrammaticalGender>,
1157 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1159 pub args: HashMap<String, MessageArgSource>,
1160 #[serde(flatten, default)]
1161 pub rendering: Rendering,
1162
1163 #[serde(skip_serializing_if = "Option::is_none")]
1165 pub custom: Option<HashMap<String, serde_json::Value>>,
1166}
1167
1168#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1170#[cfg_attr(feature = "schema", derive(JsonSchema))]
1171#[serde(untagged)]
1172pub enum MessageArgSource {
1173 Literal { literal: String },
1175 Contributor(TemplateContributor),
1177 Date(TemplateDate),
1179 Group(TemplateGroup),
1181 Title(TemplateTitle),
1183 Number(TemplateNumber),
1185 Variable(TemplateVariable),
1187 Term(TemplateTerm),
1189}
1190
1191impl MessageArgSource {
1192 #[must_use]
1195 pub fn as_template_component(&self) -> Option<TemplateComponent> {
1196 match self {
1197 Self::Literal { .. } => None,
1198 Self::Contributor(component) => Some(TemplateComponent::Contributor(component.clone())),
1199 Self::Date(component) => Some(TemplateComponent::Date(component.clone())),
1200 Self::Group(component) => Some(TemplateComponent::Group(component.clone())),
1201 Self::Title(component) => Some(TemplateComponent::Title(component.clone())),
1202 Self::Number(component) => Some(TemplateComponent::Number(component.clone())),
1203 Self::Variable(component) => Some(TemplateComponent::Variable(component.clone())),
1204 Self::Term(component) => Some(TemplateComponent::Term(component.clone())),
1205 }
1206 }
1207}
1208
1209#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1216#[cfg_attr(feature = "schema", derive(JsonSchema))]
1217#[serde(rename_all = "kebab-case")]
1218#[non_exhaustive]
1219pub enum SimpleVariable {
1220 #[default]
1221 Doi,
1222 Isbn,
1223 Issn,
1224 Url,
1225 Pmid,
1226 Pmcid,
1227 Abstract,
1228 Note,
1229 Annote,
1230 Keyword,
1231 Genre,
1232 Medium,
1233 Source,
1234 Status,
1235 Archive,
1236 ArchiveLocation,
1237 ArchiveName,
1238 ArchivePlace,
1239 ArchiveCollection,
1240 ArchiveCollectionId,
1241 ArchiveSeries,
1242 ArchiveBox,
1243 ArchiveFolder,
1244 ArchiveItem,
1245 ArchiveUrl,
1246 EprintId,
1247 EprintServer,
1248 EprintClass,
1249 Publisher,
1250 PublisherPlace,
1251 OriginalPublisher,
1252 OriginalPublisherPlace,
1253 EventPlace,
1254 Dimensions,
1255 Scale,
1256 Version,
1257 Locator,
1258 ContainerTitleShort,
1259 Authority,
1260 Code,
1261 Reporter,
1262 Page,
1263 Section,
1264 Volume,
1265 Number,
1266 DocketNumber,
1267 PatentNumber,
1268 StandardNumber,
1269 ReportNumber,
1270 AdsBibcode,
1271}
1272
1273#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1275#[cfg_attr(feature = "schema", derive(JsonSchema))]
1276#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1277pub struct TemplateTerm {
1278 pub term: GeneralTerm,
1280 #[serde(skip_serializing_if = "Option::is_none")]
1282 pub form: Option<TermForm>,
1283 #[serde(skip_serializing_if = "Option::is_none")]
1285 pub gender: Option<GrammaticalGender>,
1286 #[serde(flatten, default)]
1287 pub rendering: Rendering,
1288
1289 #[serde(skip_serializing_if = "Option::is_none")]
1291 pub custom: Option<HashMap<String, serde_json::Value>>,
1292}
1293
1294#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
1297#[cfg_attr(feature = "schema", derive(JsonSchema))]
1298#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1299pub struct TemplateGroup {
1300 pub group: Vec<TemplateComponent>,
1301 #[serde(skip_serializing_if = "Option::is_none")]
1302 pub delimiter: Option<DelimiterPunctuation>,
1303 #[serde(flatten, default)]
1304 pub rendering: Rendering,
1305
1306 #[serde(skip_serializing_if = "Option::is_none")]
1308 pub custom: Option<HashMap<String, serde_json::Value>>,
1309}
1310
1311#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
1313#[serde(rename_all = "kebab-case")]
1314pub enum DelimiterPunctuation {
1315 #[default]
1316 Comma,
1317 Semicolon,
1318 Period,
1319 Colon,
1320 Ampersand,
1321 VerticalLine,
1322 Slash,
1323 Hyphen,
1324 Space,
1325 None,
1326 #[serde(untagged)]
1328 Custom(String),
1329}
1330
1331#[cfg(feature = "schema")]
1332impl JsonSchema for DelimiterPunctuation {
1333 fn schema_name() -> std::borrow::Cow<'static, str> {
1334 "DelimiterPunctuation".into()
1335 }
1336
1337 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1338 schemars::json_schema!({"type": "string", "description": "Delimiter punctuation options."})
1339 }
1340}
1341
1342impl DelimiterPunctuation {
1343 pub fn to_string_with_space(&self) -> String {
1347 match self {
1348 Self::Comma => ", ".to_string(),
1349 Self::Semicolon => "; ".to_string(),
1350 Self::Period => ". ".to_string(),
1351 Self::Colon => ": ".to_string(),
1352 Self::Ampersand => " & ".to_string(),
1353 Self::VerticalLine => " | ".to_string(),
1354 Self::Slash => "/".to_string(),
1355 Self::Hyphen => "-".to_string(),
1356 Self::Space => " ".to_string(),
1357 Self::None => "".to_string(),
1358 Self::Custom(s) => s.clone(),
1359 }
1360 }
1361
1362 pub fn from_csl_string(s: &str) -> Self {
1367 if s == " " {
1368 return Self::Space;
1369 }
1370
1371 let trimmed = s.trim();
1372 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
1373 return Self::None;
1374 }
1375
1376 match trimmed {
1377 "," => Self::Comma,
1378 ";" => Self::Semicolon,
1379 "." => Self::Period,
1380 ":" => Self::Colon,
1381 "&" => Self::Ampersand,
1382 "|" => Self::VerticalLine,
1383 "/" => Self::Slash,
1384 "-" => Self::Hyphen,
1385 _ => Self::Custom(s.to_string()),
1386 }
1387 }
1388}
1389
1390#[cfg(test)]
1391#[allow(
1392 clippy::unwrap_used,
1393 clippy::expect_used,
1394 clippy::panic,
1395 clippy::indexing_slicing,
1396 clippy::todo,
1397 clippy::unimplemented,
1398 clippy::unreachable,
1399 clippy::get_unwrap,
1400 reason = "Panicking is acceptable and often desired in tests."
1401)]
1402mod tests {
1403 use super::*;
1404
1405 #[test]
1406 fn test_contributor_deserialization() {
1407 let yaml = r#"
1408contributor: author
1409form: long
1410"#;
1411 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1412 assert_eq!(comp.contributor, ContributorRole::Author);
1413 assert_eq!(comp.form, ContributorForm::Long);
1414 }
1415
1416 #[test]
1417 fn test_template_component_untagged() {
1418 let yaml = r#"
1419- contributor: author
1420 form: short
1421- date: issued
1422 form: year
1423- title: primary
1424"#;
1425 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1426 assert_eq!(components.len(), 3);
1427
1428 match &components[0] {
1429 TemplateComponent::Contributor(c) => {
1430 assert_eq!(c.contributor, ContributorRole::Author);
1431 }
1432 _ => panic!("Expected Contributor"),
1433 }
1434
1435 match &components[1] {
1436 TemplateComponent::Date(d) => {
1437 assert_eq!(d.date, DateVariable::Issued);
1438 }
1439 _ => panic!("Expected Date"),
1440 }
1441 }
1442
1443 #[test]
1444 fn test_flattened_rendering() {
1445 let yaml = r#"
1447- title: parent-monograph
1448 prefix: "In "
1449 emph: true
1450- date: issued
1451 form: year
1452 wrap: parentheses
1453"#;
1454 let components: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1455 assert_eq!(components.len(), 2);
1456
1457 match &components[0] {
1458 TemplateComponent::Title(t) => {
1459 assert_eq!(t.rendering.prefix, Some("In ".to_string()));
1460 assert_eq!(t.rendering.emph, Some(true));
1461 }
1462 _ => panic!("Expected Title"),
1463 }
1464
1465 match &components[1] {
1466 TemplateComponent::Date(d) => {
1467 assert_eq!(
1468 d.rendering.wrap,
1469 Some(WrapConfig {
1470 punctuation: WrapPunctuation::Parentheses,
1471 inner_prefix: None,
1472 inner_suffix: None,
1473 })
1474 );
1475 }
1476 _ => panic!("Expected Date"),
1477 }
1478 }
1479
1480 #[test]
1481 fn test_number_variable_custom_normalizes_manual_construction() {
1482 let number = NumberVariable::Custom("Reel Label".to_string());
1483
1484 assert_eq!(number.as_key(), "reel-label");
1485 assert_eq!(
1486 number,
1487 serde_yaml::from_str::<NumberVariable>("reel-label")
1488 .expect("custom number variable should parse")
1489 );
1490 assert_eq!(
1491 serde_json::to_string(&number).expect("custom number variable should serialize"),
1492 "\"reel-label\""
1493 );
1494 }
1495
1496 #[test]
1497 fn test_contributor_with_wrap() {
1498 let yaml = r#"
1499contributor: publisher
1500form: short
1501wrap: parentheses
1502"#;
1503 let comp: TemplateContributor = serde_yaml::from_str(yaml).unwrap();
1504 assert_eq!(comp.contributor, ContributorRole::Publisher);
1505 assert_eq!(
1506 comp.rendering.wrap,
1507 Some(WrapConfig {
1508 punctuation: WrapPunctuation::Parentheses,
1509 inner_prefix: None,
1510 inner_suffix: None,
1511 })
1512 );
1513 }
1514
1515 #[test]
1516 fn test_variable_deserialization() {
1517 let yaml = "variable: publisher\n";
1519 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
1520 match comp {
1521 TemplateComponent::Variable(v) => {
1522 assert_eq!(v.variable, SimpleVariable::Publisher);
1523 }
1524 _ => panic!("Expected Variable(Publisher), got {:?}", comp),
1525 }
1526 }
1527
1528 #[test]
1529 fn test_message_component_deserialization() {
1530 let yaml = r#"
1531message: pattern.in-container
1532args:
1533 container:
1534 group:
1535 - title: parent-monograph
1536 emph: true
1537text-case: capitalize-first
1538"#;
1539 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
1540
1541 match comp {
1542 TemplateComponent::Message(message) => {
1543 assert_eq!(message.message, "pattern.in-container");
1544 assert!(matches!(
1545 message.args.get("container"),
1546 Some(MessageArgSource::Group(group)) if group.group.len() == 1
1547 && matches!(
1548 group.group.first(),
1549 Some(TemplateComponent::Title(title))
1550 if title.title == TitleType::ParentMonograph
1551 && title.rendering.emph == Some(true)
1552 )
1553 ));
1554 assert_eq!(
1555 message.rendering.text_case,
1556 Some(crate::options::titles::TextCase::CapitalizeFirst)
1557 );
1558 }
1559 _ => panic!("Expected Message component, got {comp:?}"),
1560 }
1561 }
1562
1563 #[test]
1564 fn test_term_backed_message_component_deserializes_form() {
1565 let yaml = r#"
1566message: term.in
1567form: long
1568suffix: ":"
1569"#;
1570 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
1571
1572 match comp {
1573 TemplateComponent::Message(message) => {
1574 assert_eq!(message.message, "term.in");
1575 assert_eq!(message.form, Some(TermForm::Long));
1576 assert_eq!(message.rendering.suffix.as_deref(), Some(":"));
1577 }
1578 _ => panic!("Expected Message component, got {comp:?}"),
1579 }
1580 }
1581
1582 #[test]
1583 fn test_group_deserializes_term_backed_message_component_with_form() {
1584 let yaml = r#"
1585group:
1586- message: term.in
1587 form: long
1588 suffix: ":"
1589- title: parent-monograph
1590"#;
1591 let comp: TemplateComponent = serde_yaml::from_str(yaml).unwrap();
1592
1593 match comp {
1594 TemplateComponent::Group(group) => {
1595 assert!(matches!(
1596 group.group.first(),
1597 Some(TemplateComponent::Message(message))
1598 if message.message == "term.in"
1599 && message.form == Some(TermForm::Long)
1600 && message.rendering.suffix.as_deref() == Some(":")
1601 ));
1602 }
1603 _ => panic!("Expected Group component, got {comp:?}"),
1604 }
1605 }
1606
1607 #[test]
1608 fn test_variable_array_parsing() {
1609 let yaml = r#"
1610- variable: doi
1611 prefix: "https://doi.org/"
1612- variable: publisher
1613"#;
1614 let comps: Vec<TemplateComponent> = serde_yaml::from_str(yaml).unwrap();
1615 assert_eq!(comps.len(), 2);
1616 match &comps[0] {
1617 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Doi),
1618 _ => panic!("Expected Variable for doi, got {:?}", comps[0]),
1619 }
1620 match &comps[1] {
1621 TemplateComponent::Variable(v) => assert_eq!(v.variable, SimpleVariable::Publisher),
1622 _ => panic!("Expected Variable for publisher, got {:?}", comps[1]),
1623 }
1624 }
1625
1626 #[test]
1627 fn test_type_selector_default_only_matches_default_context() {
1628 let selector = TypeSelector::Single("default".to_string());
1629 assert!(selector.matches("default"));
1630 assert!(!selector.matches("article-journal"));
1631
1632 let mixed = TypeSelector::Multiple(vec!["default".to_string(), "chapter".to_string()]);
1633 assert!(mixed.matches("default"));
1634 assert!(mixed.matches("chapter"));
1635 assert!(!mixed.matches("book"));
1636 }
1637
1638 #[test]
1639 fn test_template_component_selector_matches_nested_partial_group() {
1640 let component: TemplateComponent = serde_yaml::from_str(
1641 r#"
1642delimiter: ""
1643group:
1644- number: citation-number
1645 wrap:
1646 punctuation: brackets
1647- contributor: author
1648 form: long
1649"#,
1650 )
1651 .unwrap();
1652 let selector = TemplateComponentSelector {
1653 fields: BTreeMap::from([(
1654 "group".to_string(),
1655 serde_json::json!([
1656 { "number": "citation-number" },
1657 { "contributor": "author" }
1658 ]),
1659 )]),
1660 };
1661
1662 assert!(selector.matches(&component));
1663 }
1664
1665 #[test]
1666 fn test_delimiter_from_csl_string_normalizes_none_and_trimmed_values() {
1667 assert_eq!(
1668 DelimiterPunctuation::from_csl_string("none"),
1669 DelimiterPunctuation::None
1670 );
1671 assert_eq!(
1672 DelimiterPunctuation::from_csl_string(" none "),
1673 DelimiterPunctuation::None
1674 );
1675 assert_eq!(
1676 DelimiterPunctuation::from_csl_string(" "),
1677 DelimiterPunctuation::Space
1678 );
1679 assert_eq!(
1680 DelimiterPunctuation::from_csl_string(" : "),
1681 DelimiterPunctuation::Colon
1682 );
1683 }
1684}