Skip to main content

acdc_parser/model/
location.rs

1use serde::{
2    Serialize,
3    ser::{SerializeSeq, Serializer},
4};
5
6/// A range where a specific leveloffset value applies.
7///
8/// When include directives use `leveloffset=+N`, we track the byte ranges where
9/// leveloffsets apply. The parser then queries these ranges to determine the effective
10/// leveloffset at any given position.
11#[derive(Debug, Clone, Default, PartialEq)]
12pub(crate) struct LeveloffsetRange {
13    /// Byte offset where this leveloffset begins (inclusive).
14    pub(crate) start_offset: usize,
15    /// Byte offset where this leveloffset ends (exclusive).
16    pub(crate) end_offset: usize,
17    /// The leveloffset value to apply in this range.
18    pub(crate) value: isize,
19}
20
21impl LeveloffsetRange {
22    /// Create a new leveloffset range.
23    #[must_use]
24    pub(crate) fn new(start_offset: usize, end_offset: usize, value: isize) -> Self {
25        Self {
26            start_offset,
27            end_offset,
28            value,
29        }
30    }
31
32    /// Check if a byte offset falls within this range.
33    #[must_use]
34    pub(crate) fn contains(&self, byte_offset: usize) -> bool {
35        byte_offset >= self.start_offset && byte_offset < self.end_offset
36    }
37}
38
39/// Calculate the total leveloffset at a given byte offset.
40///
41/// Sums all leveloffset values from ranges that contain the given offset.
42/// Ranges can nest (include within include), so we sum all applicable values.
43#[must_use]
44pub(crate) fn calculate_leveloffset_at(ranges: &[LeveloffsetRange], byte_offset: usize) -> isize {
45    ranges
46        .iter()
47        .filter_map(|r| {
48            if r.contains(byte_offset) {
49                Some(r.value)
50            } else {
51                None
52            }
53        })
54        .sum()
55}
56
57pub(crate) trait Locateable {
58    /// Get a reference to the location.
59    fn location(&self) -> &Location;
60}
61
62/// A `Location` represents a location in a document.
63#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
64#[non_exhaustive]
65pub struct Location {
66    /// The absolute start position of the location.
67    pub absolute_start: usize,
68    /// The absolute end position of the location.
69    pub absolute_end: usize,
70
71    /// The start position of the location.
72    pub start: Position,
73    /// The end position of the location.
74    pub end: Position,
75}
76
77impl Location {
78    /// Validates that this location satisfies all invariants.
79    ///
80    /// Checks:
81    /// - `absolute_start <= absolute_end` (valid range)
82    /// - `absolute_end <= input.len()` (within bounds)
83    /// - Both offsets are on UTF-8 character boundaries
84    ///
85    /// # Errors
86    /// Returned as strings for easier debugging.
87    pub fn validate(&self, input: &str) -> Result<(), String> {
88        // Check range validity using the canonical byte offsets
89        if self.absolute_start > self.absolute_end {
90            return Err(format!(
91                "Invalid range: start {} > end {}",
92                self.absolute_start, self.absolute_end
93            ));
94        }
95
96        // Check bounds
97        if self.absolute_end > input.len() {
98            return Err(format!(
99                "End offset {} exceeds input length {}",
100                self.absolute_end,
101                input.len()
102            ));
103        }
104
105        // Check UTF-8 boundaries on the canonical offsets
106        if !input.is_char_boundary(self.absolute_start) {
107            return Err(format!(
108                "Start offset {} not on UTF-8 boundary",
109                self.absolute_start
110            ));
111        }
112
113        if !input.is_char_boundary(self.absolute_end) {
114            return Err(format!(
115                "End offset {} not on UTF-8 boundary",
116                self.absolute_end
117            ));
118        }
119
120        Ok(())
121    }
122
123    /// Shift the start and end positions of the location by the parent location.
124    ///
125    /// This is super useful to adjust the location of a block that is inside another
126    /// block, like anything inside a delimiter block.
127    pub fn shift(&mut self, parent: Option<&Location>) {
128        if let Some(parent) = parent {
129            if parent.start.line == 0 {
130                return;
131            }
132            self.absolute_start += parent.absolute_start;
133            self.absolute_end += parent.absolute_start;
134            self.start.line += parent.start.line;
135            self.end.line += parent.start.line;
136        }
137    }
138
139    /// Shifts the location inline. We subtract 1 from the line number of the start and
140    /// end to account for the fact that inlines are always in the same line as the
141    /// parent calling the parsing function.
142    pub fn shift_inline(&mut self, parent: Option<&Location>) {
143        if let Some(parent) = parent {
144            if parent.start.line != 0 || parent.start.column != 0 {
145                self.absolute_start += parent.absolute_start;
146                self.absolute_end += parent.absolute_start;
147            }
148            if parent.start.line != 0 {
149                self.start.line += parent.start.line - 1;
150                self.end.line += parent.start.line - 1;
151            }
152            if parent.start.column != 0 {
153                self.start.column += parent.start.column - 1;
154                self.end.column += parent.start.column - 1;
155            }
156        }
157    }
158
159    pub fn shift_line_column(&mut self, line: usize, column: usize) {
160        self.start.line += line - 1;
161        self.end.line += line - 1;
162        self.start.column += column - 1;
163        self.end.column += column - 1;
164    }
165}
166
167// We need to implement `Serialize` because I prefer our current `Location` struct to the
168// `asciidoc` `ASG` definition.
169//
170// We serialize `Location` into the ASG format, which is a sequence of two elements: the
171// start and end positions as an array.
172impl Serialize for Location {
173    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
174    where
175        S: Serializer,
176    {
177        let mut state = serializer.serialize_seq(Some(4))?;
178        state.serialize_element(&self.start)?;
179        state.serialize_element(&self.end)?;
180        state.end()
181    }
182}
183
184impl std::fmt::Display for Location {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        write!(
187            f,
188            "location.start({}), location.end({})",
189            self.start, self.end
190        )
191    }
192}
193
194/// A `Position` represents a human-readable position in a document.
195///
196/// This is purely for display/error reporting purposes. For byte offsets,
197/// use `Location.absolute_start` and `Location.absolute_end`.
198#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Serialize)]
199#[non_exhaustive]
200pub struct Position {
201    /// The line number of the position (1-indexed).
202    pub line: usize,
203    /// The column number of the position (1-indexed, counted as Unicode scalar values).
204    #[serde(rename = "col")]
205    pub column: usize,
206}
207
208impl std::fmt::Display for Position {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        write!(f, "line: {}, column: {}", self.line, self.column)
211    }
212}