Repository: toml-rs/toml
Pull request #429 (2023-01-11 21:08:35)

perf(parser): Don't allocate for repr/decor for serde
Progress of performance through this PR
```
main:
de/minimal: 3.23 us
Value/minimal: 3.73 us
de/medium: 131.97 us
Value/medium: 129.48 us

d32e72a: refactor(parser): Track parse location
de/minimal: 3.98 us
Value/minimal: 4.00 us
de/medium: 142.97 us
Value/medium: 150.83 us

7a40a0770: feat(parser): Track span of changes
de/minimal: 3.82 us
Value/minimal: 4.26 us
de/medium: 141.61 us
Value/medium: 119.87 us

7ec8d71: feat(parser): Track spans for every field
de/minimal: 4.05 us
Value/minimal: 4.57 us
de/medium: 149.47 us
Value/medium: 133.46 us

4c9c71b: refactor(parser): Only track spans
de/minimal: 4.11 us
Value/minimal: 4.29 us
de/medium: 129.30 us
Value/medium: 128.25 us

3591592e8: perf(easy): Leave spans alone
de/minimal: 3.36 us
Value/minimal: 4.05 us
de/medium: 105.85 us
Value/medium: 104.95 us
```

I looked at the serde performance as that is the one we care most about.  When people are roundtripping or looking at raw data, then generally performance doesn't matter as much.  Or rather, this is the main case cargo would care about.

Switching the parsers input to `Located` slowed things down.  We made up for that in `medium` but never did in `minimal`.  It would be nice to drop that overhead.  nom-experimental#61 explores this further.

I also tried speeding up the `edit` workflow by using `str::get_unchecked` but it didn't offer a noticeable improvement.


Patch:
diff --git a/crates/toml_edit/src/array.rs b/crates/toml_edit/src/array.rs
index fd64027e..19b844ac 100644
--- a/crates/toml_edit/src/array.rs
+++ b/crates/toml_edit/src/array.rs
@@ -3,7 +3,7 @@ use std::mem;
 
 use crate::repr::Decor;
 use crate::value::{DEFAULT_LEADING_VALUE_DECOR, DEFAULT_VALUE_DECOR};
-use crate::{InternalString, Item, Value};
+use crate::{Item, RawString, Value};
 
 /// Type representing a TOML array,
 /// payload of the `Value::Array` variant's value
@@ -11,10 +11,11 @@ use crate::{InternalString, Item, Value};
 pub struct Array {
     // `trailing` represents whitespaces, newlines
     // and comments in an empty array or after the trailing comma
-    trailing: InternalString,
+    trailing: RawString,
     trailing_comma: bool,
     // prefix before `[` and suffix after `]`
     decor: Decor,
+    pub(crate) span: Option<std::ops::Range<usize>>,
     // always Vec<Item::Value>
     pub(crate) values: Vec<Item>,
 }
@@ -67,13 +68,13 @@ impl Array {
     }
 
     /// Set whitespace after last element
-    pub fn set_trailing(&mut self, trailing: impl Into<InternalString>) {
+    pub fn set_trailing(&mut self, trailing: impl Into<RawString>) {
         self.trailing = trailing.into();
     }
 
     /// Whitespace after last element
-    pub fn trailing(&self) -> &str {
-        self.trailing.as_str()
+    pub fn trailing(&self) -> &RawString {
+        &self.trailing
     }
 
     /// Returns the surrounding whitespace
@@ -85,6 +86,15 @@ impl Array {
     pub fn decor(&self) -> &Decor {
         &self.decor
     }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        self.span = None;
+        self.decor.despan(input);
+        self.trailing.despan(input);
+        for value in &mut self.values {
+            value.despan(input);
+        }
+    }
 }
 
 impl Array {
@@ -306,7 +316,7 @@ impl Array {
 
 impl std::fmt::Display for Array {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        crate::encode::Encode::encode(self, f, ("", ""))
+        crate::encode::Encode::encode(self, f, None, ("", ""))
     }
 }
 
diff --git a/crates/toml_edit/src/array_of_tables.rs b/crates/toml_edit/src/array_of_tables.rs
index 4684a5e3..a956a23e 100644
--- a/crates/toml_edit/src/array_of_tables.rs
+++ b/crates/toml_edit/src/array_of_tables.rs
@@ -30,6 +30,12 @@ impl ArrayOfTables {
         a.fmt();
         a
     }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        for value in &mut self.values {
+            value.despan(input);
+        }
+    }
 }
 
 impl ArrayOfTables {
diff --git a/crates/toml_edit/src/de/mod.rs b/crates/toml_edit/src/de/mod.rs
index 6523330e..2343d1e8 100644
--- a/crates/toml_edit/src/de/mod.rs
+++ b/crates/toml_edit/src/de/mod.rs
@@ -107,7 +107,7 @@ pub fn from_str<T>(s: &'_ str) -> Result<T, Error>
 where
     T: DeserializeOwned,
 {
-    let d = s.parse::<crate::Document>()?;
+    let d = crate::parser::parse_document(s)?;
     from_document(d)
 }
 
diff --git a/crates/toml_edit/src/document.rs b/crates/toml_edit/src/document.rs
index d1cca4a6..159c2d8c 100644
--- a/crates/toml_edit/src/document.rs
+++ b/crates/toml_edit/src/document.rs
@@ -2,14 +2,15 @@ use std::str::FromStr;
 
 use crate::parser;
 use crate::table::Iter;
-use crate::{InternalString, Item, Table};
+use crate::{Item, RawString, Table};
 
 /// Type representing a TOML document
 #[derive(Debug, Clone)]
 pub struct Document {
     pub(crate) root: Item,
     // Trailing comments and whitespaces
-    pub(crate) trailing: InternalString,
+    pub(crate) trailing: RawString,
+    pub(crate) original: Option<String>,
 }
 
 impl Document {
@@ -46,13 +47,21 @@ impl Document {
     }
 
     /// Set whitespace after last element
-    pub fn set_trailing(&mut self, trailing: impl Into<InternalString>) {
+    pub fn set_trailing(&mut self, trailing: impl Into<RawString>) {
         self.trailing = trailing.into();
     }
 
     /// Whitespace after last element
-    pub fn trailing(&self) -> &str {
-        self.trailing.as_str()
+    pub fn trailing(&self) -> &RawString {
+        &self.trailing
+    }
+
+    /// # Panics
+    ///
+    /// If run on on a `Document` not generated by the parser
+    pub(crate) fn despan(&mut self) {
+        self.root.despan(self.original.as_deref().unwrap());
+        self.trailing.despan(self.original.as_deref().unwrap());
     }
 }
 
@@ -61,6 +70,7 @@ impl Default for Document {
         Self {
             root: Item::Table(Table::with_pos(Some(0))),
             trailing: Default::default(),
+            original: Default::default(),
         }
     }
 }
@@ -70,7 +80,9 @@ impl FromStr for Document {
 
     /// Parses a document from a &str
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        parser::parse_document(s)
+        let mut d = parser::parse_document(s)?;
+        d.despan();
+        Ok(d)
     }
 }
 
diff --git a/crates/toml_edit/src/encode.rs b/crates/toml_edit/src/encode.rs
index a73f2e92..4747bd52 100644
--- a/crates/toml_edit/src/encode.rs
+++ b/crates/toml_edit/src/encode.rs
@@ -1,3 +1,4 @@
+use std::borrow::Cow;
 use std::fmt::{Display, Formatter, Result, Write};
 
 use toml_datetime::*;
@@ -13,24 +14,47 @@ use crate::value::{
 use crate::{Array, InlineTable, Item, Table, Value};
 
 pub(crate) trait Encode {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result;
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result;
 }
 
 impl Encode for Key {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result {
-        let repr = self.to_repr();
-        write!(
-            buf,
-            "{}{}{}",
-            self.decor().prefix().unwrap_or(default_decor.0),
-            repr,
-            self.decor().suffix().unwrap_or(default_decor.1)
-        )
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result {
+        let decor = self.decor();
+        decor.prefix_encode(buf, input, default_decor.0)?;
+
+        if let Some(input) = input {
+            let repr = self
+                .as_repr()
+                .map(Cow::Borrowed)
+                .unwrap_or_else(|| Cow::Owned(self.default_repr()));
+            repr.encode(buf, input)?;
+        } else {
+            let repr = self.display_repr();
+            write!(buf, "{}", repr)?;
+        };
+
+        decor.suffix_encode(buf, input, default_decor.1)?;
+        Ok(())
     }
 }
 
 impl<'k> Encode for &'k [&'k Key] {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result {
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result {
         for (i, key) in self.iter().enumerate() {
             let first = i == 0;
             let last = i + 1 == self.len();
@@ -49,7 +73,7 @@ impl<'k> Encode for &'k [&'k Key] {
             if !first {
                 write!(buf, ".")?;
             }
-            key.encode(buf, (prefix, suffix))?;
+            key.encode(buf, input, (prefix, suffix))?;
         }
         Ok(())
     }
@@ -59,21 +83,41 @@ impl<T> Encode for Formatted<T>
 where
     T: ValueRepr,
 {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result {
-        let repr = self.to_repr();
-        write!(
-            buf,
-            "{}{}{}",
-            self.decor().prefix().unwrap_or(default_decor.0),
-            repr,
-            self.decor().suffix().unwrap_or(default_decor.1)
-        )
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result {
+        let decor = self.decor();
+        decor.prefix_encode(buf, input, default_decor.0)?;
+
+        if let Some(input) = input {
+            let repr = self
+                .as_repr()
+                .map(Cow::Borrowed)
+                .unwrap_or_else(|| Cow::Owned(self.default_repr()));
+            repr.encode(buf, input)?;
+        } else {
+            let repr = self.display_repr();
+            write!(buf, "{}", repr)?;
+        };
+
+        decor.suffix_encode(buf, input, default_decor.1)?;
+        Ok(())
     }
 }
 
 impl Encode for Array {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result {
-        write!(buf, "{}[", self.decor().prefix().unwrap_or(default_decor.0))?;
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result {
+        let decor = self.decor();
+        decor.prefix_encode(buf, input, default_decor.0)?;
+        write!(buf, "[")?;
 
         for (i, elem) in self.iter().enumerate() {
             let inner_decor;
@@ -83,25 +127,31 @@ impl Encode for Array {
                 inner_decor = DEFAULT_VALUE_DECOR;
                 write!(buf, ",")?;
             }
-            elem.encode(buf, inner_decor)?;
+            elem.encode(buf, input, inner_decor)?;
         }
         if self.trailing_comma() && !self.is_empty() {
             write!(buf, ",")?;
         }
 
-        write!(buf, "{}", self.trailing())?;
-        write!(buf, "]{}", self.decor().suffix().unwrap_or(default_decor.1))
+        self.trailing().encode_with_default(buf, input, "")?;
+        write!(buf, "]")?;
+        decor.suffix_encode(buf, input, default_decor.1)?;
+
+        Ok(())
     }
 }
 
 impl Encode for InlineTable {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result {
-        write!(
-            buf,
-            "{}{{",
-            self.decor().prefix().unwrap_or(default_decor.0)
-        )?;
-        write!(buf, "{}", self.preamble())?;
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result {
+        let decor = self.decor();
+        decor.prefix_encode(buf, input, default_decor.0)?;
+        write!(buf, "{{")?;
+        self.preamble().encode_with_default(buf, input, "")?;
 
         let children = self.get_values();
         let len = children.len();
@@ -114,29 +164,35 @@ impl Encode for InlineTable {
             } else {
                 DEFAULT_VALUE_DECOR
             };
-            key_path.as_slice().encode(buf, DEFAULT_INLINE_KEY_DECOR)?;
+            key_path
+                .as_slice()
+                .encode(buf, input, DEFAULT_INLINE_KEY_DECOR)?;
             write!(buf, "=")?;
-            value.encode(buf, inner_decor)?;
+            value.encode(buf, input, inner_decor)?;
         }
 
-        write!(
-            buf,
-            "}}{}",
-            self.decor().suffix().unwrap_or(default_decor.1)
-        )
+        write!(buf, "}}")?;
+        decor.suffix_encode(buf, input, default_decor.1)?;
+
+        Ok(())
     }
 }
 
 impl Encode for Value {
-    fn encode(&self, buf: &mut dyn Write, default_decor: (&str, &str)) -> Result {
+    fn encode(
+        &self,
+        buf: &mut dyn Write,
+        input: Option<&str>,
+        default_decor: (&str, &str),
+    ) -> Result {
         match self {
-            Value::String(repr) => repr.encode(buf, default_decor),
-            Value::Integer(repr) => repr.encode(buf, default_decor),
-            Value::Float(repr) => repr.encode(buf, default_decor),
-            Value::Boolean(repr) => repr.encode(buf, default_decor),
-            Value::Datetime(repr) => repr.encode(buf, default_decor),
-            Value::Array(array) => array.encode(buf, default_decor),
-            Value::InlineTable(table) => table.encode(buf, default_decor),
+            Value::String(repr) => repr.encode(buf, input, default_decor),
+            Value::Integer(repr) => repr.encode(buf, input, default_decor),
+            Value::Float(repr) => repr.encode(buf, input, default_decor),
+            Value::Boolean(repr) => repr.encode(buf, input, default_decor),
+            Value::Datetime(repr) => repr.encode(buf, input, default_decor),
+            Value::Array(array) => array.encode(buf, input, default_decor),
+            Value::InlineTable(table) => table.encode(buf, input, default_decor),
         }
     }
 }
@@ -158,9 +214,17 @@ impl Display for Document {
         tables.sort_by_key(|&(id, _, _, _)| id);
         let mut first_table = true;
         for (_, table, path, is_array) in tables {
-            visit_table(f, table, &path, is_array, &mut first_table)?;
+            visit_table(
+                f,
+                self.original.as_deref(),
+                table,
+                &path,
+                is_array,
+                &mut first_table,
+            )?;
         }
-        self.trailing.fmt(f)
+        self.trailing()
+            .encode_with_default(f, self.original.as_deref(), "")
     }
 }
 
@@ -197,6 +261,7 @@ where
 
 fn visit_table(
     buf: &mut dyn Write,
+    input: Option<&str>,
     table: &Table,
     path: &[&Key],
     is_array_of_tables: bool,
@@ -226,9 +291,12 @@ fn visit_table(
         } else {
             DEFAULT_TABLE_DECOR
         };
-        write!(buf, "{}[[", table.decor.prefix().unwrap_or(default_decor.0))?;
-        path.encode(buf, DEFAULT_KEY_PATH_DECOR)?;
-        writeln!(buf, "]]{}", table.decor.suffix().unwrap_or(default_decor.1))?;
+        table.decor.prefix_encode(buf, input, default_decor.0)?;
+        write!(buf, "[[")?;
+        path.encode(buf, input, DEFAULT_KEY_PATH_DECOR)?;
+        write!(buf, "]]")?;
+        table.decor.suffix_encode(buf, input, default_decor.1)?;
+        writeln!(buf)?;
     } else if is_visible_std_table {
         let default_decor = if *first_table {
             *first_table = false;
@@ -236,15 +304,18 @@ fn visit_table(
         } else {
             DEFAULT_TABLE_DECOR
         };
-        write!(buf, "{}[", table.decor.prefix().unwrap_or(default_decor.0))?;
-        path.encode(buf, DEFAULT_KEY_PATH_DECOR)?;
-        writeln!(buf, "]{}", table.decor.suffix().unwrap_or(default_decor.1))?;
+        table.decor.prefix_encode(buf, input, default_decor.0)?;
+        write!(buf, "[")?;
+        path.encode(buf, input, DEFAULT_KEY_PATH_DECOR)?;
+        write!(buf, "]")?;
+        table.decor.suffix_encode(buf, input, default_decor.1)?;
+        writeln!(buf)?;
     }
     // print table body
     for (key_path, value) in children {
-        key_path.as_slice().encode(buf, DEFAULT_KEY_DECOR)?;
+        key_path.as_slice().encode(buf, input, DEFAULT_KEY_DECOR)?;
         write!(buf, "=")?;
-        value.encode(buf, DEFAULT_VALUE_DECOR)?;
+        value.encode(buf, input, DEFAULT_VALUE_DECOR)?;
         writeln!(buf)?;
     }
     Ok(())
diff --git a/crates/toml_edit/src/inline_table.rs b/crates/toml_edit/src/inline_table.rs
index ce91ab24..7f2424cf 100644
--- a/crates/toml_edit/src/inline_table.rs
+++ b/crates/toml_edit/src/inline_table.rs
@@ -3,16 +3,17 @@ use std::iter::FromIterator;
 use crate::key::Key;
 use crate::repr::Decor;
 use crate::table::{Iter, IterMut, KeyValuePairs, TableKeyValue, TableLike};
-use crate::{InternalString, Item, KeyMut, Table, Value};
+use crate::{InternalString, Item, KeyMut, RawString, Table, Value};
 
 /// Type representing a TOML inline table,
 /// payload of the `Value::InlineTable` variant
 #[derive(Debug, Default, Clone)]
 pub struct InlineTable {
     // `preamble` represents whitespaces in an empty table
-    preamble: InternalString,
+    preamble: RawString,
     // prefix before `{` and suffix after `}`
     decor: Decor,
+    pub(crate) span: Option<std::ops::Range<usize>>,
     // whether this is a proxy for dotted keys
     dotted: bool,
     pub(crate) items: KeyValuePairs,
@@ -163,13 +164,23 @@ impl InlineTable {
     }
 
     /// Set whitespace after before element
-    pub fn set_preamble(&mut self, preamble: impl Into<InternalString>) {
+    pub fn set_preamble(&mut self, preamble: impl Into<RawString>) {
         self.preamble = preamble.into();
     }
 
     /// Whitespace after before element
-    pub fn preamble(&self) -> &str {
-        self.preamble.as_str()
+    pub fn preamble(&self) -> &RawString {
+        &self.preamble
+    }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        self.span = None;
+        self.decor.despan(input);
+        self.preamble.despan(input);
+        for kv in self.items.values_mut() {
+            kv.key.despan(input);
+            kv.value.despan(input);
+        }
     }
 }
 
@@ -351,7 +362,7 @@ impl InlineTable {
 
 impl std::fmt::Display for InlineTable {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        crate::encode::Encode::encode(self, f, ("", ""))
+        crate::encode::Encode::encode(self, f, None, ("", ""))
     }
 }
 
diff --git a/crates/toml_edit/src/item.rs b/crates/toml_edit/src/item.rs
index d2dc3592..972e973b 100644
--- a/crates/toml_edit/src/item.rs
+++ b/crates/toml_edit/src/item.rs
@@ -295,6 +295,15 @@ impl Item {
     pub fn is_table_like(&self) -> bool {
         self.as_table_like().is_some()
     }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        match self {
+            Item::None => {}
+            Item::Value(v) => v.despan(input),
+            Item::Table(v) => v.despan(input),
+            Item::ArrayOfTables(v) => v.despan(input),
+        }
+    }
 }
 
 impl Default for Item {
diff --git a/crates/toml_edit/src/key.rs b/crates/toml_edit/src/key.rs
index 890d70d6..fa75d3c1 100644
--- a/crates/toml_edit/src/key.rs
+++ b/crates/toml_edit/src/key.rs
@@ -78,12 +78,24 @@ impl Key {
         &self.key
     }
 
-    /// Returns the key raw representation.
-    pub fn to_repr(&self) -> Cow<Repr> {
-        self.repr
-            .as_ref()
+    /// Returns key raw representation, if available.
+    pub fn as_repr(&self) -> Option<&Repr> {
+        self.repr.as_ref()
+    }
+
+    /// Returns the default raw representation.
+    pub fn default_repr(&self) -> Repr {
+        to_key_repr(&self.key)
+    }
+
+    /// Returns a raw representation.
+    pub fn display_repr(&self) -> Cow<'_, str> {
+        self.as_repr()
+            .and_then(|r| r.as_raw().as_str())
             .map(Cow::Borrowed)
-            .unwrap_or_else(|| Cow::Owned(to_key_repr(&self.key)))
+            .unwrap_or_else(|| {
+                Cow::Owned(self.default_repr().as_raw().as_str().unwrap().to_owned())
+            })
     }
 
     /// Returns the surrounding whitespace
@@ -96,6 +108,13 @@ impl Key {
         &self.decor
     }
 
+    pub(crate) fn despan(&mut self, input: &str) {
+        self.decor.despan(input);
+        if let Some(repr) = &mut self.repr {
+            repr.despan(input)
+        }
+    }
+
     /// Auto formats the key.
     pub fn fmt(&mut self) {
         self.repr = Some(to_key_repr(&self.key));
@@ -169,7 +188,7 @@ impl PartialEq<String> for Key {
 
 impl std::fmt::Display for Key {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        crate::encode::Encode::encode(self, f, ("", ""))
+        crate::encode::Encode::encode(self, f, None, ("", ""))
     }
 }
 
@@ -235,9 +254,19 @@ impl<'k> KeyMut<'k> {
         self.key.get()
     }
 
-    /// Returns the key raw representation.
-    pub fn to_repr(&self) -> Cow<Repr> {
-        self.key.to_repr()
+    /// Returns the raw representation, if available.
+    pub fn as_repr(&self) -> Option<&Repr> {
+        self.key.as_repr()
+    }
+
+    /// Returns the default raw representation.
+    pub fn default_repr(&self) -> Repr {
+        self.key.default_repr()
+    }
+
+    /// Returns a raw representation.
+    pub fn display_repr(&self) -> Cow<str> {
+        self.key.display_repr()
     }
 
     /// Returns the surrounding whitespace
diff --git a/crates/toml_edit/src/lib.rs b/crates/toml_edit/src/lib.rs
index 020d0b49..4f6b5745 100644
--- a/crates/toml_edit/src/lib.rs
+++ b/crates/toml_edit/src/lib.rs
@@ -74,6 +74,7 @@ mod internal_string;
 mod item;
 mod key;
 mod parser;
+mod raw_string;
 mod repr;
 mod table;
 mod value;
@@ -102,6 +103,7 @@ pub use crate::internal_string::InternalString;
 pub use crate::item::{array, table, value, Item};
 pub use crate::key::{Key, KeyMut};
 pub use crate::parser::TomlError;
+pub use crate::raw_string::RawString;
 pub use crate::repr::{Decor, Formatted, Repr};
 pub use crate::table::{
     Entry, IntoIter, Iter, IterMut, OccupiedEntry, Table, TableLike, VacantEntry,
diff --git a/crates/toml_edit/src/parser/array.rs b/crates/toml_edit/src/parser/array.rs
index d2d0cd89..93d7873e 100644
--- a/crates/toml_edit/src/parser/array.rs
+++ b/crates/toml_edit/src/parser/array.rs
@@ -5,7 +5,7 @@ use nom8::sequence::delimited;
 
 use crate::parser::trivia::ws_comment_newline;
 use crate::parser::value::value;
-use crate::{Array, Item, Value};
+use crate::{Array, Item, RawString, Value};
 
 use crate::parser::prelude::*;
 
@@ -55,12 +55,12 @@ pub(crate) fn array_values(
                         trailing.is_some(),
                     )
                 })),
-            ws_comment_newline,
+            ws_comment_newline.span(),
         )
             .map_res::<_, _, std::str::Utf8Error>(|(array, trailing)| {
                 let (mut array, comma) = array.unwrap_or_default();
                 array.set_trailing_comma(comma);
-                array.set_trailing(std::str::from_utf8(trailing)?);
+                array.set_trailing(RawString::with_span(trailing));
                 Ok(array)
             })
             .parse(input)
@@ -71,11 +71,12 @@ pub(crate) fn array_value(
     check: RecursionCheck,
 ) -> impl FnMut(Input<'_>) -> IResult<Input<'_>, Value, ParserError<'_>> {
     move |input| {
-        (ws_comment_newline, value(check), ws_comment_newline)
-            .map_res::<_, _, std::str::Utf8Error>(|(ws1, v, ws2)| {
-                let v = v.decorated(std::str::from_utf8(ws1)?, std::str::from_utf8(ws2)?);
-                Ok(v)
-            })
+        (
+            ws_comment_newline.span(),
+            value(check),
+            ws_comment_newline.span(),
+        )
+            .map(|(ws1, v, ws2)| v.decorated(RawString::with_span(ws1), RawString::with_span(ws2)))
             .parse(input)
     }
 }
@@ -123,7 +124,10 @@ mod test {
         ];
         for input in inputs {
             dbg!(input);
-            let parsed = array(Default::default()).parse(new_input(input)).finish();
+            let mut parsed = array(Default::default()).parse(new_input(input)).finish();
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan(input);
+            }
             assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
         }
     }
@@ -133,7 +137,10 @@ mod test {
         let invalid_inputs = [r#"["#, r#"[,]"#, r#"[,2]"#, r#"[1e165,,]"#];
         for input in invalid_inputs {
             dbg!(input);
-            let parsed = array(Default::default()).parse(new_input(input)).finish();
+            let mut parsed = array(Default::default()).parse(new_input(input)).finish();
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan(input);
+            }
             assert!(parsed.is_err());
         }
     }
diff --git a/crates/toml_edit/src/parser/document.rs b/crates/toml_edit/src/parser/document.rs
index 5b183be0..38e9e0ee 100644
--- a/crates/toml_edit/src/parser/document.rs
+++ b/crates/toml_edit/src/parser/document.rs
@@ -20,6 +20,7 @@ use crate::parser::trivia::{comment, line_ending, line_trailing, newline, ws};
 use crate::parser::value::value;
 use crate::table::TableKeyValue;
 use crate::Item;
+use crate::RawString;
 
 // ;; TOML
 
@@ -68,10 +69,9 @@ pub(crate) fn parse_comment<'s, 'i>(
 ) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'_>> + 's {
     move |i| {
         (comment, line_ending)
-            .map_res::<_, _, std::str::Utf8Error>(|(c, e)| {
-                let c = std::str::from_utf8(c)?;
-                state.borrow_mut().on_comment(c, e);
-                Ok(())
+            .span()
+            .map(|span| {
+                state.borrow_mut().on_comment(span);
             })
             .parse(i)
     }
@@ -80,13 +80,22 @@ pub(crate) fn parse_comment<'s, 'i>(
 pub(crate) fn parse_ws<'s, 'i>(
     state: &'s RefCell<ParseState>,
 ) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
-    move |i| ws.map(|w| state.borrow_mut().on_ws(w)).parse(i)
+    move |i| {
+        ws.span()
+            .map(|span| state.borrow_mut().on_ws(span))
+            .parse(i)
+    }
 }
 
 pub(crate) fn parse_newline<'s, 'i>(
     state: &'s RefCell<ParseState>,
 ) -> impl FnMut(Input<'i>) -> IResult<Input<'i>, (), ParserError<'i>> + 's {
-    move |i| newline.map(|_| state.borrow_mut().on_ws("\n")).parse(i)
+    move |i| {
+        newline
+            .span()
+            .map(|span| state.borrow_mut().on_ws(span))
+            .parse(i)
+    }
 }
 
 pub(crate) fn keyval<'s, 'i>(
@@ -110,7 +119,7 @@ pub(crate) fn parse_keyval(
                 .context(Context::Expected(ParserValue::CharLiteral('.')))
                 .context(Context::Expected(ParserValue::CharLiteral('='))),
             (
-                ws,
+                ws.span(),
                 value(RecursionCheck::default()),
                 line_trailing
                     .context(Context::Expected(ParserValue::CharLiteral('\n')))
@@ -123,7 +132,8 @@ pub(crate) fn parse_keyval(
             let key = path.pop().expect("grammar ensures at least 1");
 
             let (pre, v, suf) = v;
-            let suf = std::str::from_utf8(suf)?;
+            let pre = RawString::with_span(pre);
+            let suf = RawString::with_span(suf);
             let v = v.decorated(pre, suf);
             Ok((
                 path,
@@ -135,119 +145,3 @@ pub(crate) fn parse_keyval(
         })
         .parse(input)
 }
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    fn documents() {
-        let documents = [
-            "",
-            r#"
-# This is a TOML document.
-
-title = "TOML Example"
-
-    [owner]
-    name = "Tom Preston-Werner"
-    dob = 1979-05-27T07:32:00-08:00 # First class dates
-
-    [database]
-    server = "192.168.1.1"
-    ports = [ 8001, 8001, 8002 ]
-    connection_max = 5000
-    enabled = true
-
-    [servers]
-
-    # Indentation (tabs and/or spaces) is allowed but not required
-[servers.alpha]
-    ip = "10.0.0.1"
-    dc = "eqdc10"
-
-    [servers.beta]
-    ip = "10.0.0.2"
-    dc = "eqdc10"
-
-    [clients]
-    data = [ ["gamma", "delta"], [1, 2] ]
-
-    # Line breaks are OK when inside arrays
-hosts = [
-    "alpha",
-    "omega"
-]
-
-   'some.wierd .stuff'   =  """
-                         like
-                         that
-                      #   """ # this broke my sintax highlighting
-   " also. like " = '''
-that
-'''
-   double = 2e39 # this number looks familiar
-# trailing comment"#,
-            r#""#,
-            r#"  "#,
-            r#" hello = 'darkness' # my old friend
-"#,
-            r#"[parent . child]
-key = "value"
-"#,
-            r#"hello.world = "a"
-"#,
-            r#"foo = 1979-05-27 # Comment
-"#,
-        ];
-        for input in documents {
-            dbg!(input);
-            let parsed = document.parse(new_input(input)).finish();
-            let doc = match parsed {
-                Ok(doc) => doc,
-                Err(err) => {
-                    panic!(
-                        "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
-                        err, input
-                    )
-                }
-            };
-
-            snapbox::assert_eq(input, doc.to_string());
-        }
-    }
-
-    #[test]
-    fn documents_parse_only() {
-        let parse_only = ["\u{FEFF}
-[package]
-name = \"foo\"
-version = \"0.0.1\"
-authors = []
-"];
-        for input in parse_only {
-            dbg!(input);
-            let parsed = document.parse(new_input(input)).finish();
-            match parsed {
-                Ok(_) => (),
-                Err(err) => {
-                    panic!(
-                        "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
-                        err, input
-                    )
-                }
-            }
-        }
-    }
-
-    #[test]
-    fn invalid_documents() {
-        let invalid_inputs = [r#" hello = 'darkness' # my old friend
-$"#];
-        for input in invalid_inputs {
-            dbg!(input);
-            let parsed = document.parse(new_input(input)).finish();
-            assert!(parsed.is_err(), "Input: {:?}", input);
-        }
-    }
-}
diff --git a/crates/toml_edit/src/parser/errors.rs b/crates/toml_edit/src/parser/errors.rs
index 43a4e6d4..81e7ef52 100644
--- a/crates/toml_edit/src/parser/errors.rs
+++ b/crates/toml_edit/src/parser/errors.rs
@@ -16,8 +16,8 @@ pub struct TomlError {
 impl TomlError {
     pub(crate) fn new(error: ParserError<'_>, original: Input<'_>) -> Self {
         use nom8::input::Offset;
-        let offset = original.offset(error.input);
-        let position = translate_position(original, offset);
+        let offset = original.offset(&error.input);
+        let position = translate_position(&original, offset);
         let message = ParserErrorDisplay {
             error: &error,
             original,
@@ -351,8 +351,10 @@ pub(crate) enum CustomError {
 impl CustomError {
     pub(crate) fn duplicate_key(path: &[Key], i: usize) -> Self {
         assert!(i < path.len());
+        let key = &path[i];
+        let repr = key.display_repr();
         Self::DuplicateKey {
-            key: path[i].to_repr().as_ref().as_raw().into(),
+            key: repr.into(),
             table: Some(path[..i].to_vec()),
         }
     }
diff --git a/crates/toml_edit/src/parser/inline_table.rs b/crates/toml_edit/src/parser/inline_table.rs
index 028a560e..e8d38f4a 100644
--- a/crates/toml_edit/src/parser/inline_table.rs
+++ b/crates/toml_edit/src/parser/inline_table.rs
@@ -10,7 +10,7 @@ use crate::parser::prelude::*;
 use crate::parser::trivia::ws;
 use crate::parser::value::value;
 use crate::table::TableKeyValue;
-use crate::{InlineTable, InternalString, Item, Value};
+use crate::{InlineTable, InternalString, Item, RawString, Value};
 
 use indexmap::map::Entry;
 
@@ -34,7 +34,7 @@ pub(crate) fn inline_table(
 
 fn table_from_pairs(
     v: Vec<(Vec<Key>, TableKeyValue)>,
-    preamble: &str,
+    preamble: RawString,
 ) -> Result<InlineTable, CustomError> {
     let mut root = InlineTable::new();
     root.set_preamble(preamble);
@@ -98,11 +98,16 @@ pub(crate) const KEYVAL_SEP: u8 = b'=';
 
 fn inline_table_keyvals(
     check: RecursionCheck,
-) -> impl FnMut(Input<'_>) -> IResult<Input<'_>, (Vec<(Vec<Key>, TableKeyValue)>, &str), ParserError<'_>>
-{
+) -> impl FnMut(
+    Input<'_>,
+) -> IResult<Input<'_>, (Vec<(Vec<Key>, TableKeyValue)>, RawString), ParserError<'_>> {
     move |input| {
         let check = check.recursing(input)?;
-        (separated_list0(INLINE_TABLE_SEP, keyval(check)), ws).parse(input)
+        (
+            separated_list0(INLINE_TABLE_SEP, keyval(check)),
+            ws.span().map(RawString::with_span),
+        )
+            .parse(input)
     }
 }
 
@@ -116,7 +121,7 @@ fn keyval(
                 one_of(KEYVAL_SEP)
                     .context(Context::Expected(ParserValue::CharLiteral('.')))
                     .context(Context::Expected(ParserValue::CharLiteral('='))),
-                (ws, value(check), ws),
+                (ws.span(), value(check), ws.span()),
             )),
         )
             .map(|(key, (_, v))| {
@@ -124,6 +129,8 @@ fn keyval(
                 let key = path.pop().expect("grammar ensures at least 1");
 
                 let (pre, v, suf) = v;
+                let pre = RawString::with_span(pre);
+                let suf = RawString::with_span(suf);
                 let v = v.decorated(pre, suf);
                 (
                     path,
@@ -152,9 +159,12 @@ mod test {
         ];
         for input in inputs {
             dbg!(input);
-            let parsed = inline_table(Default::default())
+            let mut parsed = inline_table(Default::default())
                 .parse(new_input(input))
                 .finish();
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan(input);
+            }
             assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
         }
     }
@@ -164,9 +174,12 @@ mod test {
         let invalid_inputs = [r#"{a = 1e165"#, r#"{ hello = "world", a = 2, hello = 1}"#];
         for input in invalid_inputs {
             dbg!(input);
-            let parsed = inline_table(Default::default())
+            let mut parsed = inline_table(Default::default())
                 .parse(new_input(input))
                 .finish();
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan(input);
+            }
             assert!(parsed.is_err());
         }
     }
diff --git a/crates/toml_edit/src/parser/key.rs b/crates/toml_edit/src/parser/key.rs
index 54157b43..82424916 100644
--- a/crates/toml_edit/src/parser/key.rs
+++ b/crates/toml_edit/src/parser/key.rs
@@ -12,16 +12,20 @@ use crate::parser::strings::{basic_string, literal_string};
 use crate::parser::trivia::{from_utf8_unchecked, ws};
 use crate::repr::{Decor, Repr};
 use crate::InternalString;
+use crate::RawString;
 
 // key = simple-key / dotted-key
 // dotted-key = simple-key 1*( dot-sep simple-key )
 pub(crate) fn key(input: Input<'_>) -> IResult<Input<'_>, Vec<Key>, ParserError<'_>> {
     separated_list1(
         DOT_SEP,
-        (ws, simple_key, ws).map(|(pre, (raw, key), suffix)| {
+        (ws.span(), simple_key, ws.span()).map(|(pre, (raw, key), suffix)| {
             Key::new(key)
                 .with_repr_unchecked(Repr::new_unchecked(raw))
-                .with_decor(Decor::new(pre, suffix))
+                .with_decor(Decor::new(
+                    RawString::with_span(pre),
+                    RawString::with_span(suffix),
+                ))
         }),
     )
     .context(Context::Expression("key"))
@@ -37,19 +41,19 @@ pub(crate) fn key(input: Input<'_>) -> IResult<Input<'_>, Vec<Key>, ParserError<
 // quoted-key = basic-string / literal-string
 pub(crate) fn simple_key(
     input: Input<'_>,
-) -> IResult<Input<'_>, (&str, InternalString), ParserError<'_>> {
+) -> IResult<Input<'_>, (RawString, InternalString), ParserError<'_>> {
     dispatch! {peek(any);
         crate::parser::strings::QUOTATION_MARK => basic_string
             .map(|s: std::borrow::Cow<'_, str>| s.as_ref().into()),
         crate::parser::strings::APOSTROPHE => literal_string.map(|s: &str| s.into()),
         _ => unquoted_key.map(|s: &str| s.into()),
     }
-        .with_recognized()
-        .map(|(k, b)| {
-            let s = unsafe { from_utf8_unchecked(b, "If `quoted_key` or `unquoted_key` are valid, then their `recognize`d value is valid") };
-            (s, k)
-        })
-        .parse(input)
+    .with_span()
+    .map(|(k, span)| {
+        let raw = RawString::with_span(span);
+        (raw, k)
+    })
+    .parse(input)
 }
 
 // unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
@@ -90,7 +94,11 @@ mod test {
         for (input, expected) in cases {
             dbg!(input);
             let parsed = simple_key.parse(new_input(input)).finish();
-            assert_eq!(parsed, Ok((input, expected.into())), "Parsing {input:?}");
+            assert_eq!(
+                parsed,
+                Ok((RawString::with_span(0..(input.len())), expected.into())),
+                "Parsing {input:?}"
+            );
         }
     }
 }
diff --git a/crates/toml_edit/src/parser/mod.rs b/crates/toml_edit/src/parser/mod.rs
index cd17b321..46ec465c 100644
--- a/crates/toml_edit/src/parser/mod.rs
+++ b/crates/toml_edit/src/parser/mod.rs
@@ -22,10 +22,12 @@ pub(crate) fn parse_document(raw: &str) -> Result<crate::Document, TomlError> {
     use prelude::*;
 
     let b = new_input(raw);
-    document::document
+    let mut doc = document::document
         .parse(b)
         .finish()
-        .map_err(|e| TomlError::new(e, b))
+        .map_err(|e| TomlError::new(e, b))?;
+    doc.original = Some(raw.to_owned());
+    Ok(doc)
 }
 
 pub(crate) fn parse_key(raw: &str) -> Result<crate::Key, TomlError> {
@@ -47,7 +49,12 @@ pub(crate) fn parse_key_path(raw: &str) -> Result<Vec<crate::Key>, TomlError> {
     let b = new_input(raw);
     let result = key::key.parse(b).finish();
     match result {
-        Ok(keys) => Ok(keys),
+        Ok(mut keys) => {
+            for key in &mut keys {
+                key.despan(raw);
+            }
+            Ok(keys)
+        }
         Err(e) => Err(TomlError::new(e, b)),
     }
 }
@@ -61,6 +68,7 @@ pub(crate) fn parse_value(raw: &str) -> Result<crate::Value, TomlError> {
         Ok(mut value) => {
             // Only take the repr and not decor, as its probably not intended
             value.decor_mut().clear();
+            value.despan(raw);
             Ok(value)
         }
         Err(e) => Err(TomlError::new(e, b)),
@@ -76,10 +84,10 @@ pub(crate) mod prelude {
 
     pub(crate) use nom8::FinishIResult as _;
 
-    pub(crate) type Input<'b> = &'b [u8];
+    pub(crate) type Input<'b> = nom8::input::Located<&'b [u8]>;
 
     pub(crate) fn new_input(s: &str) -> Input<'_> {
-        s.as_bytes()
+        nom8::input::Located::new(s.as_bytes())
     }
 
     pub(crate) fn ok_error<I, O, E>(res: IResult<I, O, E>) -> Result<Option<(I, O)>, nom8::Err<E>> {
@@ -167,3 +175,128 @@ pub(crate) mod prelude {
         }
     }
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn documents() {
+        let documents = [
+            "",
+            r#"
+# This is a TOML document.
+
+title = "TOML Example"
+
+    [owner]
+    name = "Tom Preston-Werner"
+    dob = 1979-05-27T07:32:00-08:00 # First class dates
+
+    [database]
+    server = "192.168.1.1"
+    ports = [ 8001, 8001, 8002 ]
+    connection_max = 5000
+    enabled = true
+
+    [servers]
+
+    # Indentation (tabs and/or spaces) is allowed but not required
+[servers.alpha]
+    ip = "10.0.0.1"
+    dc = "eqdc10"
+
+    [servers.beta]
+    ip = "10.0.0.2"
+    dc = "eqdc10"
+
+    [clients]
+    data = [ ["gamma", "delta"], [1, 2] ]
+
+    # Line breaks are OK when inside arrays
+hosts = [
+    "alpha",
+    "omega"
+]
+
+   'some.wierd .stuff'   =  """
+                         like
+                         that
+                      #   """ # this broke my sintax highlighting
+   " also. like " = '''
+that
+'''
+   double = 2e39 # this number looks familiar
+# trailing comment"#,
+            r#""#,
+            r#"  "#,
+            r#" hello = 'darkness' # my old friend
+"#,
+            r#"[parent . child]
+key = "value"
+"#,
+            r#"hello.world = "a"
+"#,
+            r#"foo = 1979-05-27 # Comment
+"#,
+        ];
+        for input in documents {
+            dbg!(input);
+            let mut parsed = parse_document(input);
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan();
+            }
+            let doc = match parsed {
+                Ok(doc) => doc,
+                Err(err) => {
+                    panic!(
+                        "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
+                        err, input
+                    )
+                }
+            };
+
+            snapbox::assert_eq(input, doc.to_string());
+        }
+    }
+
+    #[test]
+    fn documents_parse_only() {
+        let parse_only = ["\u{FEFF}
+[package]
+name = \"foo\"
+version = \"0.0.1\"
+authors = []
+"];
+        for input in parse_only {
+            dbg!(input);
+            let mut parsed = parse_document(input);
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan();
+            }
+            match parsed {
+                Ok(_) => (),
+                Err(err) => {
+                    panic!(
+                        "Parse error: {:?}\nFailed to parse:\n```\n{}\n```",
+                        err, input
+                    )
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn invalid_documents() {
+        let invalid_inputs = [r#" hello = 'darkness' # my old friend
+$"#];
+        for input in invalid_inputs {
+            dbg!(input);
+            let mut parsed = parse_document(input);
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan();
+            }
+            assert!(parsed.is_err(), "Input: {:?}", input);
+        }
+    }
+}
diff --git a/crates/toml_edit/src/parser/state.rs b/crates/toml_edit/src/parser/state.rs
index 6733753d..45a4845a 100644
--- a/crates/toml_edit/src/parser/state.rs
+++ b/crates/toml_edit/src/parser/state.rs
@@ -2,11 +2,11 @@ use crate::key::Key;
 use crate::parser::errors::CustomError;
 use crate::repr::Decor;
 use crate::table::TableKeyValue;
-use crate::{ArrayOfTables, Document, InternalString, Item, Table};
+use crate::{ArrayOfTables, Document, InternalString, Item, RawString, Table};
 
 pub(crate) struct ParseState {
     document: Document,
-    trailing: String,
+    trailing: Option<std::ops::Range<usize>>,
     current_table_position: usize,
     current_table: Table,
     current_is_array: bool,
@@ -16,17 +16,25 @@ pub(crate) struct ParseState {
 impl ParseState {
     pub(crate) fn into_document(mut self) -> Result<Document, CustomError> {
         self.finalize_table()?;
-        let trailing = self.trailing.as_str().into();
-        self.document.trailing = trailing;
+        let trailing = self.trailing.map(RawString::with_span);
+        self.document.trailing = trailing.unwrap_or_default();
         Ok(self.document)
     }
 
-    pub(crate) fn on_ws(&mut self, w: &str) {
-        self.trailing.push_str(w);
+    pub(crate) fn on_ws(&mut self, span: std::ops::Range<usize>) {
+        if let Some(old) = self.trailing.take() {
+            self.trailing = Some(old.start..span.end);
+        } else {
+            self.trailing = Some(span);
+        }
     }
 
-    pub(crate) fn on_comment(&mut self, c: &str, e: &str) {
-        self.trailing = [&self.trailing, c, e].concat();
+    pub(crate) fn on_comment(&mut self, span: std::ops::Range<usize>) {
+        if let Some(old) = self.trailing.take() {
+            self.trailing = Some(old.start..span.end);
+        } else {
+            self.trailing = Some(span);
+        }
     }
 
     pub(crate) fn on_keyval(
@@ -35,15 +43,23 @@ impl ParseState {
         mut kv: TableKeyValue,
     ) -> Result<(), CustomError> {
         {
-            let prefix = std::mem::take(&mut self.trailing);
+            let mut prefix = self.trailing.take();
             let first_key = if path.is_empty() {
                 &mut kv.key
             } else {
                 &mut path[0]
             };
+            let prefix = match (
+                prefix.take(),
+                first_key.decor.prefix().and_then(|d| d.span()),
+            ) {
+                (Some(p), Some(k)) => Some(p.start..k.end),
+                (Some(p), None) | (None, Some(p)) => Some(p),
+                (None, None) => None,
+            };
             first_key
                 .decor
-                .set_prefix(prefix + first_key.decor.prefix().unwrap_or_default());
+                .set_prefix(prefix.map(RawString::with_span).unwrap_or_default());
         }
 
         let table = &mut self.current_table;
@@ -229,13 +245,17 @@ impl ParseState {
     pub(crate) fn on_std_header(
         &mut self,
         path: Vec<Key>,
-        trailing: &str,
+        trailing: std::ops::Range<usize>,
     ) -> Result<(), CustomError> {
         debug_assert!(!path.is_empty());
 
         self.finalize_table()?;
-        let leading = std::mem::take(&mut self.trailing);
-        self.start_table(path, Decor::new(leading, trailing))?;
+        let leading = self
+            .trailing
+            .take()
+            .map(RawString::with_span)
+            .unwrap_or_default();
+        self.start_table(path, Decor::new(leading, RawString::with_span(trailing)))?;
 
         Ok(())
     }
@@ -243,13 +263,17 @@ impl ParseState {
     pub(crate) fn on_array_header(
         &mut self,
         path: Vec<Key>,
-        trailing: &str,
+        trailing: std::ops::Range<usize>,
     ) -> Result<(), CustomError> {
         debug_assert!(!path.is_empty());
 
         self.finalize_table()?;
-        let leading = std::mem::take(&mut self.trailing);
-        self.start_aray_table(path, Decor::new(leading, trailing))?;
+        let leading = self
+            .trailing
+            .take()
+            .map(RawString::with_span)
+            .unwrap_or_default();
+        self.start_aray_table(path, Decor::new(leading, RawString::with_span(trailing)))?;
 
         Ok(())
     }
@@ -259,7 +283,7 @@ impl Default for ParseState {
     fn default() -> Self {
         Self {
             document: Document::new(),
-            trailing: String::new(),
+            trailing: None,
             current_table_position: 0,
             current_table: Table::new(),
             current_is_array: false,
diff --git a/crates/toml_edit/src/parser/table.rs b/crates/toml_edit/src/parser/table.rs
index 89294704..b11f5c9c 100644
--- a/crates/toml_edit/src/parser/table.rs
+++ b/crates/toml_edit/src/parser/table.rs
@@ -37,7 +37,7 @@ pub(crate) fn std_table<'s, 'i>(
                     .context(Context::Expected(ParserValue::CharLiteral('.')))
                     .context(Context::Expected(ParserValue::StringLiteral("]"))),
             ),
-            cut(line_trailing.map_res(std::str::from_utf8))
+            cut(line_trailing)
                 .context(Context::Expected(ParserValue::CharLiteral('\n')))
                 .context(Context::Expected(ParserValue::CharLiteral('#'))),
         )
@@ -61,7 +61,7 @@ pub(crate) fn array_table<'s, 'i>(
                     .context(Context::Expected(ParserValue::CharLiteral('.')))
                     .context(Context::Expected(ParserValue::StringLiteral("]]"))),
             ),
-            cut(line_trailing.map_res(std::str::from_utf8))
+            cut(line_trailing)
                 .context(Context::Expected(ParserValue::CharLiteral('\n')))
                 .context(Context::Expected(ParserValue::CharLiteral('#'))),
         )
diff --git a/crates/toml_edit/src/parser/trivia.rs b/crates/toml_edit/src/parser/trivia.rs
index 434611a9..981c7b23 100644
--- a/crates/toml_edit/src/parser/trivia.rs
+++ b/crates/toml_edit/src/parser/trivia.rs
@@ -107,8 +107,10 @@ pub(crate) fn line_ending(input: Input<'_>) -> IResult<Input<'_>, &str, ParserEr
 
 // note: this rule is not present in the original grammar
 // line-trailing = ws [comment] skip-line-ending
-pub(crate) fn line_trailing(input: Input<'_>) -> IResult<Input<'_>, &[u8], ParserError<'_>> {
-    terminated((ws, opt(comment)).recognize(), line_ending).parse(input)
+pub(crate) fn line_trailing(
+    input: Input<'_>,
+) -> IResult<Input<'_>, std::ops::Range<usize>, ParserError<'_>> {
+    terminated((ws, opt(comment)).span(), line_ending).parse(input)
 }
 
 #[cfg(test)]
diff --git a/crates/toml_edit/src/parser/value.rs b/crates/toml_edit/src/parser/value.rs
index 054d90ab..15ba4478 100644
--- a/crates/toml_edit/src/parser/value.rs
+++ b/crates/toml_edit/src/parser/value.rs
@@ -9,9 +9,9 @@ use crate::parser::inline_table::inline_table;
 use crate::parser::numbers::{float, integer};
 use crate::parser::prelude::*;
 use crate::parser::strings::string;
-use crate::parser::trivia::from_utf8_unchecked;
 use crate::repr::{Formatted, Repr};
 use crate::value as v;
+use crate::RawString;
 use crate::Value;
 
 // val = string / boolean / array / inline-table / date-time / float / integer
@@ -83,35 +83,40 @@ pub(crate) fn value(
                     .context(Context::Expected(ParserValue::CharLiteral('\'')))
             },
     }
-        .with_recognized()
-        .map_res(|(value, raw)| apply_raw(value, raw))
+        .with_span()
+        .map_res(|(value, span)| apply_raw(value, span))
         .parse(input)
     }
 }
 
-fn apply_raw(mut val: Value, raw: &[u8]) -> Result<Value, std::str::Utf8Error> {
+fn apply_raw(mut val: Value, span: std::ops::Range<usize>) -> Result<Value, std::str::Utf8Error> {
     match val {
         Value::String(ref mut f) => {
-            let raw = std::str::from_utf8(raw)?;
+            let raw = RawString::with_span(span);
             f.set_repr_unchecked(Repr::new_unchecked(raw));
         }
         Value::Integer(ref mut f) => {
-            let raw = unsafe { from_utf8_unchecked(raw, "`integer()` filters out non-ASCII") };
+            let raw = RawString::with_span(span);
             f.set_repr_unchecked(Repr::new_unchecked(raw));
         }
         Value::Float(ref mut f) => {
-            let raw = unsafe { from_utf8_unchecked(raw, "`float()` filters out non-ASCII") };
+            let raw = RawString::with_span(span);
             f.set_repr_unchecked(Repr::new_unchecked(raw));
         }
         Value::Boolean(ref mut f) => {
-            let raw = unsafe { from_utf8_unchecked(raw, "`boolean()` filters out non-ASCII") };
+            let raw = RawString::with_span(span);
             f.set_repr_unchecked(Repr::new_unchecked(raw));
         }
         Value::Datetime(ref mut f) => {
-            let raw = unsafe { from_utf8_unchecked(raw, "`date_time()` filters out non-ASCII") };
+            let raw = RawString::with_span(span);
             f.set_repr_unchecked(Repr::new_unchecked(raw));
         }
-        Value::Array(_) | Value::InlineTable(_) => {}
+        Value::Array(ref mut arr) => {
+            arr.span = Some(span);
+        }
+        Value::InlineTable(ref mut table) => {
+            table.span = Some(span);
+        }
     };
     val.decorate("", "");
     Ok(val)
@@ -142,7 +147,10 @@ trimmed in raw strings.
         ];
         for input in inputs {
             dbg!(input);
-            let parsed = value(Default::default()).parse(new_input(input)).finish();
+            let mut parsed = value(Default::default()).parse(new_input(input)).finish();
+            if let Ok(parsed) = &mut parsed {
+                parsed.despan(input);
+            }
             assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
         }
     }
diff --git a/crates/toml_edit/src/raw_string.rs b/crates/toml_edit/src/raw_string.rs
new file mode 100644
index 00000000..c7c4d187
--- /dev/null
+++ b/crates/toml_edit/src/raw_string.rs
@@ -0,0 +1,171 @@
+use crate::InternalString;
+
+/// Opaque string storage for raw TOML; internal to `toml_edit`
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+pub struct RawString(RawStringInner);
+
+#[derive(PartialEq, Eq, Clone, Debug, Hash)]
+enum RawStringInner {
+    Empty,
+    Explicit(InternalString),
+    Spanned(std::ops::Range<usize>),
+}
+
+impl RawString {
+    pub(crate) fn with_span(span: std::ops::Range<usize>) -> Self {
+        if span.start == span.end {
+            RawString(RawStringInner::Empty)
+        } else {
+            RawString(RawStringInner::Spanned(span))
+        }
+    }
+
+    /// Access the underlying string
+    pub fn as_str(&self) -> Option<&str> {
+        match &self.0 {
+            RawStringInner::Empty => Some(""),
+            RawStringInner::Explicit(s) => Some(s.as_str()),
+            RawStringInner::Spanned(_) => None,
+        }
+    }
+
+    pub(crate) fn to_str<'s>(&'s self, input: &'s str) -> &'s str {
+        match &self.0 {
+            RawStringInner::Empty => "",
+            RawStringInner::Explicit(s) => s.as_str(),
+            RawStringInner::Spanned(span) => input.get(span.clone()).unwrap_or_else(|| {
+                panic!("span {:?} should be in input:\n```\n{}\n```", span, input)
+            }),
+        }
+    }
+
+    pub(crate) fn to_str_with_default<'s>(
+        &'s self,
+        input: Option<&'s str>,
+        default: &'s str,
+    ) -> &'s str {
+        match &self.0 {
+            RawStringInner::Empty => "",
+            RawStringInner::Explicit(s) => s.as_str(),
+            RawStringInner::Spanned(span) => {
+                if let Some(input) = input {
+                    input.get(span.clone()).unwrap_or_else(|| {
+                        panic!("span {:?} should be in input:\n```\n{}\n```", span, input)
+                    })
+                } else {
+                    default
+                }
+            }
+        }
+    }
+
+    /// Access the underlying span
+    pub(crate) fn span(&self) -> Option<std::ops::Range<usize>> {
+        match &self.0 {
+            RawStringInner::Empty => None,
+            RawStringInner::Explicit(_) => None,
+            RawStringInner::Spanned(span) => Some(span.clone()),
+        }
+    }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        match &self.0 {
+            RawStringInner::Empty => {}
+            RawStringInner::Explicit(_) => {}
+            RawStringInner::Spanned(span) => {
+                *self = Self::from(input.get(span.clone()).unwrap_or_else(|| {
+                    panic!("span {:?} should be in input:\n```\n{}\n```", span, input)
+                }))
+            }
+        }
+    }
+
+    pub(crate) fn encode(&self, buf: &mut dyn std::fmt::Write, input: &str) -> std::fmt::Result {
+        let raw = self.to_str(input);
+        for part in raw.split('\r') {
+            write!(buf, "{}", part)?;
+        }
+        Ok(())
+    }
+
+    pub(crate) fn encode_with_default(
+        &self,
+        buf: &mut dyn std::fmt::Write,
+        input: Option<&str>,
+        default: &str,
+    ) -> std::fmt::Result {
+        let raw = self.to_str_with_default(input, default);
+        for part in raw.split('\r') {
+            write!(buf, "{}", part)?;
+        }
+        Ok(())
+    }
+}
+
+impl Default for RawString {
+    fn default() -> Self {
+        Self(RawStringInner::Empty)
+    }
+}
+
+impl From<&str> for RawString {
+    #[inline]
+    fn from(s: &str) -> Self {
+        if s.is_empty() {
+            Self(RawStringInner::Empty)
+        } else {
+            InternalString::from(s).into()
+        }
+    }
+}
+
+impl From<String> for RawString {
+    #[inline]
+    fn from(s: String) -> Self {
+        if s.is_empty() {
+            Self(RawStringInner::Empty)
+        } else {
+            InternalString::from(s).into()
+        }
+    }
+}
+
+impl From<&String> for RawString {
+    #[inline]
+    fn from(s: &String) -> Self {
+        if s.is_empty() {
+            Self(RawStringInner::Empty)
+        } else {
+            InternalString::from(s).into()
+        }
+    }
+}
+
+impl From<InternalString> for RawString {
+    #[inline]
+    fn from(inner: InternalString) -> Self {
+        Self(RawStringInner::Explicit(inner))
+    }
+}
+
+impl From<&InternalString> for RawString {
+    #[inline]
+    fn from(s: &InternalString) -> Self {
+        if s.is_empty() {
+            Self(RawStringInner::Empty)
+        } else {
+            InternalString::from(s).into()
+        }
+    }
+}
+
+impl From<Box<str>> for RawString {
+    #[inline]
+    fn from(s: Box<str>) -> Self {
+        if s.is_empty() {
+            Self(RawStringInner::Empty)
+        } else {
+            InternalString::from(s).into()
+        }
+    }
+}
diff --git a/crates/toml_edit/src/repr.rs b/crates/toml_edit/src/repr.rs
index 0bd5f515..56e39530 100644
--- a/crates/toml_edit/src/repr.rs
+++ b/crates/toml_edit/src/repr.rs
@@ -1,6 +1,6 @@
 use std::borrow::Cow;
 
-use crate::InternalString;
+use crate::RawString;
 
 /// A value together with its `to_string` representation,
 /// including surrounding it whitespaces and comments.
@@ -38,12 +38,31 @@ where
         self.value
     }
 
-    /// Returns the key raw representation.
-    pub fn to_repr(&self) -> Cow<Repr> {
-        self.repr
-            .as_ref()
+    /// Returns the raw representation, if available.
+    pub fn as_repr(&self) -> Option<&Repr> {
+        self.repr.as_ref()
+    }
+
+    /// Returns the default raw representation.
+    pub fn default_repr(&self) -> Repr {
+        self.value.to_repr()
+    }
+
+    /// Returns a raw representation.
+    pub fn display_repr(&self) -> Cow<str> {
+        self.as_repr()
+            .and_then(|r| r.as_raw().as_str())
             .map(Cow::Borrowed)
-            .unwrap_or_else(|| Cow::Owned(self.value.to_repr()))
+            .unwrap_or_else(|| {
+                Cow::Owned(self.default_repr().as_raw().as_str().unwrap().to_owned())
+            })
+    }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        self.decor.despan(input);
+        if let Some(repr) = &mut self.repr {
+            repr.despan(input);
+        }
     }
 
     /// Returns the surrounding whitespace
@@ -67,7 +86,7 @@ where
     T: ValueRepr,
 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        crate::encode::Encode::encode(self, f, ("", ""))
+        crate::encode::Encode::encode(self, f, None, ("", ""))
     }
 }
 
@@ -77,42 +96,44 @@ pub trait ValueRepr: crate::private::Sealed {
 }
 
 /// TOML-encoded value
-#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug, Hash)]
+#[derive(Eq, PartialEq, Clone, Debug, Hash)]
 pub struct Repr {
-    raw_value: InternalString,
+    raw_value: RawString,
 }
 
 impl Repr {
-    pub(crate) fn new_unchecked(raw: impl Into<InternalString>) -> Self {
+    pub(crate) fn new_unchecked(raw: impl Into<RawString>) -> Self {
         Repr {
             raw_value: raw.into(),
         }
     }
 
     /// Access the underlying value
-    pub fn as_raw(&self) -> &str {
+    pub fn as_raw(&self) -> &RawString {
         &self.raw_value
     }
-}
 
-impl std::fmt::Display for Repr {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.as_raw().fmt(f)
+    pub(crate) fn despan(&mut self, input: &str) {
+        self.raw_value.despan(input)
+    }
+
+    pub(crate) fn encode(&self, buf: &mut dyn std::fmt::Write, input: &str) -> std::fmt::Result {
+        self.as_raw().encode(buf, input)
     }
 }
 
 /// A prefix and suffix,
 ///
 /// Including comments, whitespaces and newlines.
-#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Default, Debug, Hash)]
+#[derive(Eq, PartialEq, Clone, Default, Debug, Hash)]
 pub struct Decor {
-    prefix: Option<InternalString>,
-    suffix: Option<InternalString>,
+    prefix: Option<RawString>,
+    suffix: Option<RawString>,
 }
 
 impl Decor {
     /// Creates a new decor from the given prefix and suffix.
-    pub fn new(prefix: impl Into<InternalString>, suffix: impl Into<InternalString>) -> Self {
+    pub fn new(prefix: impl Into<RawString>, suffix: impl Into<RawString>) -> Self {
         Self {
             prefix: Some(prefix.into()),
             suffix: Some(suffix.into()),
@@ -126,22 +147,57 @@ impl Decor {
     }
 
     /// Get the prefix.
-    pub fn prefix(&self) -> Option<&str> {
-        self.prefix.as_deref()
+    pub fn prefix(&self) -> Option<&RawString> {
+        self.prefix.as_ref()
+    }
+
+    pub(crate) fn prefix_encode(
+        &self,
+        buf: &mut dyn std::fmt::Write,
+        input: Option<&str>,
+        default: &str,
+    ) -> std::fmt::Result {
+        if let Some(prefix) = self.prefix() {
+            prefix.encode_with_default(buf, input, default)
+        } else {
+            write!(buf, "{}", default)
+        }
     }
 
     /// Set the prefix.
-    pub fn set_prefix(&mut self, prefix: impl Into<InternalString>) {
+    pub fn set_prefix(&mut self, prefix: impl Into<RawString>) {
         self.prefix = Some(prefix.into());
     }
 
     /// Get the suffix.
-    pub fn suffix(&self) -> Option<&str> {
-        self.suffix.as_deref()
+    pub fn suffix(&self) -> Option<&RawString> {
+        self.suffix.as_ref()
+    }
+
+    pub(crate) fn suffix_encode(
+        &self,
+        buf: &mut dyn std::fmt::Write,
+        input: Option<&str>,
+        default: &str,
+    ) -> std::fmt::Result {
+        if let Some(suffix) = self.suffix() {
+            suffix.encode_with_default(buf, input, default)
+        } else {
+            write!(buf, "{}", default)
+        }
     }
 
     /// Set the suffix.
-    pub fn set_suffix(&mut self, suffix: impl Into<InternalString>) {
+    pub fn set_suffix(&mut self, suffix: impl Into<RawString>) {
         self.suffix = Some(suffix.into());
     }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        if let Some(prefix) = &mut self.prefix {
+            prefix.despan(input);
+        }
+        if let Some(suffix) = &mut self.suffix {
+            suffix.despan(input);
+        }
+    }
 }
diff --git a/crates/toml_edit/src/table.rs b/crates/toml_edit/src/table.rs
index f6515156..f5994c57 100644
--- a/crates/toml_edit/src/table.rs
+++ b/crates/toml_edit/src/table.rs
@@ -222,6 +222,13 @@ impl Table {
     pub fn key_decor(&self, key: &str) -> Option<&Decor> {
         self.items.get(key).map(|kv| &kv.key.decor)
     }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        for kv in self.items.values_mut() {
+            kv.key.despan(input);
+            kv.value.despan(input);
+        }
+    }
 }
 
 impl Table {
@@ -390,9 +397,9 @@ impl std::fmt::Display for Table {
         let children = self.get_values();
         // print table body
         for (key_path, value) in children {
-            key_path.as_slice().encode(f, DEFAULT_KEY_DECOR)?;
+            key_path.as_slice().encode(f, None, DEFAULT_KEY_DECOR)?;
             write!(f, "=")?;
-            value.encode(f, DEFAULT_VALUE_DECOR)?;
+            value.encode(f, None, DEFAULT_VALUE_DECOR)?;
             writeln!(f)?;
         }
         Ok(())
diff --git a/crates/toml_edit/src/value.rs b/crates/toml_edit/src/value.rs
index 7de45170..5f6b9a9c 100644
--- a/crates/toml_edit/src/value.rs
+++ b/crates/toml_edit/src/value.rs
@@ -6,7 +6,7 @@ use toml_datetime::*;
 use crate::key::Key;
 use crate::parser;
 use crate::repr::{Decor, Formatted};
-use crate::{Array, InlineTable, InternalString};
+use crate::{Array, InlineTable, InternalString, RawString};
 
 /// Representation of a TOML Value (as part of a Key/Value Pair).
 #[derive(Debug, Clone)]
@@ -195,23 +195,27 @@ impl Value {
     /// let d = v.decorated(" ", " ");
     /// assert_eq!(&d.to_string(), " 42 ");
     /// ```
-    pub fn decorated(
-        mut self,
-        prefix: impl Into<InternalString>,
-        suffix: impl Into<InternalString>,
-    ) -> Self {
+    pub fn decorated(mut self, prefix: impl Into<RawString>, suffix: impl Into<RawString>) -> Self {
         self.decorate(prefix, suffix);
         self
     }
 
-    pub(crate) fn decorate(
-        &mut self,
-        prefix: impl Into<InternalString>,
-        suffix: impl Into<InternalString>,
-    ) {
+    pub(crate) fn decorate(&mut self, prefix: impl Into<RawString>, suffix: impl Into<RawString>) {
         let decor = self.decor_mut();
         *decor = Decor::new(prefix, suffix);
     }
+
+    pub(crate) fn despan(&mut self, input: &str) {
+        match self {
+            Value::String(f) => f.despan(input),
+            Value::Integer(f) => f.despan(input),
+            Value::Float(f) => f.despan(input),
+            Value::Boolean(f) => f.despan(input),
+            Value::Datetime(f) => f.despan(input),
+            Value::Array(a) => a.despan(input),
+            Value::InlineTable(t) => t.despan(input),
+        }
+    }
 }
 
 impl FromStr for Value {
@@ -331,7 +335,7 @@ impl<K: Into<Key>, V: Into<Value>> FromIterator<(K, V)> for Value {
 
 impl std::fmt::Display for Value {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        crate::encode::Encode::encode(self, f, ("", ""))
+        crate::encode::Encode::encode(self, f, None, ("", ""))
     }
 }
 


Test patch:
diff --git a/crates/toml_edit/tests/fixtures/invalid/key/dotted-redefine-table.stderr b/crates/toml_edit/tests/fixtures/invalid/key/dotted-redefine-table.stderr
index d7e6b45d..5e0d842a 100644
--- a/crates/toml_edit/tests/fixtures/invalid/key/dotted-redefine-table.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/key/dotted-redefine-table.stderr
@@ -2,5 +2,4 @@ TOML parse error at line 4, column 1
   |
 4 | a.b.c = 2
   | ^
-Dotted key `# Tries to access it as table: error
-a.b` attempted to extend non-table type (integer)
+Dotted key `a.b` attempted to extend non-table type (integer)
diff --git a/crates/toml_edit/tests/testsuite/edit.rs b/crates/toml_edit/tests/testsuite/edit.rs
index a58f9987..28f73c1a 100644
--- a/crates/toml_edit/tests/testsuite/edit.rs
+++ b/crates/toml_edit/tests/testsuite/edit.rs
@@ -477,7 +477,7 @@ fn test_sort_values_by() {
         let a = as_table!(a);
         // Sort by the representation, not the value. So "\"c\"" sorts before "a" because '"' sorts
         // before 'a'.
-        a.sort_values_by(|k1, _, k2, _| k1.to_repr().as_raw().cmp(k2.to_repr().as_raw()));
+        a.sort_values_by(|k1, _, k2, _| k1.display_repr().cmp(&k2.display_repr()));
     })
     .produces_display(
         r#"