Skip to main content

ndg_commonmark/utils/
codeblock.rs

1/// State tracking for code fence detection in markdown.
2///
3/// This tracks whether we're currently inside a fenced code block  and
4/// maintains the fence character and count for proper closing detection.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub struct FenceTracker {
7  in_code_block:    bool,
8  code_fence_char:  Option<char>,
9  code_fence_count: usize,
10}
11
12impl FenceTracker {
13  /// Create a new fence tracker.
14  #[must_use]
15  pub const fn new() -> Self {
16    Self {
17      in_code_block:    false,
18      code_fence_char:  None,
19      code_fence_count: 0,
20    }
21  }
22
23  /// Check if currently inside a code block.
24  #[must_use]
25  pub const fn in_code_block(&self) -> bool {
26    self.in_code_block
27  }
28
29  /// Process a line and update fence state.
30  ///
31  /// Returns the updated state after processing the line.
32  /// Call this for each line to maintain accurate fence tracking.
33  #[must_use]
34  pub fn process_line(&self, line: &str) -> Self {
35    let trimmed = line.trim_start();
36
37    // Check for code fences (``` or ~~~)
38    if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
39      // Get the first character to determine fence type
40      let Some(fence_char) = trimmed.chars().next() else {
41        // Empty string after trim - no state change
42        return *self;
43      };
44
45      let fence_count =
46        trimmed.chars().take_while(|&c| c == fence_char).count();
47
48      if fence_count >= 3 {
49        if !self.in_code_block {
50          // Starting a code block
51          return Self {
52            in_code_block:    true,
53            code_fence_char:  Some(fence_char),
54            code_fence_count: fence_count,
55          };
56        } else if self.code_fence_char == Some(fence_char)
57          && fence_count >= self.code_fence_count
58        {
59          // Ending a code block
60          return Self {
61            in_code_block:    false,
62            code_fence_char:  None,
63            code_fence_count: 0,
64          };
65        }
66      }
67    }
68
69    // No state change
70    *self
71  }
72}
73
74/// State tracking for code fences AND inline code in markdown.
75///
76/// This extends `FenceTracker` to also track inline code spans (`code`).
77/// This is needed for character-level processing where inline code must be
78/// skipped along with fenced code blocks.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub struct InlineTracker {
81  in_code_block:  bool,
82  in_inline_code: bool,
83  fence_char:     Option<char>,
84  fence_count:    usize,
85}
86
87impl InlineTracker {
88  /// Create a new inline code tracker.
89  #[must_use]
90  pub const fn new() -> Self {
91    Self {
92      in_code_block:  false,
93      in_inline_code: false,
94      fence_char:     None,
95      fence_count:    0,
96    }
97  }
98
99  /// Check if currently inside any kind of code (block or inline).
100  #[must_use]
101  pub const fn in_any_code(&self) -> bool {
102    self.in_code_block || self.in_inline_code
103  }
104
105  /// Check if currently inside a code block.
106  #[must_use]
107  pub const fn in_code_block(&self) -> bool {
108    self.in_code_block
109  }
110
111  /// Check if currently inside inline code.
112  #[must_use]
113  pub const fn in_inline_code(&self) -> bool {
114    self.in_inline_code
115  }
116
117  /// Process backticks and update state.
118  ///
119  /// Returns (`new_state`, `number_of_backticks_consumed`).
120  #[must_use]
121  pub fn process_backticks<I>(&self, chars: &mut I) -> (Self, usize)
122  where
123    I: Iterator<Item = char> + Clone,
124  {
125    fn inner(this: &InlineTracker, count: usize) -> (InlineTracker, usize) {
126      if count >= 3 {
127        // This is a code fence
128        if !this.in_code_block {
129          // Starting a code block
130          (
131            InlineTracker {
132              in_code_block:  true,
133              in_inline_code: false, // clear inline code when entering block
134              fence_char:     Some('`'),
135              fence_count:    count,
136            },
137            count,
138          )
139        } else if this.fence_char == Some('`') && count >= this.fence_count {
140          // Ending a code block
141          (
142            InlineTracker {
143              in_code_block:  false,
144              in_inline_code: false,
145              fence_char:     None,
146              fence_count:    0,
147            },
148            count,
149          )
150        } else {
151          // Inside a different fence type, no state change
152          (*this, count)
153        }
154      } else if count == 1 && !this.in_code_block {
155        // Single backtick - inline code toggle
156        (
157          InlineTracker {
158            in_inline_code: !this.in_inline_code,
159            ..*this
160          },
161          count,
162        )
163      } else {
164        // Multiple backticks but less than 3, or inside code block
165        (*this, count)
166      }
167    }
168
169    let mut tick_count = 1; // we've already seen the first backtick
170    let mut temp_chars = chars.clone();
171
172    // Count consecutive backticks
173    while temp_chars.next() == Some('`') {
174      tick_count += 1;
175    }
176
177    // Actually consume the backticks from the iterator
178    for _ in 1..tick_count {
179      chars.next();
180    }
181
182    inner(self, tick_count)
183  }
184
185  /// Process tildes and update state.
186  ///
187  /// Returns (`new_state`, `number_of_tildes_consumed`).
188  #[must_use]
189  pub fn process_tildes<I>(&self, chars: &mut I) -> (Self, usize)
190  where
191    I: Iterator<Item = char> + Clone,
192  {
193    fn inner(this: &InlineTracker, count: usize) -> (InlineTracker, usize) {
194      if count >= 3 {
195        if !this.in_code_block {
196          // Starting a tilde code block
197          (
198            InlineTracker {
199              in_code_block:  true,
200              in_inline_code: false, // clear inline code when entering block
201              fence_char:     Some('~'),
202              fence_count:    count,
203            },
204            count,
205          )
206        } else if this.fence_char == Some('~') && count >= this.fence_count {
207          // Ending a tilde code block
208          (
209            InlineTracker {
210              in_code_block:  false,
211              in_inline_code: false,
212              fence_char:     None,
213              fence_count:    0,
214            },
215            count,
216          )
217        } else {
218          // Inside a different fence type, no state change
219          (*this, count)
220        }
221      } else {
222        // Less than 3 tildes, no state change
223        (*this, count)
224      }
225    }
226
227    let mut tilde_count = 1; // we've already seen the first tilde
228    let mut temp_chars = chars.clone();
229
230    // Count consecutive tildes
231    while temp_chars.next() == Some('~') {
232      tilde_count += 1;
233    }
234
235    // Actually consume the tildes from the iterator
236    for _ in 1..tilde_count {
237      chars.next();
238    }
239
240    inner(self, tilde_count)
241  }
242
243  /// Process a newline and update state.
244  ///
245  /// Newlines end inline code if not properly closed.
246  #[must_use]
247  pub const fn process_newline(&self) -> Self {
248    Self {
249      in_inline_code: false,
250      ..*self
251    }
252  }
253}
254
255#[cfg(test)]
256mod tests {
257  use super::*;
258
259  #[test]
260  fn test_fence_tracker_basic() {
261    let tracker = FenceTracker::new();
262    assert!(!tracker.in_code_block());
263
264    // Opening fence
265    let tracker = tracker.process_line("```rust");
266    assert!(tracker.in_code_block());
267
268    // Inside code block
269    let tracker = tracker.process_line("fn main() {}");
270    assert!(tracker.in_code_block());
271
272    // Closing fence
273    let tracker = tracker.process_line("```");
274    assert!(!tracker.in_code_block());
275  }
276
277  #[test]
278  fn test_fence_tracker_tilde() {
279    let tracker = FenceTracker::new();
280
281    // Tilde fence
282    let tracker = tracker.process_line("~~~");
283    assert!(tracker.in_code_block());
284
285    let tracker = tracker.process_line("code");
286    assert!(tracker.in_code_block());
287
288    let tracker = tracker.process_line("~~~");
289    assert!(!tracker.in_code_block());
290  }
291
292  #[test]
293  fn test_fence_tracker_mismatched() {
294    let tracker = FenceTracker::new();
295
296    // Backtick fence
297    let tracker = tracker.process_line("```");
298    assert!(tracker.in_code_block());
299
300    // Tilde doesn't close backtick fence
301    let tracker = tracker.process_line("~~~");
302    assert!(tracker.in_code_block());
303
304    // Backtick closes
305    let tracker = tracker.process_line("```");
306    assert!(!tracker.in_code_block());
307  }
308
309  #[test]
310  fn test_fence_tracker_count() {
311    let tracker = FenceTracker::new();
312
313    // 4 backticks
314    let tracker = tracker.process_line("````");
315    assert!(tracker.in_code_block());
316
317    // 3 backticks don't close 4-backtick fence
318    let tracker = tracker.process_line("```");
319    assert!(tracker.in_code_block());
320
321    // 4+ backticks close
322    let tracker = tracker.process_line("````");
323    assert!(!tracker.in_code_block());
324  }
325
326  #[test]
327  fn test_fence_tracker_indented() {
328    let tracker = FenceTracker::new();
329
330    // Indented fence (trim_start handles this)
331    let tracker = tracker.process_line("    ```");
332    assert!(tracker.in_code_block());
333
334    let tracker = tracker.process_line("    ```");
335    assert!(!tracker.in_code_block());
336  }
337
338  #[test]
339  fn test_inline_code_tracker_basic() {
340    let tracker = InlineTracker::new();
341    assert!(!tracker.in_any_code());
342
343    // Single backtick - start inline code
344    let mut chars = "rest".chars();
345    let (tracker, count) = tracker.process_backticks(&mut chars);
346    assert_eq!(count, 1);
347    assert!(tracker.in_inline_code());
348    assert!(tracker.in_any_code());
349
350    // Another single backtick - end inline code
351    let mut chars = "rest".chars();
352    let (tracker, count) = tracker.process_backticks(&mut chars);
353    assert_eq!(count, 1);
354    assert!(!tracker.in_inline_code());
355    assert!(!tracker.in_any_code());
356  }
357
358  #[test]
359  fn test_inline_code_tracker_fence() {
360    let tracker = InlineTracker::new();
361
362    // Three backticks - code fence
363    let mut chars = "``rust".chars();
364    let (tracker, count) = tracker.process_backticks(&mut chars);
365    assert_eq!(count, 3);
366    assert!(tracker.in_code_block());
367    assert!(!tracker.in_inline_code());
368
369    // Single backtick inside fence - no inline code
370    let mut chars = "rest".chars();
371    let (tracker, _) = tracker.process_backticks(&mut chars);
372    assert!(tracker.in_code_block());
373    assert!(!tracker.in_inline_code());
374
375    // Three backticks - close fence
376    let mut chars = "``".chars();
377    let (tracker, count) = tracker.process_backticks(&mut chars);
378    assert_eq!(count, 3);
379    assert!(!tracker.in_code_block());
380    assert!(!tracker.in_inline_code());
381  }
382
383  #[test]
384  fn test_inline_code_tracker_tildes() {
385    let tracker = InlineTracker::new();
386
387    // Three tildes - code fence
388    let mut chars = "~~".chars();
389    let (tracker, count) = tracker.process_tildes(&mut chars);
390    assert_eq!(count, 3);
391    assert!(tracker.in_code_block());
392
393    // Close with tildes
394    let mut chars = "~~".chars();
395    let (tracker, count) = tracker.process_tildes(&mut chars);
396    assert_eq!(count, 3);
397    assert!(!tracker.in_code_block());
398  }
399
400  #[test]
401  fn test_inline_code_tracker_newline() {
402    let tracker = InlineTracker::new();
403
404    // Start inline code
405    let mut chars = "rest".chars();
406    let (tracker, _) = tracker.process_backticks(&mut chars);
407    assert!(tracker.in_inline_code());
408
409    // Newline ends inline code
410    let tracker = tracker.process_newline();
411    assert!(!tracker.in_inline_code());
412  }
413}