1#[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 #[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 #[must_use]
25 pub const fn in_code_block(&self) -> bool {
26 self.in_code_block
27 }
28
29 #[must_use]
34 pub fn process_line(&self, line: &str) -> Self {
35 let trimmed = line.trim_start();
36
37 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
39 let Some(fence_char) = trimmed.chars().next() else {
41 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 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 return Self {
61 in_code_block: false,
62 code_fence_char: None,
63 code_fence_count: 0,
64 };
65 }
66 }
67 }
68
69 *self
71 }
72}
73
74#[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 #[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 #[must_use]
101 pub const fn in_any_code(&self) -> bool {
102 self.in_code_block || self.in_inline_code
103 }
104
105 #[must_use]
107 pub const fn in_code_block(&self) -> bool {
108 self.in_code_block
109 }
110
111 #[must_use]
113 pub const fn in_inline_code(&self) -> bool {
114 self.in_inline_code
115 }
116
117 #[must_use]
121 pub fn process_backticks<I>(&self, chars: &mut I) -> (Self, usize)
122 where
123 I: Iterator<Item = char> + Clone,
124 {
125 let mut tick_count = 1; let mut temp_chars = chars.clone();
127
128 while temp_chars.next() == Some('`') {
130 tick_count += 1;
131 }
132
133 for _ in 1..tick_count {
135 chars.next();
136 }
137
138 fn inner(this: &InlineTracker, count: usize) -> (InlineTracker, usize) {
139 if count >= 3 {
140 if !this.in_code_block {
142 (
144 InlineTracker {
145 in_code_block: true,
146 in_inline_code: false, fence_char: Some('`'),
148 fence_count: count,
149 },
150 count,
151 )
152 } else if this.fence_char == Some('`') && count >= this.fence_count {
153 (
155 InlineTracker {
156 in_code_block: false,
157 in_inline_code: false,
158 fence_char: None,
159 fence_count: 0,
160 },
161 count,
162 )
163 } else {
164 (*this, count)
166 }
167 } else if count == 1 && !this.in_code_block {
168 (
170 InlineTracker {
171 in_inline_code: !this.in_inline_code,
172 ..*this
173 },
174 count,
175 )
176 } else {
177 (*this, count)
179 }
180 }
181
182 inner(self, tick_count)
183 }
184
185 #[must_use]
189 pub fn process_tildes<I>(&self, chars: &mut I) -> (Self, usize)
190 where
191 I: Iterator<Item = char> + Clone,
192 {
193 let mut tilde_count = 1; let mut temp_chars = chars.clone();
195
196 while temp_chars.next() == Some('~') {
198 tilde_count += 1;
199 }
200
201 for _ in 1..tilde_count {
203 chars.next();
204 }
205
206 fn inner(this: &InlineTracker, count: usize) -> (InlineTracker, usize) {
207 if count >= 3 {
208 if !this.in_code_block {
209 (
211 InlineTracker {
212 in_code_block: true,
213 in_inline_code: false, fence_char: Some('~'),
215 fence_count: count,
216 },
217 count,
218 )
219 } else if this.fence_char == Some('~') && count >= this.fence_count {
220 (
222 InlineTracker {
223 in_code_block: false,
224 in_inline_code: false,
225 fence_char: None,
226 fence_count: 0,
227 },
228 count,
229 )
230 } else {
231 (*this, count)
233 }
234 } else {
235 (*this, count)
237 }
238 }
239
240 inner(self, tilde_count)
241 }
242
243 #[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 let tracker = tracker.process_line("```rust");
266 assert!(tracker.in_code_block());
267
268 let tracker = tracker.process_line("fn main() {}");
270 assert!(tracker.in_code_block());
271
272 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 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 let tracker = tracker.process_line("```");
298 assert!(tracker.in_code_block());
299
300 let tracker = tracker.process_line("~~~");
302 assert!(tracker.in_code_block());
303
304 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 let tracker = tracker.process_line("````");
315 assert!(tracker.in_code_block());
316
317 let tracker = tracker.process_line("```");
319 assert!(tracker.in_code_block());
320
321 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 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 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 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 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 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 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 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 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 let mut chars = "rest".chars();
406 let (tracker, _) = tracker.process_backticks(&mut chars);
407 assert!(tracker.in_inline_code());
408
409 let tracker = tracker.process_newline();
411 assert!(!tracker.in_inline_code());
412 }
413}