logicaffeine_language/lgx/raw.rs
1//! Offsets inside a captured section body, mapped back to the file.
2//!
3//! The lexer captures a `View:`, `Head:` or `Style:` body as one raw block: its text with each
4//! line's indentation stripped, where its first character sits in the file, and how much
5//! indentation it stripped. Every stage that reports a position inside such a body — the template
6//! parser, the sub-lexed expressions, the lint, the stylesheet scoper — speaks in offsets into the
7//! dedented text, and [`RawMap`] is the one place those become file offsets.
8
9use crate::token::Span;
10
11/// A dedented raw body and where it came from.
12#[derive(Debug, Clone)]
13pub struct RawMap {
14 pub text: String,
15 /// The indentation stripped from every non-empty line after the first.
16 pub indent: usize,
17 /// The file offset of the body's first character.
18 pub content_start: usize,
19}
20
21impl RawMap {
22 pub fn new(text: impl Into<String>, indent: usize, content_start: usize) -> Self {
23 RawMap {
24 text: text.into(),
25 indent,
26 content_start,
27 }
28 }
29
30 /// The file offset of body offset `off`. Each line after the first is preceded by its newline
31 /// plus the indentation the capture stripped; a line that is EMPTY in the body came from an
32 /// empty line, which had none.
33 pub fn offset(&self, off: usize) -> usize {
34 let mut body_line_start = 0usize;
35 let mut file_line_start = self.content_start;
36 for (i, line) in self.text.split('\n').enumerate() {
37 if i > 0 {
38 file_line_start += 1 + if line.is_empty() { 0 } else { self.indent };
39 }
40 if off <= body_line_start + line.len() {
41 return file_line_start + (off - body_line_start);
42 }
43 body_line_start += line.len() + 1;
44 file_line_start += line.len();
45 }
46 file_line_start
47 }
48
49 /// A body span as a file span.
50 pub fn span(&self, s: Span) -> Span {
51 Span::new(self.offset(s.start), self.offset(s.end))
52 }
53}