1use std::borrow::Cow;
9use std::ops::Range;
10
11use crate::values::ScriptClass;
12use citum_schema::locale::GrammarOptions;
13use citum_schema::options::PunctuationRealization;
14use citum_schema::template::{DelimiterPunctuation, WrapPunctuation};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub(crate) enum PunctuationPosition {
19 Separator,
21 Prefix,
23 Suffix,
25}
26
27#[must_use]
32pub(crate) fn realize_punctuation<'a>(
33 punctuation: &'a DelimiterPunctuation,
34 script: ScriptClass,
35 overrides: Option<&'a PunctuationRealization>,
36 position: PunctuationPosition,
37) -> Cow<'a, str> {
38 use DelimiterPunctuation as Punctuation;
39
40 let override_value = overrides.and_then(|table| match punctuation {
41 Punctuation::Comma => table.comma.as_deref().map(Cow::Borrowed),
42 Punctuation::Colon => table.colon.as_deref().map(Cow::Borrowed),
43 Punctuation::Semicolon => table.semicolon.as_deref().map(Cow::Borrowed),
44 Punctuation::Period => table.period.as_deref().map(Cow::Borrowed),
45 Punctuation::Parentheses => table
46 .parentheses
47 .as_ref()
48 .map(|pair| pair_mark(pair, position)),
49 Punctuation::Brackets => table
50 .brackets
51 .as_ref()
52 .map(|pair| pair_mark(pair, position)),
53 Punctuation::Ampersand
54 | Punctuation::VerticalLine
55 | Punctuation::Slash
56 | Punctuation::Hyphen
57 | Punctuation::Space
58 | Punctuation::None
59 | Punctuation::Custom(_) => None,
60 });
61 if let Some(value) = override_value {
62 return value;
63 }
64
65 let default = match (punctuation, script, position) {
66 (Punctuation::Comma, ScriptClass::Latin, _) => ", ",
67 (Punctuation::Comma, ScriptClass::Cjk, _) => ",",
68 (Punctuation::Comma, ScriptClass::Mixed, _) => ",",
69 (Punctuation::Colon, ScriptClass::Latin, _) => ": ",
70 (Punctuation::Colon, ScriptClass::Cjk, _) => ":",
71 (Punctuation::Colon, ScriptClass::Mixed, _) => ":",
72 (Punctuation::Semicolon, ScriptClass::Latin, _) => "; ",
73 (Punctuation::Semicolon, ScriptClass::Cjk, _) => ";",
74 (Punctuation::Semicolon, ScriptClass::Mixed, _) => ";",
75 (Punctuation::Period, ScriptClass::Latin, _) => ". ",
76 (Punctuation::Period, ScriptClass::Cjk, _) => "。",
77 (Punctuation::Period, ScriptClass::Mixed, _) => ". ",
78 (Punctuation::Parentheses, ScriptClass::Latin, PunctuationPosition::Prefix) => "(",
79 (Punctuation::Parentheses, ScriptClass::Latin, PunctuationPosition::Suffix) => ")",
80 (Punctuation::Parentheses, ScriptClass::Cjk, PunctuationPosition::Prefix) => "(",
81 (Punctuation::Parentheses, ScriptClass::Mixed, PunctuationPosition::Prefix) => "(",
82 (Punctuation::Parentheses, ScriptClass::Cjk, PunctuationPosition::Suffix) => ")",
83 (Punctuation::Parentheses, ScriptClass::Mixed, PunctuationPosition::Suffix) => ")",
84 (Punctuation::Brackets, ScriptClass::Latin, PunctuationPosition::Prefix) => "[",
85 (Punctuation::Brackets, ScriptClass::Latin, PunctuationPosition::Suffix) => "]",
86 (Punctuation::Brackets, ScriptClass::Cjk, PunctuationPosition::Prefix) => "【",
87 (Punctuation::Brackets, ScriptClass::Mixed, PunctuationPosition::Prefix) => "[",
88 (Punctuation::Brackets, ScriptClass::Cjk, PunctuationPosition::Suffix) => "】",
89 (Punctuation::Brackets, ScriptClass::Mixed, PunctuationPosition::Suffix) => "]",
90 (Punctuation::Parentheses, ScriptClass::Latin, PunctuationPosition::Separator) => "()",
91 (Punctuation::Parentheses, ScriptClass::Cjk, PunctuationPosition::Separator) => "()",
92 (Punctuation::Parentheses, ScriptClass::Mixed, PunctuationPosition::Separator) => "()",
93 (Punctuation::Brackets, ScriptClass::Latin, PunctuationPosition::Separator) => "[]",
94 (Punctuation::Brackets, ScriptClass::Cjk, PunctuationPosition::Separator) => "【】",
95 (Punctuation::Brackets, ScriptClass::Mixed, PunctuationPosition::Separator) => "[]",
96 (
97 Punctuation::Ampersand
98 | Punctuation::VerticalLine
99 | Punctuation::Slash
100 | Punctuation::Hyphen
101 | Punctuation::Space
102 | Punctuation::None
103 | Punctuation::Custom(_),
104 _,
105 _,
106 ) => return Cow::Borrowed(punctuation.as_default_str()),
107 };
108 Cow::Borrowed(default)
109}
110
111fn pair_mark(pair: &[String; 2], position: PunctuationPosition) -> Cow<'_, str> {
112 match position {
113 PunctuationPosition::Prefix => Cow::Borrowed(pair[0].as_str()),
114 PunctuationPosition::Suffix => Cow::Borrowed(pair[1].as_str()),
115 PunctuationPosition::Separator => Cow::Owned(format!("{}{}", pair[0], pair[1])),
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
145pub(crate) struct RealizedPunctuation<'a> {
146 text: Cow<'a, str>,
147 core_len: usize,
148}
149
150impl<'a> RealizedPunctuation<'a> {
151 pub(crate) fn new(text: Cow<'a, str>) -> Self {
153 let core_len = text.chars().next().map(char::len_utf8).unwrap_or(0);
154 Self { text, core_len }
155 }
156
157 pub(crate) fn text(&self) -> &str {
159 &self.text
160 }
161
162 pub(crate) fn core(&self) -> Option<char> {
164 self.text.chars().next()
165 }
166
167 pub(crate) fn tail(&self) -> &str {
169 #[allow(
170 clippy::string_slice,
171 reason = "core_len is a char boundary derived from chars().next()"
172 )]
173 &self.text[self.core_len..]
174 }
175
176 pub(crate) fn is_empty(&self) -> bool {
178 self.text.is_empty()
179 }
180
181 pub(crate) fn into_owned(self) -> RealizedPunctuation<'static> {
183 RealizedPunctuation {
184 text: Cow::Owned(self.text.into_owned()),
185 core_len: self.core_len,
186 }
187 }
188}
189
190#[must_use]
192pub(crate) fn realize_punctuation_decomposed<'a>(
193 punctuation: &'a DelimiterPunctuation,
194 script: ScriptClass,
195 overrides: Option<&'a PunctuationRealization>,
196 position: PunctuationPosition,
197) -> RealizedPunctuation<'a> {
198 RealizedPunctuation::new(realize_punctuation(
199 punctuation,
200 script,
201 overrides,
202 position,
203 ))
204}
205
206pub(crate) fn apply_punctuation_affixes<F>(
209 fmt: &F,
210 prefix: Option<(&DelimiterPunctuation, &str)>,
211 mut content: String,
212 suffix: Option<(&DelimiterPunctuation, &str)>,
213) -> String
214where
215 F: OutputFormat<Output = String>,
216{
217 if let Some((punctuation, text)) = prefix {
218 content = if punctuation.is_semantic() {
219 fmt.join(vec![fmt.text(text), content], "")
220 } else {
221 fmt.affix(text, content, "")
222 };
223 }
224 if let Some((punctuation, text)) = suffix {
225 content = if punctuation.is_semantic() {
226 fmt.join(vec![content, fmt.text(text)], "")
227 } else {
228 fmt.affix("", content, text)
229 };
230 }
231 content
232}
233
234#[must_use]
238pub fn unicode_quote_marks(depth: usize) -> (&'static str, &'static str) {
239 if depth.is_multiple_of(2) {
240 ("\u{201C}", "\u{201D}")
241 } else {
242 ("\u{2018}", "\u{2019}")
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct QuoteMarks {
251 pub open: String,
253 pub close: String,
255 pub open_inner: String,
257 pub close_inner: String,
259 pub punctuation_realization: Option<citum_schema::options::PunctuationRealization>,
261}
262
263impl QuoteMarks {
264 #[must_use]
268 pub fn for_depth(&self, depth: usize) -> (&str, &str) {
269 if depth.is_multiple_of(2) {
270 (&self.open, &self.close)
271 } else {
272 (&self.open_inner, &self.close_inner)
273 }
274 }
275}
276
277impl Default for QuoteMarks {
278 fn default() -> Self {
280 let (open, close) = unicode_quote_marks(0);
281 let (open_inner, close_inner) = unicode_quote_marks(1);
282 Self {
283 open: open.to_string(),
284 close: close.to_string(),
285 open_inner: open_inner.to_string(),
286 close_inner: close_inner.to_string(),
287 punctuation_realization: None,
288 }
289 }
290}
291
292impl From<&GrammarOptions> for QuoteMarks {
293 fn from(options: &GrammarOptions) -> Self {
294 Self {
295 open: options.open_quote.clone(),
296 close: options.close_quote.clone(),
297 open_inner: options.open_inner_quote.clone(),
298 close_inner: options.close_inner_quote.clone(),
299 punctuation_realization: None,
300 }
301 }
302}
303
304impl From<&citum_schema::locale::Locale> for QuoteMarks {
305 fn from(locale: &citum_schema::locale::Locale) -> Self {
306 Self {
307 open: locale.grammar_options.open_quote.clone(),
308 close: locale.grammar_options.close_quote.clone(),
309 open_inner: locale.grammar_options.open_inner_quote.clone(),
310 close_inner: locale.grammar_options.close_inner_quote.clone(),
311 punctuation_realization: locale.punctuation_realization.clone(),
312 }
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct SemanticAttribute {
319 pub name: &'static str,
321 pub value: String,
323}
324
325#[must_use]
333pub(crate) fn realize_wrap<'a>(
334 wrap: &WrapPunctuation,
335 script: ScriptClass,
336 overrides: Option<&'a PunctuationRealization>,
337) -> Option<(Cow<'a, str>, Cow<'a, str>)> {
338 if let Some(pair) = overrides.and_then(|table| match wrap {
339 WrapPunctuation::Parentheses => table.parentheses.as_ref(),
340 WrapPunctuation::Brackets => table.brackets.as_ref(),
341 WrapPunctuation::Quotes => None,
342 }) {
343 return Some((
344 Cow::Borrowed(pair[0].as_str()),
345 Cow::Borrowed(pair[1].as_str()),
346 ));
347 }
348
349 match (wrap, script) {
350 (WrapPunctuation::Parentheses, ScriptClass::Latin) => {
351 Some((Cow::Borrowed("("), Cow::Borrowed(")")))
352 }
353 (WrapPunctuation::Parentheses, ScriptClass::Cjk) => {
354 Some((Cow::Borrowed("("), Cow::Borrowed(")")))
355 }
356 (WrapPunctuation::Parentheses, ScriptClass::Mixed) => {
357 Some((Cow::Borrowed("("), Cow::Borrowed(")")))
358 }
359 (WrapPunctuation::Brackets, ScriptClass::Latin) => {
360 Some((Cow::Borrowed("["), Cow::Borrowed("]")))
361 }
362 (WrapPunctuation::Brackets, ScriptClass::Cjk) => {
363 Some((Cow::Borrowed("【"), Cow::Borrowed("】")))
364 }
365 (WrapPunctuation::Brackets, ScriptClass::Mixed) => {
366 Some((Cow::Borrowed("["), Cow::Borrowed("]")))
367 }
368 (WrapPunctuation::Quotes, _) => None,
369 }
370}
371
372pub trait OutputFormat: Default + Clone {
377 type Output;
382
383 fn text(&self, s: &str) -> Self::Output;
388
389 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output;
391
392 fn finish(&self, output: Self::Output) -> String;
397
398 fn emph(&self, content: Self::Output) -> Self::Output;
400
401 fn strong(&self, content: Self::Output) -> Self::Output;
403
404 fn small_caps(&self, content: Self::Output) -> Self::Output;
406
407 fn superscript(&self, content: Self::Output) -> Self::Output;
409
410 fn quote_marks<'a>(&self, depth: usize, marks: &'a QuoteMarks) -> (&'a str, &'a str) {
417 marks.for_depth(depth)
418 }
419
420 fn quote_with_depth(
422 &self,
423 content: Self::Output,
424 depth: usize,
425 marks: &QuoteMarks,
426 ) -> Self::Output {
427 let (open, close) = self.quote_marks(depth, marks);
428 self.affix(open, content, close)
429 }
430
431 fn quote(&self, content: Self::Output, marks: &QuoteMarks) -> Self::Output {
433 self.quote_with_depth(content, 0, marks)
434 }
435
436 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output;
440
441 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output;
445
446 fn wrap_punctuation(
453 &self,
454 wrap: &WrapPunctuation,
455 content: Self::Output,
456 marks: &QuoteMarks,
457 script: ScriptClass,
458 realization: Option<&PunctuationRealization>,
459 ) -> Self::Output;
460
461 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output;
466
467 fn annotation(&self, content: Self::Output) -> Self::Output;
472
473 fn paragraph(&self, content: Self::Output) -> Self::Output {
478 content
479 }
480
481 fn block_quote(&self, content: Self::Output) -> Self::Output {
483 content
484 }
485
486 fn bullet_list(&self, items: Vec<Self::Output>) -> Self::Output {
488 self.join(items, "\n")
489 }
490
491 fn ordered_list(&self, items: Vec<Self::Output>) -> Self::Output {
493 self.join(items, "\n")
494 }
495
496 fn list_item(&self, content: Self::Output) -> Self::Output {
498 content
499 }
500
501 fn heading(&self, _level: u8, content: Self::Output) -> Self::Output {
503 content
504 }
505
506 fn unnumbered_heading(&self, level: u8, content: Self::Output) -> Self::Output {
513 self.heading(level, content)
514 }
515
516 fn code_block(&self, _lang: Option<&str>, content: Self::Output) -> Self::Output {
520 content
521 }
522
523 fn inline_code(&self, content: Self::Output) -> Self::Output {
525 content
526 }
527
528 fn strikeout(&self, content: Self::Output) -> Self::Output {
530 content
531 }
532
533 fn hard_break(&self) -> Self::Output {
535 self.text(" ")
536 }
537
538 fn semantic_with_attributes(
543 &self,
544 class: &str,
545 content: Self::Output,
546 _attributes: &[SemanticAttribute],
547 ) -> Self::Output {
548 self.semantic(class, content)
549 }
550
551 fn citation(&self, _ids: Vec<String>, content: Self::Output) -> Self::Output {
553 content
554 }
555
556 fn visible_runs(&self, fragment: &str) -> Vec<Range<usize>> {
570 let mut runs = Vec::new();
571 if !fragment.is_empty() {
572 runs.push(0..fragment.len());
573 }
574 runs
575 }
576
577 fn visible_text<'a>(&self, fragment: &'a str) -> Cow<'a, str> {
582 let runs = self.visible_runs(fragment);
583 if runs.len() == 1 && runs.first() == Some(&(0..fragment.len())) {
584 return Cow::Borrowed(fragment);
585 }
586 let mut owned = String::with_capacity(fragment.len());
587 for run in runs {
588 if let Some(slice) = fragment.get(run) {
589 owned.push_str(slice);
590 }
591 }
592 Cow::Owned(owned)
593 }
594
595 fn link(&self, url: &str, content: Self::Output) -> Self::Output;
597
598 fn format_id(&self, id: &str) -> String {
600 id.to_string()
601 }
602
603 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
607 self.join(entries, "\n\n")
608 }
609
610 fn entry(
614 &self,
615 _id: &str,
616 content: Self::Output,
617 _url: Option<&str>,
618 _metadata: &ProcEntryMetadata,
619 ) -> Self::Output {
620 content
621 }
622}
623
624#[derive(Debug, Clone, Default, PartialEq)]
626pub struct ProcEntryMetadata {
627 pub author: Option<String>,
629 pub year: Option<String>,
631 pub title: Option<String>,
633}
634
635#[cfg(test)]
636#[allow(
637 clippy::unwrap_used,
638 clippy::expect_used,
639 clippy::panic,
640 clippy::indexing_slicing,
641 clippy::todo,
642 clippy::unimplemented,
643 clippy::unreachable,
644 clippy::get_unwrap,
645 reason = "Panicking is acceptable and often desired in tests."
646)]
647mod tests {
648 use super::*;
649 use rstest::rstest;
650
651 #[derive(Default, Clone)]
652 struct DummyFormat;
653
654 impl OutputFormat for DummyFormat {
655 type Output = String;
656 fn text(&self, s: &str) -> Self::Output {
657 s.to_string()
658 }
659 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
660 items.join(delimiter)
661 }
662 fn finish(&self, output: Self::Output) -> String {
663 output
664 }
665 fn emph(&self, content: Self::Output) -> Self::Output {
666 format!("emph({content})")
667 }
668 fn strong(&self, content: Self::Output) -> Self::Output {
669 format!("strong({content})")
670 }
671 fn small_caps(&self, content: Self::Output) -> Self::Output {
672 format!("sc({content})")
673 }
674 fn superscript(&self, content: Self::Output) -> Self::Output {
675 format!("sup({content})")
676 }
677 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
678 format!("{prefix}{content}{suffix}")
679 }
680 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
681 format!("{prefix}{content}{suffix}")
682 }
683 fn wrap_punctuation(
684 &self,
685 _wrap: &WrapPunctuation,
686 content: Self::Output,
687 _marks: &QuoteMarks,
688 _script: ScriptClass,
689 _realization: Option<&PunctuationRealization>,
690 ) -> Self::Output {
691 content
692 }
693 fn semantic(&self, class: &str, content: Self::Output) -> Self::Output {
694 format!("sem[{class}]({content})")
695 }
696 fn annotation(&self, content: Self::Output) -> Self::Output {
697 format!("annot({content})")
698 }
699 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
700 format!("link[{url}]({content})")
701 }
702 }
703
704 #[test]
705 fn test_realize_wrap() {
706 for (wrap, script, expected) in [
707 (
708 WrapPunctuation::Parentheses,
709 ScriptClass::Latin,
710 Some(("(", ")")),
711 ),
712 (
713 WrapPunctuation::Parentheses,
714 ScriptClass::Cjk,
715 Some(("(", ")")),
716 ),
717 (
718 WrapPunctuation::Brackets,
719 ScriptClass::Latin,
720 Some(("[", "]")),
721 ),
722 (
723 WrapPunctuation::Brackets,
724 ScriptClass::Cjk,
725 Some(("【", "】")),
726 ),
727 (WrapPunctuation::Quotes, ScriptClass::Latin, None),
728 (WrapPunctuation::Quotes, ScriptClass::Cjk, None),
729 ] {
730 assert_eq!(
731 realize_wrap(&wrap, script, None)
732 .map(|(open, close)| (open.into_owned(), close.into_owned())),
733 expected.map(|(open, close)| (open.to_string(), close.to_string())),
734 "{wrap:?}/{script:?}"
735 );
736 }
737 }
738
739 #[test]
740 fn paired_punctuation_override_includes_both_marks_as_separator() {
741 let overrides = PunctuationRealization {
742 parentheses: Some(["〔".to_string(), "〕".to_string()]),
743 ..PunctuationRealization::default()
744 };
745
746 assert_eq!(
747 realize_punctuation(
748 &DelimiterPunctuation::Parentheses,
749 ScriptClass::Cjk,
750 Some(&overrides),
751 PunctuationPosition::Separator,
752 ),
753 "〔〕"
754 );
755 }
756
757 #[test]
758 fn test_default_methods() {
759 let fmt = DummyFormat;
760 assert_eq!(
761 fmt.semantic_with_attributes("test", "content".to_string(), &[]),
762 "sem[test](content)"
763 );
764 assert_eq!(
765 fmt.citation(vec!["id1".to_string()], "content".to_string()),
766 "content"
767 );
768 assert_eq!(fmt.format_id("id1"), "id1");
769 assert_eq!(
770 fmt.bibliography(vec!["entry1".to_string(), "entry2".to_string()]),
771 "entry1\n\nentry2"
772 );
773 assert_eq!(
774 fmt.entry(
775 "id1",
776 "content".to_string(),
777 None,
778 &ProcEntryMetadata::default()
779 ),
780 "content"
781 );
782 }
783
784 #[test]
785 fn semantic_affixes_use_each_output_formats_text_escaping() {
786 let punctuation = DelimiterPunctuation::Comma;
787
788 assert_eq!(
789 apply_punctuation_affixes(
790 &crate::render::plain::PlainText,
791 Some((&punctuation, "<&")),
792 "value".to_string(),
793 None,
794 ),
795 "<&value"
796 );
797 assert_eq!(
798 apply_punctuation_affixes(
799 &crate::render::html::Html,
800 Some((&punctuation, "<&")),
801 "value".to_string(),
802 None,
803 ),
804 "<&value"
805 );
806 assert_eq!(
807 apply_punctuation_affixes(
808 &crate::render::latex::Latex,
809 Some((&punctuation, "<&")),
810 "value".to_string(),
811 None,
812 ),
813 "<\\&value"
814 );
815 assert_eq!(
816 apply_punctuation_affixes(
817 &crate::render::typst::Typst,
818 Some((&punctuation, "<&")),
819 "value".to_string(),
820 None,
821 ),
822 "\\<&value"
823 );
824 assert_eq!(
825 apply_punctuation_affixes(
826 &crate::render::markdown::Markdown,
827 Some((&punctuation, "<&")),
828 "value".to_string(),
829 None,
830 ),
831 "\\<\\&value"
832 );
833 assert_eq!(
834 apply_punctuation_affixes(
835 &crate::render::djot::Djot,
836 Some((&punctuation, "<&")),
837 "value".to_string(),
838 None,
839 ),
840 "<&value"
841 );
842 assert_eq!(
843 apply_punctuation_affixes(
844 &crate::render::org::OrgOutputFormat,
845 Some((&punctuation, "<&")),
846 "value".to_string(),
847 None,
848 ),
849 "<&value"
850 );
851 }
852
853 #[rstest]
854 #[case::latin_comma(DelimiterPunctuation::Comma, ScriptClass::Latin, Some(','), " ")]
855 #[case::cjk_comma_has_no_tail(DelimiterPunctuation::Comma, ScriptClass::Cjk, Some(','), "")]
856 #[case::custom_period_matches_semantic_period_under_latin(
857 DelimiterPunctuation::Custom(". ".to_string()),
858 ScriptClass::Latin,
859 Some('.'),
860 " ",
861 )]
862 #[case::custom_empty_has_no_core(
863 DelimiterPunctuation::Custom(String::new()),
864 ScriptClass::Latin,
865 None,
866 ""
867 )]
868 #[case::custom_ampersand_space_led_core_is_not_terminal_punctuation(
869 DelimiterPunctuation::Custom(" & ".to_string()),
870 ScriptClass::Latin,
871 Some(' '),
872 "& ",
873 )]
874 fn realized_punctuation_decomposes_core_and_tail(
875 #[case] punctuation: DelimiterPunctuation,
876 #[case] script: ScriptClass,
877 #[case] expected_core: Option<char>,
878 #[case] expected_tail: &str,
879 ) {
880 let realized = realize_punctuation_decomposed(
881 &punctuation,
882 script,
883 None,
884 PunctuationPosition::Separator,
885 );
886
887 assert_eq!(realized.core(), expected_core);
888 assert_eq!(realized.tail(), expected_tail);
889 }
890
891 #[test]
892 fn realized_punctuation_french_colon_has_no_movable_core() {
893 let realization = citum_schema::options::PunctuationRealization {
900 colon: Some("\u{00A0}: ".to_string()),
901 ..Default::default()
902 };
903 let punctuation = DelimiterPunctuation::Colon;
904
905 let realized = realize_punctuation_decomposed(
906 &punctuation,
907 ScriptClass::Latin,
908 Some(&realization),
909 PunctuationPosition::Separator,
910 );
911
912 assert_eq!(realized.text(), "\u{00A0}: ");
913 assert_eq!(realized.core(), Some('\u{00A0}'));
914 assert!(!matches!(realized.core(), Some('.' | ',')));
915 }
916
917 #[test]
918 fn realized_punctuation_is_empty_and_into_owned_detach_from_the_input() {
919 let borrowed = RealizedPunctuation::new(Cow::Borrowed(""));
920 assert!(borrowed.is_empty());
921
922 let source = String::from(", ");
923 let realized = RealizedPunctuation::new(Cow::Borrowed(source.as_str()));
924 let owned = realized.into_owned();
925 drop(source);
926
927 assert_eq!(owned.text(), ", ");
928 assert_eq!(owned.core(), Some(','));
929 }
930}