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_table_line(line: &str) -> bool {
302 let trimmed = line.trim();
303
304 if trimmed
306 .chars()
307 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
308 && trimmed.contains('|')
309 && trimmed.contains('-')
310 {
311 return true;
312 }
313
314 if (trimmed.starts_with('|') || trimmed.ends_with('|')) && trimmed.matches('|').count() >= 2 {
316 return true;
317 }
318
319 false
320}
321
322pub fn is_in_icon_shortcode(line: &str, position: usize, _flavor: MarkdownFlavor) -> bool {
325 mkdocs_icons::is_in_any_shortcode(line, position)
328}
329
330pub fn is_in_pymdown_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
336 match flavor {
337 MarkdownFlavor::MkDocs => mkdocs_extensions::is_in_pymdown_markup(line, position),
338 MarkdownFlavor::Obsidian => {
339 mkdocs_extensions::is_in_mark(line, position)
341 }
342 _ => false,
343 }
344}
345
346pub fn is_in_inline_html_code(line: &str, position: usize) -> bool {
351 const TAGS: &[&str] = &["code", "pre", "samp", "kbd", "var"];
353
354 let bytes = line.as_bytes();
355
356 for tag in TAGS {
357 let open_bytes = format!("<{tag}").into_bytes();
358 let close_pattern = format!("</{tag}>").into_bytes();
359
360 let mut search_from = 0;
361 while search_from + open_bytes.len() <= bytes.len() {
362 let Some(open_abs) = find_case_insensitive(bytes, &open_bytes, search_from) else {
364 break;
365 };
366
367 let after_tag = open_abs + open_bytes.len();
368
369 if after_tag < bytes.len() {
371 let next = bytes[after_tag];
372 if next != b'>' && next != b' ' && next != b'\t' {
373 search_from = after_tag;
374 continue;
375 }
376 }
377
378 let Some(tag_close) = bytes[after_tag..].iter().position(|&b| b == b'>') else {
380 break;
381 };
382 let content_start = after_tag + tag_close + 1;
383
384 let Some(close_start) = find_case_insensitive(bytes, &close_pattern, content_start) else {
386 break;
387 };
388 let content_end = close_start;
389
390 if position >= content_start && position < content_end {
391 return true;
392 }
393
394 search_from = close_start + close_pattern.len();
395 }
396 }
397 false
398}
399
400fn find_case_insensitive(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
402 if needle.is_empty() || from + needle.len() > haystack.len() {
403 return None;
404 }
405 for i in from..=haystack.len() - needle.len() {
406 if haystack[i..i + needle.len()]
407 .iter()
408 .zip(needle.iter())
409 .all(|(h, n)| h.eq_ignore_ascii_case(n))
410 {
411 return Some(i);
412 }
413 }
414 None
415}
416
417pub fn is_in_mkdocs_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
421 if is_in_icon_shortcode(line, position, flavor) {
422 return true;
423 }
424 if is_in_pymdown_markup(line, position, flavor) {
425 return true;
426 }
427 false
428}
429
430fn is_in_inline_code_on_line(line: &str, byte_pos: usize) -> bool {
436 let bytes = line.as_bytes();
437 let mut i = 0;
438
439 while i < bytes.len() {
440 if bytes[i] == b'`' {
441 let open_start = i;
442 let mut backtick_count = 0;
443 while i < bytes.len() && bytes[i] == b'`' {
444 backtick_count += 1;
445 i += 1;
446 }
447
448 let mut j = i;
450 while j < bytes.len() {
451 if bytes[j] == b'`' {
452 let mut close_count = 0;
453 while j < bytes.len() && bytes[j] == b'`' {
454 close_count += 1;
455 j += 1;
456 }
457 if close_count == backtick_count {
458 if byte_pos >= open_start && byte_pos < j {
460 return true;
461 }
462 i = j;
463 break;
464 }
465 } else {
466 j += 1;
467 }
468 }
469
470 if j >= bytes.len() {
471 break;
473 }
474 } else {
475 i += 1;
476 }
477 }
478
479 false
480}
481
482fn is_byte_in_html_tag(html_tags: &[HtmlTag], byte_pos: usize) -> bool {
484 let idx = html_tags.partition_point(|tag| tag.byte_offset <= byte_pos);
485 idx > 0 && byte_pos < html_tags[idx - 1].byte_end
486}
487
488fn is_byte_in_html_code_content(code_ranges: &[(usize, usize)], byte_pos: usize) -> bool {
491 let idx = code_ranges.partition_point(|&(start, _)| start <= byte_pos);
492 idx > 0 && byte_pos < code_ranges[idx - 1].1
493}
494
495pub(crate) fn compute_html_code_ranges(html_tags: &[HtmlTag]) -> Vec<(usize, usize)> {
498 let mut ranges = Vec::new();
499 let mut open_code_end: Option<usize> = None;
500
501 for tag in html_tags {
502 if tag.tag_name == "code" {
503 if tag.is_self_closing {
504 continue;
505 } else if !tag.is_closing {
506 open_code_end = Some(tag.byte_end);
507 } else if tag.is_closing {
508 if let Some(start) = open_code_end {
509 ranges.push((start, tag.byte_offset));
510 }
511 open_code_end = None;
512 }
513 }
514 }
515 if let Some(start) = open_code_end {
517 ranges.push((start, usize::MAX));
518 }
519 ranges
520}
521
522pub(crate) fn should_skip_emphasis_span(
530 ctx: &LintContext,
531 html_tags: &[HtmlTag],
532 html_code_ranges: &[(usize, usize)],
533 span_start: usize,
534) -> bool {
535 let lines = ctx.raw_lines();
536 let (line_num, col) = ctx.offset_to_line_col(span_start);
537
538 if ctx
540 .line_info(line_num)
541 .is_some_and(|info| info.in_front_matter || info.in_mkdocstrings)
542 {
543 return true;
544 }
545
546 let in_mkdocs_markup = lines
548 .get(line_num.saturating_sub(1))
549 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
550
551 let in_inline_code = lines
553 .get(line_num.saturating_sub(1))
554 .is_some_and(|line| is_in_inline_code_on_line(line, col.saturating_sub(1)));
555
556 ctx.is_in_code_block_or_span(span_start)
557 || in_inline_code
558 || ctx.is_in_link(span_start)
559 || is_byte_in_html_tag(html_tags, span_start)
560 || is_byte_in_html_code_content(html_code_ranges, span_start)
561 || in_mkdocs_markup
562 || is_in_math_context(ctx, span_start)
563 || is_in_jsx_expression(ctx, span_start)
564 || is_in_mdx_comment(ctx, span_start)
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_html_comment_detection() {
573 let content = "Text <!-- comment --> more text";
574 let ranges = compute_html_comment_ranges(content);
575 assert!(is_in_html_comment_ranges(&ranges, 10)); assert!(!is_in_html_comment_ranges(&ranges, 0)); assert!(!is_in_html_comment_ranges(&ranges, 25)); }
579
580 #[test]
581 fn test_compute_html_comment_ranges_ignores_code_span_delimiters() {
582 let content = "a `<!--` b\n\nc `-->` d";
585 let open = content.find("<!--").unwrap();
586 let close = content.find("-->").unwrap();
587 let code_spans = [
589 (content.find('`').unwrap(), open + "<!--".len() + 1),
590 (content.rfind("` d").unwrap() - "-->".len(), close + "-->".len() + 1),
591 ];
592
593 assert!(
595 !compute_html_comment_ranges(content).is_empty(),
596 "sanity: raw pattern matches across the code spans"
597 );
598 assert!(
600 compute_html_comment_ranges_filtered(content, &code_spans, &[]).is_empty(),
601 "a `<!--`/`-->` pair inside code spans must not be treated as a comment"
602 );
603 }
604
605 #[test]
606 fn test_compute_html_comment_ranges_ignores_code_block_delimiters() {
607 let content = "```\n<!-- literal\n```\n\nhttps://example.com\n\n-->\n";
610 let block_end = content.find("```\n\n").unwrap() + "```".len();
611 let code_blocks = [(0usize, block_end)];
612 assert!(
613 compute_html_comment_ranges_filtered(content, &[], &code_blocks).is_empty(),
614 "a `<!--` inside a code block must not open a comment that spans to a later `-->`"
615 );
616 let real = "```\n<!-- literal\n```\n\n<!-- real --> tail";
618 let real_block_end = real.find("```\n\n").unwrap() + "```".len();
619 let ranges = compute_html_comment_ranges_filtered(real, &[], &[(0usize, real_block_end)]);
620 assert_eq!(ranges.len(), 1);
621 assert_eq!(ranges[0].start, real.find("<!-- real").unwrap());
622 }
623
624 #[test]
625 fn test_compute_html_comment_ranges_keeps_real_comments() {
626 let content = "text `code` <!-- real comment --> more";
629 let code_spans = [(content.find('`').unwrap(), content.find("` ").unwrap() + 1)];
630 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
631 assert_eq!(ranges.len(), 1, "the real comment must still be detected");
632 let comment_start = content.find("<!--").unwrap();
633 assert_eq!(ranges[0].start, comment_start);
634 }
635
636 #[test]
637 fn test_compute_html_comment_ranges_real_comment_after_code_span_opener() {
638 let content = "a `<!--` then <!-- real --> end";
642 let code_spans = [(content.find('`').unwrap(), content.find("` then").unwrap() + 1)];
643 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
644 assert_eq!(
645 ranges.len(),
646 1,
647 "the real comment after a code-span opener must be detected"
648 );
649 let real_open = content.find("<!-- real").unwrap();
650 assert_eq!(
651 ranges[0].start, real_open,
652 "range must start at the real comment, not the code-span opener"
653 );
654 assert_eq!(ranges[0].end, content.find("--> end").unwrap() + "-->".len());
655 }
656
657 #[test]
658 fn test_compute_html_comment_ranges_closer_inside_code_span_is_not_a_closer() {
659 let content = "<!-- open `-->` still open --> done";
662 let first_close = content.find("`-->`").unwrap() + 1;
663 let code_spans = [(content.find('`').unwrap(), content.find("` still").unwrap() + 1)];
664 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
665 assert_eq!(ranges.len(), 1);
666 assert_eq!(ranges[0].start, 0);
667 let real_close_end = content.find("--> done").unwrap() + "-->".len();
668 assert_eq!(
669 ranges[0].end, real_close_end,
670 "must close at the real --> ({real_close_end}), not the one in the code span ({first_close})"
671 );
672 }
673
674 #[test]
675 fn test_is_line_entirely_in_html_comment() {
676 let content = "<!--\ncomment\n--> Content after comment";
678 let ranges = compute_html_comment_ranges(content);
679 assert!(is_line_entirely_in_html_comment(&ranges, 0, 4));
681 assert!(is_line_entirely_in_html_comment(&ranges, 5, 12));
683 assert!(!is_line_entirely_in_html_comment(&ranges, 13, 38));
685
686 let content2 = "<!-- comment --> Not a comment";
688 let ranges2 = compute_html_comment_ranges(content2);
689 assert!(!is_line_entirely_in_html_comment(&ranges2, 0, 30));
691
692 let content3 = "<!-- comment -->";
694 let ranges3 = compute_html_comment_ranges(content3);
695 assert!(is_line_entirely_in_html_comment(&ranges3, 0, 16));
697
698 let content4 = "Text before <!-- comment -->";
700 let ranges4 = compute_html_comment_ranges(content4);
701 assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
703 }
704
705 #[test]
706 fn test_is_line_entirely_in_html_comment_indented() {
707 let content = " <!-- comment -->";
711 let ranges = compute_html_comment_ranges(content);
712 let content_start = content.find("<!--").unwrap();
713 let content_end = content.trim_end().len();
714 assert!(is_line_entirely_in_html_comment(&ranges, content_start, content_end));
715 assert!(!is_line_entirely_in_html_comment(&ranges, 0, content.len()));
717 }
718
719 #[test]
720 fn test_is_line_entirely_in_html_comment_trailing_whitespace() {
721 let content = "<!-- comment --> ";
723 let ranges = compute_html_comment_ranges(content);
724 let content_end = content.trim_end().len();
725 assert!(is_line_entirely_in_html_comment(&ranges, 0, content_end));
726 assert!(!is_line_entirely_in_html_comment(&ranges, 0, content.len()));
728 }
729
730 #[test]
731 fn test_math_block_detection() {
732 let content = "Text\n$$\nmath content\n$$\nmore text";
733 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)); }
738
739 #[test]
740 fn test_stray_double_dollar_in_prose_is_not_math() {
741 let content = "Note: $$ is used for display math and $$ closes it";
745 let between = content.find("is used").unwrap();
746 assert!(
747 !is_in_math_block(content, between),
748 "stray paired `$$` in prose must not be treated as a math block"
749 );
750 assert!(math_block_ranges(content).is_empty());
751 }
752
753 #[test]
754 fn test_blockquoted_double_dollar_opens_block() {
755 let content = "> $$\n> x = y\n> $$\n";
757 let inside = content.find("x = y").unwrap();
758 assert!(is_in_math_block(content, inside), "blockquoted math interior");
759 }
760
761 #[test]
762 fn test_self_contained_single_line_block_leaves_trailing_prose() {
763 let content = "$$ a $$ and __not math__\n";
765 let in_math = content.find('a').unwrap();
766 assert!(is_in_math_block(content, in_math), "single-line math interior");
767 let after = content.find("not math").unwrap();
768 assert!(!is_in_math_block(content, after), "trailing prose is lintable");
769 }
770
771 #[test]
772 fn test_math_block_closes_with_content_before_fence() {
773 let content = "$$\nx = y\n\\end{x}$$\nafter __text__ here";
777
778 let inside = content.find("x = y").unwrap();
779 assert!(is_in_math_block(content, inside), "interior must be math");
780
781 let after = content.find("after").unwrap();
782 assert!(
783 !is_in_math_block(content, after),
784 "content after a content-sharing closing fence must NOT be math"
785 );
786 }
787
788 #[test]
789 fn test_inline_math_detection() {
790 let content = "Text $x + y$ and $$a^2 + b^2$$ here";
791 assert!(is_in_inline_math(content, 7), "inside the single-`$` inline span");
792 assert!(!is_in_inline_math(content, 20), "mid-line $$...$$ is not inline math");
796 assert!(
797 !is_in_math_block(content, 20),
798 "mid-line $$...$$ is not a line-start display block"
799 );
800 assert!(!is_in_inline_math(content, 0), "before any math");
801 assert!(!is_in_inline_math(content, 35), "after the spans");
802 }
803
804 #[test]
805 fn test_table_line_detection() {
806 assert!(is_table_line("| Header | Column |"));
807 assert!(is_table_line("|--------|--------|"));
808 assert!(is_table_line("| Cell 1 | Cell 2 |"));
809 assert!(!is_table_line("Regular text"));
810 assert!(!is_table_line("Just a pipe | here"));
811 }
812
813 #[test]
814 fn test_is_in_icon_shortcode() {
815 let line = "Click :material-check: to confirm";
816 assert!(!is_in_icon_shortcode(line, 0, MarkdownFlavor::MkDocs));
818 assert!(is_in_icon_shortcode(line, 6, MarkdownFlavor::MkDocs));
820 assert!(is_in_icon_shortcode(line, 15, MarkdownFlavor::MkDocs));
821 assert!(is_in_icon_shortcode(line, 21, MarkdownFlavor::MkDocs));
822 assert!(!is_in_icon_shortcode(line, 22, MarkdownFlavor::MkDocs));
824 }
825
826 #[test]
827 fn test_is_in_pymdown_markup() {
828 let line = "Press ++ctrl+c++ to copy";
830 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::MkDocs));
831 assert!(is_in_pymdown_markup(line, 6, MarkdownFlavor::MkDocs));
832 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::MkDocs));
833 assert!(!is_in_pymdown_markup(line, 17, MarkdownFlavor::MkDocs));
834
835 let line2 = "This is ==highlighted== text";
837 assert!(!is_in_pymdown_markup(line2, 0, MarkdownFlavor::MkDocs));
838 assert!(is_in_pymdown_markup(line2, 8, MarkdownFlavor::MkDocs));
839 assert!(is_in_pymdown_markup(line2, 15, MarkdownFlavor::MkDocs));
840 assert!(!is_in_pymdown_markup(line2, 23, MarkdownFlavor::MkDocs));
841
842 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Standard));
844 }
845
846 #[test]
847 fn test_is_in_mkdocs_markup() {
848 let line = ":material-check: and ++ctrl++";
850 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)); }
854
855 #[test]
858 fn test_obsidian_highlight_basic() {
859 let line = "This is ==highlighted== text";
861 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)); }
868
869 #[test]
870 fn test_obsidian_highlight_multiple() {
871 let line = "Both ==one== and ==two== here";
873 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)); }
878
879 #[test]
880 fn test_obsidian_highlight_not_standard_flavor() {
881 let line = "This is ==highlighted== text";
883 assert!(!is_in_pymdown_markup(line, 8, MarkdownFlavor::Standard));
884 assert!(!is_in_pymdown_markup(line, 15, MarkdownFlavor::Standard));
885 }
886
887 #[test]
888 fn test_obsidian_highlight_with_spaces_inside() {
889 let line = "This is ==text with spaces== here";
891 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)); }
895
896 #[test]
897 fn test_obsidian_does_not_support_keys_notation() {
898 let line = "Press ++ctrl+c++ to copy";
900 assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
901 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
902 }
903
904 #[test]
905 fn test_obsidian_mkdocs_markup_function() {
906 let line = "This is ==highlighted== text";
908 assert!(is_in_mkdocs_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(!is_in_mkdocs_markup(line, 0, MarkdownFlavor::Obsidian)); }
911
912 #[test]
913 fn test_obsidian_highlight_edge_cases() {
914 let line = "Test ==== here";
916 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
918
919 let line2 = "Test ==a== here";
921 assert!(is_in_pymdown_markup(line2, 5, MarkdownFlavor::Obsidian));
922 assert!(is_in_pymdown_markup(line2, 7, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line2, 9, MarkdownFlavor::Obsidian)); let line3 = "a === b";
927 assert!(!is_in_pymdown_markup(line3, 3, MarkdownFlavor::Obsidian));
928 }
929
930 #[test]
931 fn test_obsidian_highlight_unclosed() {
932 let line = "This ==starts but never ends";
934 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian));
935 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
936 }
937
938 #[test]
939 fn test_inline_html_code_basic() {
940 let line = "The formula is <code>a * b * c</code> in math.";
941 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)); }
948
949 #[test]
950 fn test_inline_html_code_multiple_tags() {
951 let line = "<kbd>Ctrl</kbd> + <samp>output</samp>";
952 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 24)); assert!(!is_in_inline_html_code(line, 16)); }
956
957 #[test]
958 fn test_inline_html_code_with_attributes() {
959 let line = r#"<code class="lang">x * y</code>"#;
960 assert!(is_in_inline_html_code(line, 19)); assert!(is_in_inline_html_code(line, 23)); assert!(!is_in_inline_html_code(line, 0)); }
964
965 #[test]
966 fn test_inline_html_code_case_insensitive() {
967 let line = "<CODE>a * b</CODE>";
968 assert!(is_in_inline_html_code(line, 6)); assert!(is_in_inline_html_code(line, 8)); }
971
972 #[test]
973 fn test_inline_html_code_var_and_pre() {
974 let line = "<var>x * y</var> and <pre>a * b</pre>";
975 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 26)); assert!(!is_in_inline_html_code(line, 17)); }
979
980 #[test]
981 fn test_inline_html_code_unclosed() {
982 let line = "<code>a * b without closing";
984 assert!(!is_in_inline_html_code(line, 6));
985 }
986
987 #[test]
988 fn test_inline_html_code_no_substring_match() {
989 let line = "<variable>a * b</variable>";
991 assert!(!is_in_inline_html_code(line, 11));
992
993 let line2 = "<keyboard>x * y</keyboard>";
995 assert!(!is_in_inline_html_code(line2, 11));
996 }
997}