1use regex::Regex;
28use std::sync::LazyLock;
29
30#[inline]
37fn position_in_spans(position: usize, spans: &[(usize, usize)]) -> bool {
38 for &(start, end) in spans {
39 if position < start {
40 return false;
41 }
42 if position < end {
43 return true;
44 }
45 }
46 false
47}
48
49#[inline]
51fn find_regex_spans(line: &str, pattern: &Regex) -> Vec<(usize, usize)> {
52 pattern.find_iter(line).map(|m| (m.start(), m.end())).collect()
53}
54
55fn find_single_delim_spans(line: &str, delim: char, double_spans: &[(usize, usize)]) -> Vec<(usize, usize)> {
63 let mut spans = Vec::new();
64 let mut chars = line.char_indices().peekable();
65 let delim_len = delim.len_utf8();
66
67 while let Some((start_byte, ch)) = chars.next() {
68 if position_in_spans(start_byte, double_spans) {
70 continue;
71 }
72
73 if ch != delim {
74 continue;
75 }
76
77 if chars.peek().is_some_and(|(_, c)| *c == delim) {
79 chars.next();
80 continue;
81 }
82
83 let mut found_content = false;
85 let mut has_whitespace = false;
86
87 for (byte_pos, inner_ch) in chars.by_ref() {
88 if position_in_spans(byte_pos, double_spans) {
90 break;
91 }
92
93 if inner_ch == delim {
94 let is_double = chars.peek().is_some_and(|(_, c)| *c == delim);
96 if !is_double && found_content && !has_whitespace {
97 spans.push((start_byte, byte_pos + delim_len));
98 }
99 break;
100 }
101
102 found_content = true;
103 if inner_ch.is_whitespace() {
104 has_whitespace = true;
105 }
106 }
107 }
108
109 spans
110}
111
112fn merge_spans(spans: &[(usize, usize)]) -> Vec<(usize, usize)> {
114 if spans.is_empty() {
115 return Vec::new();
116 }
117
118 let mut merged = Vec::with_capacity(spans.len());
119 let mut current = spans[0];
120
121 for &(start, end) in &spans[1..] {
122 if start <= current.1 {
123 current.1 = current.1.max(end);
124 } else {
125 merged.push(current);
126 current = (start, end);
127 }
128 }
129 merged.push(current);
130 merged
131}
132
133static INLINE_HILITE_PATTERN: LazyLock<Regex> =
139 LazyLock::new(|| Regex::new(r"`#!([a-zA-Z][a-zA-Z0-9_+-]*)\s+[^`]+`").unwrap());
140
141static INLINE_HILITE_SHEBANG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^#!([a-zA-Z][a-zA-Z0-9_+-]*)").unwrap());
143
144#[inline]
146pub fn contains_inline_hilite(line: &str) -> bool {
147 line.contains('`') && line.contains("#!") && INLINE_HILITE_PATTERN.is_match(line)
148}
149
150#[inline]
152pub fn is_inline_hilite_content(content: &str) -> bool {
153 INLINE_HILITE_SHEBANG.is_match(content)
154}
155
156static KEYS_PATTERN: LazyLock<Regex> =
162 LazyLock::new(|| Regex::new(r"\+\+([a-zA-Z0-9_-]+(?:\+[a-zA-Z0-9_-]+)*)\+\+").unwrap());
163
164pub const COMMON_KEYS: &[&str] = &[
166 "ctrl",
167 "alt",
168 "shift",
169 "cmd",
170 "meta",
171 "win",
172 "windows",
173 "option",
174 "enter",
175 "return",
176 "tab",
177 "space",
178 "backspace",
179 "delete",
180 "del",
181 "insert",
182 "ins",
183 "home",
184 "end",
185 "pageup",
186 "pagedown",
187 "up",
188 "down",
189 "left",
190 "right",
191 "escape",
192 "esc",
193 "capslock",
194 "numlock",
195 "scrolllock",
196 "printscreen",
197 "pause",
198 "break",
199 "f1",
200 "f2",
201 "f3",
202 "f4",
203 "f5",
204 "f6",
205 "f7",
206 "f8",
207 "f9",
208 "f10",
209 "f11",
210 "f12",
211];
212
213#[derive(Debug, Clone, PartialEq)]
215pub struct KeyboardShortcut {
216 pub full_text: String,
217 pub keys: Vec<String>,
218 pub start: usize,
219 pub end: usize,
220}
221
222pub fn find_keys_spans(line: &str) -> Vec<(usize, usize)> {
224 if !line.contains("++") {
225 return Vec::new();
226 }
227 find_regex_spans(line, &KEYS_PATTERN)
228}
229
230#[inline]
232pub fn contains_keys(line: &str) -> bool {
233 line.contains("++") && KEYS_PATTERN.is_match(line)
234}
235
236pub fn find_keyboard_shortcuts(line: &str) -> Vec<KeyboardShortcut> {
238 if !line.contains("++") {
239 return Vec::new();
240 }
241
242 KEYS_PATTERN
243 .find_iter(line)
244 .map(|m| {
245 let full_text = m.as_str().to_string();
246 let inner = &full_text[2..full_text.len() - 2];
247 let keys = inner.split('+').map(String::from).collect();
248 KeyboardShortcut {
249 full_text,
250 keys,
251 start: m.start(),
252 end: m.end(),
253 }
254 })
255 .collect()
256}
257
258pub fn is_in_keys(line: &str, position: usize) -> bool {
260 position_in_spans(position, &find_keys_spans(line))
261}
262
263static INSERT_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\^\^[^\^]+(?:\^[^\^]+)*\^\^").unwrap());
270
271pub fn find_insert_spans(line: &str) -> Vec<(usize, usize)> {
273 if !line.contains("^^") {
274 return Vec::new();
275 }
276 find_regex_spans(line, &INSERT_PATTERN)
277}
278
279pub fn find_superscript_spans(line: &str) -> Vec<(usize, usize)> {
281 if !line.contains('^') {
282 return Vec::new();
283 }
284 let insert_spans = find_insert_spans(line);
285 find_single_delim_spans(line, '^', &insert_spans)
286}
287
288#[inline]
290pub fn contains_superscript(line: &str) -> bool {
291 !find_superscript_spans(line).is_empty()
292}
293
294#[inline]
296pub fn contains_insert(line: &str) -> bool {
297 line.contains("^^") && INSERT_PATTERN.is_match(line)
298}
299
300pub fn is_in_caret_markup(line: &str, position: usize) -> bool {
302 if !line.contains('^') {
303 return false;
304 }
305 let insert_spans = find_insert_spans(line);
306 if position_in_spans(position, &insert_spans) {
307 return true;
308 }
309 let super_spans = find_single_delim_spans(line, '^', &insert_spans);
310 position_in_spans(position, &super_spans)
311}
312
313static STRIKETHROUGH_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"~~[^~]+(?:~[^~]+)*~~").unwrap());
320
321pub fn find_strikethrough_spans(line: &str) -> Vec<(usize, usize)> {
323 if !line.contains("~~") {
324 return Vec::new();
325 }
326 find_regex_spans(line, &STRIKETHROUGH_PATTERN)
327}
328
329pub fn find_subscript_spans(line: &str) -> Vec<(usize, usize)> {
331 if !line.contains('~') {
332 return Vec::new();
333 }
334 let strike_spans = find_strikethrough_spans(line);
335 find_single_delim_spans(line, '~', &strike_spans)
336}
337
338#[inline]
340pub fn contains_subscript(line: &str) -> bool {
341 !find_subscript_spans(line).is_empty()
342}
343
344#[inline]
346pub fn contains_strikethrough(line: &str) -> bool {
347 line.contains("~~") && STRIKETHROUGH_PATTERN.is_match(line)
348}
349
350pub fn is_in_tilde_markup(line: &str, position: usize) -> bool {
352 if !line.contains('~') {
353 return false;
354 }
355 let strike_spans = find_strikethrough_spans(line);
356 if position_in_spans(position, &strike_spans) {
357 return true;
358 }
359 let sub_spans = find_single_delim_spans(line, '~', &strike_spans);
360 position_in_spans(position, &sub_spans)
361}
362
363static MARK_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"==([^=]+)==").unwrap());
369
370pub fn find_mark_spans(line: &str) -> Vec<(usize, usize)> {
372 if !line.contains("==") {
373 return Vec::new();
374 }
375 find_regex_spans(line, &MARK_PATTERN)
376}
377
378#[inline]
380pub fn contains_mark(line: &str) -> bool {
381 line.contains("==") && MARK_PATTERN.is_match(line)
382}
383
384pub fn is_in_mark(line: &str, position: usize) -> bool {
386 position_in_spans(position, &find_mark_spans(line))
387}
388
389static SMART_SYMBOL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
395 Regex::new(r"(?:\(c\)|\(C\)|\(r\)|\(R\)|\(tm\)|\(TM\)|\(p\)|\.\.\.|-{2,3}|<->|<-|->|<=>|<=|=>|1/4|1/2|3/4|\+-|!=)")
396 .unwrap()
397});
398
399pub fn find_smart_symbol_spans(line: &str) -> Vec<(usize, usize)> {
401 if !line.contains('(')
403 && !line.contains("...")
404 && !line.contains("--")
405 && !line.contains("->")
406 && !line.contains("<-")
407 && !line.contains("=>")
408 && !line.contains("<=")
409 && !line.contains("1/")
410 && !line.contains("3/")
411 && !line.contains("+-")
412 && !line.contains("!=")
413 {
414 return Vec::new();
415 }
416 find_regex_spans(line, &SMART_SYMBOL_PATTERN)
417}
418
419#[inline]
421pub fn contains_smart_symbols(line: &str) -> bool {
422 !find_smart_symbol_spans(line).is_empty()
423}
424
425pub fn is_in_smart_symbol(line: &str, position: usize) -> bool {
427 position_in_spans(position, &find_smart_symbol_spans(line))
428}
429
430pub fn is_in_pymdown_markup(line: &str, position: usize) -> bool {
436 is_in_keys(line, position)
437 || is_in_caret_markup(line, position)
438 || is_in_tilde_markup(line, position)
439 || is_in_mark(line, position)
440 || is_in_smart_symbol(line, position)
441}
442
443pub fn mask_pymdown_markup(line: &str) -> String {
448 let mut all_spans: Vec<(usize, usize)> = Vec::new();
450
451 all_spans.extend(find_keys_spans(line));
453
454 if line.contains('^') {
456 let insert_spans = find_insert_spans(line);
457 let super_spans = find_single_delim_spans(line, '^', &insert_spans);
458 all_spans.extend(insert_spans);
459 all_spans.extend(super_spans);
460 }
461
462 if line.contains('~') {
464 let strike_spans = find_strikethrough_spans(line);
465 let sub_spans = find_single_delim_spans(line, '~', &strike_spans);
466 all_spans.extend(strike_spans);
467 all_spans.extend(sub_spans);
468 }
469
470 all_spans.extend(find_mark_spans(line));
472
473 if all_spans.is_empty() {
475 return line.to_string();
476 }
477
478 all_spans.sort_unstable_by_key(|&(start, _)| start);
480 let merged = merge_spans(&all_spans);
481
482 let mut result = String::with_capacity(line.len());
484 let mut last_end = 0;
485
486 for (start, end) in merged {
487 result.push_str(&line[last_end..start]);
488 for _ in 0..(end - start) {
490 result.push(' ');
491 }
492 last_end = end;
493 }
494 result.push_str(&line[last_end..]);
495
496 result
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
508 fn test_position_in_spans_empty() {
509 assert!(!position_in_spans(0, &[]));
510 assert!(!position_in_spans(100, &[]));
511 }
512
513 #[test]
514 fn test_position_in_spans_early_exit() {
515 let spans = [(10, 20), (30, 40)];
516 assert!(!position_in_spans(5, &spans)); assert!(!position_in_spans(25, &spans)); assert!(!position_in_spans(50, &spans)); }
520
521 #[test]
522 fn test_position_in_spans_inside() {
523 let spans = [(10, 20), (30, 40)];
524 assert!(position_in_spans(10, &spans)); assert!(position_in_spans(15, &spans)); assert!(position_in_spans(19, &spans)); assert!(!position_in_spans(20, &spans)); assert!(position_in_spans(30, &spans)); }
530
531 #[test]
532 fn test_merge_spans_empty() {
533 assert!(merge_spans(&[]).is_empty());
534 }
535
536 #[test]
537 fn test_merge_spans_no_overlap() {
538 let spans = [(0, 5), (10, 15), (20, 25)];
539 let merged = merge_spans(&spans);
540 assert_eq!(merged, vec![(0, 5), (10, 15), (20, 25)]);
541 }
542
543 #[test]
544 fn test_merge_spans_overlapping() {
545 let spans = [(0, 10), (5, 15), (20, 25)];
546 let merged = merge_spans(&spans);
547 assert_eq!(merged, vec![(0, 15), (20, 25)]);
548 }
549
550 #[test]
551 fn test_merge_spans_adjacent() {
552 let spans = [(0, 10), (10, 20)];
553 let merged = merge_spans(&spans);
554 assert_eq!(merged, vec![(0, 20)]);
555 }
556
557 #[test]
562 fn test_contains_inline_hilite() {
563 assert!(contains_inline_hilite("`#!python print('hello')`"));
564 assert!(contains_inline_hilite("Use `#!js alert('hi')` for alerts"));
565 assert!(contains_inline_hilite("`#!c++ cout << x;`"));
566
567 assert!(!contains_inline_hilite("`regular code`"));
568 assert!(!contains_inline_hilite("#! not in backticks"));
569 assert!(!contains_inline_hilite("`#!` empty"));
570 }
571
572 #[test]
573 fn test_is_inline_hilite_content() {
574 assert!(is_inline_hilite_content("#!python print()"));
575 assert!(is_inline_hilite_content("#!js code"));
576
577 assert!(!is_inline_hilite_content("regular code"));
578 assert!(!is_inline_hilite_content(" #!python with space"));
579 }
580
581 #[test]
586 fn test_contains_keys() {
587 assert!(contains_keys("Press ++ctrl++ to continue"));
588 assert!(contains_keys("++ctrl+alt+delete++"));
589 assert!(contains_keys("Use ++cmd+shift+p++ for command palette"));
590
591 assert!(!contains_keys("Use + for addition"));
592 assert!(!contains_keys("a++ increment"));
593 assert!(!contains_keys("++incomplete"));
594 }
595
596 #[test]
597 fn test_find_keyboard_shortcuts() {
598 let shortcuts = find_keyboard_shortcuts("Press ++ctrl+c++ then ++ctrl+v++");
599 assert_eq!(shortcuts.len(), 2);
600 assert_eq!(shortcuts[0].keys, vec!["ctrl", "c"]);
601 assert_eq!(shortcuts[1].keys, vec!["ctrl", "v"]);
602
603 let shortcuts = find_keyboard_shortcuts("++ctrl+alt+delete++");
604 assert_eq!(shortcuts.len(), 1);
605 assert_eq!(shortcuts[0].keys, vec!["ctrl", "alt", "delete"]);
606 }
607
608 #[test]
609 fn test_is_in_keys() {
610 let line = "Press ++ctrl++ here";
611 assert!(!is_in_keys(line, 0)); assert!(!is_in_keys(line, 5)); assert!(is_in_keys(line, 6)); assert!(is_in_keys(line, 10)); assert!(is_in_keys(line, 13)); assert!(!is_in_keys(line, 14)); }
618
619 #[test]
624 fn test_contains_superscript() {
625 assert!(contains_superscript("E=mc^2^"));
626 assert!(contains_superscript("x^n^ power"));
627
628 assert!(!contains_superscript("no caret here"));
629 assert!(!contains_superscript("^^insert^^")); }
631
632 #[test]
633 fn test_contains_insert() {
634 assert!(contains_insert("^^inserted text^^"));
635 assert!(contains_insert("Some ^^new^^ text"));
636
637 assert!(!contains_insert("^superscript^"));
638 assert!(!contains_insert("no markup"));
639 }
640
641 #[test]
642 fn test_find_superscript_spans() {
643 let spans = find_superscript_spans("E=mc^2^");
644 assert_eq!(spans.len(), 1);
645 assert_eq!(&"E=mc^2^"[spans[0].0..spans[0].1], "^2^");
646 }
647
648 #[test]
649 fn test_superscript_not_inside_insert() {
650 let line = "^^some^x^text^^";
652 let spans = find_superscript_spans(line);
653 assert!(spans.is_empty(), "Superscript inside insert should not be detected");
654 }
655
656 #[test]
657 fn test_is_in_caret_markup() {
658 let line = "Text ^super^ here";
659 assert!(!is_in_caret_markup(line, 0));
660 assert!(is_in_caret_markup(line, 5)); assert!(is_in_caret_markup(line, 8)); assert!(!is_in_caret_markup(line, 13)); let line2 = "Text ^^insert^^ here";
665 assert!(is_in_caret_markup(line2, 5)); assert!(is_in_caret_markup(line2, 10)); }
668
669 #[test]
674 fn test_contains_subscript() {
675 assert!(contains_subscript("H~2~O"));
676 assert!(contains_subscript("x~n~ power"));
677
678 assert!(!contains_subscript("no tilde here"));
679 assert!(!contains_subscript("~~strikethrough~~"));
680 }
681
682 #[test]
683 fn test_contains_strikethrough() {
684 assert!(contains_strikethrough("~~deleted text~~"));
685 assert!(contains_strikethrough("Some ~~old~~ text"));
686 assert!(contains_strikethrough("~~a~b~~")); assert!(!contains_strikethrough("~subscript~"));
689 assert!(!contains_strikethrough("no markup"));
690 }
691
692 #[test]
693 fn test_find_subscript_spans() {
694 let spans = find_subscript_spans("H~2~O");
695 assert_eq!(spans.len(), 1);
696 assert_eq!(&"H~2~O"[spans[0].0..spans[0].1], "~2~");
697 }
698
699 #[test]
700 fn test_subscript_not_inside_strikethrough() {
701 let line = "~~some~x~text~~";
702 let spans = find_subscript_spans(line);
703 assert!(
704 spans.is_empty(),
705 "Subscript inside strikethrough should not be detected"
706 );
707 }
708
709 #[test]
710 fn test_multiple_subscripts() {
711 let line = "~a~ and ~b~";
712 let spans = find_subscript_spans(line);
713 assert_eq!(spans.len(), 2);
714 assert_eq!(&line[spans[0].0..spans[0].1], "~a~");
715 assert_eq!(&line[spans[1].0..spans[1].1], "~b~");
716 }
717
718 #[test]
719 fn test_subscript_no_whitespace() {
720 let line = "~no spaces allowed~";
721 let spans = find_subscript_spans(line);
722 assert!(spans.is_empty(), "Subscript with whitespace should not match");
723 }
724
725 #[test]
726 fn test_is_in_tilde_markup() {
727 let line = "Text ~sub~ here";
728 assert!(!is_in_tilde_markup(line, 0));
729 assert!(is_in_tilde_markup(line, 5)); assert!(is_in_tilde_markup(line, 7)); assert!(!is_in_tilde_markup(line, 12)); let line2 = "Text ~~strike~~ here";
734 assert!(is_in_tilde_markup(line2, 5)); assert!(is_in_tilde_markup(line2, 10)); }
737
738 #[test]
739 fn test_subscript_vs_strikethrough_coexist() {
740 let line = "H~2~O is ~~not~~ water";
741 assert!(contains_subscript(line));
742 assert!(contains_strikethrough(line));
743 }
744
745 #[test]
746 fn test_strikethrough_with_internal_tilde() {
747 let line = "~~a~b~~";
749 assert!(contains_strikethrough(line));
750
751 let strike_spans = find_strikethrough_spans(line);
752 assert_eq!(strike_spans.len(), 1);
753 assert_eq!(&line[strike_spans[0].0..strike_spans[0].1], "~~a~b~~");
754
755 assert!(!contains_subscript(line));
757 }
758
759 #[test]
764 fn test_contains_mark() {
765 assert!(contains_mark("This is ==highlighted== text"));
766 assert!(contains_mark("==important=="));
767
768 assert!(!contains_mark("no highlight"));
769 assert!(!contains_mark("a == b comparison")); }
771
772 #[test]
773 fn test_is_in_mark() {
774 let line = "Text ==highlight== more";
775 assert!(!is_in_mark(line, 0));
776 assert!(is_in_mark(line, 5)); assert!(is_in_mark(line, 10)); assert!(!is_in_mark(line, 19)); }
780
781 #[test]
786 fn test_contains_smart_symbols() {
787 assert!(contains_smart_symbols("Copyright (c) 2024"));
788 assert!(contains_smart_symbols("This is (tm) trademarked"));
789 assert!(contains_smart_symbols("Left arrow <- here"));
790 assert!(contains_smart_symbols("Right arrow -> there"));
791 assert!(contains_smart_symbols("Em dash --- here"));
792 assert!(contains_smart_symbols("Fraction 1/2"));
793
794 assert!(!contains_smart_symbols("No symbols here"));
795 assert!(!contains_smart_symbols("(other) parentheses"));
796 }
797
798 #[test]
799 fn test_is_in_smart_symbol() {
800 let line = "Copyright (c) text";
801 assert!(!is_in_smart_symbol(line, 0));
802 assert!(is_in_smart_symbol(line, 10)); assert!(is_in_smart_symbol(line, 11)); assert!(is_in_smart_symbol(line, 12)); assert!(!is_in_smart_symbol(line, 14)); }
807
808 #[test]
813 fn test_is_in_pymdown_markup() {
814 assert!(is_in_pymdown_markup("++ctrl++", 2));
815 assert!(is_in_pymdown_markup("^super^", 1));
816 assert!(is_in_pymdown_markup("~sub~", 1));
817 assert!(is_in_pymdown_markup("~~strike~~", 2));
818 assert!(is_in_pymdown_markup("==mark==", 2));
819 assert!(is_in_pymdown_markup("(c)", 1));
820
821 assert!(!is_in_pymdown_markup("plain text", 5));
822 }
823
824 #[test]
825 fn test_mask_pymdown_markup() {
826 let line = "Press ++ctrl++ and ^super^ with ==mark==";
827 let masked = mask_pymdown_markup(line);
828 assert!(!masked.contains("++"));
829 assert!(!masked.contains("^super^"));
830 assert!(!masked.contains("==mark=="));
831 assert!(masked.contains("Press"));
832 assert!(masked.contains("and"));
833 assert!(masked.contains("with"));
834 assert_eq!(masked.len(), line.len());
835 }
836
837 #[test]
838 fn test_mask_pymdown_markup_with_tilde() {
839 let line = "H~2~O is ~~deleted~~ water";
840 let masked = mask_pymdown_markup(line);
841 assert!(!masked.contains("~2~"));
842 assert!(!masked.contains("~~deleted~~"));
843 assert!(masked.contains("H"));
844 assert!(masked.contains("O is"));
845 assert!(masked.contains("water"));
846 assert_eq!(masked.len(), line.len());
847 }
848
849 #[test]
850 fn test_mask_preserves_unmasked_text() {
851 let line = "plain text without markup";
852 let masked = mask_pymdown_markup(line);
853 assert_eq!(masked, line);
854 }
855
856 #[test]
857 fn test_mask_complex_mixed_markup() {
858 let line = "++ctrl++ ^2^ ~x~ ~~old~~ ==new==";
859 let masked = mask_pymdown_markup(line);
860 assert!(!masked.contains("++"));
862 assert!(!masked.contains("^2^"));
863 assert!(!masked.contains("~x~"));
864 assert!(!masked.contains("~~old~~"));
865 assert!(!masked.contains("==new=="));
866 assert_eq!(masked.len(), line.len());
868 }
869
870 #[test]
875 fn test_empty_line() {
876 assert!(!contains_keys(""));
877 assert!(!contains_superscript(""));
878 assert!(!contains_subscript(""));
879 assert!(!contains_mark(""));
880 assert_eq!(mask_pymdown_markup(""), "");
881 }
882
883 #[test]
884 fn test_unclosed_delimiters() {
885 assert!(!contains_superscript("^unclosed"));
886 assert!(!contains_subscript("~unclosed"));
887 assert!(!contains_mark("==unclosed"));
888 assert!(!contains_keys("++unclosed"));
889 }
890
891 #[test]
892 fn test_adjacent_markup() {
893 let line = "^a^^b^";
894 let spans = find_superscript_spans(line);
902 assert_eq!(spans.len(), 1);
916 assert_eq!(&line[spans[0].0..spans[0].1], "^b^");
917 }
918
919 #[test]
920 fn test_triple_tilde() {
921 let line = "~~~a~~~";
923 let strike_spans = find_strikethrough_spans(line);
924 assert_eq!(strike_spans.len(), 1);
931 assert_eq!(&line[strike_spans[0].0..strike_spans[0].1], "~~a~~");
932 }
933}