Skip to main content

yaml_subset/yaml/
string.rs

1use super::BlockChomping;
2
3/// handle ending newlines for literal and folded strings
4fn handle_ending_newlines(s: String, chomping: BlockChomping) -> String {
5    match chomping {
6        BlockChomping::Keep => {
7            let mut result = s;
8            result.push('\n');
9            result
10        }
11        BlockChomping::Clip => {
12            let mut result = s.trim_end_matches('\n').to_string();
13            result.push('\n');
14            result
15        }
16        BlockChomping::Strip => s.trim_end_matches('\n').to_string(),
17    }
18}
19
20/// Parse literal string to string
21pub fn parse_literal(lines: Vec<&str>, chomping: BlockChomping) -> String {
22    let s = lines.join("\n");
23    handle_ending_newlines(s, chomping)
24}
25
26// Create literal string for string.
27// Strips the trailing "" that split("\n") produces for strings ending with \n,
28// because Keep chomping adds that \n back during deserialization.
29pub fn create_literal(s: String) -> Vec<String> {
30    let mut lines: Vec<String> = s.split('\n').map(str::to_string).collect();
31    if s.ends_with('\n') {
32        if lines.last().map(|l| l.is_empty()).unwrap_or(false) {
33            lines.pop();
34        }
35    }
36    lines
37}
38
39/// Parse folded string to string
40pub fn parse_folded(lines: Vec<&str>, chomping: BlockChomping) -> String {
41    if lines.is_empty() {
42        return String::new();
43    }
44    let start = lines[0].to_string();
45
46    let result = lines
47        .into_iter()
48        .skip(1)
49        .fold((start, false), |(s, prev_was_empty), element| {
50            let mut c = s.clone(); // TODO
51            let is_empty = if element.is_empty() {
52                c.push_str("\n");
53                true
54            } else {
55                let without_indent = &element; // TODO: remove
56
57                if without_indent.starts_with(" ") {
58                    c.push_str("\n");
59                    c.push_str(without_indent);
60                    c.push_str("\n");
61                    true
62                } else {
63                    if !prev_was_empty {
64                        c.push_str(" ");
65                    }
66                    c.push_str(without_indent);
67                    false
68                }
69            };
70            (c, is_empty)
71        })
72        .0;
73    handle_ending_newlines(result, chomping)
74}
75
76fn split_on_length(input: String, length: usize) -> Vec<String> {
77    let mut result = Vec::new();
78    let mut s = String::new();
79    let mut first = true;
80    for word in input.split(" ") {
81        if s.len() + word.len() + 1 > length {
82            result.push(s.clone());
83            s = word.to_string();
84        } else {
85            if !first {
86                s.push_str(" ");
87            }
88            s.push_str(word);
89            first = false;
90        }
91    }
92    if !s.is_empty() {
93        result.push(s.clone());
94    }
95    result
96}
97
98// Create folded string for string.
99// Strips two trailing ""s that the chain([""])  pattern adds for strings ending
100// with \n: one for the paragraph-break after the last wrapped line, and one for
101// the empty split result of the trailing \n.  Keep chomping adds the \n back.
102pub fn create_folded(s: String, line_length: usize) -> Vec<String> {
103    let ends_with_newline = s.ends_with('\n');
104    let mut lines: Vec<String> = s
105        .split('\n')
106        .flat_map(|x| {
107            split_on_length(x.to_string(), line_length)
108                .into_iter()
109                .chain(std::iter::once(String::new()))
110        })
111        .collect();
112    if ends_with_newline {
113        if lines.last().map(|l| l.is_empty()).unwrap_or(false) { lines.pop(); }
114        if lines.last().map(|l| l.is_empty()).unwrap_or(false) { lines.pop(); }
115    }
116    lines
117}
118
119#[derive(Debug, Clone, PartialEq)]
120pub enum SingleQuotedStringPart {
121    String(String),
122    EscapedChar(SingleQuotedStringEscapedChar),
123    BlankLines(usize),
124    RemovableNewline,
125}
126
127#[derive(Debug, PartialEq, Clone, Copy)]
128pub enum SingleQuotedStringEscapedChar {
129    SingleQuote,
130}
131
132impl SingleQuotedStringEscapedChar {
133    pub fn char(&self) -> char {
134        match self {
135            SingleQuotedStringEscapedChar::SingleQuote => '\'',
136        }
137    }
138}
139
140pub fn parse_single_quoted_string(parts: &Vec<SingleQuotedStringPart>) -> String {
141    let mut result = String::new();
142    for part in parts.iter() {
143        match part {
144            SingleQuotedStringPart::String(s) => result.push_str(&s),
145            SingleQuotedStringPart::EscapedChar(c) => result.push(c.char()),
146            SingleQuotedStringPart::BlankLines(nb) => {
147                for _ in 0..*nb {
148                    result.push_str("\n");
149                }
150            }
151            SingleQuotedStringPart::RemovableNewline => (),
152        }
153    }
154    result
155}
156
157#[derive(Debug, Clone, PartialEq)]
158pub enum DoubleQuotedStringPart {
159    String(String),
160    EscapedChar(DoubleQuotedStringEscapedChar),
161    BlankLines(usize),
162    RemovableNewline,
163}
164
165#[derive(Debug, PartialEq, Clone, Copy)]
166pub enum DoubleQuotedStringEscapedChar {
167    Quote,
168    Backslash,
169    Tab,
170    CarriageReturn,
171    Newline,
172    RealNewline,
173}
174
175impl DoubleQuotedStringEscapedChar {
176    pub fn char(&self) -> char {
177        match self {
178            DoubleQuotedStringEscapedChar::Quote => '"',
179            DoubleQuotedStringEscapedChar::Backslash => '\\',
180            DoubleQuotedStringEscapedChar::Tab => 't',
181            DoubleQuotedStringEscapedChar::CarriageReturn => 'r',
182            DoubleQuotedStringEscapedChar::Newline => 'n',
183            DoubleQuotedStringEscapedChar::RealNewline => '\n',
184        }
185    }
186    pub fn real_char(&self) -> char {
187        match self {
188            DoubleQuotedStringEscapedChar::Quote => '"',
189            DoubleQuotedStringEscapedChar::Backslash => '\\',
190            DoubleQuotedStringEscapedChar::Tab => '\t',
191            DoubleQuotedStringEscapedChar::CarriageReturn => '\r',
192            DoubleQuotedStringEscapedChar::Newline => '\n',
193            DoubleQuotedStringEscapedChar::RealNewline => '\n',
194        }
195    }
196}
197
198pub fn parse_double_quoted_string(parts: &Vec<DoubleQuotedStringPart>) -> String {
199    let mut s = String::new();
200    for part in parts {
201        match part {
202            DoubleQuotedStringPart::String(s2) => s.push_str(&s2),
203            DoubleQuotedStringPart::EscapedChar(c) => s.push(c.real_char()),
204            DoubleQuotedStringPart::BlankLines(n) => {
205                for _ in 0..*n {
206                    s.push_str("\n");
207                }
208            }
209            DoubleQuotedStringPart::RemovableNewline => (),
210        }
211    }
212    s
213}
214
215#[cfg(test)]
216mod tests {
217    use crate::yaml::BlockChomping;
218
219    #[test]
220    fn parse_folded() {
221        let input = vec![
222            "Several lines of text,",
223            r#"with some "quotes" of various 'types',"#,
224            "and also a blank line:",
225            "",
226            "and some text with",
227            "  extra indentation",
228            "on the next line,",
229            "plus another line at the end.",
230            "",
231            "",
232        ];
233        assert_eq!(
234            super::parse_folded(input, BlockChomping::Clip),
235            r#"Several lines of text, with some "quotes" of various 'types', and also a blank line:
236and some text with
237  extra indentation
238on the next line, plus another line at the end.
239"#
240        )
241    }
242    /* TODO: remove
243        #[test]
244        fn parse_folded_indent() {
245            let input = vec![
246                "    Several lines of text,",
247                r#"  with some "quotes" of various 'types',"#,
248                "  and also a blank line:",
249                "",
250                "  and some text with",
251                "    extra indentation",
252                "  on the next line,",
253                "  plus another line at the end.",
254                "",
255                "",
256            ];
257            assert_eq!(
258                super::parse_folded(input),
259                r#"  Several lines of text, with some "quotes" of various 'types', and also a blank line:
260    and some text with
261      extra indentation
262    on the next line, plus another line at the end.
263    "#
264            )
265        }*/
266
267    #[test]
268    fn create_folded() {
269        let input = r#"Several lines of text, with some "quotes" of various 'types', and also a blank line:
270and some text with
271  extra indentation
272on the next line, plus another line at the end.
273"#;
274        assert_eq!(
275            super::create_folded(input.to_string(), 40),
276            vec![
277                r#"Several lines of text, with some"#,
278                r#""quotes" of various 'types', and also a"#,
279                "blank line:",
280                "",
281                "and some text with",
282                "",
283                "  extra indentation",
284                "",
285                "on the next line, plus another line at",
286                "the end.",
287            ]
288        )
289    }
290
291    #[test]
292    fn parse_literal() {
293        let mut input = vec![
294            "Several lines of text,",
295            r#"with some "quotes" of various 'types',"#,
296            "and also a blank line:",
297            "",
298            "and some text with",
299            "  extra indentation",
300            "on the next line,",
301            "plus another line at the end.",
302            "",
303            "",
304        ];
305        let result = r#"Several lines of text,
306with some "quotes" of various 'types',
307and also a blank line:
308
309and some text with
310  extra indentation
311on the next line,
312plus another line at the end.
313"#;
314        assert_eq!(
315            super::parse_literal(input.clone(), BlockChomping::default()),
316            result
317        );
318        // Round-trip: create_literal of the parsed string gives back the original lines
319        // (minus the two trailing empty strings that represent chomping).
320        input.pop();
321        input.pop();
322        assert_eq!(input, super::create_literal(result.to_string()));
323
324        // Leading spaces in the first line must be preserved (needed for |2 indicator).
325        let with_leading = format!(" {}", result);
326        let lines_with_leading = super::create_literal(with_leading);
327        assert!(lines_with_leading[0].starts_with(' '), "leading space must be preserved")
328    }
329
330    /* TODO: remove
331        #[test]
332        fn parse_literal_indent() {
333            let mut input = vec![
334                "    Several lines of text,",
335                r#"  with some "quotes" of various 'types',"#,
336                "  and also a blank line:",
337                "",
338                "  and some text with",
339                "    extra indentation",
340                "  on the next line,",
341                "  plus another line at the end.",
342                "",
343                "",
344            ];
345            let result = r#"  Several lines of text,
346    with some "quotes" of various 'types',
347    and also a blank line:
348
349    and some text with
350      extra indentation
351    on the next line,
352    plus another line at the end.
353    "#;
354            assert_eq!(super::parse_literal(&Some(2), input.clone()), result);
355            let result = format!(" {}", result); // add leading space as test
356            input.pop(); // Remove extra one
357            assert_eq!(input, super::create_literal(result))
358        }*/
359}