Skip to main content

toml_writer/
string.rs

1use core::num::Saturating;
2
3/// Describes how a TOML string (key or value) should be formatted.
4///
5/// # Example
6///
7/// ```rust
8/// # #[cfg(feature = "alloc")] {
9/// # use toml_writer::ToTomlValue as _;
10/// let string = "Hello
11/// world!
12/// ";
13/// let string = toml_writer::TomlStringBuilder::new(string).as_default();
14/// let string = string.to_toml_value();
15/// assert_eq!(string, r#""""
16/// Hello
17/// world!
18/// """"#);
19/// # }
20/// ```
21#[derive(Copy, Clone, Debug)]
22pub struct TomlStringBuilder<'s> {
23    decoded: &'s str,
24    metrics: ValueMetrics,
25}
26
27impl<'s> TomlStringBuilder<'s> {
28    pub fn new(decoded: &'s str) -> Self {
29        Self {
30            decoded,
31            metrics: ValueMetrics::calculate(decoded),
32        }
33    }
34
35    pub fn as_default(&self) -> TomlString<'s> {
36        self.as_basic_pretty()
37            .or_else(|| self.as_literal())
38            .or_else(|| self.as_ml_basic_pretty())
39            .or_else(|| self.as_ml_literal())
40            .unwrap_or_else(|| {
41                if self.metrics.newline {
42                    self.as_ml_basic()
43                } else {
44                    self.as_basic()
45                }
46            })
47    }
48
49    pub fn as_literal(&self) -> Option<TomlString<'s>> {
50        if self.metrics.escape_codes
51            || 0 < self.metrics.max_seq_single_quotes
52            || self.metrics.newline
53        {
54            None
55        } else {
56            Some(TomlString {
57                decoded: self.decoded,
58                encoding: Encoding::LiteralString,
59                newline: self.metrics.newline,
60            })
61        }
62    }
63
64    pub fn as_ml_literal(&self) -> Option<TomlString<'s>> {
65        if self.metrics.escape_codes || 2 < self.metrics.max_seq_single_quotes {
66            None
67        } else {
68            Some(TomlString {
69                decoded: self.decoded,
70                encoding: Encoding::MlLiteralString,
71                newline: self.metrics.newline,
72            })
73        }
74    }
75
76    pub fn as_basic_pretty(&self) -> Option<TomlString<'s>> {
77        if self.metrics.escape_codes
78            || self.metrics.escape
79            || 0 < self.metrics.max_seq_double_quotes
80            || self.metrics.newline
81        {
82            None
83        } else {
84            Some(self.as_basic())
85        }
86    }
87
88    pub fn as_ml_basic_pretty(&self) -> Option<TomlString<'s>> {
89        if self.metrics.escape_codes
90            || self.metrics.escape
91            || 2 < self.metrics.max_seq_double_quotes
92        {
93            None
94        } else {
95            Some(self.as_ml_basic())
96        }
97    }
98
99    pub fn as_basic(&self) -> TomlString<'s> {
100        TomlString {
101            decoded: self.decoded,
102            encoding: Encoding::BasicString,
103            newline: self.metrics.newline,
104        }
105    }
106
107    pub fn as_ml_basic(&self) -> TomlString<'s> {
108        TomlString {
109            decoded: self.decoded,
110            encoding: Encoding::MlBasicString,
111            newline: self.metrics.newline,
112        }
113    }
114}
115
116#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
117pub struct TomlString<'s> {
118    decoded: &'s str,
119    encoding: Encoding,
120    newline: bool,
121}
122
123impl crate::WriteTomlValue for TomlString<'_> {
124    fn write_toml_value<W: crate::TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
125        write_toml_value(self.decoded, Some(self.encoding), self.newline, writer)
126    }
127}
128
129#[derive(Copy, Clone, Debug)]
130pub struct TomlKeyBuilder<'s> {
131    decoded: &'s str,
132    metrics: KeyMetrics,
133}
134
135impl<'s> TomlKeyBuilder<'s> {
136    pub fn new(decoded: &'s str) -> Self {
137        Self {
138            decoded,
139            metrics: KeyMetrics::calculate(decoded),
140        }
141    }
142
143    pub fn as_default(&self) -> TomlKey<'s> {
144        self.as_unquoted()
145            .or_else(|| self.as_basic_pretty())
146            .or_else(|| self.as_literal())
147            .unwrap_or_else(|| self.as_basic())
148    }
149
150    pub fn as_unquoted(&self) -> Option<TomlKey<'s>> {
151        if self.metrics.unquoted {
152            Some(TomlKey {
153                decoded: self.decoded,
154                encoding: None,
155            })
156        } else {
157            None
158        }
159    }
160
161    pub fn as_literal(&self) -> Option<TomlKey<'s>> {
162        if self.metrics.escape_codes || self.metrics.single_quotes {
163            None
164        } else {
165            Some(TomlKey {
166                decoded: self.decoded,
167                encoding: Some(Encoding::LiteralString),
168            })
169        }
170    }
171
172    pub fn as_basic_pretty(&self) -> Option<TomlKey<'s>> {
173        if self.metrics.escape_codes || self.metrics.escape || self.metrics.double_quotes {
174            None
175        } else {
176            Some(self.as_basic())
177        }
178    }
179
180    pub fn as_basic(&self) -> TomlKey<'s> {
181        TomlKey {
182            decoded: self.decoded,
183            encoding: Some(Encoding::BasicString),
184        }
185    }
186}
187
188#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
189pub struct TomlKey<'s> {
190    decoded: &'s str,
191    encoding: Option<Encoding>,
192}
193
194impl crate::WriteTomlKey for TomlKey<'_> {
195    fn write_toml_key<W: crate::TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
196        let newline = false;
197        write_toml_value(self.decoded, self.encoding, newline, writer)
198    }
199}
200
201#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
202#[repr(u8)]
203#[allow(clippy::enum_variant_names)]
204enum Encoding {
205    LiteralString,
206    BasicString,
207    MlLiteralString,
208    MlBasicString,
209}
210
211impl Encoding {}
212
213fn write_toml_value<W: crate::TomlWrite + ?Sized>(
214    decoded: &str,
215    encoding: Option<Encoding>,
216    newline: bool,
217    writer: &mut W,
218) -> core::fmt::Result {
219    let delimiter = match encoding {
220        Some(Encoding::LiteralString) => "'",
221        Some(Encoding::BasicString) => "\"",
222        Some(Encoding::MlLiteralString) => "'''",
223        Some(Encoding::MlBasicString) => "\"\"\"",
224        None => "",
225    };
226    let escaped = match encoding {
227        Some(Encoding::LiteralString) | Some(Encoding::MlLiteralString) => false,
228        Some(Encoding::BasicString) | Some(Encoding::MlBasicString) => true,
229        None => false,
230    };
231    let is_ml = match encoding {
232        Some(Encoding::LiteralString) | Some(Encoding::BasicString) => false,
233        Some(Encoding::MlLiteralString) | Some(Encoding::MlBasicString) => true,
234        None => false,
235    };
236    let newline_prefix = newline && is_ml;
237
238    write!(writer, "{delimiter}")?;
239    if newline_prefix {
240        writer.newline()?;
241    }
242    if escaped {
243        // ```bnf
244        // basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
245        // wschar =  %x20  ; Space
246        // wschar =/ %x09  ; Horizontal tab
247        // escape = %x5C                   ; \
248        // ```
249        let max_seq_double_quotes = if is_ml { 2 } else { 0 };
250        let mut stream = decoded;
251        while !stream.is_empty() {
252            let mut unescaped_end = 0;
253            let mut escaped = None;
254            let mut seq_double_quotes = 0;
255            for (i, b) in stream.as_bytes().iter().enumerate() {
256                if *b == b'"' {
257                    seq_double_quotes += 1;
258                    if max_seq_double_quotes < seq_double_quotes {
259                        escaped = Some(r#"\""#);
260                        break;
261                    }
262                } else {
263                    seq_double_quotes = 0;
264                }
265
266                match *b {
267                    0x8 => {
268                        escaped = Some(r#"\b"#);
269                        break;
270                    }
271                    0x9 => {
272                        escaped = Some(r#"\t"#);
273                        break;
274                    }
275                    0xa => {
276                        if !is_ml {
277                            escaped = Some(r#"\n"#);
278                            break;
279                        }
280                    }
281                    0xc => {
282                        escaped = Some(r#"\f"#);
283                        break;
284                    }
285                    0xd => {
286                        escaped = Some(r#"\r"#);
287                        break;
288                    }
289                    0x22 => {} // double quote handled earlier
290                    0x5c => {
291                        escaped = Some(r#"\\"#);
292                        break;
293                    }
294                    c if c <= 0x1f || c == 0x7f => {
295                        break;
296                    }
297                    _ => {}
298                }
299
300                unescaped_end = i + 1;
301            }
302            let unescaped = &stream[0..unescaped_end];
303            let escaped_str = escaped.unwrap_or("");
304            let end = unescaped_end + if escaped.is_some() { 1 } else { 0 };
305            stream = &stream[end..];
306            write!(writer, "{unescaped}{escaped_str}")?;
307            if escaped.is_none() && !stream.is_empty() {
308                let b = stream.as_bytes().first().unwrap();
309                write!(writer, "\\u{:04X}", *b as u32)?;
310                stream = &stream[1..];
311            }
312        }
313    } else {
314        write!(writer, "{decoded}")?;
315    }
316    write!(writer, "{delimiter}")?;
317    Ok(())
318}
319
320#[derive(Copy, Clone, Debug)]
321struct ValueMetrics {
322    max_seq_single_quotes: u8,
323    max_seq_double_quotes: u8,
324    escape_codes: bool,
325    escape: bool,
326    newline: bool,
327}
328
329impl ValueMetrics {
330    fn new() -> Self {
331        Self {
332            max_seq_single_quotes: 0,
333            max_seq_double_quotes: 0,
334            escape_codes: false,
335            escape: false,
336            newline: false,
337        }
338    }
339
340    fn calculate(s: &str) -> Self {
341        let mut metrics = Self::new();
342
343        let mut prev_single_quotes = Saturating(0);
344        let mut prev_double_quotes = Saturating(0);
345        for byte in s.as_bytes() {
346            if *byte == b'\'' {
347                prev_single_quotes += 1;
348                metrics.max_seq_single_quotes =
349                    metrics.max_seq_single_quotes.max(prev_single_quotes.0);
350            } else {
351                prev_single_quotes = Saturating(0);
352            }
353            if *byte == b'"' {
354                prev_double_quotes += 1;
355                metrics.max_seq_double_quotes =
356                    metrics.max_seq_double_quotes.max(prev_double_quotes.0);
357            } else {
358                prev_double_quotes = Saturating(0);
359            }
360
361            // ```bnf
362            // literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
363            //
364            // basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
365            // wschar =  %x20  ; Space
366            // wschar =/ %x09  ; Horizontal tab
367            // escape = %x5C                   ; \
368            // ```
369            match *byte {
370                b'\\' => metrics.escape = true,
371                // Escape codes are needed if any ascii control
372                // characters are present, including \b \f \r.
373                b'\t' => {} // always allowed; remaining neutral on this
374                b'\n' => metrics.newline = true,
375                c if c <= 0x1f || c == 0x7f => metrics.escape_codes = true,
376                _ => {}
377            }
378        }
379
380        metrics
381    }
382}
383
384#[derive(Copy, Clone, Debug)]
385struct KeyMetrics {
386    unquoted: bool,
387    single_quotes: bool,
388    double_quotes: bool,
389    escape_codes: bool,
390    escape: bool,
391}
392
393impl KeyMetrics {
394    fn new() -> Self {
395        Self {
396            unquoted: true,
397            single_quotes: false,
398            double_quotes: false,
399            escape_codes: false,
400            escape: false,
401        }
402    }
403
404    fn calculate(s: &str) -> Self {
405        let mut metrics = Self::new();
406
407        metrics.unquoted = !s.is_empty();
408
409        for byte in s.as_bytes() {
410            if !matches!(*byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_') {
411                metrics.unquoted = false;
412            }
413
414            // ```bnf
415            // unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
416            //
417            // literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
418            //
419            // basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
420            // wschar =  %x20  ; Space
421            // wschar =/ %x09  ; Horizontal tab
422            // escape = %x5C                   ; \
423            // ```
424            match *byte {
425                b'\'' => metrics.single_quotes = true,
426                b'"' => metrics.double_quotes = true,
427                b'\\' => metrics.escape = true,
428                // Escape codes are needed if any ascii control
429                // characters are present, including \b \f \r.
430                b'\t' => {} // always allowed
431                c if c <= 0x1f || c == 0x7f => metrics.escape_codes = true,
432                _ => {}
433            }
434        }
435
436        metrics
437    }
438}