rumdl_lib/utils/
regex_cache.rs1use fancy_regex::Regex as FancyRegex;
22use regex::Regex;
23use std::collections::HashMap;
24use std::sync::LazyLock;
25use std::sync::{Arc, Mutex};
26
27#[derive(Debug)]
29pub struct RegexCache {
30 cache: HashMap<String, (Arc<Regex>, u64)>,
31}
32
33impl Default for RegexCache {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl RegexCache {
40 pub fn new() -> Self {
41 Self { cache: HashMap::new() }
42 }
43
44 pub fn get_regex(&mut self, pattern: &str) -> Result<Arc<Regex>, regex::Error> {
46 if let Some((regex, uses)) = self.cache.get_mut(pattern) {
47 *uses += 1;
48 return Ok(regex.clone());
49 }
50
51 let regex = Arc::new(Regex::new(pattern)?);
52 self.cache.insert(pattern.to_string(), (regex.clone(), 1));
53 Ok(regex)
54 }
55
56 pub fn get_stats(&self) -> HashMap<String, u64> {
58 self.cache
59 .iter()
60 .map(|(pattern, (_, uses))| (pattern.clone(), *uses))
61 .collect()
62 }
63
64 pub fn clear(&mut self) {
66 self.cache.clear();
67 }
68}
69
70static GLOBAL_REGEX_CACHE: LazyLock<Mutex<RegexCache>> = LazyLock::new(|| Mutex::new(RegexCache::new()));
72
73pub fn get_cached_regex(pattern: &str) -> Result<Arc<Regex>, regex::Error> {
79 let mut cache = GLOBAL_REGEX_CACHE.lock().unwrap_or_else(|poisoned| {
80 let mut guard = poisoned.into_inner();
82 guard.clear();
83 guard
84 });
85 cache.get_regex(pattern)
86}
87
88pub fn get_cache_stats() -> HashMap<String, u64> {
92 match GLOBAL_REGEX_CACHE.lock() {
93 Ok(cache) => cache.get_stats(),
94 Err(_) => HashMap::new(),
95 }
96}
97
98#[macro_export]
117macro_rules! regex_lazy {
118 ($pattern:expr) => {{
119 static REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new($pattern).unwrap());
120 &*REGEX
121 }};
122}
123
124#[macro_export]
131macro_rules! regex_cached {
132 ($pattern:expr) => {{ $crate::utils::regex_cache::get_cached_regex($pattern).expect("Failed to compile regex") }};
133}
134
135pub use crate::regex_lazy;
137
138pub const URL_STANDARD_STR: &str = concat!(
172 r#"(?:https?|ftps?|ftp)://"#, r#"(?:"#,
174 r#"\[[0-9a-fA-F:%.\-a-zA-Z]+\]"#, r#"|"#,
176 r#"[^\s<>\[\]()\\'\"`/]+"#, r#")"#,
178 r#"(?::\d+)?"#, r#"(?:/[^\s<>\[\]\\'\"`]*)?"#, r#"(?:\?[^\s<>\[\]\\'\"`]*)?"#, r#"(?:#[^\s<>\[\]\\'\"`]*)?"#, );
183
184pub const URL_WWW_STR: &str = concat!(
197 r#"www\.(?:[a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}"#, r#"(?::\d+)?"#, r#"(?:/[^\s<>\[\]\\'\"`]*)?"#, r#"(?:\?[^\s<>\[\]\\'\"`]*)?"#, r#"(?:#[^\s<>\[\]\\'\"`]*)?"#, );
203
204pub const URL_IPV6_STR: &str = concat!(
209 r#"(?:https?|ftps?|ftp)://"#,
210 r#"\[[0-9a-fA-F:%.\-a-zA-Z]+\]"#, r#"(?::\d+)?"#, r#"(?:/[^\s<>\[\]\\'\"`]*)?"#, r#"(?:\?[^\s<>\[\]\\'\"`]*)?"#, r#"(?:#[^\s<>\[\]\\'\"`]*)?"#, );
216
217pub const XMPP_URI_STR: &str = r#"xmpp:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s<>\[\]\\'\"`]*)?"#;
226
227pub const URL_QUICK_CHECK_STR: &str = r#"(?:https?|ftps?|ftp|xmpp)://|xmpp:|@|www\."#;
233
234pub const URL_SIMPLE_STR: &str = r#"(?:https?|ftps?|ftp)://[^\s<>]+[^\s<>.,]"#;
240
241pub static URL_STANDARD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_STANDARD_STR).unwrap());
246
247pub static URL_WWW_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_WWW_STR).unwrap());
250
251pub static URL_IPV6_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_IPV6_STR).unwrap());
254
255pub static URL_QUICK_CHECK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_QUICK_CHECK_STR).unwrap());
258
259pub static URL_SIMPLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(URL_SIMPLE_STR).unwrap());
262
263pub static URL_PATTERN: LazyLock<Regex> = LazyLock::new(|| URL_SIMPLE_REGEX.clone());
265
266pub static XMPP_URI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(XMPP_URI_STR).unwrap());
269
270pub static ATX_HEADING_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)(#{1,6})(\s+|$)").unwrap());
272
273pub static UNORDERED_LIST_MARKER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)([*+-])(\s+)").unwrap());
275pub static ORDERED_LIST_MARKER_REGEX: LazyLock<Regex> =
276 LazyLock::new(|| Regex::new(r"^(\s*)(\d+)([.)])(\s+)").unwrap());
277
278pub static ASTERISK_EMPHASIS: LazyLock<Regex> =
284 LazyLock::new(|| Regex::new(r"(?:^|[^*])\*(\s+[^*]+\s*|\s*[^*]+\s+)\*(?:[^*]|$)").unwrap());
285pub static UNDERSCORE_EMPHASIS: LazyLock<Regex> =
286 LazyLock::new(|| Regex::new(r"(?:^|[^_])_(\s+[^_]+\s*|\s*[^_]+\s+)_(?:[^_]|$)").unwrap());
287pub static DOUBLE_UNDERSCORE_EMPHASIS: LazyLock<Regex> =
288 LazyLock::new(|| Regex::new(r"(?:^|[^_])__(\s+[^_]+\s*|\s*[^_]+\s+)__(?:[^_]|$)").unwrap());
289pub static FENCED_CODE_BLOCK_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)```(?:[^`\r\n]*)$").unwrap());
291pub static FENCED_CODE_BLOCK_END: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)```\s*$").unwrap());
292
293pub static HTML_TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<([a-zA-Z][^>]*)>").unwrap());
295pub static HTML_TAG_QUICK_CHECK: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)</?[a-zA-Z]").unwrap());
296
297pub static IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap());
299
300pub static BLOCKQUOTE_PREFIX_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*>+\s*)").unwrap());
302
303pub fn is_blank_in_blockquote_context(line: &str) -> bool {
326 let mut rest = line;
332 loop {
333 if rest.trim().is_empty() {
334 return true;
335 }
336 match BLOCKQUOTE_PREFIX_RE.find(rest) {
337 Some(m) if m.end() > 0 => rest = &rest[m.end()..],
340 _ => return false,
341 }
342 }
343}
344
345pub static IMAGE_REF_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^!\[.*?\]\[.*?\]$").unwrap());
347pub static LINK_REF_PATTERN: LazyLock<Regex> =
348 LazyLock::new(|| Regex::new(r#"^\[.*?\]:\s*\S+(\s+["'(].*)?\s*$"#).unwrap());
349pub static ABBREVIATION: LazyLock<Regex> = LazyLock::new(|| {
350 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()
351});
352pub static LIST_ITEM: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\d+\.\s+").unwrap());
353
354pub static EMAIL_PATTERN: LazyLock<Regex> =
356 LazyLock::new(|| Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap());
357
358pub static REF_LINK_REGEX: LazyLock<FancyRegex> =
362 LazyLock::new(|| FancyRegex::new(r"(?<!\\)\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]\[([^\]]*)\]").unwrap());
363
364pub static SHORTCUT_REF_REGEX: LazyLock<FancyRegex> =
369 LazyLock::new(|| FancyRegex::new(r"(?<![\\)\]])\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\](?!\s*[\[\(])").unwrap());
370
371pub static INLINE_LINK_FANCY_REGEX: LazyLock<FancyRegex> =
373 LazyLock::new(|| FancyRegex::new(r"(?<!\\)\[([^\]]+)\]\(([^)]+)\)").unwrap());
374
375pub static INLINE_IMAGE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap());
377
378pub static LINKED_IMAGE_INLINE_INLINE: LazyLock<Regex> =
386 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)").unwrap());
387
388pub static LINKED_IMAGE_REF_INLINE: LazyLock<Regex> =
390 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\[([^\]]*)\]\]\(([^)]+)\)").unwrap());
391
392pub static LINKED_IMAGE_INLINE_REF: LazyLock<Regex> =
394 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\(([^)]+)\)\]\[([^\]]*)\]").unwrap());
395
396pub static LINKED_IMAGE_REF_REF: LazyLock<Regex> =
398 LazyLock::new(|| Regex::new(r"\[!\[([^\]]*)\]\[([^\]]*)\]\]\[([^\]]*)\]").unwrap());
399
400pub static REF_IMAGE_REGEX: LazyLock<Regex> =
402 LazyLock::new(|| Regex::new(r"!\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]\[([^\]]*)\]").unwrap());
403
404pub static FOOTNOTE_REF_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[\^([^\]]+)\]").unwrap());
406
407pub static WIKI_LINK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[\[([^\]]+)\]\]").unwrap());
409
410pub static INLINE_MATH_REGEX: LazyLock<FancyRegex> =
412 LazyLock::new(|| FancyRegex::new(r"(?<!\$)\$(?!\$)([^\$]+)\$(?!\$)").unwrap());
413pub static DISPLAY_MATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$([^\$]+)\$\$").unwrap());
414
415pub static EMOJI_SHORTCODE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":([a-zA-Z0-9_+-]+):").unwrap());
417
418pub static HTML_TAG_PATTERN: LazyLock<Regex> =
420 LazyLock::new(|| Regex::new(r"</?[a-zA-Z][^>]*>|<[a-zA-Z][^>]*/\s*>").unwrap());
421
422pub static HTML_ENTITY_REGEX: LazyLock<Regex> =
424 LazyLock::new(|| Regex::new(r"&[a-zA-Z][a-zA-Z0-9]*;|&#\d+;|&#[xX][0-9a-fA-F]+;").unwrap());
425
426pub static HUGO_SHORTCODE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{[<%][\s\S]*?[%>]\}\}").unwrap());
430
431pub static HTML_HEADING_PATTERN: LazyLock<FancyRegex> =
434 LazyLock::new(|| FancyRegex::new(r"^\s*<h([1-6])(?:\s[^>]*)?>.*</h\1>\s*$").unwrap());
435
436pub fn escape_regex(s: &str) -> String {
438 let mut result = String::with_capacity(s.len() * 2);
439
440 for c in s.chars() {
441 if matches!(
443 c,
444 '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\'
445 ) {
446 result.push('\\');
447 }
448 result.push(c);
449 }
450
451 result
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_regex_cache_new() {
460 let cache = RegexCache::new();
461 assert!(cache.cache.is_empty());
462 assert!(cache.get_stats().is_empty());
463 }
464
465 #[test]
466 fn test_regex_cache_default() {
467 let cache = RegexCache::default();
468 assert!(cache.cache.is_empty());
469 assert!(cache.get_stats().is_empty());
470 }
471
472 #[test]
473 fn test_get_regex_compilation() {
474 let mut cache = RegexCache::new();
475
476 let regex1 = cache.get_regex(r"\d+").unwrap();
478 assert_eq!(cache.cache.len(), 1);
479 assert_eq!(cache.get_stats().get(r"\d+"), Some(&1));
480
481 let regex2 = cache.get_regex(r"\d+").unwrap();
483 assert_eq!(cache.cache.len(), 1);
484 assert_eq!(cache.get_stats().get(r"\d+"), Some(&2));
485
486 assert!(Arc::ptr_eq(®ex1, ®ex2));
488 }
489
490 #[test]
491 fn test_get_regex_invalid_pattern() {
492 let mut cache = RegexCache::new();
493 let result = cache.get_regex(r"[unterminated");
494 assert!(result.is_err());
495 assert!(cache.cache.is_empty());
496 }
497
498 #[test]
499 fn test_get_stats() {
500 let mut cache = RegexCache::new();
501
502 let _ = cache.get_regex(r"\d+").unwrap();
504 let _ = cache.get_regex(r"\d+").unwrap();
505 let _ = cache.get_regex(r"\w+").unwrap();
506
507 let stats = cache.get_stats();
508 assert_eq!(stats.get(r"\d+"), Some(&2));
509 assert_eq!(stats.get(r"\w+"), Some(&1));
510 }
511
512 #[test]
513 fn test_clear_cache() {
514 let mut cache = RegexCache::new();
515
516 let _ = cache.get_regex(r"\d+").unwrap();
518
519 assert!(!cache.cache.is_empty());
520 assert!(!cache.get_stats().is_empty());
521
522 cache.clear();
524
525 assert!(cache.cache.is_empty());
526 assert!(cache.get_stats().is_empty());
527 }
528
529 #[test]
530 fn test_global_cache_functions() {
531 let regex1 = get_cached_regex(r"\d{3}").unwrap();
533 let regex2 = get_cached_regex(r"\d{3}").unwrap();
534 assert!(Arc::ptr_eq(®ex1, ®ex2));
535
536 let stats = get_cache_stats();
538 assert!(stats.contains_key(r"\d{3}"));
539 }
540
541 #[test]
542 fn test_regex_lazy_macro() {
543 let re = regex_lazy!(r"^test.*end$");
544 assert!(re.is_match("test something end"));
545 assert!(!re.is_match("test something"));
546
547 let re2 = regex_lazy!(r"^start.*finish$");
551 assert!(re2.is_match("start and finish"));
552 assert!(!re2.is_match("start without end"));
553 }
554
555 #[test]
556 fn test_escape_regex() {
557 assert_eq!(escape_regex("a.b"), "a\\.b");
558 assert_eq!(escape_regex("a+b*c"), "a\\+b\\*c");
559 assert_eq!(escape_regex("(test)"), "\\(test\\)");
560 assert_eq!(escape_regex("[a-z]"), "\\[a-z\\]");
561 assert_eq!(escape_regex("normal text"), "normal text");
562
563 assert_eq!(escape_regex(".$^{[(|)*+?\\"), "\\.\\$\\^\\{\\[\\(\\|\\)\\*\\+\\?\\\\");
565
566 assert_eq!(escape_regex(""), "");
568
569 assert_eq!(escape_regex("test.com/path?query=1"), "test\\.com/path\\?query=1");
571 }
572
573 #[test]
574 fn test_static_regex_patterns() {
575 assert!(URL_SIMPLE_REGEX.is_match("https://example.com"));
577 assert!(URL_SIMPLE_REGEX.is_match("http://test.org/path"));
578 assert!(URL_SIMPLE_REGEX.is_match("ftp://files.com"));
579 assert!(!URL_SIMPLE_REGEX.is_match("not a url"));
580
581 assert!(ATX_HEADING_REGEX.is_match("# Heading"));
583 assert!(ATX_HEADING_REGEX.is_match(" ## Indented"));
584 assert!(ATX_HEADING_REGEX.is_match("### "));
585 assert!(!ATX_HEADING_REGEX.is_match("Not a heading"));
586
587 assert!(UNORDERED_LIST_MARKER_REGEX.is_match("* Item"));
589 assert!(UNORDERED_LIST_MARKER_REGEX.is_match("- Item"));
590 assert!(UNORDERED_LIST_MARKER_REGEX.is_match("+ Item"));
591 assert!(ORDERED_LIST_MARKER_REGEX.is_match("1. Item"));
592 assert!(ORDERED_LIST_MARKER_REGEX.is_match("99. Item"));
593
594 assert!(HTML_TAG_REGEX.is_match("<div>"));
596 assert!(HTML_TAG_REGEX.is_match("<span class='test'>"));
597
598 assert!(BLOCKQUOTE_PREFIX_RE.is_match("> Quote"));
600 assert!(BLOCKQUOTE_PREFIX_RE.is_match(" > Indented quote"));
601 assert!(BLOCKQUOTE_PREFIX_RE.is_match(">> Nested"));
602 }
603
604 #[test]
605 fn test_thread_safety() {
606 use std::thread;
607
608 let handles: Vec<_> = (0..10)
609 .map(|i| {
610 thread::spawn(move || {
611 let pattern = format!(r"\d{{{i}}}");
612 let regex = get_cached_regex(&pattern).unwrap();
613 assert!(regex.is_match(&"1".repeat(i)));
614 })
615 })
616 .collect();
617
618 for handle in handles {
619 handle.join().unwrap();
620 }
621 }
622
623 #[test]
628 fn test_url_standard_basic() {
629 assert!(URL_STANDARD_REGEX.is_match("https://example.com"));
631 assert!(URL_STANDARD_REGEX.is_match("http://example.com"));
632 assert!(URL_STANDARD_REGEX.is_match("https://example.com/"));
633 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path"));
634 assert!(URL_STANDARD_REGEX.is_match("ftp://files.example.com"));
635 assert!(URL_STANDARD_REGEX.is_match("ftps://secure.example.com"));
636
637 assert!(!URL_STANDARD_REGEX.is_match("not a url"));
639 assert!(!URL_STANDARD_REGEX.is_match("example.com"));
640 assert!(!URL_STANDARD_REGEX.is_match("www.example.com"));
641 }
642
643 #[test]
644 fn test_url_standard_with_path() {
645 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path/to/page"));
646 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path/to/page.html"));
647 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path/to/page/"));
648 }
649
650 #[test]
651 fn test_url_standard_with_query() {
652 assert!(URL_STANDARD_REGEX.is_match("https://example.com?query=value"));
653 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path?query=value"));
654 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path?a=1&b=2"));
655 }
656
657 #[test]
658 fn test_url_standard_with_fragment() {
659 assert!(URL_STANDARD_REGEX.is_match("https://example.com#section"));
660 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path#section"));
661 assert!(URL_STANDARD_REGEX.is_match("https://example.com/path?query=value#section"));
662 }
663
664 #[test]
665 fn test_url_standard_with_port() {
666 assert!(URL_STANDARD_REGEX.is_match("https://example.com:8080"));
667 assert!(URL_STANDARD_REGEX.is_match("https://example.com:443/path"));
668 assert!(URL_STANDARD_REGEX.is_match("http://localhost:3000"));
669 assert!(URL_STANDARD_REGEX.is_match("https://192.168.1.1:8080/path"));
670 }
671
672 #[test]
673 fn test_url_standard_wikipedia_style_parentheses() {
674 let url = "https://en.wikipedia.org/wiki/Rust_(programming_language)";
676 assert!(URL_STANDARD_REGEX.is_match(url));
677
678 let cap = URL_STANDARD_REGEX.find(url).unwrap();
680 assert_eq!(cap.as_str(), url);
681
682 let url2 = "https://example.com/path_(foo)_(bar)";
684 let cap2 = URL_STANDARD_REGEX.find(url2).unwrap();
685 assert_eq!(cap2.as_str(), url2);
686 }
687
688 #[test]
689 fn test_url_standard_ipv6() {
690 assert!(URL_STANDARD_REGEX.is_match("https://[::1]/path"));
692 assert!(URL_STANDARD_REGEX.is_match("https://[2001:db8::1]:8080/path"));
693 assert!(URL_STANDARD_REGEX.is_match("http://[fe80::1%eth0]/"));
694 }
695
696 #[test]
697 fn test_url_www_basic() {
698 assert!(URL_WWW_REGEX.is_match("www.example.com"));
700 assert!(URL_WWW_REGEX.is_match("www.example.co.uk"));
701 assert!(URL_WWW_REGEX.is_match("www.sub.example.com"));
702
703 assert!(!URL_WWW_REGEX.is_match("example.com"));
705
706 assert!(URL_WWW_REGEX.is_match("https://www.example.com"));
710 }
711
712 #[test]
713 fn test_url_www_with_path() {
714 assert!(URL_WWW_REGEX.is_match("www.example.com/path"));
715 assert!(URL_WWW_REGEX.is_match("www.example.com/path/to/page"));
716 assert!(URL_WWW_REGEX.is_match("www.example.com/path_(with_parens)"));
717 }
718
719 #[test]
720 fn test_url_ipv6_basic() {
721 assert!(URL_IPV6_REGEX.is_match("https://[::1]/"));
723 assert!(URL_IPV6_REGEX.is_match("http://[2001:db8::1]/path"));
724 assert!(URL_IPV6_REGEX.is_match("https://[fe80::1]:8080/path"));
725 assert!(URL_IPV6_REGEX.is_match("ftp://[::ffff:192.168.1.1]/file"));
726 }
727
728 #[test]
729 fn test_url_ipv6_with_zone_id() {
730 assert!(URL_IPV6_REGEX.is_match("https://[fe80::1%eth0]/path"));
732 assert!(URL_IPV6_REGEX.is_match("http://[fe80::1%25eth0]:8080/"));
733 }
734
735 #[test]
736 fn test_url_simple_detection() {
737 assert!(URL_SIMPLE_REGEX.is_match("https://example.com"));
739 assert!(URL_SIMPLE_REGEX.is_match("http://test.org/path"));
740 assert!(URL_SIMPLE_REGEX.is_match("ftp://files.com/file.zip"));
741 assert!(!URL_SIMPLE_REGEX.is_match("not a url"));
742 }
743
744 #[test]
745 fn test_url_quick_check() {
746 assert!(URL_QUICK_CHECK_REGEX.is_match("https://example.com"));
748 assert!(URL_QUICK_CHECK_REGEX.is_match("http://example.com"));
749 assert!(URL_QUICK_CHECK_REGEX.is_match("ftp://files.com"));
750 assert!(URL_QUICK_CHECK_REGEX.is_match("www.example.com"));
751 assert!(URL_QUICK_CHECK_REGEX.is_match("user@example.com"));
752 assert!(!URL_QUICK_CHECK_REGEX.is_match("just plain text"));
753 }
754
755 #[test]
756 fn test_url_edge_cases() {
757 let url = "https://example.com/path";
759 assert!(URL_STANDARD_REGEX.is_match(url));
760
761 let text = "Check https://example.com, it's great!";
764 let cap = URL_STANDARD_REGEX.find(text).unwrap();
765 assert!(cap.as_str().ends_with(','));
767
768 let text2 = "See <https://example.com> for more";
770 assert!(URL_STANDARD_REGEX.is_match(text2));
771
772 let cap2 = URL_STANDARD_REGEX.find(text2).unwrap();
774 assert!(!cap2.as_str().contains('>'));
775 }
776
777 #[test]
778 fn test_url_with_complex_paths() {
779 let urls = [
781 "https://github.com/owner/repo/blob/main/src/file.rs#L123",
782 "https://docs.example.com/api/v2/endpoint?format=json&page=1",
783 "https://cdn.example.com/assets/images/logo.png?v=2023",
784 "https://search.example.com/results?q=test+query&filter=all",
785 ];
786
787 for url in urls {
788 assert!(URL_STANDARD_REGEX.is_match(url), "Should match: {url}");
789 }
790 }
791
792 #[test]
793 fn test_url_pattern_strings_are_valid() {
794 assert!(URL_STANDARD_REGEX.is_match("https://example.com"));
796 assert!(URL_WWW_REGEX.is_match("www.example.com"));
797 assert!(URL_IPV6_REGEX.is_match("https://[::1]/"));
798 assert!(URL_QUICK_CHECK_REGEX.is_match("https://example.com"));
799 assert!(URL_SIMPLE_REGEX.is_match("https://example.com"));
800 }
801
802 #[test]
809 fn test_is_blank_in_blockquote_context_regular_blanks() {
810 assert!(is_blank_in_blockquote_context(""));
812 assert!(is_blank_in_blockquote_context(" "));
813 assert!(is_blank_in_blockquote_context("\t"));
814 assert!(is_blank_in_blockquote_context(" \t "));
815 }
816
817 #[test]
818 fn test_is_blank_in_blockquote_context_blockquote_blanks() {
819 assert!(is_blank_in_blockquote_context(">"));
821 assert!(is_blank_in_blockquote_context("> "));
822 assert!(is_blank_in_blockquote_context("> "));
823 assert!(is_blank_in_blockquote_context(">>"));
824 assert!(is_blank_in_blockquote_context(">> "));
825 assert!(is_blank_in_blockquote_context(">>>"));
826 assert!(is_blank_in_blockquote_context(">>> "));
827 }
828
829 #[test]
830 fn test_is_blank_in_blockquote_context_spaced_nested() {
831 assert!(is_blank_in_blockquote_context("> > "));
833 assert!(is_blank_in_blockquote_context("> > > "));
834 assert!(is_blank_in_blockquote_context("> > "));
835 }
836
837 #[test]
838 fn test_is_blank_in_blockquote_context_with_leading_space() {
839 assert!(is_blank_in_blockquote_context(" >"));
841 assert!(is_blank_in_blockquote_context(" > "));
842 assert!(is_blank_in_blockquote_context(" >>"));
843 }
844
845 #[test]
846 fn test_is_blank_in_blockquote_context_not_blank() {
847 assert!(!is_blank_in_blockquote_context("text"));
849 assert!(!is_blank_in_blockquote_context("> text"));
850 assert!(!is_blank_in_blockquote_context(">> text"));
851 assert!(!is_blank_in_blockquote_context("> | table |"));
852 assert!(!is_blank_in_blockquote_context("| table |"));
853 assert!(!is_blank_in_blockquote_context("> # Heading"));
854 assert!(!is_blank_in_blockquote_context(">text")); }
856
857 #[test]
858 fn test_is_blank_in_blockquote_context_edge_cases() {
859 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")); }
865
866 #[test]
867 fn test_is_blank_in_blockquote_context_deeply_nested_no_stack_overflow() {
868 let blank_markers = "> ".repeat(500_000);
874 assert!(is_blank_in_blockquote_context(&blank_markers));
875
876 let markers_with_content = format!("{}text", "> ".repeat(500_000));
879 assert!(!is_blank_in_blockquote_context(&markers_with_content));
880 }
881}