/// Describes how a TOML string (key or value) should be formatted.
///
/// # Example
///
/// ```rust
/// # #[cfg(feature = "alloc")] {
/// # use toml_writer::ToTomlValue as _;
/// let string = "Hello
/// world!
/// ";
/// let string = toml_writer::TomlStringBuilder::new(string).as_default();
/// let string = string.to_toml_value();
/// assert_eq!(string, r#""""
/// Hello
/// world!
/// """"#);
/// # }
/// ```
#[ derive(Copy, Clone, Debug)]
pub struct TomlStringBuilder<'s> {
decoded: &'s str,
metrics: ValueMetrics,
}
impl <'s> TomlStringBuilder<' s> {
pub fn new(decoded: &'s str) -> Self {
Self {
decoded,
metrics: ValueMetrics::calculate(decoded),
}
}
pub fn as_default(&self ) -> TomlString<'s> {
self .as_basic_pretty()
.or_else(|| self .as_literal())
.or_else(|| self .as_ml_basic_pretty())
.or_else(|| self .as_ml_literal())
.unwrap_or_else(|| {
if self .metrics.newline {
self .as_ml_basic()
} else {
self .as_basic()
}
})
}
pub fn as_literal(&self ) -> Option<TomlString<'s>> {
if self .metrics.escape_codes
|| 0 < self .metrics.max_seq_single_quotes
|| self .metrics.newline
{
None
} else {
Some(TomlString {
decoded: self .decoded,
encoding: Encoding::LiteralString,
newline: self .metrics.newline,
})
}
}
pub fn as_ml_literal(&self ) -> Option<TomlString<'s>> {
if self .metrics.escape_codes || 2 < self .metrics.max_seq_single_quotes {
None
} else {
Some(TomlString {
decoded: self .decoded,
encoding: Encoding::MlLiteralString,
newline: self .metrics.newline,
})
}
}
pub fn as_basic_pretty(&self ) -> Option<TomlString<'s>> {
if self .metrics.escape_codes
|| self .metrics.escape
|| 0 < self .metrics.max_seq_double_quotes
|| self .metrics.newline
{
None
} else {
Some(self .as_basic())
}
}
pub fn as_ml_basic_pretty(&self ) -> Option<TomlString<'s>> {
if self .metrics.escape_codes
|| self .metrics.escape
|| 2 < self .metrics.max_seq_double_quotes
{
None
} else {
Some(self .as_ml_basic())
}
}
pub fn as_basic(&self ) -> TomlString<'s> {
TomlString {
decoded: self .decoded,
encoding: Encoding::BasicString,
newline: self .metrics.newline,
}
}
pub fn as_ml_basic(&self ) -> TomlString<'s> {
TomlString {
decoded: self .decoded,
encoding: Encoding::MlBasicString,
newline: self .metrics.newline,
}
}
}
#[ derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct TomlString<'s> {
decoded: &'s str,
encoding: Encoding,
newline: bool,
}
impl crate ::WriteTomlValue for TomlString<'_> {
fn write_toml_value<W: crate ::TomlWrite + ?Sized>(&self , writer: &pan style='color:red'>mut W) -> core::fmt::Result {
write_toml_value(self .decoded, Some(self .encoding), self .newline, writer)
}
}
#[ derive(Copy, Clone, Debug)]
pub struct TomlKeyBuilder<'s> {
decoded: &'s str,
metrics: KeyMetrics,
}
impl <'s> TomlKeyBuilder<' s> {
pub fn new(decoded: &'s str) -> Self {
Self {
decoded,
metrics: KeyMetrics::calculate(decoded),
}
}
pub fn as_default(&self ) -> TomlKey<'s> {
self .as_unquoted()
.or_else(|| self .as_basic_pretty())
.or_else(|| self .as_literal())
.unwrap_or_else(|| self .as_basic())
}
pub fn as_unquoted(&self ) -> Option<TomlKey<'s>> {
if self .metrics.unquoted {
Some(TomlKey {
decoded: self .decoded,
encoding: None,
})
} else {
None
}
}
pub fn as_literal(&self ) -> Option<TomlKey<'s>> {
if self .metrics.escape_codes || self .metrics.single_quotes {
None
} else {
Some(TomlKey {
decoded: self .decoded,
encoding: Some(Encoding::LiteralString),
})
}
}
pub fn as_basic_pretty(&self ) -> Option<TomlKey<'s>> {
if self .metrics.escape_codes || self .metrics.escape || self .metrics.double_quotes {
None
} else {
Some(self .as_basic())
}
}
pub fn as_basic(&self ) -> TomlKey<'s> {
TomlKey {
decoded: self .decoded,
encoding: Some(Encoding::BasicString),
}
}
}
#[ derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct TomlKey<'s> {
decoded: &'s str,
encoding: Option<Encoding>,
}
impl crate ::WriteTomlKey for TomlKey<'_> {
fn write_toml_key<W: crate ::TomlWrite + ?Sized>(&self , writer: &n style='color:red'>mut W) -> core::fmt::Result {
let newline = false ;
write_toml_value(self .decoded, self .encoding, newline, writer)
}
}
#[ derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[ repr(u8)]
#[ allow(clippy::enum_variant_names)]
enum Encoding {
LiteralString,
BasicString,
MlLiteralString,
MlBasicString,
}
impl Encoding {}
fn write_toml_value<W: crate ::TomlWrite + ?Sized>(
decoded: &str,
encoding: Option<Encoding>,
newline: bool,
writer: &mut W,
) -> core::fmt::Result {
let delimiter = match encoding {
Some(Encoding::LiteralString) => "'" ,
Some(Encoding::BasicString) => "\" ",
Some(Encoding::MlLiteralString) => "'''" ,
Some(Encoding::MlBasicString) => "\" \"\" ",
None => "" ,
};
let escaped = match encoding {
Some(Encoding::LiteralString) | Some(Encoding::MlLiteralString) => false ,
Some(Encoding::BasicString) | Some(Encoding::MlBasicString) => true ,
None => false ,
};
let is_ml = match encoding {
Some(Encoding::LiteralString) | Some(Encoding::BasicString) => false ,
Some(Encoding::MlLiteralString) | Some(Encoding::MlBasicString) => true ,
None => false ,
};
let newline_prefix = newline && is_ml;
write!(writer, "{delimiter}" )?;
if newline_prefix {
writer.newline()?;
}
if escaped {
// ```bnf
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// wschar = %x20 ; Space
// wschar =/ %x09 ; Horizontal tab
// escape = %x5C ; \
// ```
let max_seq_double_quotes = if is_ml { 2 } else { 0 };
let mut stream = decoded;
while !stream.is_empty() {
let mut unescaped_end = 0 ;
let mut escaped = None;
let mut seq_double_quotes = 0 ;
for (i, b) in stream.as_bytes().iter().enumerate() {
if *b == b'"' {
seq_double_quotes += 1 ;
if max_seq_double_quotes < seq_double_quotes {
escaped = Some(r#" \"" #) ;
break ;
}
} else {
seq_double_quotes = 0 ;
}
match *b {
0 x8 => {
escaped = Some(r#" \b"#);
break ;
}
0 x9 => {
escaped = Some(r#" \t"#);
break ;
}
0 xa => {
if !is_ml {
escaped = Some(r#" \n"#);
break ;
}
}
0 xc => {
escaped = Some(r#" \f"#);
break ;
}
0 xd => {
escaped = Some(r#" \r"#);
break ;
}
0 x22 => {} // double quote handled earlier
0 x5c => {
escaped = Some(r#" \\"#);
break ;
}
c if c <= 0 x1f || c == 0 x7f => {
break ;
}
_ => {}
}
unescaped_end = i + 1 ;
}
let unescaped = &stream[0 ..unescaped_end];
let escaped_str = escaped.unwrap_or("" );
let end = unescaped_end + if escaped.is_some() { 1 } else { 0 };
stream = &stream[end..];
write!(writer, "{unescaped}{escaped_str}" )?;
if escaped.is_none() && !stream.is_empty() {
let b = stream.as_bytes().first().unwrap();
write!(writer, "\\u{:04X}" , *b as u32)?;
stream = &stream[1 ..];
}
}
} else {
write!(writer, "{decoded}" )?;
}
write!(writer, "{delimiter}" )?;
Ok(())
}
#[ derive(Copy, Clone, Debug)]
struct ValueMetrics {
max_seq_single_quotes: u8,
max_seq_double_quotes: u8,
escape_codes: bool,
escape: bool,
newline: bool,
}
impl ValueMetrics {
fn new() -> Self {
Self {
max_seq_single_quotes: 0 ,
max_seq_double_quotes: 0 ,
escape_codes: false ,
escape: false ,
newline: false ,
}
}
fn calculate(s: &str) -> Self {
let mut metrics = Self ::new();
let mut prev_single_quotes = 0 ;
let mut prev_double_quotes = 0 ;
for byte in s.as_bytes() {
if *byte == b'\' ' {
prev_single_quotes += 1 ;
metrics.max_seq_single_quotes =
metrics.max_seq_single_quotes.max(prev_single_quotes);
} else {
prev_single_quotes = 0 ;
}
if *byte == b'"' {
prev_double_quotes += 1 ;
metrics.max_seq_double_quotes =
metrics.max_seq_double_quotes.max(prev_double_quotes);
} else {
prev_double_quotes = 0 ;
}
// ```bnf
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
//
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// wschar = %x20 ; Space
// wschar =/ %x09 ; Horizontal tab
// escape = %x5C ; \
// ```
match *byte {
b'\\' => metrics.escape = true ,
// Escape codes are needed if any ascii control
// characters are present, including \b \f \r.
b'\t' => {} // always allowed; remaining neutral on this
b'\n' => metrics.newline = true ,
c if c <= 0 x1f || c == 0 x7f => metrics.escape_codes = true ,
_ => {}
}
}
metrics
}
}
#[ derive(Copy, Clone, Debug)]
struct KeyMetrics {
unquoted: bool,
single_quotes: bool,
double_quotes: bool,
escape_codes: bool,
escape: bool,
}
impl KeyMetrics {
fn new() -> Self {
Self {
unquoted: true ,
single_quotes: false ,
double_quotes: false ,
escape_codes: false ,
escape: false ,
}
}
fn calculate(s: &str) -> Self {
let mut metrics = Self ::new();
metrics.unquoted = !s.is_empty();
for byte in s.as_bytes() {
if !matches!(*byte, b'a' ..=b'z' | b'A' ..=b'Z' | b'0' ..=b'9' | b'-' | b'_' ) {
metrics.unquoted = false ;
}
// ```bnf
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
//
// literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
//
// basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
// wschar = %x20 ; Space
// wschar =/ %x09 ; Horizontal tab
// escape = %x5C ; \
// ```
match *byte {
b'\' ' => metrics.single_quotes = true,
b'"' => metrics.double_quotes = true ,
b'\\' => metrics.escape = true ,
// Escape codes are needed if any ascii control
// characters are present, including \b \f \r.
b'\t' => {} // always allowed
c if c <= 0 x1f || c == 0 x7f => metrics.escape_codes = true ,
_ => {}
}
}
metrics
}
}
Messung V0.5 in Prozent C=91 H=95 G=92
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-08-25)
¤
*© Formatika GbR, Deutschland