1use crate::config::MarkdownFlavor;
7use crate::lint_context::{HtmlTag, LintContext};
8use crate::utils::mkdocs_admonitions;
9use crate::utils::mkdocs_critic;
10use crate::utils::mkdocs_extensions;
11use crate::utils::mkdocs_footnotes;
12use crate::utils::mkdocs_icons;
13use crate::utils::mkdocs_snippets;
14use crate::utils::mkdocs_tabs;
15use regex::Regex;
16use std::sync::LazyLock;
17
18static INLINE_MATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$[^$]*\$\$|\$[^$\n]*\$").unwrap());
27
28#[derive(Debug, Clone, Copy)]
30pub struct ByteRange {
31 pub start: usize,
32 pub end: usize,
33}
34
35pub fn compute_html_comment_ranges(content: &str) -> Vec<ByteRange> {
38 compute_html_comment_ranges_filtered(content, &[], &[])
39}
40
41pub fn compute_html_comment_ranges_filtered(
61 content: &str,
62 code_span_ranges: &[(usize, usize)],
63 code_block_ranges: &[(usize, usize)],
64) -> Vec<ByteRange> {
65 let in_code = |pos: usize| {
66 code_span_ranges.iter().any(|&(start, end)| pos >= start && pos < end)
67 || code_block_ranges.iter().any(|&(start, end)| pos >= start && pos < end)
68 };
69
70 let mut ranges = Vec::new();
71 let mut search_from = 0;
72 while let Some(rel) = content[search_from..].find("<!--") {
73 let open = search_from + rel;
74 if in_code(open) {
75 search_from = open + "<!--".len();
77 continue;
78 }
79 let mut close_from = open + "<!--".len();
81 let end = loop {
82 let Some(crel) = content[close_from..].find("-->") else {
83 break None;
84 };
85 let close = close_from + crel;
86 if in_code(close) {
87 close_from = close + "-->".len();
88 continue;
89 }
90 break Some(close + "-->".len());
91 };
92 match end {
93 Some(end) => {
94 ranges.push(ByteRange { start: open, end });
95 search_from = end;
96 }
97 None => break,
100 }
101 }
102 ranges
103}
104
105pub fn is_in_html_comment_ranges(ranges: &[ByteRange], byte_pos: usize) -> bool {
108 ranges
110 .binary_search_by(|range| {
111 if byte_pos < range.start {
112 std::cmp::Ordering::Greater
113 } else if byte_pos >= range.end {
114 std::cmp::Ordering::Less
115 } else {
116 std::cmp::Ordering::Equal
117 }
118 })
119 .is_ok()
120}
121
122pub fn is_line_entirely_in_html_comment(ranges: &[ByteRange], content_start: usize, content_end: usize) -> bool {
133 for range in ranges {
134 if content_start >= range.start && content_start < range.end {
136 return content_end <= range.end;
137 }
138 }
139 false
140}
141
142#[inline]
144pub fn is_in_jsx_expression(ctx: &LintContext, byte_pos: usize) -> bool {
145 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_jsx_expression(byte_pos)
146}
147
148#[inline]
150pub fn is_in_mdx_comment(ctx: &LintContext, byte_pos: usize) -> bool {
151 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_mdx_comment(byte_pos)
152}
153
154pub fn is_mkdocs_snippet_line(line: &str, flavor: MarkdownFlavor) -> bool {
156 flavor == MarkdownFlavor::MkDocs && mkdocs_snippets::is_snippet_marker(line)
157}
158
159pub fn is_mkdocs_admonition_line(line: &str, flavor: MarkdownFlavor) -> bool {
161 flavor == MarkdownFlavor::MkDocs && mkdocs_admonitions::is_admonition_marker(line)
162}
163
164pub fn is_mkdocs_footnote_line(line: &str, flavor: MarkdownFlavor) -> bool {
166 flavor == MarkdownFlavor::MkDocs && mkdocs_footnotes::is_footnote_definition(line)
167}
168
169pub fn is_mkdocs_tab_line(line: &str, flavor: MarkdownFlavor) -> bool {
171 flavor == MarkdownFlavor::MkDocs && mkdocs_tabs::is_tab_marker(line)
172}
173
174pub fn is_mkdocs_critic_line(line: &str, flavor: MarkdownFlavor) -> bool {
176 flavor == MarkdownFlavor::MkDocs && mkdocs_critic::contains_critic_markup(line)
177}
178
179pub fn is_in_html_tag(ctx: &LintContext, byte_pos: usize) -> bool {
181 for html_tag in ctx.html_tags().iter() {
182 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
183 return true;
184 }
185 }
186 false
187}
188
189pub fn is_in_math_context(ctx: &LintContext, byte_pos: usize) -> bool {
196 ctx.math_byte_ranges()
199 .iter()
200 .any(|&(start, end)| byte_pos >= start && byte_pos < end)
201}
202
203pub(crate) fn math_block_ranges(content: &str) -> Vec<(usize, usize)> {
214 let bytes = content.as_bytes();
215 let mut ranges = Vec::new();
216 let mut open: Option<usize> = None;
217 let mut line_start = 0usize;
218 let mut i = 0;
219 while i < bytes.len() {
220 match bytes[i] {
221 b'\n' => {
222 line_start = i + 1;
223 i += 1;
224 }
225 b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'$' => {
226 match open {
227 None => {
228 let starts_line = bytes[line_start..i]
231 .iter()
232 .all(|&b| b == b' ' || b == b'\t' || b == b'>');
233 if starts_line {
234 open = Some(i);
235 }
236 }
237 Some(start) => {
238 ranges.push((start, i + 2));
239 open = None;
240 }
241 }
242 i += 2;
243 }
244 _ => i += 1,
245 }
246 }
247 ranges
248}
249
250pub fn is_in_math_block(content: &str, byte_pos: usize) -> bool {
257 math_block_ranges(content)
258 .iter()
259 .any(|&(start, end)| byte_pos >= start && byte_pos < end)
260}
261
262pub fn is_in_inline_math(content: &str, byte_pos: usize) -> bool {
271 for m in INLINE_MATH_REGEX.find_iter(content) {
272 if content[m.start()..m.end()].starts_with("$$") {
273 continue;
274 }
275 if m.start() <= byte_pos && byte_pos < m.end() {
276 return true;
277 }
278 }
279 false
280}
281
282pub fn math_byte_ranges(content: &str) -> Vec<(usize, usize)> {
290 let mut ranges = math_block_ranges(content);
291 for m in INLINE_MATH_REGEX.find_iter(content) {
292 if content[m.start()..m.end()].starts_with("$$") {
293 continue;
294 }
295 ranges.push((m.start(), m.end()));
296 }
297 ranges
298}
299
300pub fn is_in_table_cell(ctx: &LintContext, line_num: usize, _col: usize) -> bool {
302 for table_row in ctx.table_rows().iter() {
304 if table_row.line == line_num {
305 return true;
309 }
310 }
311 false
312}
313
314pub fn is_table_line(line: &str) -> bool {
316 let trimmed = line.trim();
317
318 if trimmed
320 .chars()
321 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
322 && trimmed.contains('|')
323 && trimmed.contains('-')
324 {
325 return true;
326 }
327
328 if (trimmed.starts_with('|') || trimmed.ends_with('|')) && trimmed.matches('|').count() >= 2 {
330 return true;
331 }
332
333 false
334}
335
336pub fn is_in_icon_shortcode(line: &str, position: usize, _flavor: MarkdownFlavor) -> bool {
339 mkdocs_icons::is_in_any_shortcode(line, position)
342}
343
344pub fn is_in_pymdown_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
350 match flavor {
351 MarkdownFlavor::MkDocs => mkdocs_extensions::is_in_pymdown_markup(line, position),
352 MarkdownFlavor::Obsidian => {
353 mkdocs_extensions::is_in_mark(line, position)
355 }
356 _ => false,
357 }
358}
359
360pub fn is_in_inline_html_code(line: &str, position: usize) -> bool {
365 const TAGS: &[&str] = &["code", "pre", "samp", "kbd", "var"];
367
368 let bytes = line.as_bytes();
369
370 for tag in TAGS {
371 let open_bytes = format!("<{tag}").into_bytes();
372 let close_pattern = format!("</{tag}>").into_bytes();
373
374 let mut search_from = 0;
375 while search_from + open_bytes.len() <= bytes.len() {
376 let Some(open_abs) = find_case_insensitive(bytes, &open_bytes, search_from) else {
378 break;
379 };
380
381 let after_tag = open_abs + open_bytes.len();
382
383 if after_tag < bytes.len() {
385 let next = bytes[after_tag];
386 if next != b'>' && next != b' ' && next != b'\t' {
387 search_from = after_tag;
388 continue;
389 }
390 }
391
392 let Some(tag_close) = bytes[after_tag..].iter().position(|&b| b == b'>') else {
394 break;
395 };
396 let content_start = after_tag + tag_close + 1;
397
398 let Some(close_start) = find_case_insensitive(bytes, &close_pattern, content_start) else {
400 break;
401 };
402 let content_end = close_start;
403
404 if position >= content_start && position < content_end {
405 return true;
406 }
407
408 search_from = close_start + close_pattern.len();
409 }
410 }
411 false
412}
413
414fn find_case_insensitive(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
416 if needle.is_empty() || from + needle.len() > haystack.len() {
417 return None;
418 }
419 for i in from..=haystack.len() - needle.len() {
420 if haystack[i..i + needle.len()]
421 .iter()
422 .zip(needle.iter())
423 .all(|(h, n)| h.eq_ignore_ascii_case(n))
424 {
425 return Some(i);
426 }
427 }
428 None
429}
430
431pub fn is_in_mkdocs_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
435 if is_in_icon_shortcode(line, position, flavor) {
436 return true;
437 }
438 if is_in_pymdown_markup(line, position, flavor) {
439 return true;
440 }
441 false
442}
443
444fn is_in_inline_code_on_line(line: &str, byte_pos: usize) -> bool {
450 let bytes = line.as_bytes();
451 let mut i = 0;
452
453 while i < bytes.len() {
454 if bytes[i] == b'`' {
455 let open_start = i;
456 let mut backtick_count = 0;
457 while i < bytes.len() && bytes[i] == b'`' {
458 backtick_count += 1;
459 i += 1;
460 }
461
462 let mut j = i;
464 while j < bytes.len() {
465 if bytes[j] == b'`' {
466 let mut close_count = 0;
467 while j < bytes.len() && bytes[j] == b'`' {
468 close_count += 1;
469 j += 1;
470 }
471 if close_count == backtick_count {
472 if byte_pos >= open_start && byte_pos < j {
474 return true;
475 }
476 i = j;
477 break;
478 }
479 } else {
480 j += 1;
481 }
482 }
483
484 if j >= bytes.len() {
485 break;
487 }
488 } else {
489 i += 1;
490 }
491 }
492
493 false
494}
495
496fn is_byte_in_html_tag(html_tags: &[HtmlTag], byte_pos: usize) -> bool {
498 let idx = html_tags.partition_point(|tag| tag.byte_offset <= byte_pos);
499 idx > 0 && byte_pos < html_tags[idx - 1].byte_end
500}
501
502fn is_byte_in_html_code_content(code_ranges: &[(usize, usize)], byte_pos: usize) -> bool {
505 let idx = code_ranges.partition_point(|&(start, _)| start <= byte_pos);
506 idx > 0 && byte_pos < code_ranges[idx - 1].1
507}
508
509pub(crate) fn compute_html_code_ranges(html_tags: &[HtmlTag]) -> Vec<(usize, usize)> {
512 let mut ranges = Vec::new();
513 let mut open_code_end: Option<usize> = None;
514
515 for tag in html_tags {
516 if tag.tag_name == "code" {
517 if tag.is_self_closing {
518 continue;
519 } else if !tag.is_closing {
520 open_code_end = Some(tag.byte_end);
521 } else if tag.is_closing {
522 if let Some(start) = open_code_end {
523 ranges.push((start, tag.byte_offset));
524 }
525 open_code_end = None;
526 }
527 }
528 }
529 if let Some(start) = open_code_end {
531 ranges.push((start, usize::MAX));
532 }
533 ranges
534}
535
536pub(crate) fn should_skip_emphasis_span(
544 ctx: &LintContext,
545 html_tags: &[HtmlTag],
546 html_code_ranges: &[(usize, usize)],
547 span_start: usize,
548) -> bool {
549 let lines = ctx.raw_lines();
550 let (line_num, col) = ctx.offset_to_line_col(span_start);
551
552 if ctx
554 .line_info(line_num)
555 .is_some_and(|info| info.in_front_matter || info.in_mkdocstrings)
556 {
557 return true;
558 }
559
560 let in_mkdocs_markup = lines
562 .get(line_num.saturating_sub(1))
563 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
564
565 let in_inline_code = lines
567 .get(line_num.saturating_sub(1))
568 .is_some_and(|line| is_in_inline_code_on_line(line, col.saturating_sub(1)));
569
570 ctx.is_in_code_block_or_span(span_start)
571 || in_inline_code
572 || ctx.is_in_link(span_start)
573 || is_byte_in_html_tag(html_tags, span_start)
574 || is_byte_in_html_code_content(html_code_ranges, span_start)
575 || in_mkdocs_markup
576 || is_in_math_context(ctx, span_start)
577 || is_in_jsx_expression(ctx, span_start)
578 || is_in_mdx_comment(ctx, span_start)
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn test_html_comment_detection() {
587 let content = "Text <!-- comment --> more text";
588 let ranges = compute_html_comment_ranges(content);
589 assert!(is_in_html_comment_ranges(&ranges, 10)); assert!(!is_in_html_comment_ranges(&ranges, 0)); assert!(!is_in_html_comment_ranges(&ranges, 25)); }
593
594 #[test]
595 fn test_compute_html_comment_ranges_ignores_code_span_delimiters() {
596 let content = "a `<!--` b\n\nc `-->` d";
599 let open = content.find("<!--").unwrap();
600 let close = content.find("-->").unwrap();
601 let code_spans = [
603 (content.find('`').unwrap(), open + "<!--".len() + 1),
604 (content.rfind("` d").unwrap() - "-->".len(), close + "-->".len() + 1),
605 ];
606
607 assert!(
609 !compute_html_comment_ranges(content).is_empty(),
610 "sanity: raw pattern matches across the code spans"
611 );
612 assert!(
614 compute_html_comment_ranges_filtered(content, &code_spans, &[]).is_empty(),
615 "a `<!--`/`-->` pair inside code spans must not be treated as a comment"
616 );
617 }
618
619 #[test]
620 fn test_compute_html_comment_ranges_ignores_code_block_delimiters() {
621 let content = "```\n<!-- literal\n```\n\nhttps://example.com\n\n-->\n";
624 let block_end = content.find("```\n\n").unwrap() + "```".len();
625 let code_blocks = [(0usize, block_end)];
626 assert!(
627 compute_html_comment_ranges_filtered(content, &[], &code_blocks).is_empty(),
628 "a `<!--` inside a code block must not open a comment that spans to a later `-->`"
629 );
630 let real = "```\n<!-- literal\n```\n\n<!-- real --> tail";
632 let real_block_end = real.find("```\n\n").unwrap() + "```".len();
633 let ranges = compute_html_comment_ranges_filtered(real, &[], &[(0usize, real_block_end)]);
634 assert_eq!(ranges.len(), 1);
635 assert_eq!(ranges[0].start, real.find("<!-- real").unwrap());
636 }
637
638 #[test]
639 fn test_compute_html_comment_ranges_keeps_real_comments() {
640 let content = "text `code` <!-- real comment --> more";
643 let code_spans = [(content.find('`').unwrap(), content.find("` ").unwrap() + 1)];
644 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
645 assert_eq!(ranges.len(), 1, "the real comment must still be detected");
646 let comment_start = content.find("<!--").unwrap();
647 assert_eq!(ranges[0].start, comment_start);
648 }
649
650 #[test]
651 fn test_compute_html_comment_ranges_real_comment_after_code_span_opener() {
652 let content = "a `<!--` then <!-- real --> end";
656 let code_spans = [(content.find('`').unwrap(), content.find("` then").unwrap() + 1)];
657 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
658 assert_eq!(
659 ranges.len(),
660 1,
661 "the real comment after a code-span opener must be detected"
662 );
663 let real_open = content.find("<!-- real").unwrap();
664 assert_eq!(
665 ranges[0].start, real_open,
666 "range must start at the real comment, not the code-span opener"
667 );
668 assert_eq!(ranges[0].end, content.find("--> end").unwrap() + "-->".len());
669 }
670
671 #[test]
672 fn test_compute_html_comment_ranges_closer_inside_code_span_is_not_a_closer() {
673 let content = "<!-- open `-->` still open --> done";
676 let first_close = content.find("`-->`").unwrap() + 1;
677 let code_spans = [(content.find('`').unwrap(), content.find("` still").unwrap() + 1)];
678 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
679 assert_eq!(ranges.len(), 1);
680 assert_eq!(ranges[0].start, 0);
681 let real_close_end = content.find("--> done").unwrap() + "-->".len();
682 assert_eq!(
683 ranges[0].end, real_close_end,
684 "must close at the real --> ({real_close_end}), not the one in the code span ({first_close})"
685 );
686 }
687
688 #[test]
689 fn test_is_line_entirely_in_html_comment() {
690 let content = "<!--\ncomment\n--> Content after comment";
692 let ranges = compute_html_comment_ranges(content);
693 assert!(is_line_entirely_in_html_comment(&ranges, 0, 4));
695 assert!(is_line_entirely_in_html_comment(&ranges, 5, 12));
697 assert!(!is_line_entirely_in_html_comment(&ranges, 13, 38));
699
700 let content2 = "<!-- comment --> Not a comment";
702 let ranges2 = compute_html_comment_ranges(content2);
703 assert!(!is_line_entirely_in_html_comment(&ranges2, 0, 30));
705
706 let content3 = "<!-- comment -->";
708 let ranges3 = compute_html_comment_ranges(content3);
709 assert!(is_line_entirely_in_html_comment(&ranges3, 0, 16));
711
712 let content4 = "Text before <!-- comment -->";
714 let ranges4 = compute_html_comment_ranges(content4);
715 assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
717 }
718
719 #[test]
720 fn test_is_line_entirely_in_html_comment_indented() {
721 let content = " <!-- comment -->";
725 let ranges = compute_html_comment_ranges(content);
726 let content_start = content.find("<!--").unwrap();
727 let content_end = content.trim_end().len();
728 assert!(is_line_entirely_in_html_comment(&ranges, content_start, content_end));
729 assert!(!is_line_entirely_in_html_comment(&ranges, 0, content.len()));
731 }
732
733 #[test]
734 fn test_is_line_entirely_in_html_comment_trailing_whitespace() {
735 let content = "<!-- comment --> ";
737 let ranges = compute_html_comment_ranges(content);
738 let content_end = content.trim_end().len();
739 assert!(is_line_entirely_in_html_comment(&ranges, 0, content_end));
740 assert!(!is_line_entirely_in_html_comment(&ranges, 0, content.len()));
742 }
743
744 #[test]
745 fn test_math_block_detection() {
746 let content = "Text\n$$\nmath content\n$$\nmore text";
747 assert!(is_in_math_block(content, 8)); assert!(is_in_math_block(content, 15)); assert!(!is_in_math_block(content, 0)); assert!(!is_in_math_block(content, 30)); }
752
753 #[test]
754 fn test_stray_double_dollar_in_prose_is_not_math() {
755 let content = "Note: $$ is used for display math and $$ closes it";
759 let between = content.find("is used").unwrap();
760 assert!(
761 !is_in_math_block(content, between),
762 "stray paired `$$` in prose must not be treated as a math block"
763 );
764 assert!(math_block_ranges(content).is_empty());
765 }
766
767 #[test]
768 fn test_blockquoted_double_dollar_opens_block() {
769 let content = "> $$\n> x = y\n> $$\n";
771 let inside = content.find("x = y").unwrap();
772 assert!(is_in_math_block(content, inside), "blockquoted math interior");
773 }
774
775 #[test]
776 fn test_self_contained_single_line_block_leaves_trailing_prose() {
777 let content = "$$ a $$ and __not math__\n";
779 let in_math = content.find('a').unwrap();
780 assert!(is_in_math_block(content, in_math), "single-line math interior");
781 let after = content.find("not math").unwrap();
782 assert!(!is_in_math_block(content, after), "trailing prose is lintable");
783 }
784
785 #[test]
786 fn test_math_block_closes_with_content_before_fence() {
787 let content = "$$\nx = y\n\\end{x}$$\nafter __text__ here";
791
792 let inside = content.find("x = y").unwrap();
793 assert!(is_in_math_block(content, inside), "interior must be math");
794
795 let after = content.find("after").unwrap();
796 assert!(
797 !is_in_math_block(content, after),
798 "content after a content-sharing closing fence must NOT be math"
799 );
800 }
801
802 #[test]
803 fn test_inline_math_detection() {
804 let content = "Text $x + y$ and $$a^2 + b^2$$ here";
805 assert!(is_in_inline_math(content, 7), "inside the single-`$` inline span");
806 assert!(!is_in_inline_math(content, 20), "mid-line $$...$$ is not inline math");
810 assert!(
811 !is_in_math_block(content, 20),
812 "mid-line $$...$$ is not a line-start display block"
813 );
814 assert!(!is_in_inline_math(content, 0), "before any math");
815 assert!(!is_in_inline_math(content, 35), "after the spans");
816 }
817
818 #[test]
819 fn test_table_line_detection() {
820 assert!(is_table_line("| Header | Column |"));
821 assert!(is_table_line("|--------|--------|"));
822 assert!(is_table_line("| Cell 1 | Cell 2 |"));
823 assert!(!is_table_line("Regular text"));
824 assert!(!is_table_line("Just a pipe | here"));
825 }
826
827 #[test]
828 fn test_is_in_icon_shortcode() {
829 let line = "Click :material-check: to confirm";
830 assert!(!is_in_icon_shortcode(line, 0, MarkdownFlavor::MkDocs));
832 assert!(is_in_icon_shortcode(line, 6, MarkdownFlavor::MkDocs));
834 assert!(is_in_icon_shortcode(line, 15, MarkdownFlavor::MkDocs));
835 assert!(is_in_icon_shortcode(line, 21, MarkdownFlavor::MkDocs));
836 assert!(!is_in_icon_shortcode(line, 22, MarkdownFlavor::MkDocs));
838 }
839
840 #[test]
841 fn test_is_in_pymdown_markup() {
842 let line = "Press ++ctrl+c++ to copy";
844 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::MkDocs));
845 assert!(is_in_pymdown_markup(line, 6, MarkdownFlavor::MkDocs));
846 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::MkDocs));
847 assert!(!is_in_pymdown_markup(line, 17, MarkdownFlavor::MkDocs));
848
849 let line2 = "This is ==highlighted== text";
851 assert!(!is_in_pymdown_markup(line2, 0, MarkdownFlavor::MkDocs));
852 assert!(is_in_pymdown_markup(line2, 8, MarkdownFlavor::MkDocs));
853 assert!(is_in_pymdown_markup(line2, 15, MarkdownFlavor::MkDocs));
854 assert!(!is_in_pymdown_markup(line2, 23, MarkdownFlavor::MkDocs));
855
856 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Standard));
858 }
859
860 #[test]
861 fn test_is_in_mkdocs_markup() {
862 let line = ":material-check: and ++ctrl++";
864 assert!(is_in_mkdocs_markup(line, 5, MarkdownFlavor::MkDocs)); assert!(is_in_mkdocs_markup(line, 23, MarkdownFlavor::MkDocs)); assert!(!is_in_mkdocs_markup(line, 17, MarkdownFlavor::MkDocs)); }
868
869 #[test]
872 fn test_obsidian_highlight_basic() {
873 let line = "This is ==highlighted== text";
875 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 8, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 15, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 22, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 23, MarkdownFlavor::Obsidian)); }
882
883 #[test]
884 fn test_obsidian_highlight_multiple() {
885 let line = "Both ==one== and ==two== here";
887 assert!(is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 8, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 12, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 17, MarkdownFlavor::Obsidian)); }
892
893 #[test]
894 fn test_obsidian_highlight_not_standard_flavor() {
895 let line = "This is ==highlighted== text";
897 assert!(!is_in_pymdown_markup(line, 8, MarkdownFlavor::Standard));
898 assert!(!is_in_pymdown_markup(line, 15, MarkdownFlavor::Standard));
899 }
900
901 #[test]
902 fn test_obsidian_highlight_with_spaces_inside() {
903 let line = "This is ==text with spaces== here";
905 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 15, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line, 27, MarkdownFlavor::Obsidian)); }
909
910 #[test]
911 fn test_obsidian_does_not_support_keys_notation() {
912 let line = "Press ++ctrl+c++ to copy";
914 assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
915 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
916 }
917
918 #[test]
919 fn test_obsidian_mkdocs_markup_function() {
920 let line = "This is ==highlighted== text";
922 assert!(is_in_mkdocs_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(!is_in_mkdocs_markup(line, 0, MarkdownFlavor::Obsidian)); }
925
926 #[test]
927 fn test_obsidian_highlight_edge_cases() {
928 let line = "Test ==== here";
930 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
932
933 let line2 = "Test ==a== here";
935 assert!(is_in_pymdown_markup(line2, 5, MarkdownFlavor::Obsidian));
936 assert!(is_in_pymdown_markup(line2, 7, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line2, 9, MarkdownFlavor::Obsidian)); let line3 = "a === b";
941 assert!(!is_in_pymdown_markup(line3, 3, MarkdownFlavor::Obsidian));
942 }
943
944 #[test]
945 fn test_obsidian_highlight_unclosed() {
946 let line = "This ==starts but never ends";
948 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian));
949 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
950 }
951
952 #[test]
953 fn test_inline_html_code_basic() {
954 let line = "The formula is <code>a * b * c</code> in math.";
955 assert!(is_in_inline_html_code(line, 21)); assert!(is_in_inline_html_code(line, 25)); assert!(!is_in_inline_html_code(line, 0)); assert!(!is_in_inline_html_code(line, 40)); }
962
963 #[test]
964 fn test_inline_html_code_multiple_tags() {
965 let line = "<kbd>Ctrl</kbd> + <samp>output</samp>";
966 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 24)); assert!(!is_in_inline_html_code(line, 16)); }
970
971 #[test]
972 fn test_inline_html_code_with_attributes() {
973 let line = r#"<code class="lang">x * y</code>"#;
974 assert!(is_in_inline_html_code(line, 19)); assert!(is_in_inline_html_code(line, 23)); assert!(!is_in_inline_html_code(line, 0)); }
978
979 #[test]
980 fn test_inline_html_code_case_insensitive() {
981 let line = "<CODE>a * b</CODE>";
982 assert!(is_in_inline_html_code(line, 6)); assert!(is_in_inline_html_code(line, 8)); }
985
986 #[test]
987 fn test_inline_html_code_var_and_pre() {
988 let line = "<var>x * y</var> and <pre>a * b</pre>";
989 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 26)); assert!(!is_in_inline_html_code(line, 17)); }
993
994 #[test]
995 fn test_inline_html_code_unclosed() {
996 let line = "<code>a * b without closing";
998 assert!(!is_in_inline_html_code(line, 6));
999 }
1000
1001 #[test]
1002 fn test_inline_html_code_no_substring_match() {
1003 let line = "<variable>a * b</variable>";
1005 assert!(!is_in_inline_html_code(line, 11));
1006
1007 let line2 = "<keyboard>x * y</keyboard>";
1009 assert!(!is_in_inline_html_code(line2, 11));
1010 }
1011}