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 fn inner(this: &InlineTracker, count: usize) -> (InlineTracker, usize) {
126 if count >= 3 {
127 if !this.in_code_block {
129 (
131 InlineTracker {
132 in_code_block: true,
133 in_inline_code: false, fence_char: Some('`'),
135 fence_count: count,
136 },
137 count,
138 )
139 } else if this.fence_char == Some('`') && count >= this.fence_count {
140 (
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 (*this, count)
153 }
154 } else if count == 1 && !this.in_code_block {
155 (
157 InlineTracker {
158 in_inline_code: !this.in_inline_code,
159 ..*this
160 },
161 count,
162 )
163 } else {
164 (*this, count)
166 }
167 }
168
169 let mut tick_count = 1; let mut temp_chars = chars.clone();
171
172 while temp_chars.next() == Some('`') {
174 tick_count += 1;
175 }
176
177 for _ in 1..tick_count {
179 chars.next();
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 fn inner(this: &InlineTracker, count: usize) -> (InlineTracker, usize) {
194 if count >= 3 {
195 if !this.in_code_block {
196 (
198 InlineTracker {
199 in_code_block: true,
200 in_inline_code: false, fence_char: Some('~'),
202 fence_count: count,
203 },
204 count,
205 )
206 } else if this.fence_char == Some('~') && count >= this.fence_count {
207 (
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 (*this, count)
220 }
221 } else {
222 (*this, count)
224 }
225 }
226
227 let mut tilde_count = 1; let mut temp_chars = chars.clone();
229
230 while temp_chars.next() == Some('~') {
232 tilde_count += 1;
233 }
234
235 for _ in 1..tilde_count {
237 chars.next();
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}