1use crate::ast::{
10 Block, Blockquote, Fence, Heading, ListItem, Paragraph, Row, SegmentOffset, Table,
11 ThematicBreak,
12};
13use crate::offsets::{java_trim, utf16_index, utf16_len};
14use crate::span::Span;
15use crate::table_cells::parse_row_cells;
16use regex::Regex;
17use std::sync::LazyLock;
18
19struct RawLine {
21 text: String,
22 start_offset: usize,
23 end_offset: usize,
24 start_byte: usize,
25 end_byte: usize,
26}
27
28static THEMATIC_RE: LazyLock<Regex> = LazyLock::new(|| {
32 Regex::new(r"^\s*(?:-(?:\s*-){2,}|\*(?:\s*\*){2,}|_(?:\s*_){2,})\s*$").unwrap()
33});
34static UL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)([-*+])\s+(.*)$").unwrap());
35static OL_RE: LazyLock<Regex> =
36 LazyLock::new(|| Regex::new(r"^(\s*)([0-9]+)([.)])\s+(.*)$").unwrap());
37static BQ_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^>\s?(.*)$").unwrap());
38static FENCE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(`{3,})\s*(\S*)\s*$").unwrap());
39static ROW_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\|(.+)\|\s*$").unwrap());
40static DELIM_RE: LazyLock<Regex> =
41 LazyLock::new(|| Regex::new(r"^\|\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|\s*$").unwrap());
42static HEADING_RE: LazyLock<Regex> =
43 LazyLock::new(|| Regex::new(r"^(#{1,6})\s+(.*?)(?:\s+#+)?\s*$").unwrap());
44static HEADING_PREFIX_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^#{1,6}\s+").unwrap());
45
46pub fn scan(source: &str) -> Vec<Block> {
48 let lines = split_lines(source);
49 let mut blocks = Vec::new();
50
51 let mut i = 0;
52 while i < lines.len() {
53 if java_trim(&lines[i].text).is_empty() {
54 i += 1;
55 continue;
56 }
57 if let Some((fence, next)) = try_fence(source, &lines, i) {
58 blocks.push(Block::Fence(fence));
59 i = next;
60 continue;
61 }
62 if let Some((table, next)) = try_table(source, &lines, i) {
63 blocks.push(Block::Table(table));
64 i = next;
65 continue;
66 }
67 if let Some(tb) = try_thematic_break(source, &lines[i]) {
68 blocks.push(Block::ThematicBreak(tb));
69 i += 1;
70 continue;
71 }
72 if let Some((quote, next)) = try_blockquote(source, &lines, i) {
73 blocks.push(Block::Blockquote(quote));
74 i = next;
75 continue;
76 }
77 if let Some(heading) = try_heading(source, &lines[i]) {
78 blocks.push(Block::Heading(heading));
79 i += 1;
80 continue;
81 }
82 if let Some(item) = try_list_item(source, &lines[i]) {
83 blocks.push(Block::ListItem(item));
84 i += 1;
85 continue;
86 }
87 let (paragraph, next) = consume_paragraph(source, &lines, i);
88 blocks.push(Block::Paragraph(paragraph));
89 i = next;
90 }
91 blocks
92}
93
94fn split_lines(source: &str) -> Vec<RawLine> {
95 let mut out = Vec::new();
96 let mut byte_start = 0;
97 let mut u16_start = 0;
98 let mut u16 = 0;
99 for (byte_i, c) in source.char_indices() {
100 if c == '\n' {
101 out.push(RawLine {
102 text: source[byte_start..byte_i].to_string(),
103 start_offset: u16_start,
104 end_offset: u16,
105 start_byte: byte_start,
106 end_byte: byte_i,
107 });
108 byte_start = byte_i + 1;
109 u16_start = u16 + 1;
110 }
111 u16 += c.len_utf16();
112 }
113 out.push(RawLine {
114 text: source[byte_start..].to_string(),
115 start_offset: u16_start,
116 end_offset: u16,
117 start_byte: byte_start,
118 end_byte: source.len(),
119 });
120 out
121}
122
123fn try_thematic_break(source: &str, line: &RawLine) -> Option<ThematicBreak> {
124 if !THEMATIC_RE.is_match(&line.text) {
125 return None;
126 }
127 Some(ThematicBreak {
128 span: Span::from_offsets(source, line.start_offset, line.end_offset),
129 })
130}
131
132fn try_heading(source: &str, line: &RawLine) -> Option<Heading> {
133 let m = HEADING_RE.captures(&line.text)?;
134 let hashes = m.get(1).unwrap().as_str();
135 let text = java_trim(m.get(2).unwrap().as_str()).to_string();
136 Some(Heading {
137 level: hashes.len(),
138 text,
139 span: Span::from_offsets(source, line.start_offset, line.end_offset),
140 })
141}
142
143fn try_list_item(source: &str, line: &RawLine) -> Option<ListItem> {
144 if let Some(ul) = UL_RE.captures(&line.text) {
145 let text = ul.get(3).unwrap().as_str();
146 let marker_start = line.start_offset + utf16_len(ul.get(1).unwrap().as_str());
147 let marker_end = marker_start + utf16_len(ul.get(2).unwrap().as_str());
148 let text_start = line.start_offset + utf16_index(&line.text, line.text.find(text).unwrap());
149 return Some(ListItem {
150 text: text.to_string(),
151 span: Span::from_offsets(source, line.start_offset, line.end_offset),
152 segment_map: vec![SegmentOffset::new(0, text_start)],
153 ordered: false,
154 marker_span: Span::from_offsets(source, marker_start, marker_end),
155 });
156 }
157 if let Some(ol) = OL_RE.captures(&line.text) {
158 let text = ol.get(4).unwrap().as_str();
159 let marker_start = line.start_offset + utf16_len(ol.get(1).unwrap().as_str());
160 let marker_end = marker_start
161 + utf16_len(ol.get(2).unwrap().as_str())
162 + utf16_len(ol.get(3).unwrap().as_str());
163 let text_start = line.start_offset + utf16_index(&line.text, line.text.find(text).unwrap());
164 return Some(ListItem {
165 text: text.to_string(),
166 span: Span::from_offsets(source, line.start_offset, line.end_offset),
167 segment_map: vec![SegmentOffset::new(0, text_start)],
168 ordered: true,
169 marker_span: Span::from_offsets(source, marker_start, marker_end),
170 });
171 }
172 None
173}
174
175fn try_blockquote(
176 source: &str,
177 lines: &[RawLine],
178 start_idx: usize,
179) -> Option<(Blockquote, usize)> {
180 let first = &lines[start_idx];
181 let m = BQ_RE.captures(&first.text)?;
182 let first_segment = m.get(1).unwrap().as_str().to_string();
183
184 let mut segments = vec![first_segment.clone()];
185 let mut segment_map = vec![SegmentOffset::new(
186 0,
187 first.start_offset + utf16_index(&first.text, first.text.find(&first_segment).unwrap()),
188 )];
189 let mut joined_text_offset = utf16_len(&first_segment);
190
191 let mut i = start_idx + 1;
192 let mut end_offset = first.end_offset;
193 while i < lines.len() {
194 let ln = &lines[i];
195 let Some(next) = BQ_RE.captures(&ln.text) else {
196 break;
197 };
198 let segment = next.get(1).unwrap().as_str().to_string();
199 joined_text_offset += 1; segment_map.push(SegmentOffset::new(
201 joined_text_offset,
202 ln.start_offset + utf16_index(&ln.text, ln.text.find(&segment).unwrap()),
203 ));
204 joined_text_offset += utf16_len(&segment);
205 segments.push(segment);
206 end_offset = ln.end_offset;
207 i += 1;
208 }
209 let quote = Blockquote {
210 text: segments.join("\n"),
211 span: Span::from_offsets(source, first.start_offset, end_offset),
212 segment_map,
213 };
214 Some((quote, i))
215}
216
217fn consume_paragraph(source: &str, lines: &[RawLine], start_idx: usize) -> (Paragraph, usize) {
218 let first = &lines[start_idx];
219 let mut end_idx = start_idx;
220 while end_idx + 1 < lines.len() {
221 let candidate = &lines[end_idx + 1];
222 let t = &candidate.text;
223 if java_trim(t).is_empty()
224 || HEADING_PREFIX_RE.is_match(t)
225 || UL_RE.is_match(t)
226 || OL_RE.is_match(t)
227 || BQ_RE.is_match(t)
228 || FENCE_RE.is_match(t)
229 || ROW_RE.is_match(t)
230 || THEMATIC_RE.is_match(t)
231 {
232 break;
233 }
234 end_idx += 1;
235 }
236 let last = &lines[end_idx];
237 let paragraph = Paragraph {
238 text: source[first.start_byte..last.end_byte].to_string(),
239 span: Span::from_offsets(source, first.start_offset, last.end_offset),
240 segment_map: vec![SegmentOffset::new(0, first.start_offset)],
241 };
242 (paragraph, end_idx + 1)
243}
244
245fn try_fence(source: &str, lines: &[RawLine], start_idx: usize) -> Option<(Fence, usize)> {
246 let start = &lines[start_idx];
247 let open = FENCE_RE.captures(&start.text)?;
248 let fence_marker = open.get(1).unwrap().as_str().to_string();
249 let info = java_trim(open.get(2).unwrap().as_str()).to_string();
250
251 let mut i = start_idx + 1;
252 let mut body_start: Option<(usize, usize)> = None; let mut body_end: Option<(usize, usize)> = None;
254 let mut end_offset = start.end_offset;
255 while i < lines.len() {
256 let ln = &lines[i];
257 if let Some(close) = FENCE_RE.captures(&ln.text) {
258 if close.get(1).unwrap().as_str().len() >= fence_marker.len() {
259 end_offset = ln.end_offset;
260 break;
261 }
262 }
263 if body_start.is_none() {
264 body_start = Some((ln.start_offset, ln.start_byte));
265 }
266 body_end = Some((ln.end_offset + 1, ln.end_byte + 1));
268 i += 1;
269 }
270
271 let source_u16 = utf16_len(source);
272 let clamped_end_u16 = body_end.map_or(0, |(u16, _)| u16.min(source_u16));
273 let clamped_end_byte = body_end.map_or(0, |(_, byte)| byte.min(source.len()));
274 let body = match (body_start, body_end) {
275 (Some((_, sb)), Some(_)) => source[sb..clamped_end_byte].to_string(),
276 _ => String::new(),
277 };
278 let fallback = start.end_offset;
279 let body_span = Span::from_offsets(
280 source,
281 body_start.map_or(fallback, |(u16, _)| u16),
282 if body_end.is_some() {
283 clamped_end_u16
284 } else {
285 fallback
286 },
287 );
288 let fence = Fence {
289 span: Span::from_offsets(source, start.start_offset, end_offset),
290 info,
291 body,
292 body_span,
293 };
294 Some((fence, i + 1))
295}
296
297fn try_table(source: &str, lines: &[RawLine], start_idx: usize) -> Option<(Table, usize)> {
298 if start_idx + 1 >= lines.len() {
299 return None;
300 }
301 let header_line = &lines[start_idx];
302 let delim_line = &lines[start_idx + 1];
303 if !ROW_RE.is_match(&header_line.text) || !DELIM_RE.is_match(&delim_line.text) {
304 return None;
305 }
306
307 let header_parsed = parse_row_cells(&header_line.text, header_line.start_offset, source);
308 let header = Row {
309 cells: header_parsed.cells,
310 cell_spans: header_parsed.cell_spans,
311 span: Span::from_offsets(source, header_line.start_offset, header_line.end_offset),
312 };
313
314 let mut rows = Vec::new();
315 let mut i = start_idx + 2;
316 while i < lines.len() {
317 let ln = &lines[i];
318 if !ROW_RE.is_match(&ln.text) {
319 break;
320 }
321 let parsed = parse_row_cells(&ln.text, ln.start_offset, source);
322 rows.push(Row {
323 cells: parsed.cells,
324 cell_spans: parsed.cell_spans,
325 span: Span::from_offsets(source, ln.start_offset, ln.end_offset),
326 });
327 i += 1;
328 }
329 let end_offset = rows
330 .last()
331 .map_or(delim_line.end_offset, |r| r.span.end_offset);
332 let table = Table {
333 span: Span::from_offsets(source, header_line.start_offset, end_offset),
334 header,
335 rows,
336 };
337 Some((table, i))
338}