Skip to main content

toml_spanner/
span.rs

1//! Byte-offset span types for source location tracking.
2
3#[cfg(test)]
4#[path = "./span_tests.rs"]
5mod tests;
6
7/// A byte-offset range within a TOML document.
8///
9/// Convertible to and from [`Range<u32>`](std::ops::Range) and
10/// [`Range<usize>`](std::ops::Range).
11#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
12pub struct Span {
13    /// Start byte offset (inclusive).
14    pub start: u32,
15    /// End byte offset (exclusive).
16    pub end: u32,
17}
18
19impl Span {
20    /// Creates a new [`Span`] from start and end byte offsets.
21    #[inline]
22    pub fn new(start: u32, end: u32) -> Self {
23        Self { start, end }
24    }
25    /// Converts this span into a [`Range<usize>`](std::ops::Range).
26    pub fn range(self) -> std::ops::Range<usize> {
27        self.start as usize..self.end as usize
28    }
29
30    /// Returns `true` if both start and end are zero.
31    #[inline]
32    pub fn is_empty(&self) -> bool {
33        self.start == 0 && self.end == 0
34    }
35
36    /// Grows a key span to cover the full header brackets.
37    ///
38    /// For `[a.b.c]` with a key span on `b`, produces the span of `[a.b.c]`.
39    /// Handles `[[aot]]` as well.
40    pub fn grow_key_to_header(&mut self, text: &[u8]) {
41        let mut start = self.start as usize;
42        while start > 0 && text[start - 1] != b'\n' {
43            start -= 1;
44        }
45        // Skip to the opening bracket (handles leading whitespace and BOM).
46        while text[start] != b'[' {
47            start += 1;
48        }
49        let end = find_header_end(text, start);
50        self.start = start as u32;
51        self.end = end as u32;
52    }
53
54    /// Extracts the header-line span from a HEADER table span.
55    ///
56    /// The HEADER span covers `[` through the last body value. This returns
57    /// just the `[key.path]` or `[[aot]]` portion.
58    pub fn extract_header_span(&self, text: &[u8]) -> Span {
59        let start = self.start as usize;
60        let end = find_header_end(text, start);
61        Span::new(self.start, end as u32)
62    }
63}
64
65/// Given the position of the opening `[` of a header, returns the position
66/// after the closing `]` (or `]]` for AOT). Handles quoted keys correctly.
67fn find_header_end(text: &[u8], start: usize) -> usize {
68    let mut pos = start;
69    debug_assert!(text[pos] == b'[');
70    pos += 1;
71    let is_aot = text[pos] == b'[';
72    if is_aot {
73        pos += 1;
74    }
75    loop {
76        match text[pos] {
77            b']' => {
78                pos += 1;
79                if is_aot {
80                    pos += 1;
81                }
82                return pos;
83            }
84            b'"' => {
85                pos += 1;
86                while text[pos] != b'"' {
87                    if text[pos] == b'\\' {
88                        pos += 1;
89                    }
90                    pos += 1;
91                }
92                pos += 1;
93            }
94            b'\'' => {
95                pos += 1;
96                while text[pos] != b'\'' {
97                    pos += 1;
98                }
99                pos += 1;
100            }
101            _ => pos += 1,
102        }
103    }
104}
105
106impl From<Span> for (u32, u32) {
107    fn from(s: Span) -> (u32, u32) {
108        (s.start, s.end)
109    }
110}
111
112impl From<Span> for (usize, usize) {
113    fn from(s: Span) -> (usize, usize) {
114        (s.start as usize, s.end as usize)
115    }
116}
117
118impl From<std::ops::Range<u32>> for Span {
119    fn from(s: std::ops::Range<u32>) -> Self {
120        Self::new(s.start, s.end)
121    }
122}
123
124impl From<Span> for std::ops::Range<u32> {
125    fn from(s: Span) -> Self {
126        s.start..s.end
127    }
128}
129
130impl From<Span> for std::ops::Range<usize> {
131    fn from(s: Span) -> Self {
132        s.start as usize..s.end as usize
133    }
134}
135
136/// Wraps a value `T` with its source [`Span`].
137///
138/// Use this as a field type in your [`FromToml`](crate::FromToml) structs
139/// when you need to preserve span information alongside the converted value.
140///
141/// # Examples
142///
143/// ```
144/// use toml_spanner::{Arena, Spanned};
145///
146/// let arena = Arena::new();
147/// let mut doc = toml_spanner::parse("name = \"hello\"", &arena)?;
148/// let name: Spanned<String> = {
149///     let mut helper = doc.table_helper();
150///     helper.required("name").ok().unwrap()
151/// };
152/// assert_eq!(name.value, "hello");
153/// assert!(name.span.start < name.span.end);
154/// # Ok::<(), toml_spanner::Error>(())
155/// ```
156pub struct Spanned<T> {
157    /// The inner value.
158    pub value: T,
159    /// The byte-offset span in the source document.
160    pub span: Span,
161}
162
163impl<T> Spanned<T> {
164    /// Creates a [`Spanned`] with the given value and a zero span.
165    #[inline]
166    pub const fn new(value: T) -> Self {
167        Self {
168            value,
169            span: Span { start: 0, end: 0 },
170        }
171    }
172
173    /// Creates a [`Spanned`] from a value and a [`Span`].
174    #[inline]
175    pub const fn with_span(value: T, span: Span) -> Self {
176        Self { value, span }
177    }
178
179    /// Consumes the wrapper, returning the inner value.
180    #[inline]
181    pub fn take(self) -> T {
182        self.value
183    }
184
185    /// Maps the inner value via [`From`], preserving the span.
186    #[inline]
187    pub fn map<V>(self) -> Spanned<V>
188    where
189        V: From<T>,
190    {
191        Spanned {
192            value: self.value.into(),
193            span: self.span,
194        }
195    }
196}
197
198impl<T> Default for Spanned<T>
199where
200    T: Default,
201{
202    fn default() -> Self {
203        Self {
204            value: Default::default(),
205            span: Span::default(),
206        }
207    }
208}
209
210impl<T> AsRef<T> for Spanned<T> {
211    fn as_ref(&self) -> &T {
212        &self.value
213    }
214}
215
216impl<T> std::fmt::Debug for Spanned<T>
217where
218    T: std::fmt::Debug,
219{
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        self.value.fmt(f)
222    }
223}
224
225impl<T> Clone for Spanned<T>
226where
227    T: Clone,
228{
229    fn clone(&self) -> Self {
230        Self {
231            value: self.value.clone(),
232            span: self.span,
233        }
234    }
235}
236
237impl<T> PartialOrd for Spanned<T>
238where
239    T: PartialOrd,
240{
241    fn partial_cmp(&self, o: &Spanned<T>) -> Option<std::cmp::Ordering> {
242        self.value.partial_cmp(&o.value)
243    }
244}
245
246impl<T> Ord for Spanned<T>
247where
248    T: Ord,
249{
250    fn cmp(&self, o: &Spanned<T>) -> std::cmp::Ordering {
251        self.value.cmp(&o.value)
252    }
253}
254
255impl<T> PartialEq for Spanned<T>
256where
257    T: PartialEq,
258{
259    fn eq(&self, o: &Spanned<T>) -> bool {
260        self.value == o.value
261    }
262}
263
264impl<T> Eq for Spanned<T> where T: Eq {}
265
266impl<T> PartialEq<T> for Spanned<T>
267where
268    T: PartialEq,
269{
270    fn eq(&self, o: &T) -> bool {
271        &self.value == o
272    }
273}
274
275#[cfg(feature = "to-toml")]
276impl<T: crate::ser::ToToml> crate::ser::ToToml for Spanned<T> {
277    fn to_toml<'a>(
278        &'a self,
279        arena: &'a crate::Arena,
280    ) -> Result<crate::Item<'a>, crate::ser::ToTomlError> {
281        self.value.to_toml(arena)
282    }
283}
284
285#[cfg(feature = "from-toml")]
286impl<'de, T> crate::de::FromToml<'de> for Spanned<T>
287where
288    T: crate::de::FromToml<'de>,
289{
290    #[inline]
291    fn from_toml(
292        ctx: &mut crate::de::Context<'de>,
293        value: &crate::item::Item<'de>,
294    ) -> Result<Self, crate::de::Failed> {
295        let span = value.span_unchecked();
296        let inner = T::from_toml(ctx, value)?;
297        Ok(Self { span, value: inner })
298    }
299}