Repository: toml-rs/toml
Pull request #856 (2025-04-18 20:47:15)

feat(write): Add `toml_write`
This pulls the logic for writing TOML out of `toml_edit`, making it available for reuse in other places, like `toml` or `facet-toml`.

Along the way, I did greatly simplify how the default string style is selected which had a minor change when it comes to quotes which I would consider a bug fix.  I also considered not escaping `\r` when its before `\n` but am holding off for now.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 74ae83dd..0429c6c1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -959,6 +959,7 @@ dependencies = [
  "toml-test-data",
  "toml-test-harness",
  "toml_datetime",
+ "toml_write",
  "winnow",
 ]
 
@@ -970,6 +971,13 @@ dependencies = [
  "toml_edit",
 ]
 
+[[package]]
+name = "toml_write"
+version = "0.1.0"
+dependencies = [
+ "snapbox",
+]
+
 [[package]]
 name = "typeid"
 version = "1.0.0"
diff --git a/crates/toml_edit/Cargo.toml b/crates/toml_edit/Cargo.toml
index 55a54704..042560c2 100644
--- a/crates/toml_edit/Cargo.toml
+++ b/crates/toml_edit/Cargo.toml
@@ -29,7 +29,7 @@ pre-release-replacements = [
 [features]
 default = ["parse", "display"]
 parse = ["dep:winnow"]
-display = []
+display = ["dep:toml_write"]
 perf = ["dep:kstring"]
 serde = ["dep:serde", "toml_datetime/serde", "dep:serde_spanned"]
 # Provide a method disable_recursion_limit to parse arbitrarily deep structures
@@ -46,6 +46,7 @@ serde = { version = "1.0.145", optional = true }
 kstring = { version = "2.0.0", features = ["max_inline"], optional = true }
 toml_datetime = { version = "0.6.8", path = "../toml_datetime" }
 serde_spanned = { version = "0.6.8", path = "../serde_spanned", features = ["serde"], optional = true }
+toml_write = { version = "0.1.0", path = "../toml_write", optional = true }
 
 [dev-dependencies]
 serde_json = "1.0.116"
diff --git a/crates/toml_edit/src/encode.rs b/crates/toml_edit/src/encode.rs
index 993fb6ac..fc59be35 100644
--- a/crates/toml_edit/src/encode.rs
+++ b/crates/toml_edit/src/encode.rs
@@ -2,6 +2,8 @@ use std::borrow::Cow;
 use std::fmt::{Display, Formatter, Result, Write};
 
 use toml_datetime::Datetime;
+use toml_write::ToTomlValue as _;
+use toml_write::TomlWrite as _;
 
 use crate::inline_table::DEFAULT_INLINE_KEY_DECOR;
 use crate::key::Key;
@@ -32,7 +34,7 @@ pub(crate) fn encode_key(this: &Key, buf: &mut dyn Write, input: Option<&str>) -
 
 fn encode_key_path(
     this: &[Key],
-    buf: &mut dyn Write,
+    mut buf: &mut dyn Write,
     input: Option<&str>,
     default_decor: (&str, &str),
 ) -> Result {
@@ -46,7 +48,7 @@ fn encode_key_path(
         if first {
             leaf_decor.prefix_encode(buf, input, default_decor.0)?;
         } else {
-            write!(buf, ".")?;
+            buf.key_sep()?;
             dotted_decor.prefix_encode(buf, input, DEFAULT_KEY_PATH_DECOR.0)?;
         }
 
@@ -63,7 +65,7 @@ fn encode_key_path(
 
 pub(crate) fn encode_key_path_ref(
     this: &[&Key],
-    buf: &mut dyn Write,
+    mut buf: &mut dyn Write,
     input: Option<&str>,
     default_decor: (&str, &str),
 ) -> Result {
@@ -77,7 +79,7 @@ pub(crate) fn encode_key_path_ref(
         if first {
             leaf_decor.prefix_encode(buf, input, default_decor.0)?;
         } else {
-            write!(buf, ".")?;
+            buf.key_sep()?;
             dotted_decor.prefix_encode(buf, input, DEFAULT_KEY_PATH_DECOR.0)?;
         }
 
@@ -118,13 +120,13 @@ pub(crate) fn encode_formatted<T: ValueRepr>(
 
 pub(crate) fn encode_array(
     this: &Array,
-    buf: &mut dyn Write,
+    mut buf: &mut dyn Write,
     input: Option<&str>,
     default_decor: (&str, &str),
 ) -> Result {
     let decor = this.decor();
     decor.prefix_encode(buf, input, default_decor.0)?;
-    write!(buf, "[")?;
+    buf.open_array()?;
 
     for (i, elem) in this.iter().enumerate() {
         let inner_decor;
@@ -132,16 +134,16 @@ pub(crate) fn encode_array(
             inner_decor = DEFAULT_LEADING_VALUE_DECOR;
         } else {
             inner_decor = DEFAULT_VALUE_DECOR;
-            write!(buf, ",")?;
+            buf.val_sep()?;
         }
         encode_value(elem, buf, input, inner_decor)?;
     }
     if this.trailing_comma() && !this.is_empty() {
-        write!(buf, ",")?;
+        buf.val_sep()?;
     }
 
     this.trailing().encode_with_default(buf, input, "")?;
-    write!(buf, "]")?;
+    buf.close_array()?;
     decor.suffix_encode(buf, input, default_decor.1)?;
 
     Ok(())
@@ -149,20 +151,20 @@ pub(crate) fn encode_array(
 
 pub(crate) fn encode_table(
     this: &InlineTable,
-    buf: &mut dyn Write,
+    mut buf: &mut dyn Write,
     input: Option<&str>,
     default_decor: (&str, &str),
 ) -> Result {
     let decor = this.decor();
     decor.prefix_encode(buf, input, default_decor.0)?;
-    write!(buf, "{{")?;
+    buf.open_inline_table()?;
     this.preamble().encode_with_default(buf, input, "")?;
 
     let children = this.get_values();
     let len = children.len();
     for (i, (key_path, value)) in children.into_iter().enumerate() {
         if i != 0 {
-            write!(buf, ",")?;
+            buf.val_sep()?;
         }
         let inner_decor = if i == len - 1 {
             DEFAULT_TRAILING_VALUE_DECOR
@@ -170,11 +172,11 @@ pub(crate) fn encode_table(
             DEFAULT_VALUE_DECOR
         };
         encode_key_path_ref(&key_path, buf, input, DEFAULT_INLINE_KEY_DECOR)?;
-        write!(buf, "=")?;
+        buf.keyval_sep()?;
         encode_value(value, buf, input, inner_decor)?;
     }
 
-    write!(buf, "}}")?;
+    buf.close_inline_table()?;
     decor.suffix_encode(buf, input, default_decor.1)?;
 
     Ok(())
@@ -260,7 +262,7 @@ where
 }
 
 fn visit_table(
-    buf: &mut dyn Write,
+    mut buf: &mut dyn Write,
     input: Option<&str>,
     table: &Table,
     path: &[Key],
@@ -292,9 +294,9 @@ fn visit_table(
             DEFAULT_TABLE_DECOR
         };
         table.decor.prefix_encode(buf, input, default_decor.0)?;
-        write!(buf, "[[")?;
+        buf.open_array_of_tables_header()?;
         encode_key_path(path, buf, input, DEFAULT_KEY_PATH_DECOR)?;
-        write!(buf, "]]")?;
+        buf.close_array_of_tables_header()?;
         table.decor.suffix_encode(buf, input, default_decor.1)?;
         writeln!(buf)?;
     } else if is_visible_std_table {
@@ -305,16 +307,16 @@ fn visit_table(
             DEFAULT_TABLE_DECOR
         };
         table.decor.prefix_encode(buf, input, default_decor.0)?;
-        write!(buf, "[")?;
+        buf.open_table_header()?;
         encode_key_path(path, buf, input, DEFAULT_KEY_PATH_DECOR)?;
-        write!(buf, "]")?;
+        buf.close_table_header()?;
         table.decor.suffix_encode(buf, input, default_decor.1)?;
         writeln!(buf)?;
     }
     // print table body
     for (key_path, value) in children {
         encode_key_path_ref(&key_path, buf, input, DEFAULT_KEY_DECOR)?;
-        write!(buf, "=")?;
+        buf.keyval_sep()?;
         encode_value(value, buf, input, DEFAULT_VALUE_DECOR)?;
         writeln!(buf)?;
     }
@@ -323,228 +325,31 @@ fn visit_table(
 
 impl ValueRepr for String {
     fn to_repr(&self) -> Repr {
-        to_string_repr(self, None, None)
-    }
-}
-
-pub(crate) fn to_string_repr(
-    value: &str,
-    style: Option<StringStyle>,
-    literal: Option<bool>,
-) -> Repr {
-    let (style, literal) = infer_style(value, style, literal);
-
-    let mut output = String::with_capacity(value.len() * 2);
-    if literal {
-        output.push_str(style.literal_start());
-        output.push_str(value);
-        output.push_str(style.literal_end());
-    } else {
-        output.push_str(style.standard_start());
-        for ch in value.chars() {
-            match ch {
-                '\u{8}' => output.push_str("\\b"),
-                '\u{9}' => output.push_str("\\t"),
-                '\u{a}' => match style {
-                    StringStyle::NewlineTriple => output.push('\n'),
-                    StringStyle::OnelineSingle => output.push_str("\\n"),
-                    StringStyle::OnelineTriple => unreachable!(),
-                },
-                '\u{c}' => output.push_str("\\f"),
-                '\u{d}' => output.push_str("\\r"),
-                '\u{22}' => output.push_str("\\\""),
-                '\u{5c}' => output.push_str("\\\\"),
-                c if c <= '\u{1f}' || c == '\u{7f}' => {
-                    write!(output, "\\u{:04X}", ch as u32).unwrap();
-                }
-                ch => output.push(ch),
-            }
-        }
-        output.push_str(style.standard_end());
-    }
-
-    Repr::new_unchecked(output)
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub(crate) enum StringStyle {
-    NewlineTriple,
-    OnelineTriple,
-    OnelineSingle,
-}
-
-impl StringStyle {
-    fn literal_start(self) -> &'static str {
-        match self {
-            Self::NewlineTriple => "'''\n",
-            Self::OnelineTriple => "'''",
-            Self::OnelineSingle => "'",
-        }
-    }
-    fn literal_end(self) -> &'static str {
-        match self {
-            Self::NewlineTriple => "'''",
-            Self::OnelineTriple => "'''",
-            Self::OnelineSingle => "'",
-        }
-    }
-
-    fn standard_start(self) -> &'static str {
-        match self {
-            Self::NewlineTriple => "\"\"\"\n",
-            // note: OnelineTriple can happen if do_pretty wants to do
-            // '''it's one line'''
-            // but literal == false
-            Self::OnelineTriple | Self::OnelineSingle => "\"",
-        }
-    }
-
-    fn standard_end(self) -> &'static str {
-        match self {
-            Self::NewlineTriple => "\"\"\"",
-            // note: OnelineTriple can happen if do_pretty wants to do
-            // '''it's one line'''
-            // but literal == false
-            Self::OnelineTriple | Self::OnelineSingle => "\"",
-        }
-    }
-}
-
-fn infer_style(
-    value: &str,
-    style: Option<StringStyle>,
-    literal: Option<bool>,
-) -> (StringStyle, bool) {
-    match (style, literal) {
-        (Some(style), Some(literal)) => (style, literal),
-        (None, Some(literal)) => (infer_all_style(value).0, literal),
-        (Some(style), None) => {
-            let literal = infer_literal(value);
-            (style, literal)
-        }
-        (None, None) => infer_all_style(value),
-    }
-}
-
-fn infer_literal(value: &str) -> bool {
-    #[cfg(feature = "parse")]
-    {
-        use winnow::stream::ContainsToken as _;
-        (value.contains('"') | value.contains('\\'))
-            && value
-                .chars()
-                .all(|c| crate::parser::strings::LITERAL_CHAR.contains_token(c))
-    }
-    #[cfg(not(feature = "parse"))]
-    {
-        false
-    }
-}
-
-fn infer_all_style(value: &str) -> (StringStyle, bool) {
-    // We need to determine:
-    // - if we are a "multi-line" pretty (if there are \n)
-    // - if ['''] appears if multi or ['] if single
-    // - if there are any invalid control characters
-    //
-    // Doing it any other way would require multiple passes
-    // to determine if a pretty string works or not.
-    let mut ty = StringStyle::OnelineSingle;
-    // found consecutive single quotes
-    let mut max_found_singles = 0;
-    let mut found_singles = 0;
-    let mut prefer_literal = false;
-    let mut can_be_pretty = true;
-
-    for ch in value.chars() {
-        if can_be_pretty {
-            if ch == '\'' {
-                found_singles += 1;
-                if found_singles >= 3 {
-                    can_be_pretty = false;
-                }
-            } else {
-                if found_singles > max_found_singles {
-                    max_found_singles = found_singles;
-                }
-                found_singles = 0;
-            }
-            match ch {
-                '\t' => {}
-                '"' => {
-                    prefer_literal = true;
-                }
-                '\\' => {
-                    prefer_literal = true;
-                }
-                '\n' => ty = StringStyle::NewlineTriple,
-                // Escape codes are needed if any ascii control
-                // characters are present, including \b \f \r.
-                c if c <= '\u{1f}' || c == '\u{7f}' => can_be_pretty = false,
-                _ => {}
-            }
-        } else {
-            // the string cannot be represented as pretty,
-            // still check if it should be multiline
-            if ch == '\n' {
-                ty = StringStyle::NewlineTriple;
-            }
-        }
-    }
-    if found_singles > 0 && value.ends_with('\'') {
-        // We cannot escape the ending quote so we must use """
-        can_be_pretty = false;
-    }
-    if !prefer_literal {
-        can_be_pretty = false;
-    }
-    if !can_be_pretty {
-        debug_assert!(ty != StringStyle::OnelineTriple);
-        return (ty, false);
-    }
-    if found_singles > max_found_singles {
-        max_found_singles = found_singles;
-    }
-    debug_assert!(max_found_singles < 3);
-    if ty == StringStyle::OnelineSingle && max_found_singles >= 1 {
-        // no newlines, but must use ''' because it has ' in it
-        ty = StringStyle::OnelineTriple;
+        let output = toml_write::TomlStringBuilder::new(self.as_str())
+            .as_default()
+            .to_toml_value();
+        Repr::new_unchecked(output)
     }
-    (ty, true)
 }
 
 impl ValueRepr for i64 {
     fn to_repr(&self) -> Repr {
-        Repr::new_unchecked(self.to_string())
+        let repr = self.to_toml_value();
+        Repr::new_unchecked(repr)
     }
 }
 
 impl ValueRepr for f64 {
     fn to_repr(&self) -> Repr {
-        to_f64_repr(*self)
+        let repr = self.to_toml_value();
+        Repr::new_unchecked(repr)
     }
 }
 
-fn to_f64_repr(f: f64) -> Repr {
-    let repr = match (f.is_sign_negative(), f.is_nan(), f == 0.0) {
-        (true, true, _) => "-nan".to_owned(),
-        (false, true, _) => "nan".to_owned(),
-        (true, false, true) => "-0.0".to_owned(),
-        (false, false, true) => "0.0".to_owned(),
-        (_, false, false) => {
-            if f % 1.0 == 0.0 {
-                format!("{f}.0")
-            } else {
-                format!("{f}")
-            }
-        }
-    };
-    Repr::new_unchecked(repr)
-}
-
 impl ValueRepr for bool {
     fn to_repr(&self) -> Repr {
-        Repr::new_unchecked(self.to_string())
+        let repr = self.to_toml_value();
+        Repr::new_unchecked(repr)
     }
 }
 
diff --git a/crates/toml_edit/src/key.rs b/crates/toml_edit/src/key.rs
index a5ad40aa..dc36943a 100644
--- a/crates/toml_edit/src/key.rs
+++ b/crates/toml_edit/src/key.rs
@@ -1,6 +1,9 @@
 use std::borrow::Cow;
 use std::str::FromStr;
 
+#[cfg(feature = "display")]
+use toml_write::ToTomlKey as _;
+
 use crate::repr::{Decor, Repr};
 use crate::InternalString;
 
@@ -95,7 +98,10 @@ impl Key {
     /// Returns the default raw representation.
     #[cfg(feature = "display")]
     pub fn default_repr(&self) -> Repr {
-        to_key_repr(&self.key)
+        let output = toml_write::TomlKeyBuilder::new(&self.key)
+            .as_default()
+            .to_toml_key();
+        Repr::new_unchecked(output)
     }
 
     /// Returns a raw representation.
@@ -277,32 +283,6 @@ impl FromStr for Key {
     }
 }
 
-#[cfg(feature = "display")]
-fn to_key_repr(key: &str) -> Repr {
-    #[cfg(feature = "parse")]
-    {
-        if key
-            .as_bytes()
-            .iter()
-            .copied()
-            .all(crate::parser::key::is_unquoted_char)
-            && !key.is_empty()
-        {
-            Repr::new_unchecked(key)
-        } else {
-            crate::encode::to_string_repr(
-                key,
-                Some(crate::encode::StringStyle::OnelineSingle),
-                None,
-            )
-        }
-    }
-    #[cfg(not(feature = "parse"))]
-    {
-        crate::encode::to_string_repr(key, Some(crate::encode::StringStyle::OnelineSingle), None)
-    }
-}
-
 impl<'b> From<&'b str> for Key {
     fn from(s: &'b str) -> Self {
         Key::new(s)
diff --git a/crates/toml_edit/src/parser/key.rs b/crates/toml_edit/src/parser/key.rs
index d941259f..bb272d89 100644
--- a/crates/toml_edit/src/parser/key.rs
+++ b/crates/toml_edit/src/parser/key.rs
@@ -96,11 +96,6 @@ fn unquoted_key<'i>(input: &mut Input<'i>) -> ModalResult<&'i str> {
     .parse_next(input)
 }
 
-pub(crate) fn is_unquoted_char(c: u8) -> bool {
-    use winnow::stream::ContainsToken;
-    UNQUOTED_CHAR.contains_token(c)
-}
-
 const UNQUOTED_CHAR: (
     RangeInclusive<u8>,
     RangeInclusive<u8>,
diff --git a/crates/toml_write/CHANGELOG.md b/crates/toml_write/CHANGELOG.md
new file mode 100644
index 00000000..ddde3c8f
--- /dev/null
+++ b/crates/toml_write/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+The format is based on [Keep a Changelog].
+
+[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/
+
+<!-- next-header -->
+## [Unreleased] - ReleaseDate
+
+<!-- next-url -->
+[Unreleased]: https://github.com/toml-rs/toml/compare/toml-v0.8.20...HEAD
diff --git a/crates/toml_write/Cargo.toml b/crates/toml_write/Cargo.toml
new file mode 100644
index 00000000..186726b6
--- /dev/null
+++ b/crates/toml_write/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "toml_write"
+version = "0.1.0"
+keywords = ["encoding", "toml"]
+categories = ["encoding"]
+description = """
+A low-level interface for writing out TOML
+"""
+repository.workspace = true
+homepage.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+include.workspace = true
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[package.metadata.release]
+pre-release-replacements = [
+  {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1},
+  {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
+  {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1},
+  {file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## [Unreleased] - ReleaseDate\n", exactly=1},
+  {file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/toml-rs/toml/compare/{{tag_name}}...HEAD", exactly=1},
+]
+
+[features]
+default = ["std"]
+std = ["alloc"]
+alloc = []
+
+[dependencies]
+
+[dev-dependencies]
+snapbox = "0.6.0"
+
+[lints]
+workspace = true
diff --git a/crates/toml_write/LICENSE-APACHE b/crates/toml_write/LICENSE-APACHE
new file mode 100644
index 00000000..8f71f43f
--- /dev/null
+++ b/crates/toml_write/LICENSE-APACHE
@@ -0,0 +1,202 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
diff --git a/crates/toml_write/LICENSE-MIT b/crates/toml_write/LICENSE-MIT
new file mode 100644
index 00000000..a2d01088
--- /dev/null
+++ b/crates/toml_write/LICENSE-MIT
@@ -0,0 +1,19 @@
+Copyright (c) Individual contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/crates/toml_write/README.md b/crates/toml_write/README.md
new file mode 100644
index 00000000..787bebcf
--- /dev/null
+++ b/crates/toml_write/README.md
@@ -0,0 +1,23 @@
+# toml_write
+
+[![Latest Version](https://img.shields.io/crates/v/toml.svg)](https://crates.io/crates/toml)
+[![Documentation](https://docs.rs/toml/badge.svg)](https://docs.rs/toml)
+
+A low-level interface for writing out TOML
+
+# License
+
+This project is licensed under either of
+
+ * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
+   http://www.apache.org/licenses/LICENSE-2.0)
+ * MIT license ([LICENSE-MIT](LICENSE-MIT) or
+   http://opensource.org/licenses/MIT)
+
+at your option.
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in toml-rs by you, as defined in the Apache-2.0 license, shall be
+dual licensed as above, without any additional terms or conditions.
diff --git a/crates/toml_write/src/key.rs b/crates/toml_write/src/key.rs
new file mode 100644
index 00000000..95e4f558
--- /dev/null
+++ b/crates/toml_write/src/key.rs
@@ -0,0 +1,55 @@
+#[cfg(feature = "alloc")]
+use alloc::borrow::Cow;
+#[cfg(feature = "alloc")]
+use alloc::string::String;
+
+use crate::TomlWrite;
+
+#[cfg(feature = "alloc")]
+pub trait ToTomlKey {
+    fn to_toml_key(&self) -> String;
+}
+
+#[cfg(feature = "alloc")]
+impl<T> ToTomlKey for T
+where
+    T: WriteTomlKey,
+{
+    fn to_toml_key(&self) -> String {
+        let mut result = String::new();
+        let _ = self.write_toml_key(&mut result);
+        result
+    }
+}
+
+pub trait WriteTomlKey {
+    fn write_toml_key<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result;
+}
+
+impl WriteTomlKey for str {
+    fn write_toml_key<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        crate::TomlKeyBuilder::new(self)
+            .as_default()
+            .write_toml_key(writer)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl WriteTomlKey for String {
+    fn write_toml_key<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        self.as_str().write_toml_key(writer)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl WriteTomlKey for Cow<'_, str> {
+    fn write_toml_key<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        self.as_ref().write_toml_key(writer)
+    }
+}
+
+impl<V: WriteTomlKey> WriteTomlKey for &V {
+    fn write_toml_key<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        (*self).write_toml_key(writer)
+    }
+}
diff --git a/crates/toml_write/src/lib.rs b/crates/toml_write/src/lib.rs
new file mode 100644
index 00000000..9795905f
--- /dev/null
+++ b/crates/toml_write/src/lib.rs
@@ -0,0 +1,29 @@
+//! A low-level interface for writing out TOML
+
+#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+#![forbid(unsafe_code)]
+#![warn(clippy::std_instead_of_core)]
+#![warn(clippy::std_instead_of_alloc)]
+#![warn(clippy::print_stderr)]
+#![warn(clippy::print_stdout)]
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+
+mod key;
+mod string;
+mod value;
+mod write;
+
+#[cfg(feature = "alloc")]
+pub use key::ToTomlKey;
+pub use key::WriteTomlKey;
+pub use string::TomlKey;
+pub use string::TomlKeyBuilder;
+pub use string::TomlString;
+pub use string::TomlStringBuilder;
+#[cfg(feature = "alloc")]
+pub use value::ToTomlValue;
+pub use value::WriteTomlValue;
+pub use write::TomlWrite;
diff --git a/crates/toml_write/src/string.rs b/crates/toml_write/src/string.rs
new file mode 100644
index 00000000..38ebc6f3
--- /dev/null
+++ b/crates/toml_write/src/string.rs
@@ -0,0 +1,418 @@
+#[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: &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: &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 {
+                    0x8 => {
+                        escaped = Some(r#"\b"#);
+                        break;
+                    }
+                    0x9 => {
+                        escaped = Some(r#"\t"#);
+                        break;
+                    }
+                    0xa => {
+                        if !is_ml {
+                            escaped = Some(r#"\n"#);
+                            break;
+                        }
+                    }
+                    0xc => {
+                        escaped = Some(r#"\f"#);
+                        break;
+                    }
+                    0xd => {
+                        escaped = Some(r#"\r"#);
+                        break;
+                    }
+                    0x22 => {} // double quote handled earlier
+                    0x5c => {
+                        escaped = Some(r#"\\"#);
+                        break;
+                    }
+                    c if c <= 0x1f || c == 0x7f => {
+                        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 <= 0x1f || c == 0x7f => 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 <= 0x1f || c == 0x7f => metrics.escape_codes = true,
+                _ => {}
+            }
+        }
+
+        metrics
+    }
+}
diff --git a/crates/toml_write/src/value.rs b/crates/toml_write/src/value.rs
new file mode 100644
index 00000000..8d14ac70
--- /dev/null
+++ b/crates/toml_write/src/value.rs
@@ -0,0 +1,229 @@
+#[cfg(feature = "alloc")]
+use alloc::borrow::Cow;
+#[cfg(feature = "alloc")]
+use alloc::string::String;
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+
+use crate::TomlWrite;
+use crate::WriteTomlKey;
+
+#[cfg(feature = "alloc")]
+pub trait ToTomlValue {
+    fn to_toml_value(&self) -> String;
+}
+
+#[cfg(feature = "alloc")]
+impl<T> ToTomlValue for T
+where
+    T: WriteTomlValue,
+{
+    fn to_toml_value(&self) -> String {
+        let mut result = String::new();
+        let _ = self.write_toml_value(&mut result);
+        result
+    }
+}
+
+pub trait WriteTomlValue {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result;
+}
+
+impl WriteTomlValue for bool {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for u8 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for i8 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for u16 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for i16 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for u32 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for i32 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for u64 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for i64 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for u128 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for i128 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for f32 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write!(writer, "{self}")
+    }
+}
+
+impl WriteTomlValue for f64 {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        match (self.is_sign_negative(), self.is_nan(), *self == 0.0) {
+            (true, true, _) => write!(writer, "-nan"),
+            (false, true, _) => write!(writer, "nan"),
+            (true, false, true) => write!(writer, "-0.0"),
+            (false, false, true) => write!(writer, "0.0"),
+            (_, false, false) => {
+                if self % 1.0 == 0.0 {
+                    write!(writer, "{self}.0")
+                } else {
+                    write!(writer, "{self}")
+                }
+            }
+        }
+    }
+}
+
+impl WriteTomlValue for str {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        crate::TomlStringBuilder::new(self)
+            .as_default()
+            .write_toml_value(writer)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl WriteTomlValue for String {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        self.as_str().write_toml_value(writer)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl WriteTomlValue for Cow<'_, str> {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        self.as_ref().write_toml_value(writer)
+    }
+}
+
+impl<V: WriteTomlValue> WriteTomlValue for [V] {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        writer.open_array()?;
+        let mut iter = self.iter();
+        if let Some(v) = iter.next() {
+            writer.value(v)?;
+        }
+        for v in iter {
+            writer.val_sep()?;
+            writer.space()?;
+            writer.value(v)?;
+        }
+        writer.close_array()?;
+        Ok(())
+    }
+}
+
+impl<V: WriteTomlValue, const N: usize> WriteTomlValue for [V; N] {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        self.as_slice().write_toml_value(writer)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl<V: WriteTomlValue> WriteTomlValue for Vec<V> {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        self.as_slice().write_toml_value(writer)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl<K: WriteTomlKey, V: WriteTomlValue> WriteTomlValue for alloc::collections::BTreeMap<K, V> {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write_toml_inline_table(self.iter(), writer)
+    }
+}
+
+#[cfg(feature = "std")]
+impl<K: WriteTomlKey, V: WriteTomlValue> WriteTomlValue for std::collections::HashMap<K, V> {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        write_toml_inline_table(self.iter(), writer)
+    }
+}
+
+fn write_toml_inline_table<
+    'i,
+    I: Iterator<Item = (&'i K, &'i V)>,
+    K: WriteTomlKey + 'i,
+    V: WriteTomlValue + 'i,
+    W: TomlWrite + ?Sized,
+>(
+    mut iter: I,
+    writer: &mut W,
+) -> core::fmt::Result {
+    writer.open_inline_table()?;
+    let mut trailing_space = false;
+    if let Some((key, value)) = iter.next() {
+        writer.space()?;
+        writer.key(key)?;
+        writer.space()?;
+        writer.keyval_sep()?;
+        writer.space()?;
+        writer.value(value)?;
+        trailing_space = true;
+    }
+    for (key, value) in iter {
+        writer.val_sep()?;
+        writer.space()?;
+        writer.key(key)?;
+        writer.space()?;
+        writer.keyval_sep()?;
+        writer.space()?;
+        writer.value(value)?;
+    }
+    if trailing_space {
+        writer.space()?;
+    }
+    writer.close_inline_table()?;
+    Ok(())
+}
+
+impl<V: WriteTomlValue> WriteTomlValue for &V {
+    fn write_toml_value<W: TomlWrite + ?Sized>(&self, writer: &mut W) -> core::fmt::Result {
+        (*self).write_toml_value(writer)
+    }
+}
diff --git a/crates/toml_write/src/write.rs b/crates/toml_write/src/write.rs
new file mode 100644
index 00000000..45fd89cf
--- /dev/null
+++ b/crates/toml_write/src/write.rs
@@ -0,0 +1,63 @@
+pub trait TomlWrite: core::fmt::Write {
+    fn open_table_header(&mut self) -> core::fmt::Result {
+        write!(self, "[")
+    }
+    fn close_table_header(&mut self) -> core::fmt::Result {
+        write!(self, "]")
+    }
+
+    fn open_array_of_tables_header(&mut self) -> core::fmt::Result {
+        write!(self, "[[")
+    }
+    fn close_array_of_tables_header(&mut self) -> core::fmt::Result {
+        write!(self, "]]")
+    }
+
+    fn open_inline_table(&mut self) -> core::fmt::Result {
+        write!(self, "{{")
+    }
+    fn close_inline_table(&mut self) -> core::fmt::Result {
+        write!(self, "}}")
+    }
+
+    fn open_array(&mut self) -> core::fmt::Result {
+        write!(self, "[")
+    }
+    fn close_array(&mut self) -> core::fmt::Result {
+        write!(self, "]")
+    }
+
+    fn key_sep(&mut self) -> core::fmt::Result {
+        write!(self, ".")
+    }
+
+    fn keyval_sep(&mut self) -> core::fmt::Result {
+        write!(self, "=")
+    }
+
+    fn key(&mut self, value: &impl crate::WriteTomlKey) -> core::fmt::Result {
+        value.write_toml_key(self)
+    }
+
+    fn value(&mut self, value: &impl crate::WriteTomlValue) -> core::fmt::Result {
+        value.write_toml_value(self)
+    }
+
+    fn val_sep(&mut self) -> core::fmt::Result {
+        write!(self, ",")
+    }
+
+    fn space(&mut self) -> core::fmt::Result {
+        write!(self, " ")
+    }
+
+    fn open_comment(&mut self) -> core::fmt::Result {
+        write!(self, "#")
+    }
+
+    fn newline(&mut self) -> core::fmt::Result {
+        writeln!(self)
+    }
+}
+
+impl<W> TomlWrite for W where W: core::fmt::Write {}


Test patch:
diff --git a/crates/toml/tests/testsuite/pretty.rs b/crates/toml/tests/testsuite/pretty.rs
index e8b96269..8c209a03 100644
--- a/crates/toml/tests/testsuite/pretty.rs
+++ b/crates/toml/tests/testsuite/pretty.rs
@@ -69,8 +69,8 @@ this is pretty standard
 """
 text = """
 this is the first line.
-This has a ''' in it and \"\"\" cuz it's tricky yo
-Also ' and \" because why not
+This has a ''' in it and ""\" cuz it's tricky yo
+Also ' and " because why not
 this is the fourth line
 """
 "#;
diff --git a/crates/toml_edit/tests/testsuite/main.rs b/crates/toml_edit/tests/testsuite/main.rs
index 0592f3c0..fe3869df 100644
--- a/crates/toml_edit/tests/testsuite/main.rs
+++ b/crates/toml_edit/tests/testsuite/main.rs
@@ -7,3 +7,4 @@ mod float;
 mod invalid;
 mod parse;
 mod stackoverflow;
+mod string;
diff --git a/crates/toml_edit/tests/testsuite/parse.rs b/crates/toml_edit/tests/testsuite/parse.rs
index 3668473f..0c201cee 100644
--- a/crates/toml_edit/tests/testsuite/parse.rs
+++ b/crates/toml_edit/tests/testsuite/parse.rs
@@ -1715,7 +1715,7 @@ crlf \r
     );
     assert_string_value_roundtrip(
         r#""mixed quoted \"start\" 'end'' mixed quote""#,
-        str![[r#"'''mixed quoted "start" 'end'' mixed quote'''"#]],
+        str![[r#""""mixed quoted "start" 'end'' mixed quote""""#]],
     );
 }
 
diff --git a/crates/toml_edit/tests/testsuite/string.rs b/crates/toml_edit/tests/testsuite/string.rs
new file mode 100644
index 00000000..dc50045a
--- /dev/null
+++ b/crates/toml_edit/tests/testsuite/string.rs
@@ -0,0 +1,260 @@
+use snapbox::prelude::*;
+use snapbox::str;
+
+#[track_caller]
+fn t(decoded: &str, expected: impl IntoData) {
+    let results = StringResults {
+        decoded,
+        key_default: toml_edit::Key::new(decoded).to_string(),
+        string_default: toml_edit::Value::from(decoded).to_string(),
+    };
+    snapbox::assert_data_eq!(results.to_debug(), expected.raw());
+}
+
+#[derive(Debug)]
+#[allow(dead_code)]
+struct StringResults<'i> {
+    decoded: &'i str,
+    key_default: String,
+    string_default: String,
+}
+
+#[test]
+fn empty() {
+    t(
+        "",
+        str![[r#"
+StringResults {
+    decoded: "",
+    key_default: "\"\"",
+    string_default: "\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn alpha() {
+    t(
+        "helloworld",
+        str![[r#"
+StringResults {
+    decoded: "helloworld",
+    key_default: "helloworld",
+    string_default: "\"helloworld\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn ident() {
+    t(
+        "_hello-world_",
+        str![[r#"
+StringResults {
+    decoded: "_hello-world_",
+    key_default: "_hello-world_",
+    string_default: "\"_hello-world_\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn one_single_quote() {
+    t(
+        "'hello'world'",
+        str![[r#"
+StringResults {
+    decoded: "'hello'world'",
+    key_default: "\"'hello'world'\"",
+    string_default: "\"'hello'world'\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn two_single_quote() {
+    t(
+        "''hello''world''",
+        str![[r#"
+StringResults {
+    decoded: "''hello''world''",
+    key_default: "\"''hello''world''\"",
+    string_default: "\"''hello''world''\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn three_single_quote() {
+    t(
+        "'''hello'''world'''",
+        str![[r#"
+StringResults {
+    decoded: "'''hello'''world'''",
+    key_default: "\"'''hello'''world'''\"",
+    string_default: "\"'''hello'''world'''\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn one_double_quote() {
+    t(
+        r#""hello"world""#,
+        str![[r#"
+StringResults {
+    decoded: "\"hello\"world\"",
+    key_default: "'\"hello\"world\"'",
+    string_default: "'\"hello\"world\"'",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn two_double_quote() {
+    t(
+        r#"""hello""world"""#,
+        str![[r#"
+StringResults {
+    decoded: "\"\"hello\"\"world\"\"",
+    key_default: "'\"\"hello\"\"world\"\"'",
+    string_default: "'\"\"hello\"\"world\"\"'",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn three_double_quote() {
+    t(
+        r#""""hello"""world""""#,
+        str![[r#"
+StringResults {
+    decoded: "\"\"\"hello\"\"\"world\"\"\"",
+    key_default: "'\"\"\"hello\"\"\"world\"\"\"'",
+    string_default: "'\"\"\"hello\"\"\"world\"\"\"'",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn mixed_quote_1() {
+    t(
+        r#""'"#,
+        str![[r#"
+StringResults {
+    decoded: "\"'",
+    key_default: "\"\\\"'\"",
+    string_default: "\"\"\"\"'\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn mixed_quote_2() {
+    t(
+        r#"mixed quoted \"start\" 'end'' mixed quote"#,
+        str![[r#"
+StringResults {
+    decoded: "mixed quoted \\\"start\\\" 'end'' mixed quote",
+    key_default: "\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\"",
+    string_default: "'''mixed quoted \\\"start\\\" 'end'' mixed quote'''",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn escape() {
+    t(
+        r#"\windows\system32\"#,
+        str![[r#"
+StringResults {
+    decoded: "\\windows\\system32\\",
+    key_default: "'\\windows\\system32\\'",
+    string_default: "'\\windows\\system32\\'",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn cr() {
+    t(
+        "\rhello\rworld\r",
+        str![[r#"
+StringResults {
+    decoded: "\rhello\rworld\r",
+    key_default: "\"\\rhello\\rworld\\r\"",
+    string_default: "\"\\rhello\\rworld\\r\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn lf() {
+    t(
+        "\nhello\nworld\n",
+        str![[r#"
+StringResults {
+    decoded: "\nhello\nworld\n",
+    key_default: "\"\\nhello\\nworld\\n\"",
+    string_default: "\"\"\"\n\nhello\nworld\n\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn crlf() {
+    t(
+        "\r\nhello\r\nworld\r\n",
+        str![[r#"
+StringResults {
+    decoded: "\r\nhello\r\nworld\r\n",
+    key_default: "\"\\r\\nhello\\r\\nworld\\r\\n\"",
+    string_default: "\"\"\"\n\\r\nhello\\r\nworld\\r\n\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn tab() {
+    t(
+        "\thello\tworld\t",
+        str![[r#"
+StringResults {
+    decoded: "\thello\tworld\t",
+    key_default: "\"\\thello\\tworld\\t\"",
+    string_default: "\"\\thello\\tworld\\t\"",
+}
+
+"#]],
+    );
+}
diff --git a/crates/toml_write/tests/string.rs b/crates/toml_write/tests/string.rs
new file mode 100644
index 00000000..91cf6583
--- /dev/null
+++ b/crates/toml_write/tests/string.rs
@@ -0,0 +1,564 @@
+#![cfg(feature = "alloc")]
+#![allow(clippy::dbg_macro)] // unsure why config isn't working
+
+use snapbox::prelude::*;
+use snapbox::str;
+
+use toml_write::ToTomlKey;
+use toml_write::ToTomlValue;
+use toml_write::TomlKeyBuilder;
+use toml_write::TomlStringBuilder;
+
+#[track_caller]
+fn t(decoded: &str, expected: impl IntoData) {
+    let key = TomlKeyBuilder::new(decoded);
+    let string = TomlStringBuilder::new(decoded);
+    dbg!(&key);
+    dbg!(&string);
+    let results = StringResults {
+        decoded,
+        key_default: key.as_default().to_toml_key(),
+        key_unquoted: key.as_unquoted().map(|k| k.to_toml_key()),
+        key_literal: key.as_literal().map(|k| k.to_toml_key()),
+        key_basic_pretty: key.as_basic_pretty().map(|k| k.to_toml_key()),
+        key_basic: key.as_basic().to_toml_key(),
+        string_default: string.as_default().to_toml_value(),
+        string_literal: string.as_literal().map(|k| k.to_toml_value()),
+        string_ml_literal: string.as_ml_literal().map(|k| k.to_toml_value()),
+        string_basic_pretty: string.as_basic_pretty().map(|k| k.to_toml_value()),
+        string_ml_basic_pretty: string.as_ml_basic_pretty().map(|k| k.to_toml_value()),
+        string_basic: string.as_basic().to_toml_value(),
+        string_ml_basic: string.as_ml_basic().to_toml_value(),
+    };
+    snapbox::assert_data_eq!(results.to_debug(), expected.raw());
+}
+
+#[derive(Debug)]
+#[allow(dead_code)]
+struct StringResults<'i> {
+    decoded: &'i str,
+    key_default: String,
+    key_unquoted: Option<String>,
+    key_literal: Option<String>,
+    key_basic_pretty: Option<String>,
+    key_basic: String,
+    string_default: String,
+    string_literal: Option<String>,
+    string_ml_literal: Option<String>,
+    string_basic_pretty: Option<String>,
+    string_ml_basic_pretty: Option<String>,
+    string_basic: String,
+    string_ml_basic: String,
+}
+
+#[test]
+fn empty() {
+    t(
+        "",
+        str![[r#"
+StringResults {
+    decoded: "",
+    key_default: "\"\"",
+    key_unquoted: None,
+    key_literal: Some(
+        "''",
+    ),
+    key_basic_pretty: Some(
+        "\"\"",
+    ),
+    key_basic: "\"\"",
+    string_default: "\"\"",
+    string_literal: Some(
+        "''",
+    ),
+    string_ml_literal: Some(
+        "''''''",
+    ),
+    string_basic_pretty: Some(
+        "\"\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"\"\"\"",
+    ),
+    string_basic: "\"\"",
+    string_ml_basic: "\"\"\"\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn alpha() {
+    t(
+        "helloworld",
+        str![[r#"
+StringResults {
+    decoded: "helloworld",
+    key_default: "helloworld",
+    key_unquoted: Some(
+        "helloworld",
+    ),
+    key_literal: Some(
+        "'helloworld'",
+    ),
+    key_basic_pretty: Some(
+        "\"helloworld\"",
+    ),
+    key_basic: "\"helloworld\"",
+    string_default: "\"helloworld\"",
+    string_literal: Some(
+        "'helloworld'",
+    ),
+    string_ml_literal: Some(
+        "'''helloworld'''",
+    ),
+    string_basic_pretty: Some(
+        "\"helloworld\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"helloworld\"\"\"",
+    ),
+    string_basic: "\"helloworld\"",
+    string_ml_basic: "\"\"\"helloworld\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn ident() {
+    t(
+        "_hello-world_",
+        str![[r#"
+StringResults {
+    decoded: "_hello-world_",
+    key_default: "_hello-world_",
+    key_unquoted: Some(
+        "_hello-world_",
+    ),
+    key_literal: Some(
+        "'_hello-world_'",
+    ),
+    key_basic_pretty: Some(
+        "\"_hello-world_\"",
+    ),
+    key_basic: "\"_hello-world_\"",
+    string_default: "\"_hello-world_\"",
+    string_literal: Some(
+        "'_hello-world_'",
+    ),
+    string_ml_literal: Some(
+        "'''_hello-world_'''",
+    ),
+    string_basic_pretty: Some(
+        "\"_hello-world_\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"_hello-world_\"\"\"",
+    ),
+    string_basic: "\"_hello-world_\"",
+    string_ml_basic: "\"\"\"_hello-world_\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn one_single_quote() {
+    t(
+        "'hello'world'",
+        str![[r#"
+StringResults {
+    decoded: "'hello'world'",
+    key_default: "\"'hello'world'\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: Some(
+        "\"'hello'world'\"",
+    ),
+    key_basic: "\"'hello'world'\"",
+    string_default: "\"'hello'world'\"",
+    string_literal: None,
+    string_ml_literal: Some(
+        "''''hello'world''''",
+    ),
+    string_basic_pretty: Some(
+        "\"'hello'world'\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"'hello'world'\"\"\"",
+    ),
+    string_basic: "\"'hello'world'\"",
+    string_ml_basic: "\"\"\"'hello'world'\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn two_single_quote() {
+    t(
+        "''hello''world''",
+        str![[r#"
+StringResults {
+    decoded: "''hello''world''",
+    key_default: "\"''hello''world''\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: Some(
+        "\"''hello''world''\"",
+    ),
+    key_basic: "\"''hello''world''\"",
+    string_default: "\"''hello''world''\"",
+    string_literal: None,
+    string_ml_literal: Some(
+        "'''''hello''world'''''",
+    ),
+    string_basic_pretty: Some(
+        "\"''hello''world''\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"''hello''world''\"\"\"",
+    ),
+    string_basic: "\"''hello''world''\"",
+    string_ml_basic: "\"\"\"''hello''world''\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn three_single_quote() {
+    t(
+        "'''hello'''world'''",
+        str![[r#"
+StringResults {
+    decoded: "'''hello'''world'''",
+    key_default: "\"'''hello'''world'''\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: Some(
+        "\"'''hello'''world'''\"",
+    ),
+    key_basic: "\"'''hello'''world'''\"",
+    string_default: "\"'''hello'''world'''\"",
+    string_literal: None,
+    string_ml_literal: None,
+    string_basic_pretty: Some(
+        "\"'''hello'''world'''\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"'''hello'''world'''\"\"\"",
+    ),
+    string_basic: "\"'''hello'''world'''\"",
+    string_ml_basic: "\"\"\"'''hello'''world'''\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn one_double_quote() {
+    t(
+        r#""hello"world""#,
+        str![[r#"
+StringResults {
+    decoded: "\"hello\"world\"",
+    key_default: "'\"hello\"world\"'",
+    key_unquoted: None,
+    key_literal: Some(
+        "'\"hello\"world\"'",
+    ),
+    key_basic_pretty: None,
+    key_basic: "\"\\\"hello\\\"world\\\"\"",
+    string_default: "'\"hello\"world\"'",
+    string_literal: Some(
+        "'\"hello\"world\"'",
+    ),
+    string_ml_literal: Some(
+        "'''\"hello\"world\"'''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: Some(
+        "\"\"\"\"hello\"world\"\"\"\"",
+    ),
+    string_basic: "\"\\\"hello\\\"world\\\"\"",
+    string_ml_basic: "\"\"\"\"hello\"world\"\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn two_double_quote() {
+    t(
+        r#"""hello""world"""#,
+        str![[r#"
+StringResults {
+    decoded: "\"\"hello\"\"world\"\"",
+    key_default: "'\"\"hello\"\"world\"\"'",
+    key_unquoted: None,
+    key_literal: Some(
+        "'\"\"hello\"\"world\"\"'",
+    ),
+    key_basic_pretty: None,
+    key_basic: "\"\\\"\\\"hello\\\"\\\"world\\\"\\\"\"",
+    string_default: "'\"\"hello\"\"world\"\"'",
+    string_literal: Some(
+        "'\"\"hello\"\"world\"\"'",
+    ),
+    string_ml_literal: Some(
+        "'''\"\"hello\"\"world\"\"'''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: Some(
+        "\"\"\"\"\"hello\"\"world\"\"\"\"\"",
+    ),
+    string_basic: "\"\\\"\\\"hello\\\"\\\"world\\\"\\\"\"",
+    string_ml_basic: "\"\"\"\"\"hello\"\"world\"\"\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn three_double_quote() {
+    t(
+        r#""""hello"""world""""#,
+        str![[r#"
+StringResults {
+    decoded: "\"\"\"hello\"\"\"world\"\"\"",
+    key_default: "'\"\"\"hello\"\"\"world\"\"\"'",
+    key_unquoted: None,
+    key_literal: Some(
+        "'\"\"\"hello\"\"\"world\"\"\"'",
+    ),
+    key_basic_pretty: None,
+    key_basic: "\"\\\"\\\"\\\"hello\\\"\\\"\\\"world\\\"\\\"\\\"\"",
+    string_default: "'\"\"\"hello\"\"\"world\"\"\"'",
+    string_literal: Some(
+        "'\"\"\"hello\"\"\"world\"\"\"'",
+    ),
+    string_ml_literal: Some(
+        "'''\"\"\"hello\"\"\"world\"\"\"'''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: None,
+    string_basic: "\"\\\"\\\"\\\"hello\\\"\\\"\\\"world\\\"\\\"\\\"\"",
+    string_ml_basic: "\"\"\"\"\"\\\"hello\"\"\\\"world\"\"\\\"\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn mixed_quote_1() {
+    t(
+        r#""'"#,
+        str![[r#"
+StringResults {
+    decoded: "\"'",
+    key_default: "\"\\\"'\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: None,
+    key_basic: "\"\\\"'\"",
+    string_default: "\"\"\"\"'\"\"\"",
+    string_literal: None,
+    string_ml_literal: Some(
+        "'''\"''''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: Some(
+        "\"\"\"\"'\"\"\"",
+    ),
+    string_basic: "\"\\\"'\"",
+    string_ml_basic: "\"\"\"\"'\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn mixed_quote_2() {
+    t(
+        r#"mixed quoted \"start\" 'end'' mixed quote"#,
+        str![[r#"
+StringResults {
+    decoded: "mixed quoted \\\"start\\\" 'end'' mixed quote",
+    key_default: "\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: None,
+    key_basic: "\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\"",
+    string_default: "'''mixed quoted \\\"start\\\" 'end'' mixed quote'''",
+    string_literal: None,
+    string_ml_literal: Some(
+        "'''mixed quoted \\\"start\\\" 'end'' mixed quote'''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: None,
+    string_basic: "\"mixed quoted \\\\\\\"start\\\\\\\" 'end'' mixed quote\"",
+    string_ml_basic: "\"\"\"mixed quoted \\\\\"start\\\\\" 'end'' mixed quote\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn escape() {
+    t(
+        r#"\windows\system32\"#,
+        str![[r#"
+StringResults {
+    decoded: "\\windows\\system32\\",
+    key_default: "'\\windows\\system32\\'",
+    key_unquoted: None,
+    key_literal: Some(
+        "'\\windows\\system32\\'",
+    ),
+    key_basic_pretty: None,
+    key_basic: "\"\\\\windows\\\\system32\\\\\"",
+    string_default: "'\\windows\\system32\\'",
+    string_literal: Some(
+        "'\\windows\\system32\\'",
+    ),
+    string_ml_literal: Some(
+        "'''\\windows\\system32\\'''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: None,
+    string_basic: "\"\\\\windows\\\\system32\\\\\"",
+    string_ml_basic: "\"\"\"\\\\windows\\\\system32\\\\\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn cr() {
+    t(
+        "\rhello\rworld\r",
+        str![[r#"
+StringResults {
+    decoded: "\rhello\rworld\r",
+    key_default: "\"\\rhello\\rworld\\r\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: None,
+    key_basic: "\"\\rhello\\rworld\\r\"",
+    string_default: "\"\\rhello\\rworld\\r\"",
+    string_literal: None,
+    string_ml_literal: None,
+    string_basic_pretty: None,
+    string_ml_basic_pretty: None,
+    string_basic: "\"\\rhello\\rworld\\r\"",
+    string_ml_basic: "\"\"\"\\rhello\\rworld\\r\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn lf() {
+    t(
+        "\nhello\nworld\n",
+        str![[r#"
+StringResults {
+    decoded: "\nhello\nworld\n",
+    key_default: "\"\\nhello\\nworld\\n\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: None,
+    key_basic: "\"\\nhello\\nworld\\n\"",
+    string_default: "\"\"\"\n\nhello\nworld\n\"\"\"",
+    string_literal: None,
+    string_ml_literal: Some(
+        "'''\n\nhello\nworld\n'''",
+    ),
+    string_basic_pretty: None,
+    string_ml_basic_pretty: Some(
+        "\"\"\"\n\nhello\nworld\n\"\"\"",
+    ),
+    string_basic: "\"\\nhello\\nworld\\n\"",
+    string_ml_basic: "\"\"\"\n\nhello\nworld\n\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn crlf() {
+    t(
+        "\r\nhello\r\nworld\r\n",
+        str![[r#"
+StringResults {
+    decoded: "\r\nhello\r\nworld\r\n",
+    key_default: "\"\\r\\nhello\\r\\nworld\\r\\n\"",
+    key_unquoted: None,
+    key_literal: None,
+    key_basic_pretty: None,
+    key_basic: "\"\\r\\nhello\\r\\nworld\\r\\n\"",
+    string_default: "\"\"\"\n\\r\nhello\\r\nworld\\r\n\"\"\"",
+    string_literal: None,
+    string_ml_literal: None,
+    string_basic_pretty: None,
+    string_ml_basic_pretty: None,
+    string_basic: "\"\\r\\nhello\\r\\nworld\\r\\n\"",
+    string_ml_basic: "\"\"\"\n\\r\nhello\\r\nworld\\r\n\"\"\"",
+}
+
+"#]],
+    );
+}
+
+#[test]
+fn tab() {
+    t(
+        "\thello\tworld\t",
+        str![[r#"
+StringResults {
+    decoded: "\thello\tworld\t",
+    key_default: "\"\\thello\\tworld\\t\"",
+    key_unquoted: None,
+    key_literal: Some(
+        "'\thello\tworld\t'",
+    ),
+    key_basic_pretty: Some(
+        "\"\\thello\\tworld\\t\"",
+    ),
+    key_basic: "\"\\thello\\tworld\\t\"",
+    string_default: "\"\\thello\\tworld\\t\"",
+    string_literal: Some(
+        "'\thello\tworld\t'",
+    ),
+    string_ml_literal: Some(
+        "'''\thello\tworld\t'''",
+    ),
+    string_basic_pretty: Some(
+        "\"\\thello\\tworld\\t\"",
+    ),
+    string_ml_basic_pretty: Some(
+        "\"\"\"\\thello\\tworld\\t\"\"\"",
+    ),
+    string_basic: "\"\\thello\\tworld\\t\"",
+    string_ml_basic: "\"\"\"\\thello\\tworld\\t\"\"\"",
+}
+
+"#]],
+    );
+}