ephemeris_documents/yaml/
literal.rs1use std::iter;
4
5use crate::text::is_unsafe_text_char;
6
7pub(crate) struct LiteralBlock<'a>(&'a str);
9
10impl<'a> LiteralBlock<'a> {
11 pub(crate) const fn new(text: &'a str) -> Self {
12 Self(text)
13 }
14
15 pub(crate) fn is_representable(&self) -> bool {
22 (self.0.is_empty() || !self.0.trim_matches('\n').is_empty())
23 && !self.0.split('\n').any(|line| line.starts_with('\t'))
24 && !self
25 .0
26 .chars()
27 .any(|c| is_unsafe_text_char(c) || c == '\u{feff}')
28 }
29
30 pub(super) fn header(&self) -> String {
38 let mut header = String::from("|");
39 if self.0.starts_with('\n') || self.0.starts_with(' ') {
40 header.push('2');
41 }
42 match self.trailing_newlines() {
43 0 => header.push('-'),
44 1 => {}
45 _ => header.push('+'),
46 }
47 header
48 }
49
50 pub(super) fn lines(&self) -> impl Iterator<Item = &str> {
53 let body = self.0.trim_end_matches('\n');
54 let extra = self.trailing_newlines().saturating_sub(1);
55 (!self.0.is_empty())
56 .then(|| body.split('\n'))
57 .into_iter()
58 .flatten()
59 .chain(iter::repeat_n("", extra))
60 }
61
62 fn trailing_newlines(&self) -> usize {
63 self.0
64 .len()
65 .saturating_sub(self.0.trim_end_matches('\n').len())
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::LiteralBlock;
72
73 #[test]
74 fn chomping_follows_trailing_newlines() {
75 assert_eq!(LiteralBlock("a").header(), "|-");
76 assert_eq!(LiteralBlock("a\n").header(), "|");
77 assert_eq!(LiteralBlock("a\n\n").header(), "|+");
78 }
79
80 #[test]
81 fn leading_space_needs_an_indentation_indicator() {
82 assert_eq!(LiteralBlock(" a\n").header(), "|2");
83 assert_eq!(LiteralBlock("\n a").header(), "|2-");
84 assert_eq!(LiteralBlock("\nb").header(), "|2-");
85 }
86
87 #[test]
88 fn unrepresentable_text_is_detected() {
89 for text in ["\n", "a\rb", "\ta", "a\n\tb", "a\u{1}", "a\u{2028}b"] {
90 assert!(!LiteralBlock(text).is_representable(), "{text:?}");
91 }
92 for text in ["a\nb\n", "", "a\tb", "a \tb\n"] {
93 assert!(LiteralBlock(text).is_representable(), "{text:?}");
94 }
95 }
96
97 #[test]
98 fn empty_text_has_no_lines() {
99 assert_eq!(LiteralBlock("").header(), "|-");
100 assert_eq!(LiteralBlock("").lines().count(), 0);
101 }
102
103 #[test]
104 fn keep_chomping_adds_empty_lines() {
105 let lines: Vec<_> = LiteralBlock("a\nb\n\n\n").lines().collect();
106 assert_eq!(lines, ["a", "b", "", ""]);
107 }
108}