rumdl/utils/
regex_cache.rs1use fancy_regex::Regex as FancyRegex;
23use lazy_static::lazy_static;
24use regex::Regex;
25use std::collections::HashMap;
26use std::sync::{Arc, Mutex};
27
28#[derive(Debug)]
30pub struct RegexCache {
31 cache: HashMap<String, Arc<Regex>>,
32 fancy_cache: HashMap<String, Arc<FancyRegex>>,
33 usage_stats: HashMap<String, u64>,
34}
35
36impl Default for RegexCache {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl RegexCache {
43 pub fn new() -> Self {
44 Self {
45 cache: HashMap::new(),
46 fancy_cache: HashMap::new(),
47 usage_stats: HashMap::new(),
48 }
49 }
50
51 pub fn get_regex(&mut self, pattern: &str) -> Result<Arc<Regex>, regex::Error> {
53 if let Some(regex) = self.cache.get(pattern) {
54 *self.usage_stats.entry(pattern.to_string()).or_insert(0) += 1;
55 return Ok(regex.clone());
56 }
57
58 let regex = Arc::new(Regex::new(pattern)?);
59 self.cache.insert(pattern.to_string(), regex.clone());
60 *self.usage_stats.entry(pattern.to_string()).or_insert(0) += 1;
61 Ok(regex)
62 }
63
64 pub fn get_fancy_regex(&mut self, pattern: &str) -> Result<Arc<FancyRegex>, Box<fancy_regex::Error>> {
66 if let Some(regex) = self.fancy_cache.get(pattern) {
67 *self.usage_stats.entry(pattern.to_string()).or_insert(0) += 1;
68 return Ok(regex.clone());
69 }
70
71 match FancyRegex::new(pattern) {
72 Ok(regex) => {
73 let arc_regex = Arc::new(regex);
74 self.fancy_cache.insert(pattern.to_string(), arc_regex.clone());
75 *self.usage_stats.entry(pattern.to_string()).or_insert(0) += 1;
76 Ok(arc_regex)
77 }
78 Err(e) => Err(Box::new(e)),
79 }
80 }
81
82 pub fn get_stats(&self) -> HashMap<String, u64> {
84 self.usage_stats.clone()
85 }
86
87 pub fn clear(&mut self) {
89 self.cache.clear();
90 self.fancy_cache.clear();
91 self.usage_stats.clear();
92 }
93}
94
95lazy_static! {
96 static ref GLOBAL_REGEX_CACHE: Arc<Mutex<RegexCache>> = Arc::new(Mutex::new(RegexCache::new()));
98}
99
100pub fn get_cached_regex(pattern: &str) -> Result<Arc<Regex>, regex::Error> {
102 let mut cache = GLOBAL_REGEX_CACHE.lock().unwrap();
103 cache.get_regex(pattern)
104}
105
106pub fn get_cached_fancy_regex(pattern: &str) -> Result<Arc<FancyRegex>, Box<fancy_regex::Error>> {
108 let mut cache = GLOBAL_REGEX_CACHE.lock().unwrap();
109 cache.get_fancy_regex(pattern)
110}
111
112pub fn get_cache_stats() -> HashMap<String, u64> {
114 let cache = GLOBAL_REGEX_CACHE.lock().unwrap();
115 cache.get_stats()
116}
117
118#[macro_export]
127macro_rules! regex_lazy {
128 ($pattern:expr) => {{
129 lazy_static::lazy_static! {
130 static ref REGEX: regex::Regex = regex::Regex::new($pattern).unwrap();
131 }
132 &*REGEX
133 }};
134}
135
136#[macro_export]
138macro_rules! regex_cached {
139 ($pattern:expr) => {{ $crate::utils::regex_cache::get_cached_regex($pattern).expect("Failed to compile regex") }};
140}
141
142#[macro_export]
144macro_rules! fancy_regex_cached {
145 ($pattern:expr) => {{ $crate::utils::regex_cache::get_cached_fancy_regex($pattern).expect("Failed to compile fancy regex") }};
146}
147
148pub use crate::regex_lazy;
150
151lazy_static! {
152 pub static ref URL_REGEX: Regex = Regex::new(r#"(?:https?|ftp)://[^\s<>\[\]()'"]+[^\s<>\[\]()"'.,]"#).unwrap();
154 pub static ref BARE_URL_REGEX: Regex = Regex::new(r"(?:https?|ftp)://[^\s<>]+[^\s<>.]").unwrap();
155 pub static ref URL_PATTERN: Regex = Regex::new(r"((?:https?|ftp)://[^\s\)<>]+[^\s\)<>.,])").unwrap();
156
157 pub static ref ATX_HEADING_REGEX: Regex = Regex::new(r"^(\s*)(#{1,6})(\s+|$)").unwrap();
159 pub static ref CLOSED_ATX_HEADING_REGEX: Regex = Regex::new(r"^(\s*)(#{1,6})(\s+)(.*)(\s+)(#+)(\s*)$").unwrap();
160 pub static ref SETEXT_HEADING_REGEX: Regex = Regex::new(r"^(\s*)[^\s]+.*\n(\s*)(=+|-+)\s*$").unwrap();
161 pub static ref TRAILING_PUNCTUATION_REGEX: Regex = Regex::new(r"[.,:;!?]$").unwrap();
162
163 pub static ref ATX_HEADING_WITH_CAPTURE: Regex = Regex::new(r"^(#{1,6})\s+(.+?)(?:\s+#*\s*)?$").unwrap();
165 pub static ref SETEXT_HEADING_WITH_CAPTURE: FancyRegex = FancyRegex::new(r"^([^\n]+)\n([=\-])\2+\s*$").unwrap();
166
167 pub static ref UNORDERED_LIST_MARKER_REGEX: Regex = Regex::new(r"^(\s*)([*+-])(\s+)").unwrap();
169 pub static ref ORDERED_LIST_MARKER_REGEX: Regex = Regex::new(r"^(\s*)(\d+)([.)])(\s+)").unwrap();
170 pub static ref LIST_MARKER_ANY_REGEX: Regex = Regex::new(r"^(\s*)(?:([*+-])|(\d+)[.)])(\s+)").unwrap();
171
172 pub static ref FENCED_CODE_BLOCK_START_REGEX: Regex = Regex::new(r"^(\s*)(```|~~~)(.*)$").unwrap();
174 pub static ref FENCED_CODE_BLOCK_END_REGEX: Regex = Regex::new(r"^(\s*)(```|~~~)(\s*)$").unwrap();
175 pub static ref INDENTED_CODE_BLOCK_REGEX: Regex = Regex::new(r"^(\s{4,})(.*)$").unwrap();
176 pub static ref CODE_FENCE_REGEX: Regex = Regex::new(r"^(`{3,}|~{3,})").unwrap();
177
178 pub static ref EMPHASIS_REGEX: FancyRegex = FancyRegex::new(r"(\s|^)(\*{1,2}|_{1,2})(?=\S)(.+?)(?<=\S)(\2)(\s|$)").unwrap();
180 pub static ref SPACE_IN_EMPHASIS_REGEX: FancyRegex = FancyRegex::new(r"(\*|_)(\s+)(.+?)(\s+)(\1)").unwrap();
181
182 pub static ref ASTERISK_EMPHASIS: Regex = Regex::new(r"(?:^|[^*])\*(\s+[^*]+\s*|\s*[^*]+\s+)\*(?:[^*]|$)").unwrap();
186 pub static ref UNDERSCORE_EMPHASIS: Regex = Regex::new(r"(?:^|[^_])_(\s+[^_]+\s*|\s*[^_]+\s+)_(?:[^_]|$)").unwrap();
187 pub static ref DOUBLE_UNDERSCORE_EMPHASIS: Regex = Regex::new(r"(?:^|[^_])__(\s+[^_]+\s*|\s*[^_]+\s+)__(?:[^_]|$)").unwrap();
188 pub static ref DOUBLE_ASTERISK_EMPHASIS: FancyRegex = FancyRegex::new(r"\*\*\s+([^*]+?)\s+\*\*").unwrap();
189 pub static ref DOUBLE_ASTERISK_SPACE_START: FancyRegex = FancyRegex::new(r"\*\*\s+([^*]+?)\*\*").unwrap();
190 pub static ref DOUBLE_ASTERISK_SPACE_END: FancyRegex = FancyRegex::new(r"\*\*([^*]+?)\s+\*\*").unwrap();
191
192 pub static ref FENCED_CODE_BLOCK_START: Regex = Regex::new(r"^(\s*)```(?:[^`\r\n]*)$").unwrap();
194 pub static ref FENCED_CODE_BLOCK_END: Regex = Regex::new(r"^(\s*)```\s*$").unwrap();
195 pub static ref ALTERNATE_FENCED_CODE_BLOCK_START: Regex = Regex::new(r"^(\s*)~~~(?:[^~\r\n]*)$").unwrap();
196 pub static ref ALTERNATE_FENCED_CODE_BLOCK_END: Regex = Regex::new(r"^(\s*)~~~\s*$").unwrap();
197 pub static ref INDENTED_CODE_BLOCK_PATTERN: Regex = Regex::new(r"^(\s{4,})").unwrap();
198
199 pub static ref HTML_TAG_REGEX: Regex = Regex::new(r"<([a-zA-Z][^>]*)>").unwrap();
201 pub static ref HTML_SELF_CLOSING_TAG_REGEX: Regex = Regex::new(r"<([a-zA-Z][^>]*/)>").unwrap();
202 pub static ref HTML_TAG_FINDER: Regex = Regex::new("(?i)</?[a-zA-Z][^>]*>").unwrap();
203 pub static ref HTML_TAG_QUICK_CHECK: Regex = Regex::new("(?i)</?[a-zA-Z]").unwrap();
204
205 pub static ref LINK_REFERENCE_DEFINITION_REGEX: Regex = Regex::new(r"^\s*\[([^\]]+)\]:\s+(.+)$").unwrap();
207 pub static ref INLINE_LINK_REGEX: Regex = Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap();
208 pub static ref LINK_TEXT_REGEX: Regex = Regex::new(r"\[([^\]]*)\]").unwrap();
209 pub static ref LINK_REGEX: FancyRegex = FancyRegex::new(r"(?<!\\)\[([^\]]*)\]\(([^)#]*)#([^)]+)\)").unwrap();
210 pub static ref EXTERNAL_URL_REGEX: FancyRegex = FancyRegex::new(r"^(https?://|ftp://|www\.|[^/]+\.[a-z]{2,})").unwrap();
211
212 pub static ref IMAGE_REGEX: Regex = Regex::new(r"!\[([^\]]*)\]\(([^)]+)\)").unwrap();
214
215 pub static ref TRAILING_WHITESPACE_REGEX: Regex = Regex::new(r"\s+$").unwrap();
217 pub static ref MULTIPLE_BLANK_LINES_REGEX: Regex = Regex::new(r"\n{3,}").unwrap();
218
219 pub static ref FRONT_MATTER_REGEX: Regex = Regex::new(r"^---\n.*?\n---\n").unwrap();
221
222 pub static ref INLINE_CODE_REGEX: FancyRegex = FancyRegex::new(r"`[^`]+`").unwrap();
224 pub static ref BOLD_ASTERISK_REGEX: Regex = Regex::new(r"\*\*(.+?)\*\*").unwrap();
225 pub static ref BOLD_UNDERSCORE_REGEX: Regex = Regex::new(r"__(.+?)__").unwrap();
226 pub static ref ITALIC_ASTERISK_REGEX: Regex = Regex::new(r"\*([^*]+?)\*").unwrap();
227 pub static ref ITALIC_UNDERSCORE_REGEX: Regex = Regex::new(r"_([^_]+?)_").unwrap();
228 pub static ref LINK_TEXT_FULL_REGEX: FancyRegex = FancyRegex::new(r"\[([^\]]*)\]\([^)]*\)").unwrap();
229 pub static ref STRIKETHROUGH_REGEX: Regex = Regex::new(r"~~(.+?)~~").unwrap();
230 pub static ref MULTIPLE_HYPHENS: Regex = Regex::new(r"-{2,}").unwrap();
231 pub static ref TOC_SECTION_START: Regex = Regex::new(r"^#+\s*(?:Table of Contents|Contents|TOC)\s*$").unwrap();
232
233 pub static ref BLOCKQUOTE_PREFIX_RE: Regex = Regex::new(r"^(\s*>+\s*)").unwrap();
235}
236
237pub fn has_heading_markers(content: &str) -> bool {
240 content.contains('#')
241}
242
243pub fn has_list_markers(content: &str) -> bool {
245 content.contains('*')
246 || content.contains('-')
247 || content.contains('+')
248 || (content.contains('.') && content.contains(|c: char| c.is_ascii_digit()))
249}
250
251pub fn has_code_block_markers(content: &str) -> bool {
253 content.contains("```") || content.contains("~~~") || content.contains("\n ")
254 }
256
257pub fn has_emphasis_markers(content: &str) -> bool {
259 content.contains('*') || content.contains('_')
260}
261
262pub fn has_html_tags(content: &str) -> bool {
264 content.contains('<') && (content.contains('>') || content.contains("/>"))
265}
266
267pub fn has_link_markers(content: &str) -> bool {
269 (content.contains('[') && content.contains(']'))
270 || content.contains("http://")
271 || content.contains("https://")
272 || content.contains("ftp://")
273}
274
275pub fn has_image_markers(content: &str) -> bool {
277 content.contains("![")
278}
279
280pub fn contains_url(content: &str) -> bool {
283 if !content.contains("://") {
285 return false;
286 }
287
288 let chars: Vec<char> = content.chars().collect();
289 let mut i = 0;
290
291 while i < chars.len() {
292 if i + 2 < chars.len()
294 && ((chars[i] == 'h' && chars[i + 1] == 't' && chars[i + 2] == 't')
295 || (chars[i] == 'f' && chars[i + 1] == 't' && chars[i + 2] == 'p'))
296 {
297 let mut j = i;
299 while j + 2 < chars.len() {
300 if chars[j] == ':' && chars[j + 1] == '/' && chars[j + 2] == '/' {
301 return true;
302 }
303 j += 1;
304
305 if j > i + 10 {
307 break;
308 }
309 }
310 }
311 i += 1;
312 }
313
314 false
315}
316
317pub fn escape_regex(s: &str) -> String {
319 let special_chars = ['.', '+', '*', '?', '^', '$', '(', ')', '[', ']', '{', '}', '|', '\\'];
320 let mut result = String::with_capacity(s.len() * 2);
321
322 for c in s.chars() {
323 if special_chars.contains(&c) {
324 result.push('\\');
325 }
326 result.push(c);
327 }
328
329 result
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_regex_cache_new() {
338 let cache = RegexCache::new();
339 assert!(cache.cache.is_empty());
340 assert!(cache.fancy_cache.is_empty());
341 assert!(cache.usage_stats.is_empty());
342 }
343
344 #[test]
345 fn test_regex_cache_default() {
346 let cache = RegexCache::default();
347 assert!(cache.cache.is_empty());
348 assert!(cache.fancy_cache.is_empty());
349 assert!(cache.usage_stats.is_empty());
350 }
351
352 #[test]
353 fn test_get_regex_compilation() {
354 let mut cache = RegexCache::new();
355
356 let regex1 = cache.get_regex(r"\d+").unwrap();
358 assert_eq!(cache.cache.len(), 1);
359 assert_eq!(cache.usage_stats.get(r"\d+"), Some(&1));
360
361 let regex2 = cache.get_regex(r"\d+").unwrap();
363 assert_eq!(cache.cache.len(), 1);
364 assert_eq!(cache.usage_stats.get(r"\d+"), Some(&2));
365
366 assert!(Arc::ptr_eq(®ex1, ®ex2));
368 }
369
370 #[test]
371 fn test_get_regex_invalid_pattern() {
372 let mut cache = RegexCache::new();
373 let result = cache.get_regex(r"[unterminated");
374 assert!(result.is_err());
375 assert!(cache.cache.is_empty());
376 }
377
378 #[test]
379 fn test_get_fancy_regex_compilation() {
380 let mut cache = RegexCache::new();
381
382 let regex1 = cache.get_fancy_regex(r"(?<=foo)bar").unwrap();
384 assert_eq!(cache.fancy_cache.len(), 1);
385 assert_eq!(cache.usage_stats.get(r"(?<=foo)bar"), Some(&1));
386
387 let regex2 = cache.get_fancy_regex(r"(?<=foo)bar").unwrap();
389 assert_eq!(cache.fancy_cache.len(), 1);
390 assert_eq!(cache.usage_stats.get(r"(?<=foo)bar"), Some(&2));
391
392 assert!(Arc::ptr_eq(®ex1, ®ex2));
394 }
395
396 #[test]
397 fn test_get_fancy_regex_invalid_pattern() {
398 let mut cache = RegexCache::new();
399 let result = cache.get_fancy_regex(r"(?<=invalid");
400 assert!(result.is_err());
401 assert!(cache.fancy_cache.is_empty());
402 }
403
404 #[test]
405 fn test_get_stats() {
406 let mut cache = RegexCache::new();
407
408 let _ = cache.get_regex(r"\d+").unwrap();
410 let _ = cache.get_regex(r"\d+").unwrap();
411 let _ = cache.get_regex(r"\w+").unwrap();
412 let _ = cache.get_fancy_regex(r"(?<=foo)bar").unwrap();
413
414 let stats = cache.get_stats();
415 assert_eq!(stats.get(r"\d+"), Some(&2));
416 assert_eq!(stats.get(r"\w+"), Some(&1));
417 assert_eq!(stats.get(r"(?<=foo)bar"), Some(&1));
418 }
419
420 #[test]
421 fn test_clear_cache() {
422 let mut cache = RegexCache::new();
423
424 let _ = cache.get_regex(r"\d+").unwrap();
426 let _ = cache.get_fancy_regex(r"(?<=foo)bar").unwrap();
427
428 assert!(!cache.cache.is_empty());
429 assert!(!cache.fancy_cache.is_empty());
430 assert!(!cache.usage_stats.is_empty());
431
432 cache.clear();
434
435 assert!(cache.cache.is_empty());
436 assert!(cache.fancy_cache.is_empty());
437 assert!(cache.usage_stats.is_empty());
438 }
439
440 #[test]
441 fn test_global_cache_functions() {
442 let regex1 = get_cached_regex(r"\d{3}").unwrap();
444 let regex2 = get_cached_regex(r"\d{3}").unwrap();
445 assert!(Arc::ptr_eq(®ex1, ®ex2));
446
447 let fancy1 = get_cached_fancy_regex(r"(?<=test)ing").unwrap();
449 let fancy2 = get_cached_fancy_regex(r"(?<=test)ing").unwrap();
450 assert!(Arc::ptr_eq(&fancy1, &fancy2));
451
452 let stats = get_cache_stats();
454 assert!(stats.contains_key(r"\d{3}"));
455 assert!(stats.contains_key(r"(?<=test)ing"));
456 }
457
458 #[test]
459 fn test_regex_lazy_macro() {
460 let re = regex_lazy!(r"^test.*end$");
461 assert!(re.is_match("test something end"));
462 assert!(!re.is_match("test something"));
463
464 let re2 = regex_lazy!(r"^start.*finish$");
468 assert!(re2.is_match("start and finish"));
469 assert!(!re2.is_match("start without end"));
470 }
471
472 #[test]
473 fn test_has_heading_markers() {
474 assert!(has_heading_markers("# Heading"));
475 assert!(has_heading_markers("Text with # symbol"));
476 assert!(!has_heading_markers("Text without heading marker"));
477 }
478
479 #[test]
480 fn test_has_list_markers() {
481 assert!(has_list_markers("* Item"));
482 assert!(has_list_markers("- Item"));
483 assert!(has_list_markers("+ Item"));
484 assert!(has_list_markers("1. Item"));
485 assert!(!has_list_markers("Text without list markers"));
486 }
487
488 #[test]
489 fn test_has_code_block_markers() {
490 assert!(has_code_block_markers("```code```"));
491 assert!(has_code_block_markers("~~~code~~~"));
492 assert!(has_code_block_markers("Text\n indented code"));
493 assert!(!has_code_block_markers("Text without code blocks"));
494 }
495
496 #[test]
497 fn test_has_emphasis_markers() {
498 assert!(has_emphasis_markers("*emphasis*"));
499 assert!(has_emphasis_markers("_emphasis_"));
500 assert!(has_emphasis_markers("**bold**"));
501 assert!(has_emphasis_markers("__bold__"));
502 assert!(!has_emphasis_markers("no emphasis"));
503 }
504
505 #[test]
506 fn test_has_html_tags() {
507 assert!(has_html_tags("<div>content</div>"));
508 assert!(has_html_tags("<br/>"));
509 assert!(has_html_tags("<img src='test.jpg'>"));
510 assert!(!has_html_tags("no html tags"));
511 assert!(!has_html_tags("less than < but no tag"));
512 }
513
514 #[test]
515 fn test_has_link_markers() {
516 assert!(has_link_markers("[text](url)"));
517 assert!(has_link_markers("[reference][1]"));
518 assert!(has_link_markers("http://example.com"));
519 assert!(has_link_markers("https://example.com"));
520 assert!(has_link_markers("ftp://example.com"));
521 assert!(!has_link_markers("no links here"));
522 }
523
524 #[test]
525 fn test_has_image_markers() {
526 assert!(has_image_markers(""));
527 assert!(has_image_markers(""));
528 assert!(!has_image_markers("[link](url)"));
529 assert!(!has_image_markers("no images"));
530 }
531
532 #[test]
533 fn test_contains_url() {
534 assert!(contains_url("http://example.com"));
535 assert!(contains_url("Text with https://example.com link"));
536 assert!(contains_url("ftp://example.com"));
537 assert!(!contains_url("Text without URL"));
538 assert!(!contains_url("http not followed by ://"));
539
540 assert!(!contains_url("http"));
542 assert!(!contains_url("https"));
543 assert!(!contains_url("://"));
544 assert!(contains_url("Visit http://site.com now"));
545 assert!(contains_url("See https://secure.site.com/path"));
546 }
547
548 #[test]
549 fn test_contains_url_performance() {
550 let long_text = "a".repeat(10000);
552 assert!(!contains_url(&long_text));
553
554 let text_with_url = format!("{long_text}https://example.com");
556 assert!(contains_url(&text_with_url));
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_REGEX.is_match("https://example.com"));
581 assert!(URL_REGEX.is_match("http://test.org/path"));
582 assert!(URL_REGEX.is_match("ftp://files.com"));
583 assert!(!URL_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!(FENCED_CODE_BLOCK_START_REGEX.is_match("```"));
600 assert!(FENCED_CODE_BLOCK_START_REGEX.is_match("```rust"));
601 assert!(FENCED_CODE_BLOCK_START_REGEX.is_match("~~~"));
602 assert!(FENCED_CODE_BLOCK_END_REGEX.is_match("```"));
603 assert!(FENCED_CODE_BLOCK_END_REGEX.is_match("~~~"));
604
605 assert!(BOLD_ASTERISK_REGEX.is_match("**bold**"));
607 assert!(BOLD_UNDERSCORE_REGEX.is_match("__bold__"));
608 assert!(ITALIC_ASTERISK_REGEX.is_match("*italic*"));
609 assert!(ITALIC_UNDERSCORE_REGEX.is_match("_italic_"));
610
611 assert!(HTML_TAG_REGEX.is_match("<div>"));
613 assert!(HTML_TAG_REGEX.is_match("<span class='test'>"));
614 assert!(HTML_SELF_CLOSING_TAG_REGEX.is_match("<br/>"));
615 assert!(HTML_SELF_CLOSING_TAG_REGEX.is_match("<img src='test'/>"));
616
617 assert!(TRAILING_WHITESPACE_REGEX.is_match("line with spaces "));
619 assert!(TRAILING_WHITESPACE_REGEX.is_match("tabs\t\t"));
620 assert!(MULTIPLE_BLANK_LINES_REGEX.is_match("\n\n\n"));
621 assert!(MULTIPLE_BLANK_LINES_REGEX.is_match("\n\n\n\n"));
622
623 assert!(BLOCKQUOTE_PREFIX_RE.is_match("> Quote"));
625 assert!(BLOCKQUOTE_PREFIX_RE.is_match(" > Indented quote"));
626 assert!(BLOCKQUOTE_PREFIX_RE.is_match(">> Nested"));
627 }
628
629 #[test]
630 fn test_thread_safety() {
631 use std::thread;
632
633 let handles: Vec<_> = (0..10)
634 .map(|i| {
635 thread::spawn(move || {
636 let pattern = format!(r"\d{{{i}}}");
637 let regex = get_cached_regex(&pattern).unwrap();
638 assert!(regex.is_match(&"1".repeat(i)));
639 })
640 })
641 .collect();
642
643 for handle in handles {
644 handle.join().unwrap();
645 }
646 }
647}