supercode_frontend_tui/foundation/
table_detect.rs1pub fn parse_table_segments(line: &str) -> Option<Vec<&str>> {
45 let trimmed = line.trim();
46 if trimmed.is_empty() {
47 return None;
48 }
49
50 let has_outer_pipe = trimmed.starts_with('|') || trimmed.ends_with('|');
51 let content = trimmed.strip_prefix('|').unwrap_or(trimmed);
52 let content = content.strip_suffix('|').unwrap_or(content);
53 let raw_segments = split_unescaped_pipe(content);
54 if !has_outer_pipe && raw_segments.len() <= 1 {
55 return None;
56 }
57
58 let segments: Vec<&str> = raw_segments.into_iter().map(str::trim).collect();
59 (!segments.is_empty()).then_some(segments)
60}
61
62fn split_unescaped_pipe(content: &str) -> Vec<&str> {
68 let mut segments = Vec::with_capacity(8);
69 let mut start = 0;
70 let bytes = content.as_bytes();
71 let mut i = 0;
72 while i < bytes.len() {
73 if bytes[i] == b'\\' {
74 i += 2;
76 } else if bytes[i] == b'|' {
77 segments.push(&content[start..i]);
78 start = i + 1;
79 i += 1;
80 } else {
81 i += 1;
82 }
83 }
84 segments.push(&content[start..]);
85 segments
86}
87
88#[inline]
94pub fn is_table_header_line(line: &str) -> bool {
95 parse_table_segments(line).is_some_and(|segments| segments.iter().any(|s| !s.is_empty()))
96}
97
98#[inline]
101fn is_table_delimiter_segment(segment: &str) -> bool {
102 let trimmed = segment.trim();
103 if trimmed.is_empty() {
104 return false;
105 }
106 let without_leading = trimmed.strip_prefix(':').unwrap_or(trimmed);
107 let without_ends = without_leading.strip_suffix(':').unwrap_or(without_leading);
108 without_ends.len() >= 3 && without_ends.chars().all(|c| c == '-')
109}
110
111#[inline]
114pub fn is_table_delimiter_line(line: &str) -> bool {
115 parse_table_segments(line)
116 .is_some_and(|segments| segments.into_iter().all(is_table_delimiter_segment))
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum FenceKind {
131 Outside,
133 Markdown,
135 Other,
137}
138
139pub struct FenceTracker {
150 state: Option<(char, usize, FenceKind)>,
151}
152
153impl Default for FenceTracker {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl FenceTracker {
160 #[inline]
161 pub fn new() -> Self {
162 Self { state: None }
163 }
164
165 pub fn advance(&mut self, raw_line: &str) {
170 let leading_spaces = raw_line
171 .as_bytes()
172 .iter()
173 .take_while(|byte| **byte == b' ')
174 .count();
175 if leading_spaces > 3 {
176 return;
177 }
178
179 let trimmed = &raw_line[leading_spaces..];
180 let fence_scan_text = strip_blockquote_prefix(trimmed);
181 if let Some((marker, len)) = parse_fence_marker(fence_scan_text) {
182 if let Some((open_char, open_len, _)) = self.state {
183 if marker == open_char
185 && len >= open_len
186 && fence_scan_text[len..].trim().is_empty()
187 {
188 self.state = None;
189 }
190 } else {
191 let kind = if is_markdown_fence_info(fence_scan_text, len) {
193 FenceKind::Markdown
194 } else {
195 FenceKind::Other
196 };
197 self.state = Some((marker, len, kind));
198 }
199 }
200 }
201
202 #[inline]
204 pub fn kind(&self) -> FenceKind {
205 self.state.map_or(FenceKind::Outside, |(_, _, k)| k)
206 }
207}
208
209#[inline]
215pub fn parse_fence_marker(line: &str) -> Option<(char, usize)> {
216 let first = line.as_bytes().first().copied()?;
217 if first != b'`' && first != b'~' {
218 return None;
219 }
220 let len = line.bytes().take_while(|&b| b == first).count();
221 if len < 3 {
222 return None;
223 }
224 Some((first as char, len))
225}
226
227#[inline]
231pub fn is_markdown_fence_info(trimmed_line: &str, marker_len: usize) -> bool {
232 let info = trimmed_line[marker_len..]
233 .split_whitespace()
234 .next()
235 .unwrap_or_default();
236 info.eq_ignore_ascii_case("md") || info.eq_ignore_ascii_case("markdown")
237}
238
239#[inline]
244pub fn strip_blockquote_prefix(line: &str) -> &str {
245 let mut rest = line.trim_start();
246 loop {
247 let Some(stripped) = rest.strip_prefix('>') else {
248 return rest;
249 };
250 rest = stripped.strip_prefix(' ').unwrap_or(stripped).trim_start();
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn parse_table_segments_basic() {
260 assert_eq!(
261 parse_table_segments("| A | B | C |"),
262 Some(vec!["A", "B", "C"])
263 );
264 }
265
266 #[test]
267 fn parse_table_segments_no_outer_pipes() {
268 assert_eq!(parse_table_segments("A | B | C"), Some(vec!["A", "B", "C"]));
269 }
270
271 #[test]
272 fn parse_table_segments_no_leading_pipe() {
273 assert_eq!(
274 parse_table_segments("A | B | C |"),
275 Some(vec!["A", "B", "C"])
276 );
277 }
278
279 #[test]
280 fn parse_table_segments_no_trailing_pipe() {
281 assert_eq!(
282 parse_table_segments("| A | B | C"),
283 Some(vec!["A", "B", "C"])
284 );
285 }
286
287 #[test]
288 fn parse_table_segments_single_segment_is_allowed() {
289 assert_eq!(parse_table_segments("| only |"), Some(vec!["only"]));
290 }
291
292 #[test]
293 fn parse_table_segments_without_pipe_returns_none() {
294 assert_eq!(parse_table_segments("just text"), None);
295 }
296
297 #[test]
298 fn parse_table_segments_empty_returns_none() {
299 assert_eq!(parse_table_segments(""), None);
300 assert_eq!(parse_table_segments(" "), None);
301 }
302
303 #[test]
304 fn parse_table_segments_escaped_pipe() {
305 assert_eq!(
307 parse_table_segments(r"| A \| B | C |"),
308 Some(vec![r"A \| B", "C"])
309 );
310 }
311
312 #[test]
313 fn is_table_delimiter_segment_valid() {
314 assert!(is_table_delimiter_segment("---"));
315 assert!(is_table_delimiter_segment(":---"));
316 assert!(is_table_delimiter_segment("---:"));
317 assert!(is_table_delimiter_segment(":---:"));
318 assert!(is_table_delimiter_segment(":-------:"));
319 }
320
321 #[test]
322 fn is_table_delimiter_segment_invalid() {
323 assert!(!is_table_delimiter_segment(""));
324 assert!(!is_table_delimiter_segment("--"));
325 assert!(!is_table_delimiter_segment("abc"));
326 assert!(!is_table_delimiter_segment(":--"));
327 }
328
329 #[test]
330 fn is_table_delimiter_line_valid() {
331 assert!(is_table_delimiter_line("| --- | --- |"));
332 assert!(is_table_delimiter_line("|:---:|---:|"));
333 assert!(is_table_delimiter_line("--- | --- | ---"));
334 }
335
336 #[test]
337 fn is_table_delimiter_line_invalid() {
338 assert!(!is_table_delimiter_line("| A | B |"));
339 assert!(!is_table_delimiter_line("| -- | -- |"));
340 }
341
342 #[test]
343 fn is_table_header_line_valid() {
344 assert!(is_table_header_line("| A | B |"));
345 assert!(is_table_header_line("Name | Value"));
346 }
347
348 #[test]
349 fn is_table_header_line_all_empty_segments() {
350 assert!(!is_table_header_line("| | |"));
351 }
352
353 #[test]
358 fn fence_tracker_outside_by_default() {
359 let tracker = FenceTracker::new();
360 assert_eq!(tracker.kind(), FenceKind::Outside);
361 }
362
363 #[test]
364 fn fence_tracker_opens_and_closes_backtick_fence() {
365 let mut tracker = FenceTracker::new();
366 tracker.advance("```rust");
367 assert_eq!(tracker.kind(), FenceKind::Other);
368
369 tracker.advance("let x = 1;");
370 assert_eq!(tracker.kind(), FenceKind::Other);
371
372 tracker.advance("```");
373 assert_eq!(tracker.kind(), FenceKind::Outside);
374 }
375
376 #[test]
377 fn fence_tracker_opens_and_closes_tilde_fence() {
378 let mut tracker = FenceTracker::new();
379 tracker.advance("~~~python");
380 assert_eq!(tracker.kind(), FenceKind::Other);
381 tracker.advance("~~~");
382 assert_eq!(tracker.kind(), FenceKind::Outside);
383 }
384
385 #[test]
386 fn fence_tracker_markdown_fence() {
387 let mut tracker = FenceTracker::new();
388 tracker.advance("```md");
389 assert_eq!(tracker.kind(), FenceKind::Markdown);
390 tracker.advance("| A | B |");
391 assert_eq!(tracker.kind(), FenceKind::Markdown);
392 tracker.advance("```");
393 assert_eq!(tracker.kind(), FenceKind::Outside);
394 }
395
396 #[test]
397 fn fence_tracker_markdown_case_insensitive() {
398 let mut tracker = FenceTracker::new();
399 tracker.advance("```Markdown");
400 assert_eq!(tracker.kind(), FenceKind::Markdown);
401 tracker.advance("```");
402 assert_eq!(tracker.kind(), FenceKind::Outside);
403 }
404
405 #[test]
406 fn fence_tracker_nested_shorter_marker_does_not_close() {
407 let mut tracker = FenceTracker::new();
408 tracker.advance("````sh");
409 assert_eq!(tracker.kind(), FenceKind::Other);
410 tracker.advance("```");
412 assert_eq!(tracker.kind(), FenceKind::Other);
413 tracker.advance("````");
415 assert_eq!(tracker.kind(), FenceKind::Outside);
416 }
417
418 #[test]
419 fn fence_tracker_mismatched_char_does_not_close() {
420 let mut tracker = FenceTracker::new();
421 tracker.advance("```sh");
422 assert_eq!(tracker.kind(), FenceKind::Other);
423 tracker.advance("~~~");
425 assert_eq!(tracker.kind(), FenceKind::Other);
426 tracker.advance("```");
427 assert_eq!(tracker.kind(), FenceKind::Outside);
428 }
429
430 #[test]
431 fn fence_tracker_indented_4_spaces_ignored() {
432 let mut tracker = FenceTracker::new();
433 tracker.advance(" ```sh");
434 assert_eq!(tracker.kind(), FenceKind::Outside);
435 }
436
437 #[test]
438 fn fence_tracker_blockquote_prefix_stripped() {
439 let mut tracker = FenceTracker::new();
440 tracker.advance("> ```sh");
441 assert_eq!(tracker.kind(), FenceKind::Other);
442 tracker.advance("> ```");
443 assert_eq!(tracker.kind(), FenceKind::Outside);
444 }
445
446 #[test]
447 fn fence_tracker_close_with_trailing_content_does_not_close() {
448 let mut tracker = FenceTracker::new();
449 tracker.advance("```sh");
450 assert_eq!(tracker.kind(), FenceKind::Other);
451 tracker.advance("``` extra");
453 assert_eq!(tracker.kind(), FenceKind::Other);
454 tracker.advance("```");
455 assert_eq!(tracker.kind(), FenceKind::Outside);
456 }
457
458 #[test]
463 fn parse_fence_marker_backtick() {
464 assert_eq!(parse_fence_marker("```rust"), Some(('`', 3)));
465 assert_eq!(parse_fence_marker("````"), Some(('`', 4)));
466 }
467
468 #[test]
469 fn parse_fence_marker_tilde() {
470 assert_eq!(parse_fence_marker("~~~python"), Some(('~', 3)));
471 }
472
473 #[test]
474 fn parse_fence_marker_too_short() {
475 assert_eq!(parse_fence_marker("``"), None);
476 assert_eq!(parse_fence_marker("~~"), None);
477 }
478
479 #[test]
480 fn parse_fence_marker_not_fence() {
481 assert_eq!(parse_fence_marker("hello"), None);
482 assert_eq!(parse_fence_marker(""), None);
483 }
484
485 #[test]
486 fn is_markdown_fence_info_basic() {
487 assert!(is_markdown_fence_info("```md", 3));
488 assert!(is_markdown_fence_info("```markdown", 3));
489 assert!(is_markdown_fence_info("```MD", 3));
490 assert!(!is_markdown_fence_info("```rust", 3));
491 assert!(!is_markdown_fence_info("```", 3));
492 }
493
494 #[test]
495 fn strip_blockquote_prefix_basic() {
496 assert_eq!(strip_blockquote_prefix("> hello"), "hello");
497 assert_eq!(strip_blockquote_prefix("> > nested"), "nested");
498 assert_eq!(strip_blockquote_prefix("no prefix"), "no prefix");
499 }
500}