Skip to main content

textlane_core/models/
location.rs

1use crate::Allocator;
2use crate::models::nlcst::Node;
3use crate::models::position::{Point, Position, UnistPoint, UnistPosition};
4
5/// A source indexed as vfile-location reads it: offsets in UTF-16 code units, turned into
6/// lines, columns and the byte points of core's positions, each in constant or log time.
7#[derive(Clone, Debug)]
8pub struct Location<'a> {
9    source: &'a str,
10    starts: Vec<usize>,
11    // Empty when the source is ASCII, where bytes and units coincide
12    units: Vec<u32>,
13    bytes: Vec<u32>,
14}
15
16// Marks the second unit of a surrogate pair, which no byte starts
17const INSIDE: u32 = u32::MAX;
18
19impl<'a> Location<'a> {
20    pub fn new(source: &'a str) -> Self {
21        let bytes = source.as_bytes();
22        let mut starts = vec![0];
23        for (index, byte) in bytes.iter().enumerate() {
24            match byte {
25                b'\n' => starts.push(index + 1),
26                b'\r' if bytes.get(index + 1) != Some(&b'\n') => starts.push(index + 1),
27                _ => {}
28            }
29        }
30        let mut location = Self {
31            source,
32            starts,
33            units: Vec::new(),
34            bytes: Vec::new(),
35        };
36        if !source.is_ascii() {
37            let mut units = Vec::with_capacity(source.len() + 1);
38            let mut offsets = Vec::with_capacity(source.len() + 1);
39            let mut unit = 0;
40            for (index, character) in source.char_indices() {
41                units.resize(index + character.len_utf8(), unit);
42                offsets.push(index as u32);
43                if character.len_utf16() == 2 {
44                    offsets.push(INSIDE);
45                }
46                unit += character.len_utf16() as u32;
47            }
48            units.push(unit);
49            offsets.push(source.len() as u32);
50            location.units = units;
51            location.bytes = offsets;
52        }
53        location
54    }
55
56    pub fn source(&self) -> &'a str {
57        self.source
58    }
59
60    /// The source's length in UTF-16 code units.
61    pub fn len(&self) -> usize {
62        self.to_unit(self.source.len())
63    }
64
65    pub fn is_empty(&self) -> bool {
66        self.source.is_empty()
67    }
68
69    /// The UTF-16 offset of a byte offset, inside a character counting as its start.
70    pub fn to_unit(&self, byte: usize) -> usize {
71        let byte = byte.min(self.source.len());
72        match self.units.get(byte) {
73            Some(unit) => *unit as usize,
74            None => byte,
75        }
76    }
77
78    /// The byte offset of a UTF-16 offset, unless it is past the end or inside a pair.
79    pub fn to_byte(&self, unit: usize) -> Option<usize> {
80        if self.units.is_empty() {
81            return (unit <= self.source.len()).then_some(unit);
82        }
83        match self.bytes.get(unit) {
84            Some(&INSIDE) | None => None,
85            Some(byte) => Some(*byte as usize),
86        }
87    }
88
89    /// The unist point of a byte point, as `Lines` gives it, with its UTF-16 offset.
90    pub fn to_unist_point(&self, point: Point) -> UnistPoint {
91        let offset = point.offset().min(self.source.len());
92        let line = self.line(offset);
93        let extra = usize::from(point.virtual_spaces() > 0) + (point.offset() - offset);
94        let unit = self.to_unit(offset);
95        UnistPoint {
96            line,
97            column: unit - self.to_unit(self.starts[line - 1]) + 1 + extra,
98            offset: Some(unit + extra),
99        }
100    }
101
102    /// vfile-location's `toPoint`: the point at a UTF-16 offset, unless past the end.
103    pub fn to_point(&self, unit: usize) -> Option<UnistPoint> {
104        if unit > self.len() {
105            return None;
106        }
107        let byte = match self.to_byte(unit) {
108            Some(byte) => byte,
109            None => self.to_byte(unit.checked_sub(1)?)?,
110        };
111        let line = self.line(byte);
112        Some(UnistPoint {
113            line,
114            column: unit - self.to_unit(self.starts[line - 1]) + 1,
115            offset: Some(unit),
116        })
117    }
118
119    /// vfile-location's `toOffset`: the UTF-16 offset of a line and column, unless the
120    /// column is past its line.
121    pub fn to_offset(&self, line: usize, column: usize) -> Option<usize> {
122        let start = self.to_unit(*self.starts.get(line.checked_sub(1)?)?);
123        let next = match self.starts.get(line) {
124            Some(next) => self.to_unit(*next),
125            None => self.len() + 1,
126        };
127        let offset = (start + column).checked_sub(1)?;
128        (offset < next).then_some(offset)
129    }
130
131    /// The position of a span of UTF-16 offsets: byte points when both ends start a
132    /// character, unist points otherwise, none when either is past the end.
133    pub fn to_position<'b>(
134        &self,
135        start: usize,
136        end: usize,
137        alloc: &'b Allocator,
138    ) -> Option<Position<'b>> {
139        match (self.to_byte(start), self.to_byte(end)) {
140            (Some(start), Some(end)) => {
141                Some(Position::span(Point::new(start, 0), Point::new(end, 0)))
142            }
143            _ => {
144                let start = self.to_point(start)?;
145                let end = self.to_point(end)?;
146                Some(Position::Unist(alloc.alloc(UnistPosition { start, end })))
147            }
148        }
149    }
150
151    /// Place nlcst nodes one after another from a UTF-16 offset, as mdast-util-to-nlcst's
152    /// and hast-util-to-nlcst's `patch` do: each spans its text's length, and a node
153    /// without a start or past the end has no position. The offset after them.
154    pub fn place_nlcst<'b>(
155        &self,
156        nodes: &mut [Node<'b>],
157        start: Option<usize>,
158        alloc: &'b Allocator,
159    ) -> Option<usize> {
160        let mut from = start;
161        for node in nodes {
162            let to = match node {
163                Node::Root(parent)
164                | Node::Paragraph(parent)
165                | Node::Sentence(parent)
166                | Node::Word(parent) => {
167                    // Bounded: a bridge places what `tokenize` made, words of literals
168                    let to = self.place_nlcst(&mut parent.children, from, alloc);
169                    parent.position = self.place(from, to, alloc);
170                    to
171                }
172                Node::Text(literal)
173                | Node::Symbol(literal)
174                | Node::Punctuation(literal)
175                | Node::WhiteSpace(literal)
176                | Node::Source(literal) => {
177                    let to = from.map(|from| from + count_units(literal.value));
178                    literal.position = self.place(from, to, alloc);
179                    to
180                }
181                Node::Unknown(unknown) => {
182                    unknown.position = None;
183                    from
184                }
185            };
186            from = to;
187        }
188        from
189    }
190
191    /// The position from one's start to another's end, as unist-util-position reads them.
192    pub fn join<'b>(
193        &self,
194        start: &Position<'b>,
195        end: &Position<'b>,
196        alloc: &'b Allocator,
197    ) -> Position<'b> {
198        match (start, end) {
199            (Position::Span { start, .. }, Position::Span { end, .. }) => {
200                Position::span(*start, *end)
201            }
202            _ => Position::Unist(alloc.alloc(UnistPosition {
203                start: self.to_unist_position(start).start,
204                end: self.to_unist_position(end).end,
205            })),
206        }
207    }
208
209    fn to_unist_position(&self, position: &Position<'_>) -> UnistPosition {
210        match position {
211            Position::Span { start, end } => UnistPosition {
212                start: self.to_unist_point(*start),
213                end: self.to_unist_point(*end),
214            },
215            Position::Unist(position) => (*position).clone(),
216        }
217    }
218
219    fn place<'b>(
220        &self,
221        from: Option<usize>,
222        to: Option<usize>,
223        alloc: &'b Allocator,
224    ) -> Option<Position<'b>> {
225        self.to_position(from?, to?, alloc)
226    }
227
228    // The line, from 1, a byte offset is on
229    fn line(&self, byte: usize) -> usize {
230        self.starts.partition_point(|start| *start <= byte)
231    }
232}
233
234// The length of a string in UTF-16 code units, as JavaScript counts it
235fn count_units(value: &str) -> usize {
236    if value.is_ascii() {
237        value.len()
238    } else {
239        value.chars().map(char::len_utf16).sum()
240    }
241}
242
243#[cfg(test)]
244#[path = "location_unit.rs"]
245mod unit;