1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Fence {
8 pub marker: char,
9 pub length: usize,
10 pub indentation: usize,
11}
12
13#[derive(Debug, Default)]
14pub struct FenceTracker {
15 open: Option<Fence>,
16}
17
18impl FenceTracker {
19 pub fn visit(&mut self, line: &str) -> bool {
21 if let Some(open) = self.open() {
22 if is_closing_fence(line, open) {
23 self.open = None;
24 }
25 return true;
26 }
27
28 if let Some(open) = opening_fence(line) {
29 self.open = Some(open);
30 return true;
31 }
32
33 false
34 }
35
36 pub fn open(&self) -> Option<Fence> {
37 self.open
38 }
39}
40
41fn opening_fence(line: &str) -> Option<Fence> {
42 let (indentation, content) = after_fence_indentation(line)?;
43 let marker = match content.as_bytes().first()? {
44 b'`' => b'`',
45 b'~' => b'~',
46 _ => return None,
47 };
48 let length = content
49 .as_bytes()
50 .iter()
51 .take_while(|candidate| **candidate == marker)
52 .count();
53 if length < 3 || (marker == b'`' && content[length..].contains('`')) {
54 return None;
55 }
56
57 Some(Fence {
58 marker: char::from(marker),
59 length,
60 indentation,
61 })
62}
63
64fn is_closing_fence(line: &str, open: Fence) -> bool {
65 let Some((_, content)) = after_fence_indentation(line) else {
66 return false;
67 };
68 let marker = open.marker as u8;
69 let length = content
70 .as_bytes()
71 .iter()
72 .take_while(|candidate| **candidate == marker)
73 .count();
74 length >= open.length
75 && content.as_bytes().first() == Some(&marker)
76 && without_line_ending(&content[length..])
77 .bytes()
78 .all(|byte| matches!(byte, b' ' | b'\t'))
79}
80
81fn after_fence_indentation(line: &str) -> Option<(usize, &str)> {
82 let indentation = line
83 .as_bytes()
84 .iter()
85 .take_while(|byte| **byte == b' ')
86 .count();
87 (indentation <= 3).then_some((indentation, &line[indentation..]))
88}
89
90fn without_line_ending(line: &str) -> &str {
91 let line = line.strip_suffix('\n').unwrap_or(line);
92 line.strip_suffix('\r').unwrap_or(line)
93}