1use regex::Regex;
21use std::sync::LazyLock;
22
23static INLINE_HILITE_PATTERN: LazyLock<Regex> =
30 LazyLock::new(|| Regex::new(r"`#!([a-zA-Z][a-zA-Z0-9_+-]*)\s+[^`]+`").unwrap());
31
32static INLINE_HILITE_SHEBANG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^#!([a-zA-Z][a-zA-Z0-9_+-]*)").unwrap());
34
35#[inline]
37pub fn contains_inline_hilite(line: &str) -> bool {
38 if !line.contains('`') || !line.contains("#!") {
39 return false;
40 }
41 INLINE_HILITE_PATTERN.is_match(line)
42}
43
44#[inline]
46pub fn is_inline_hilite_content(content: &str) -> bool {
47 INLINE_HILITE_SHEBANG.is_match(content)
48}
49
50static KEYS_PATTERN: LazyLock<Regex> =
57 LazyLock::new(|| Regex::new(r"\+\+([a-zA-Z0-9_-]+(?:\+[a-zA-Z0-9_-]+)*)\+\+").unwrap());
58
59pub const COMMON_KEYS: &[&str] = &[
61 "ctrl",
62 "alt",
63 "shift",
64 "cmd",
65 "meta",
66 "win",
67 "windows",
68 "option",
69 "enter",
70 "return",
71 "tab",
72 "space",
73 "backspace",
74 "delete",
75 "del",
76 "insert",
77 "ins",
78 "home",
79 "end",
80 "pageup",
81 "pagedown",
82 "up",
83 "down",
84 "left",
85 "right",
86 "escape",
87 "esc",
88 "capslock",
89 "numlock",
90 "scrolllock",
91 "printscreen",
92 "pause",
93 "break",
94 "f1",
95 "f2",
96 "f3",
97 "f4",
98 "f5",
99 "f6",
100 "f7",
101 "f8",
102 "f9",
103 "f10",
104 "f11",
105 "f12",
106 ];
108
109#[derive(Debug, Clone, PartialEq)]
111pub struct KeyboardShortcut {
112 pub full_text: String,
114 pub keys: Vec<String>,
116 pub start: usize,
118 pub end: usize,
120}
121
122#[inline]
124pub fn contains_keys(line: &str) -> bool {
125 if !line.contains("++") {
126 return false;
127 }
128 KEYS_PATTERN.is_match(line)
129}
130
131pub fn find_keyboard_shortcuts(line: &str) -> Vec<KeyboardShortcut> {
133 if !line.contains("++") {
134 return Vec::new();
135 }
136
137 let mut results = Vec::new();
138
139 for m in KEYS_PATTERN.find_iter(line) {
140 let full_text = m.as_str().to_string();
141 let inner = &full_text[2..full_text.len() - 2];
143 let keys: Vec<String> = inner.split('+').map(|s| s.to_string()).collect();
144
145 results.push(KeyboardShortcut {
146 full_text,
147 keys,
148 start: m.start(),
149 end: m.end(),
150 });
151 }
152
153 results
154}
155
156pub fn is_in_keys(line: &str, position: usize) -> bool {
158 for shortcut in find_keyboard_shortcuts(line) {
159 if shortcut.start <= position && position < shortcut.end {
160 return true;
161 }
162 }
163 false
164}
165
166static INSERT_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\^\^([^\^]+)\^\^").unwrap());
173
174#[inline]
177pub fn contains_superscript(line: &str) -> bool {
178 if !line.contains('^') {
179 return false;
180 }
181
182 let masked = mask_insert_patterns(line);
184
185 let bytes = masked.as_bytes();
188 let mut i = 0;
189 while i < bytes.len() {
190 if bytes[i] == b'^' {
191 if let Some(end) = masked[i + 1..].find('^') {
194 let end_pos = i + 1 + end;
195 let content = &masked[i + 1..end_pos];
197 if !content.is_empty() && !content.contains('^') {
198 return true;
199 }
200 }
201 }
202 i += 1;
203 }
204 false
205}
206
207fn mask_insert_patterns(line: &str) -> String {
209 if !line.contains("^^") {
210 return line.to_string();
211 }
212
213 let mut result = line.to_string();
214 for m in INSERT_PATTERN.find_iter(line) {
215 let replacement = " ".repeat(m.end() - m.start());
216 result.replace_range(m.start()..m.end(), &replacement);
217 }
218 result
219}
220
221#[inline]
223pub fn contains_insert(line: &str) -> bool {
224 if !line.contains("^^") {
225 return false;
226 }
227 INSERT_PATTERN.is_match(line)
228}
229
230pub fn is_in_caret_markup(line: &str, position: usize) -> bool {
232 if !line.contains('^') {
233 return false;
234 }
235
236 for m in INSERT_PATTERN.find_iter(line) {
238 if m.start() <= position && position < m.end() {
239 return true;
240 }
241 }
242
243 let masked = mask_insert_patterns(line);
245 let bytes = masked.as_bytes();
246 let mut i = 0;
247 while i < bytes.len() {
248 if bytes[i] == b'^' {
249 if let Some(end) = masked[i + 1..].find('^') {
251 let end_pos = i + 1 + end;
252 let content = &masked[i + 1..end_pos];
254 if !content.is_empty() && !content.contains('^') && position >= i && position <= end_pos + 1 {
255 return true;
256 }
257 i = end_pos + 1;
259 continue;
260 }
261 }
262 i += 1;
263 }
264
265 false
266}
267
268static MARK_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"==([^=]+)==").unwrap());
274
275#[inline]
277pub fn contains_mark(line: &str) -> bool {
278 if !line.contains("==") {
279 return false;
280 }
281 MARK_PATTERN.is_match(line)
282}
283
284pub fn is_in_mark(line: &str, position: usize) -> bool {
286 if !line.contains("==") {
287 return false;
288 }
289
290 for m in MARK_PATTERN.find_iter(line) {
291 if m.start() <= position && position < m.end() {
292 return true;
293 }
294 }
295
296 false
297}
298
299#[allow(dead_code)]
308const SMART_SYMBOLS_DOC: &str =
309 "(c)©, (r)®, (tm)™, ...→…, --→–, ---→—, <->↔, =>⇒, <=⇐, <=>⇔, 1/4¼, 1/2½, 3/4¾, +-±, !=≠";
310
311static SMART_SYMBOL_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
313 Regex::new(r"(?:\(c\)|\(C\)|\(r\)|\(R\)|\(tm\)|\(TM\)|\(p\)|\.\.\.|-{2,3}|<->|<-|->|<=>|<=|=>|1/4|1/2|3/4|\+-|!=)")
314 .unwrap()
315});
316
317#[inline]
319pub fn contains_smart_symbols(line: &str) -> bool {
320 if !line.contains('(')
322 && !line.contains("...")
323 && !line.contains("--")
324 && !line.contains("->")
325 && !line.contains("<-")
326 && !line.contains("=>")
327 && !line.contains("<=")
328 && !line.contains("1/")
329 && !line.contains("3/")
330 && !line.contains("+-")
331 && !line.contains("!=")
332 {
333 return false;
334 }
335 SMART_SYMBOL_PATTERN.is_match(line)
336}
337
338pub fn is_in_smart_symbol(line: &str, position: usize) -> bool {
340 for m in SMART_SYMBOL_PATTERN.find_iter(line) {
341 if m.start() <= position && position < m.end() {
342 return true;
343 }
344 }
345 false
346}
347
348pub fn is_in_pymdown_markup(line: &str, position: usize) -> bool {
357 is_in_keys(line, position)
358 || is_in_caret_markup(line, position)
359 || is_in_mark(line, position)
360 || is_in_smart_symbol(line, position)
361}
362
363pub fn mask_pymdown_markup(line: &str) -> String {
368 let mut result = line.to_string();
369
370 if line.contains("++") {
374 for m in KEYS_PATTERN.find_iter(line) {
375 let replacement = " ".repeat(m.end() - m.start());
376 result.replace_range(m.start()..m.end(), &replacement);
379 }
380 }
381
382 if result.contains("^^") {
384 let temp = result.clone();
385 for m in INSERT_PATTERN.find_iter(&temp) {
386 let replacement = " ".repeat(m.end() - m.start());
387 result.replace_range(m.start()..m.end(), &replacement);
388 }
389 }
390
391 if result.contains('^') {
393 let mut new_result = result.clone();
394 let bytes = result.as_bytes();
395 let mut superscript_ranges = Vec::new();
396
397 let mut i = 0;
398 while i < bytes.len() {
399 if bytes[i] == b'^' {
400 if let Some(end) = result[i + 1..].find('^') {
402 let end_pos = i + 1 + end;
403 let content = &result[i + 1..end_pos];
404 if !content.is_empty() && !content.contains('^') {
406 superscript_ranges.push((i, end_pos + 1));
407 i = end_pos + 1;
408 continue;
409 }
410 }
411 }
412 i += 1;
413 }
414
415 for (start, end) in superscript_ranges.into_iter().rev() {
417 let replacement = " ".repeat(end - start);
418 new_result.replace_range(start..end, &replacement);
419 }
420 result = new_result;
421 }
422
423 if result.contains("==") {
425 let temp = result.clone();
426 for m in MARK_PATTERN.find_iter(&temp) {
427 let replacement = " ".repeat(m.end() - m.start());
428 result.replace_range(m.start()..m.end(), &replacement);
429 }
430 }
431
432 result
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
441 fn test_contains_inline_hilite() {
442 assert!(contains_inline_hilite("`#!python print('hello')`"));
443 assert!(contains_inline_hilite("Use `#!js alert('hi')` for alerts"));
444 assert!(contains_inline_hilite("`#!c++ cout << x;`"));
445
446 assert!(!contains_inline_hilite("`regular code`"));
448 assert!(!contains_inline_hilite("#! not in backticks"));
449 assert!(!contains_inline_hilite("`#!` empty"));
450 }
451
452 #[test]
453 fn test_is_inline_hilite_content() {
454 assert!(is_inline_hilite_content("#!python print()"));
455 assert!(is_inline_hilite_content("#!js code"));
456
457 assert!(!is_inline_hilite_content("regular code"));
458 assert!(!is_inline_hilite_content(" #!python with space"));
459 }
460
461 #[test]
463 fn test_contains_keys() {
464 assert!(contains_keys("Press ++ctrl++ to continue"));
465 assert!(contains_keys("++ctrl+alt+delete++"));
466 assert!(contains_keys("Use ++cmd+shift+p++ for command palette"));
467
468 assert!(!contains_keys("Use + for addition"));
469 assert!(!contains_keys("a++ increment"));
470 assert!(!contains_keys("++incomplete"));
471 }
472
473 #[test]
474 fn test_find_keyboard_shortcuts() {
475 let shortcuts = find_keyboard_shortcuts("Press ++ctrl+c++ then ++ctrl+v++");
476 assert_eq!(shortcuts.len(), 2);
477 assert_eq!(shortcuts[0].keys, vec!["ctrl", "c"]);
478 assert_eq!(shortcuts[1].keys, vec!["ctrl", "v"]);
479
480 let shortcuts = find_keyboard_shortcuts("++ctrl+alt+delete++");
481 assert_eq!(shortcuts.len(), 1);
482 assert_eq!(shortcuts[0].keys, vec!["ctrl", "alt", "delete"]);
483 }
484
485 #[test]
486 fn test_is_in_keys() {
487 let line = "Press ++ctrl++ here";
488 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)); }
495
496 #[test]
498 fn test_contains_superscript() {
499 assert!(contains_superscript("E=mc^2^"));
500 assert!(contains_superscript("x^n^ power"));
501
502 assert!(!contains_superscript("no caret here"));
503 assert!(!contains_superscript("^^insert^^")); }
505
506 #[test]
507 fn test_contains_insert() {
508 assert!(contains_insert("^^inserted text^^"));
509 assert!(contains_insert("Some ^^new^^ text"));
510
511 assert!(!contains_insert("^superscript^"));
512 assert!(!contains_insert("no markup"));
513 }
514
515 #[test]
516 fn test_is_in_caret_markup() {
517 let line = "Text ^super^ here";
518 assert!(!is_in_caret_markup(line, 0));
519 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";
524 assert!(is_in_caret_markup(line2, 5)); assert!(is_in_caret_markup(line2, 10)); }
527
528 #[test]
530 fn test_contains_mark() {
531 assert!(contains_mark("This is ==highlighted== text"));
532 assert!(contains_mark("==important=="));
533
534 assert!(!contains_mark("no highlight"));
535 assert!(!contains_mark("a == b comparison")); }
537
538 #[test]
539 fn test_is_in_mark() {
540 let line = "Text ==highlight== more";
541 assert!(!is_in_mark(line, 0));
542 assert!(is_in_mark(line, 5)); assert!(is_in_mark(line, 10)); assert!(!is_in_mark(line, 19)); }
546
547 #[test]
549 fn test_contains_smart_symbols() {
550 assert!(contains_smart_symbols("Copyright (c) 2024"));
551 assert!(contains_smart_symbols("This is (tm) trademarked"));
552 assert!(contains_smart_symbols("Left arrow <- here"));
553 assert!(contains_smart_symbols("Right arrow -> there"));
554 assert!(contains_smart_symbols("Em dash --- here"));
555 assert!(contains_smart_symbols("Fraction 1/2"));
556
557 assert!(!contains_smart_symbols("No symbols here"));
558 assert!(!contains_smart_symbols("(other) parentheses"));
559 }
560
561 #[test]
562 fn test_is_in_smart_symbol() {
563 let line = "Copyright (c) text";
564 assert!(!is_in_smart_symbol(line, 0));
565 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)); }
570
571 #[test]
573 fn test_is_in_pymdown_markup() {
574 assert!(is_in_pymdown_markup("++ctrl++", 2));
575 assert!(is_in_pymdown_markup("^super^", 1));
576 assert!(is_in_pymdown_markup("==mark==", 2));
577 assert!(is_in_pymdown_markup("(c)", 1));
578
579 assert!(!is_in_pymdown_markup("plain text", 5));
580 }
581
582 #[test]
583 fn test_mask_pymdown_markup() {
584 let line = "Press ++ctrl++ and ^super^ with ==mark==";
585 let masked = mask_pymdown_markup(line);
586 assert!(!masked.contains("++"));
587 assert!(!masked.contains("^super^"));
588 assert!(!masked.contains("==mark=="));
589 assert!(masked.contains("Press"));
590 assert!(masked.contains("and"));
591 assert!(masked.contains("with"));
592 assert_eq!(masked.len(), line.len());
594 }
595}