rumdl_lib/utils/
regex_cache.rs1use fancy_regex::Regex as FancyRegex;
23use regex::Regex;
24use std::collections::HashMap;
25use std::sync::LazyLock;
26use std::sync::{Arc, Mutex};
27
28#[derive(Debug)]
30pub struct RegexCache {
31 cache: HashMap<String, Arc<Regex>>,
32 usage_stats: HashMap<String, u64>,
33}
34
35impl Default for RegexCache {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl RegexCache {
42 pub fn new() -> Self {
43 Self {
44 cache: HashMap::new(),
45 usage_stats: HashMap::new(),
46 }
47 }
48
49 pub fn get_regex(&mut self, pattern: &str) -> Result<Arc<Regex>, regex::Error> {
51 if let Some(regex) = self.cache.get(pattern) {
52 *self.usage_stats.entry(pattern.to_string()).or_insert(0) += 1;
53 return Ok(regex.clone());
54 }
55
56 let regex = Arc::new(Regex::new(pattern)?);
57 self.cache.insert(pattern.to_string(), regex.clone());
58 *self.usage_stats.entry(pattern.to_string()).or_insert(0) += 1;
59 Ok(regex)
60 }
61
62 pub fn get_stats(&self) -> HashMap<String, u64> {
64 self.usage_stats.clone()
65 }
66
67 pub fn clear(&mut self) {
69 self.cache.clear();
70 self.usage_stats.clear();
71 }
72}
73
74static GLOBAL_REGEX_CACHE: LazyLock<Arc<Mutex<RegexCache>>> = LazyLock::new(|| Arc::new(Mutex::new(RegexCache::new())));
76
77pub fn get_cached_regex(pattern: &str) -> Result<Arc<Regex>, regex::Error> {
83 let mut cache = GLOBAL_REGEX_CACHE.lock().unwrap_or_else(|poisoned| {
84 let mut guard = poisoned.into_inner();
86 guard.clear();
87 guard
88 });
89 cache.get_regex(pattern)
90}
91
92pub fn get_cache_stats() -> HashMap<String, u64> {
96 match GLOBAL_REGEX_CACHE.lock() {
97 Ok(cache) => cache.get_stats(),
98 Err(_) => HashMap::new(),
99 }
100}
101
102#[macro_export]
121macro_rules! regex_lazy {
122 ($pattern:expr) => {{
123 static REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new($pattern).unwrap());
124 &*REGEX
125 }};
126}
127
128#[macro_export]
135macro_rules! regex_cached {
136 ($pattern:expr) => {{ $crate::utils::regex_cache::get_cached_regex($pattern).expect("Failed to compile regex") }};
137}
138
139pub use crate::regex_lazy;
141
142pub const URL_STANDARD_STR: &str = concat!(
176 r#"(?:https?|ftps?|ftp)://"#, r#"(?:"#,
178 r#"\[[0-9a-fA-F:%.\-a-zA-Z]+\]"#, r#"|"#,
180 r#"[^\s<>\[\]()\\'\"`/]+"#, r#")"#,
182 r#"(?::\d+)?"#, r#"(?:/[^\s<>\[\]\\'\"`]*)?"#, r#"(?:\?[^\s<>\[\]\\'\"`]*)?"#, r#"(?:#[^\s<>\[\]\\'\"`]*)?"#, );
187
188pub const URL_WWW_STR: &str = concat!(
201 r#"www\.(?:[a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}"#, r#"(?::\d+)?"#, r#"(?:/[^\s<>\[\]\\'\"`]*)?"#, r#"(?:\?[^\s<>\[\]\\'\"`]*)?"#, r#"(?:#[^\s<>\[\]\\'\"`]*)?"#, );
207
208pub const URL_IPV6_STR: &str = concat!(
213 r#"(?:https?|ftps?|ftp)://"#,
214 r#"\[[0-9a-fA-F:%.\-a-zA-Z]+\]"#, r#"(?::\d+)?"#, r#"(?:/[^\s<>\[\]\\'\"`]*)?"#, r#"(?:\?[^\s<>\[\]\\'\"`]*)?"#, r#"(?:#[^\s<>\[\]\\'\"`]*)?"#, );
220
221pub const XMPP_URI_STR: &str = r#"xmpp:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s<>\[\]\\'\"`]*)?"#;
230
231pub const URL_QUICK_CHECK_STR: &str = r#"(?:https?|ftps?|ftp|xmpp)://|xmpp:|@|www\."#;
237
238pub const URL_SIMPLE_STR: &str = r#"(?:https?|ftps?|ftp)://[^\s<>]+[^\s<>.,]"#;
244
245pub static URL_STANDARD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_STANDARD_STR).unwrap());
250
251pub static URL_WWW_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_WWW_STR).unwrap());
254
255pub static URL_IPV6_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_IPV6_STR).unwrap());
258
259pub static URL_QUICK_CHECK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_QUICK_CHECK_STR).unwrap());
262
263pub static URL_SIMPLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_SIMPLE_STR).unwrap());
266
267pub static URL_PATTERN: LazyLock<Regex> = LazyLock::new(|| URL_SIMPLE_REGEX.clone());
269
270pub static XMPP_URI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(XMPP_URI_STR).unwrap());
273
274pub static ATX_HEADING_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(#{1,6})(\s+|$)").unwrap());
276
277pub static UNORDERED_LIST_MARKER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)([*+-])(\s+)").unwrap());
279pub static ORDERED_LIST_MARKER_REGEX: LazyLock<Regex> =
280 LazyLock::new(|| Regex::new(r"^(\s*)(\d+)([.)])(\s+)").unwrap());
281
282pub static ASTERISK_EMPHASIS: LazyLock<Regex> =
288 LazyLock::new(|| Regex::new(r"(?:^|[^*])\*(\s+[^*]+\s*|\s*[^*]+\s+)\*(?:[^*]|$)").unwrap());
289pub static UNDERSCORE_EMPHASIS: LazyLock<Regex> =
290 LazyLock::new(|| Regex::new(r"(?:^|[^_])_(\s+[^_]+\s*|\s*[^_]+\s+)_(?:[^_]|$)").unwrap());
291pub static DOUBLE_UNDERSCORE_EMPHASIS: LazyLock<Regex> =
292 LazyLock::new(|| Regex::new(r"(?:^|[^_])__(\s+[^_]+\s*|\s*[^_]+\s+)__(?:[^_]|$)").unwrap());
293pub static FENCED_CODE_BLOCK_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)```(?:[^`\r\n]*)$").unwrap());
295pub static FENCED_CODE_BLOCK_END: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)```\s*$").unwrap());
296
297pub static HTML_TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<([a-zA-Z][^>]*)>").unwrap());
299pub static HTML_TAG_QUICK_CHECK: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)</?[a-zA-Z]").unwrap());
300
301pub static IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap());
303
304pub static BLOCKQUOTE_PREFIX_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*>+\s*)").unwrap());
306
307pub fn is_blank_in_blockquote_context(line: &str) -> bool {
330 let mut rest = line;
336 loop {
337 if rest.trim().is_empty() {
338 return true;
339 }
340 match BLOCKQUOTE_PREFIX_RE.find(rest) {
341 Some(m) if m.end() > 0 => rest = &rest[m.end()..],
344 _ => return false,
345 }
346 }
347}
348
349pub static IMAGE_REF_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^!\[.*?\]\[.*?\]$").unwrap());
351pub static LINK_REF_PATTERN: LazyLock<Regex> =
352 LazyLock::new(|| Regex::new(r#"^\[.*?\]:\s*\S+(\s+["'(].*)?\s*$"#).unwrap());
353pub static ABBREVIATION: LazyLock<Regex> = LazyLock::new(|| {
354 Regex::new(r"\b(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|vs|etc|i\.e|e\.g|Inc|Corp|Ltd|Co|St|Ave|Blvd|Rd|Ph\.D|M\.D|B\.A|M\.A|Ph\.D|U\.S|U\.K|U\.N|N\.Y|L\.A|D\.C)\.\s+[A-Z]").unwrap()
355});
356pub static LIST_ITEM: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\d+\.\s+").unwrap());
357
358pub static EMAIL_PATTERN: LazyLock<Regex> =
360 LazyLock::new(|| Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap());
361
362pub static REF_LINK_REGEX: LazyLock<FancyRegex> =
366 LazyLock::new(|| FancyRegex::new(r"(?<!\\)\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]\[([^\]]*)\]").unwrap());
367
368pub static SHORTCUT_REF_REGEX: LazyLock<FancyRegex> =
373 LazyLock::new(|| FancyRegex::new(r"(?<![\\)\]])\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\](?!\s*[\[\(])").unwrap());
374
375pub static INLINE_LINK_FANCY_REGEX: LazyLock<FancyRegex> =
377 LazyLock::new(|| FancyRegex::new(r"(?<!\\)\[([^\]]+)\]\(([^)]+)\)").unwrap());
378
379pub static INLINE_IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap());
381
382pub static LINKED_IMAGE_INLINE_INLINE: LazyLock<Regex> =
390 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)").unwrap());
391
392pub static LINKED_IMAGE_REF_INLINE: LazyLock<Regex> =
394 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\[([^\]]*)\]\]\(([^)]+)\)").unwrap());
395
396pub static LINKED_IMAGE_INLINE_REF: LazyLock<Regex> =
398 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\(([^)]+)\)\]\[([^\]]*)\]").unwrap());
399
400pub static LINKED_IMAGE_REF_REF: LazyLock<Regex> =
402 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\[([^\]]*)\]\]\[([^\]]*)\]").unwrap());
403
404pub static REF_IMAGE_REGEX: LazyLock<Regex> =
406 LazyLock::new(|| Regex::new(r"!\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]\[([^\]]*)\]").unwrap());
407
408pub static FOOTNOTE_REF_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[\^([^\]]+)\]").unwrap());
410
411pub static WIKI_LINK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[\[([^\]]+)\]\]").unwrap());
413
414pub static INLINE_MATH_REGEX: LazyLock<FancyRegex> =
416 LazyLock::new(|| FancyRegex::new(r"(?<!\$)\$(?!\$)([^\$]+)\$(?!\$)").unwrap());
417pub static DISPLAY_MATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$([^\$]+)\$\$").unwrap());
418
419pub static EMOJI_SHORTCODE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":([a-zA-Z0-9_+-]+):").unwrap());
421
422pub static HTML_TAG_PATTERN: LazyLock<Regex> =
424 LazyLock::new(|| Regex::new(r"</?[a-zA-Z][^>]*>|<[a-zA-Z][^>]*/\s*>").unwrap());
425
426pub static HTML_ENTITY_REGEX: LazyLock<Regex> =
428 LazyLock::new(|| Regex::new(r"&[a-zA-Z][a-zA-Z0-9]*;|&#\d+;|&#x[0-9a-fA-F]+;").unwrap());
429
430pub static HUGO_SHORTCODE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{[<%][\s\S]*?[%>]\}\}").unwrap());
434
435pub static HTML_HEADING_PATTERN: LazyLock<FancyRegex> =
438 LazyLock::new(|| FancyRegex::new(r"^\s*<h([1-6])(?:\s[^>]*)?>.*</h\1>\s*$").unwrap());
439
440pub fn escape_regex(s: &str) -> String {
442 let mut result = String::with_capacity(s.len() * 2);
443
444 for c in s.chars() {
445 if matches!(
447 c,
448 '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\'
449 ) {
450 result.push('\\');
451 }
452 result.push(c);
453 }
454
455 result
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn test_regex_cache_new() {
464 let cache = RegexCache::new();
465 assert!(cache.cache.is_empty());
466 assert!(cache.usage_stats.is_empty());
467 }
468
469 #[test]
470 fn test_regex_cache_default() {
471 let cache = RegexCache::default();
472 assert!(cache.cache.is_empty());
473 assert!(cache.usage_stats.is_empty());
474 }
475
476 #[test]
477 fn test_get_regex_compilation() {
478 let mut cache = RegexCache::new();
479
480 let regex1 = cache.get_regex(r"\d+").unwrap();
482 assert_eq!(cache.cache.len(), 1);
483 assert_eq!(cache.usage_stats.get(r"\d+"), Some(&1));
484
485 let regex2 = cache.get_regex(r"\d+").unwrap();
487 assert_eq!(cache.cache.len(), 1);
488 assert_eq!(cache.usage_stats.get(r"\d+"), Some(&2));
489
490 assert!(Arc::ptr_eq(®ex1, ®ex2));
492 }
493
494 #[test]
495 fn test_get_regex_invalid_pattern() {
496 let mut cache = RegexCache::new();
497 let result = cache.get_regex(r"[unterminated");
498 assert!(result.is_err());
499 assert!(cache.cache.is_empty());
500 }
501
502 #[test]
503 fn test_get_stats() {
504 let mut cache = RegexCache::new();
505
506 let _ = cache.get_regex(r"\d+").unwrap();
508 let _ = cache.get_regex(r"\d+").unwrap();
509 let _ = cache.get_regex(r"\w+").unwrap();
510
511 let stats = cache.get_stats();
512 assert_eq!(stats.get(r"\d+"), Some(&2));
513 assert_eq!(stats.get(r"\w+"), Some(&1));
514 }
515
516 #[test]
517 fn test_clear_cache() {
518 let mut cache = RegexCache::new();
519
520 let _ = cache.get_regex(r"\d+").unwrap();
522
523 assert!(!cache.cache.is_empty());
524 assert!(!cache.usage_stats.is_empty());
525
526 cache.clear();
528
529 assert!(cache.cache.is_empty());
530 assert!(cache.usage_stats.is_empty());
531 }
532
533 #[test]
534 fn test_global_cache_functions() {
535 let regex1 = get_cached_regex(r"\d{3}").unwrap();
537 let regex2 = get_cached_regex(r"\d{3}").unwrap();
538 assert!(Arc::ptr_eq(®ex1, ®ex2));
539
540 let stats = get_cache_stats();
542 assert!(stats.contains_key(r"\d{3}"));
543 }
544
545 #[test]
546 fn test_regex_lazy_macro() {
547 let re = regex_lazy!(r"^test.*end$");
548 assert!(re.is_match("test something end"));
549 assert!(!re.is_match("test something"));
550
551 let re2 = regex_lazy!(r"^start.*finish$");
555 assert!(re2.is_match("start and finish"));
556 assert!(!re2.is_match("start without end"));
557 }
558
559 #[test]
560 fn test_escape_regex() {
561 assert_eq!(escape_regex("a.b"), "a\\.b");
562 assert_eq!(escape_regex("a+b*c"), "a\\+b\\*c");
563 assert_eq!(escape_regex("(test)"), "\\(test\\)");
564 assert_eq!(escape_regex("[a-z]"), "\\[a-z\\]");
565 assert_eq!(escape_regex("normal text"), "normal text");
566
567 assert_eq!(escape_regex(".$^{[(|)*+?\\"), "\\.\\$\\^\\{\\[\\(\\|\\)\\*\\+\\?\\\\");
569
570 assert_eq!(escape_regex(""), "");
572
573 assert_eq!(escape_regex("test.com/path?query=1"), "test\\.com/path\\?query=1");
575 }
576
577 #[test]
578 fn test_static_regex_patterns() {
579 assert!(URL_SIMPLE_REGEX.is_match("https://example.com"));
581 assert!(URL_SIMPLE_REGEX.is_match("http://test.org/path"));
582 assert!(URL_SIMPLE_REGEX.is_match("ftp://files.com"));
583 assert!(!URL_SIMPLE_REGEX.is_match("not a url"));
584
585 assert!(ATX_HEADING_REGEX.is_match("# Heading"));
587 assert!(ATX_HEADING_REGEX.is_match(" ## Indented"));
588 assert!(ATX_HEADING_REGEX.is_match("### "));
589 assert!(!ATX_HEADING_REGEX.is_match("Not a heading"));
590
591 assert!(UNORDERED_LIST_MARKER_REGEX.is_match("* Item"));
593 assert!(UNORDERED_LIST_MARKER_REGEX.is_match("- Item"));
594 assert!(UNORDERED_LIST_MARKER_REGEX.is_match("+ Item"));
595 assert!(ORDERED_LIST_MARKER_REGEX.is_match("1. Item"));
596 assert!(ORDERED_LIST_MARKER_REGEX.is_match("99. Item"));
597
598 assert!(HTML_TAG_REGEX.is_match("<div>"));
600 assert!(HTML_TAG_REGEX.is_match("<span class='test'>"));
601
602 assert!(BLOCKQUOTE_PREFIX_RE.is_match("> Quote"));
604 assert!(BLOCKQUOTE_PREFIX_RE.is_match(" > Indented quote"));
605 assert!(BLOCKQUOTE_PREFIX_RE.is_match(">> Nested"));
606 }
607
608 #[test]
609 fn test_thread_safety() {
610 use std::thread;
611
612 let handles: Vec<_> = (0..10)
613 .map(|i| {
614 thread::spawn(move || {
615 let pattern = format!(r"\d{{{i}}}");
616 let regex = get_cached_regex(&pattern).unwrap();
617 assert!(regex.is_match(&"1".repeat(i)));
618 })
619 })
620 .collect();
621
622 for handle in handles {
623 handle.join().unwrap();
624 }
625 }
626
627 #[test]
632 fn test_url_standard_basic() {
633 assert!(URL_STANDARD_REGEX.is_match("https://example.com"));
635 assert!(URL_STANDARD_REGEX.is_match("http://example.com"));
636 assert!(URL_STANDARD_REGEX.is_match("https://example.com/"));
637 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path"));
638 assert!(URL_STANDARD_REGEX.is_match("ftp://files.example.com"));
639 assert!(URL_STANDARD_REGEX.is_match("ftps://secure.example.com"));
640
641 assert!(!URL_STANDARD_REGEX.is_match("not a url"));
643 assert!(!URL_STANDARD_REGEX.is_match("example.com"));
644 assert!(!URL_STANDARD_REGEX.is_match("www.example.com"));
645 }
646
647 #[test]
648 fn test_url_standard_with_path() {
649 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path/to/page"));
650 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path/to/page.html"));
651 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path/to/page/"));
652 }
653
654 #[test]
655 fn test_url_standard_with_query() {
656 assert!(URL_STANDARD_REGEX.is_match("https://example.com?query=value"));
657 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path?query=value"));
658 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path?a=1&b=2"));
659 }
660
661 #[test]
662 fn test_url_standard_with_fragment() {
663 assert!(URL_STANDARD_REGEX.is_match("https://example.com#section"));
664 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path#section"));
665 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path?query=value#section"));
666 }
667
668 #[test]
669 fn test_url_standard_with_port() {
670 assert!(URL_STANDARD_REGEX.is_match("https://example.com:8080"));
671 assert!(URL_STANDARD_REGEX.is_match("https://example.com:443/path"));
672 assert!(URL_STANDARD_REGEX.is_match("http://localhost:3000"));
673 assert!(URL_STANDARD_REGEX.is_match("https://192.168.1.1:8080/path"));
674 }
675
676 #[test]
677 fn test_url_standard_wikipedia_style_parentheses() {
678 let url = "https://en.wikipedia.org/wiki/Rust_(programming_language)";
680 assert!(URL_STANDARD_REGEX.is_match(url));
681
682 let cap = URL_STANDARD_REGEX.find(url).unwrap();
684 assert_eq!(cap.as_str(), url);
685
686 let url2 = "https://example.com/path_(foo)_(bar)";
688 let cap2 = URL_STANDARD_REGEX.find(url2).unwrap();
689 assert_eq!(cap2.as_str(), url2);
690 }
691
692 #[test]
693 fn test_url_standard_ipv6() {
694 assert!(URL_STANDARD_REGEX.is_match("https://[::1]/path"));
696 assert!(URL_STANDARD_REGEX.is_match("https://[2001:db8::1]:8080/path"));
697 assert!(URL_STANDARD_REGEX.is_match("http://[fe80::1%eth0]/"));
698 }
699
700 #[test]
701 fn test_url_www_basic() {
702 assert!(URL_WWW_REGEX.is_match("www.example.com"));
704 assert!(URL_WWW_REGEX.is_match("www.example.co.uk"));
705 assert!(URL_WWW_REGEX.is_match("www.sub.example.com"));
706
707 assert!(!URL_WWW_REGEX.is_match("example.com"));
709
710 assert!(URL_WWW_REGEX.is_match("https://www.example.com"));
714 }
715
716 #[test]
717 fn test_url_www_with_path() {
718 assert!(URL_WWW_REGEX.is_match("www.example.com/path"));
719 assert!(URL_WWW_REGEX.is_match("www.example.com/path/to/page"));
720 assert!(URL_WWW_REGEX.is_match("www.example.com/path_(with_parens)"));
721 }
722
723 #[test]
724 fn test_url_ipv6_basic() {
725 assert!(URL_IPV6_REGEX.is_match("https://[::1]/"));
727 assert!(URL_IPV6_REGEX.is_match("http://[2001:db8::1]/path"));
728 assert!(URL_IPV6_REGEX.is_match("https://[fe80::1]:8080/path"));
729 assert!(URL_IPV6_REGEX.is_match("ftp://[::ffff:192.168.1.1]/file"));
730 }
731
732 #[test]
733 fn test_url_ipv6_with_zone_id() {
734 assert!(URL_IPV6_REGEX.is_match("https://[fe80::1%eth0]/path"));
736 assert!(URL_IPV6_REGEX.is_match("http://[fe80::1%25eth0]:8080/"));
737 }
738
739 #[test]
740 fn test_url_simple_detection() {
741 assert!(URL_SIMPLE_REGEX.is_match("https://example.com"));
743 assert!(URL_SIMPLE_REGEX.is_match("http://test.org/path"));
744 assert!(URL_SIMPLE_REGEX.is_match("ftp://files.com/file.zip"));
745 assert!(!URL_SIMPLE_REGEX.is_match("not a url"));
746 }
747
748 #[test]
749 fn test_url_quick_check() {
750 assert!(URL_QUICK_CHECK_REGEX.is_match("https://example.com"));
752 assert!(URL_QUICK_CHECK_REGEX.is_match("http://example.com"));
753 assert!(URL_QUICK_CHECK_REGEX.is_match("ftp://files.com"));
754 assert!(URL_QUICK_CHECK_REGEX.is_match("www.example.com"));
755 assert!(URL_QUICK_CHECK_REGEX.is_match("user@example.com"));
756 assert!(!URL_QUICK_CHECK_REGEX.is_match("just plain text"));
757 }
758
759 #[test]
760 fn test_url_edge_cases() {
761 let url = "https://example.com/path";
763 assert!(URL_STANDARD_REGEX.is_match(url));
764
765 let text = "Check https://example.com, it's great!";
768 let cap = URL_STANDARD_REGEX.find(text).unwrap();
769 assert!(cap.as_str().ends_with(','));
771
772 let text2 = "See <https://example.com> for more";
774 assert!(URL_STANDARD_REGEX.is_match(text2));
775
776 let cap2 = URL_STANDARD_REGEX.find(text2).unwrap();
778 assert!(!cap2.as_str().contains('>'));
779 }
780
781 #[test]
782 fn test_url_with_complex_paths() {
783 let urls = [
785 "https://github.com/owner/repo/blob/main/src/file.rs#L123",
786 "https://docs.example.com/api/v2/endpoint?format=json&page=1",
787 "https://cdn.example.com/assets/images/logo.png?v=2023",
788 "https://search.example.com/results?q=test+query&filter=all",
789 ];
790
791 for url in urls {
792 assert!(URL_STANDARD_REGEX.is_match(url), "Should match: {url}");
793 }
794 }
795
796 #[test]
797 fn test_url_pattern_strings_are_valid() {
798 assert!(URL_STANDARD_REGEX.is_match("https://example.com"));
800 assert!(URL_WWW_REGEX.is_match("www.example.com"));
801 assert!(URL_IPV6_REGEX.is_match("https://[::1]/"));
802 assert!(URL_QUICK_CHECK_REGEX.is_match("https://example.com"));
803 assert!(URL_SIMPLE_REGEX.is_match("https://example.com"));
804 }
805
806 #[test]
813 fn test_is_blank_in_blockquote_context_regular_blanks() {
814 assert!(is_blank_in_blockquote_context(""));
816 assert!(is_blank_in_blockquote_context(" "));
817 assert!(is_blank_in_blockquote_context("\t"));
818 assert!(is_blank_in_blockquote_context(" \t "));
819 }
820
821 #[test]
822 fn test_is_blank_in_blockquote_context_blockquote_blanks() {
823 assert!(is_blank_in_blockquote_context(">"));
825 assert!(is_blank_in_blockquote_context("> "));
826 assert!(is_blank_in_blockquote_context("> "));
827 assert!(is_blank_in_blockquote_context(">>"));
828 assert!(is_blank_in_blockquote_context(">> "));
829 assert!(is_blank_in_blockquote_context(">>>"));
830 assert!(is_blank_in_blockquote_context(">>> "));
831 }
832
833 #[test]
834 fn test_is_blank_in_blockquote_context_spaced_nested() {
835 assert!(is_blank_in_blockquote_context("> > "));
837 assert!(is_blank_in_blockquote_context("> > > "));
838 assert!(is_blank_in_blockquote_context("> > "));
839 }
840
841 #[test]
842 fn test_is_blank_in_blockquote_context_with_leading_space() {
843 assert!(is_blank_in_blockquote_context(" >"));
845 assert!(is_blank_in_blockquote_context(" > "));
846 assert!(is_blank_in_blockquote_context(" >>"));
847 }
848
849 #[test]
850 fn test_is_blank_in_blockquote_context_not_blank() {
851 assert!(!is_blank_in_blockquote_context("text"));
853 assert!(!is_blank_in_blockquote_context("> text"));
854 assert!(!is_blank_in_blockquote_context(">> text"));
855 assert!(!is_blank_in_blockquote_context("> | table |"));
856 assert!(!is_blank_in_blockquote_context("| table |"));
857 assert!(!is_blank_in_blockquote_context("> # Heading"));
858 assert!(!is_blank_in_blockquote_context(">text")); }
860
861 #[test]
862 fn test_is_blank_in_blockquote_context_edge_cases() {
863 assert!(!is_blank_in_blockquote_context(">a")); assert!(!is_blank_in_blockquote_context("> a")); assert!(is_blank_in_blockquote_context("> ")); assert!(!is_blank_in_blockquote_context("> text")); }
869
870 #[test]
871 fn test_is_blank_in_blockquote_context_deeply_nested_no_stack_overflow() {
872 let blank_markers = "> ".repeat(500_000);
878 assert!(is_blank_in_blockquote_context(&blank_markers));
879
880 let markers_with_content = format!("{}text", "> ".repeat(500_000));
883 assert!(!is_blank_in_blockquote_context(&markers_with_content));
884 }
885}