1use core::num::Saturating;
2
3#[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 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 => {} 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 match *byte {
370 b'\\' => metrics.escape = true,
371 b'\t' => {} 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 match *byte {
425 b'\'' => metrics.single_quotes = true,
426 b'"' => metrics.double_quotes = true,
427 b'\\' => metrics.escape = true,
428 b'\t' => {} c if c <= 0x1f || c == 0x7f => metrics.escape_codes = true,
432 _ => {}
433 }
434 }
435
436 metrics
437 }
438}