1use indexmap::IndexMap;
7
8pub fn emit_scalar(s: &str) -> String {
9 if s.is_empty() {
10 return "''".into();
11 }
12 if needs_ansi_c(s) {
13 return ansi_c(s);
14 }
15 let mut out = String::with_capacity(s.len() + 2);
16 out.push('\'');
17 for c in s.chars() {
18 if c == '\'' {
19 out.push_str("'\\''");
20 } else {
21 out.push(c);
22 }
23 }
24 out.push('\'');
25 out
26}
27
28pub fn emit_q_words(words: &[String]) -> String {
29 words
30 .iter()
31 .map(|word| emit_scalar(word))
32 .collect::<Vec<_>>()
33 .join(" ")
34}
35
36pub fn emit_array(words: &[String]) -> String {
40 literal(words.iter().map(|word| emit_scalar(word)))
41}
42
43pub fn emit_indexed(m: &IndexMap<usize, String>) -> String {
44 literal(
45 m.iter()
46 .map(|(key, value)| format!("[{key}]={}", emit_scalar(value))),
47 )
48}
49
50pub fn emit_assoc(m: &IndexMap<String, String>) -> String {
51 literal(m.iter().map(|(key, value)| {
52 format!(
53 "[{}]={}",
54 emit_scalar(key),
55 emit_scalar(value)
56 )
57 }))
58}
59
60fn literal(pairs: impl IntoIterator<Item = String>) -> String {
61 format!(
62 "({})",
63 pairs.into_iter().collect::<Vec<_>>().join(" ")
64 )
65}
66
67fn needs_ansi_c(s: &str) -> bool {
68 s.bytes().any(|b| b < 0x20 || b == 0x7f)
69}
70
71fn ansi_c(s: &str) -> String {
72 let mut out = String::with_capacity(s.len() + 4);
73 out.push_str("$'");
74 for c in s.chars() {
75 match c {
76 '\'' => out.push_str("\\'"),
77 '\\' => out.push_str("\\\\"),
78 '\n' => out.push_str("\\n"),
79 '\r' => out.push_str("\\r"),
80 '\t' => out.push_str("\\t"),
81 '\x07' => out.push_str("\\a"),
82 '\x08' => out.push_str("\\b"),
83 '\x1B' => out.push_str("\\E"),
84 '\x0C' => out.push_str("\\f"),
85 '\x0B' => out.push_str("\\v"),
86 c if (c as u32) < 0x20 || c == '\x7f' => {
87 out.push_str(&format!("\\{:03o}", c as u32));
88 }
89 c => out.push(c),
90 }
91 }
92 out.push('\'');
93 out
94}
95
96#[cfg(test)]
97mod tests {
98 use super::super::parser::{parse_assoc, parse_indexed, parse_q_words, parse_scalar};
99 use super::*;
100
101 fn ix<I: IntoIterator<Item = (usize, &'static str)>>(it: I) -> IndexMap<usize, String> {
102 it.into_iter().map(|(k, v)| (k, v.to_string())).collect()
103 }
104 fn ax<I: IntoIterator<Item = (&'static str, &'static str)>>(it: I) -> IndexMap<String, String> {
105 it.into_iter()
106 .map(|(k, v)| (k.to_string(), v.to_string()))
107 .collect()
108 }
109
110 #[test]
111 fn scalar_forms() {
112 assert_eq!(emit_scalar(""), "''");
113 assert_eq!(emit_scalar("hello"), "'hello'");
114 assert_eq!(emit_scalar("it's"), "'it'\\''s'");
115 assert_eq!(emit_scalar("a\nb"), "$'a\\nb'");
116 assert_eq!(emit_scalar("a\tb"), "$'a\\tb'");
117 }
118
119 #[test]
120 fn q_words_emit() {
121 assert_eq!(emit_q_words(&[]), "");
122 assert_eq!(
123 emit_q_words(&["a".into(), "b c".into()]),
124 "'a' 'b c'"
125 );
126 }
127
128 #[test]
129 fn indexed_emit() {
130 assert_eq!(emit_indexed(&ix([])), "()");
131 assert_eq!(
132 emit_indexed(&ix([(0, "a"), (5, "b c")])),
133 "([0]='a' [5]='b c')"
134 );
135 }
136
137 #[test]
138 fn assoc_emit() {
139 assert_eq!(emit_assoc(&ax([])), "()");
140 assert_eq!(
141 emit_assoc(&ax([("k", "v"), ("k 2", "v 2")])),
142 "(['k']='v' ['k 2']='v 2')"
143 );
144 }
145
146 #[test]
147 fn roundtrip_scalar() {
148 for s in ["", "simple", "it's", "a\nb", "a\tb", "c\x01d", "café"] {
149 assert_eq!(
150 parse_scalar(&emit_scalar(s)).unwrap(),
151 s
152 );
153 }
154 }
155
156 #[test]
157 fn roundtrip_q_words() {
158 let v = vec![
159 "a".to_string(),
160 "b c".to_string(),
161 "d\ne".to_string(),
162 "".to_string(),
163 ];
164 assert_eq!(
165 parse_q_words(&emit_q_words(&v)).unwrap(),
166 v
167 );
168 }
169
170 #[test]
171 fn roundtrip_indexed() {
172 let m = ix([(0, "a"), (2, "d\ne"), (5, "b c")]);
173 assert_eq!(
174 parse_indexed(&emit_indexed(&m)).unwrap(),
175 m
176 );
177 }
178
179 #[test]
180 fn roundtrip_assoc() {
181 let m = ax([("foo", "1"), ("k 2", "v\n2")]);
182 assert_eq!(parse_assoc(&emit_assoc(&m)).unwrap(), m);
183 }
184}