1use crate::parser::Region;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub struct ByteSpan {
13 pub start: usize,
14 pub end: usize,
15}
16
17impl ByteSpan {
18 pub const fn new(start: usize, end: usize) -> Self {
19 Self { start, end }
20 }
21
22 pub const fn is_empty(self) -> bool {
23 self.start == self.end
24 }
25
26 pub fn slice(self, input: &str) -> Option<&str> {
28 input.get(self.start..self.end)
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum RegionOrigin {
35 Whole(ByteSpan),
37 Code {
39 header: ByteSpan,
40 body: ByteSpan,
41 footer: ByteSpan,
42 },
43}
44
45impl RegionOrigin {
46 pub fn whole(self) -> ByteSpan {
47 match self {
48 RegionOrigin::Whole(s) => s,
49 RegionOrigin::Code { header, footer, .. } => {
50 ByteSpan::new(header.start, footer.end.max(header.end))
51 }
52 }
53 }
54
55 pub fn code(self) -> Option<CodeSpans> {
56 match self {
57 RegionOrigin::Code {
58 header,
59 body,
60 footer,
61 } => Some(CodeSpans {
62 header,
63 body,
64 footer,
65 }),
66 RegionOrigin::Whole(_) => None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct CodeSpans {
74 pub header: ByteSpan,
75 pub body: ByteSpan,
76 pub footer: ByteSpan,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SpannedRegion {
85 pub region: Region,
86 pub origin: Option<RegionOrigin>,
87}
88
89impl SpannedRegion {
90 pub fn unspanned(region: Region) -> Self {
91 Self {
92 region,
93 origin: None,
94 }
95 }
96
97 pub fn prose(text: String, span: ByteSpan) -> Self {
98 Self {
99 region: Region::Prose(text),
100 origin: Some(RegionOrigin::Whole(span)),
101 }
102 }
103
104 pub fn structure(input: &str, span: ByteSpan) -> Self {
105 Self {
106 region: Region::Structure(input[span.start..span.end].to_string()),
107 origin: Some(RegionOrigin::Whole(span)),
108 }
109 }
110
111 pub fn blank(input: &str, span: ByteSpan) -> Self {
112 Self {
113 region: Region::BlankLines(input[span.start..span.end].to_string()),
114 origin: Some(RegionOrigin::Whole(span)),
115 }
116 }
117
118 pub fn code(
119 input: &str,
120 lang: Option<String>,
121 header: ByteSpan,
122 body: ByteSpan,
123 footer: ByteSpan,
124 ) -> Self {
125 Self {
126 region: Region::Code {
127 lang,
128 header: input[header.start..header.end].to_string(),
129 body: input[body.start..body.end].to_string(),
130 footer: input[footer.start..footer.end].to_string(),
131 },
132 origin: Some(RegionOrigin::Code {
133 header,
134 body,
135 footer,
136 }),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct Line<'a> {
144 pub start: usize,
146 pub end: usize,
148 pub text: &'a str,
150}
151
152impl Line<'_> {
153 pub fn span(self) -> ByteSpan {
154 ByteSpan::new(self.start, self.end)
155 }
156
157 pub fn content_span(self) -> ByteSpan {
159 ByteSpan::new(self.start, self.start + self.text.len())
160 }
161
162 pub fn terminator_span(self) -> ByteSpan {
164 ByteSpan::new(self.start + self.text.len(), self.end)
165 }
166}
167
168pub fn iter_lines(input: &str) -> Vec<Line<'_>> {
173 let mut lines = Vec::new();
174 let bytes = input.as_bytes();
175 let mut start = 0;
176 let mut i = 0;
177 while i < bytes.len() {
178 if bytes[i] == b'\n' {
179 let content_end = if i > start && bytes[i - 1] == b'\r' {
180 i - 1
181 } else {
182 i
183 };
184 lines.push(Line {
185 start,
186 end: i + 1,
187 text: &input[start..content_end],
188 });
189 start = i + 1;
190 }
191 i += 1;
192 }
193 if start < input.len() {
194 lines.push(Line {
195 start,
196 end: input.len(),
197 text: &input[start..],
198 });
199 }
200 lines
201}
202
203pub fn push_prose_line(
209 prose: &mut String,
210 prose_span: &mut Option<ByteSpan>,
211 line: &Line<'_>,
212 join_space: bool,
213 include_terminator: bool,
214) {
215 if !prose.is_empty() && join_space {
216 prose.push(' ');
217 }
218 prose.push_str(line.text.trim());
219 let end = if include_terminator {
220 line.end
221 } else {
222 line.start + line.text.len()
223 };
224 match prose_span {
225 None => *prose_span = Some(ByteSpan::new(line.start, end)),
226 Some(s) => s.end = end,
227 }
228}
229
230pub fn flush_prose_spanned(
232 prose: &mut String,
233 prose_span: &mut Option<ByteSpan>,
234 regions: &mut Vec<SpannedRegion>,
235) {
236 if prose.is_empty() {
237 return;
238 }
239 let span = prose_span.take().unwrap_or(ByteSpan::new(0, 0));
240 regions.push(SpannedRegion::prose(std::mem::take(prose), span));
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn iter_lines_matches_str_lines_and_keeps_newlines() {
249 let input = "a\nb\n\nc";
250 let lines = iter_lines(input);
251 let plain: Vec<&str> = input.lines().collect();
252 assert_eq!(lines.iter().map(|l| l.text).collect::<Vec<_>>(), plain);
253 assert_eq!(lines[0].span().slice(input), Some("a\n"));
254 assert_eq!(lines[1].span().slice(input), Some("b\n"));
255 assert_eq!(lines[2].span().slice(input), Some("\n"));
256 assert_eq!(lines[3].span().slice(input), Some("c"));
257 assert!(lines[3].terminator_span().is_empty());
258 }
259
260 #[test]
261 fn iter_lines_empty() {
262 assert!(iter_lines("").is_empty());
263 }
264
265 #[test]
266 fn iter_lines_crlf() {
267 let input = "a\r\nb\r\n";
268 let lines = iter_lines(input);
269 assert_eq!(lines.len(), 2);
270 assert_eq!(lines[0].text, "a");
271 assert_eq!(lines[0].span().slice(input), Some("a\r\n"));
272 assert_eq!(lines[1].text, "b");
273 }
274}