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], line_start: usize, line_end: usize) -> bool {
125 for range in ranges {
126 if line_start >= range.start && line_start < range.end {
128 return line_end <= range.end;
129 }
130 }
131 false
132}
133
134#[inline]
136pub fn is_in_jsx_expression(ctx: &LintContext, byte_pos: usize) -> bool {
137 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_jsx_expression(byte_pos)
138}
139
140#[inline]
142pub fn is_in_mdx_comment(ctx: &LintContext, byte_pos: usize) -> bool {
143 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_mdx_comment(byte_pos)
144}
145
146pub fn is_mkdocs_snippet_line(line: &str, flavor: MarkdownFlavor) -> bool {
148 flavor == MarkdownFlavor::MkDocs && mkdocs_snippets::is_snippet_marker(line)
149}
150
151pub fn is_mkdocs_admonition_line(line: &str, flavor: MarkdownFlavor) -> bool {
153 flavor == MarkdownFlavor::MkDocs && mkdocs_admonitions::is_admonition_marker(line)
154}
155
156pub fn is_mkdocs_footnote_line(line: &str, flavor: MarkdownFlavor) -> bool {
158 flavor == MarkdownFlavor::MkDocs && mkdocs_footnotes::is_footnote_definition(line)
159}
160
161pub fn is_mkdocs_tab_line(line: &str, flavor: MarkdownFlavor) -> bool {
163 flavor == MarkdownFlavor::MkDocs && mkdocs_tabs::is_tab_marker(line)
164}
165
166pub fn is_mkdocs_critic_line(line: &str, flavor: MarkdownFlavor) -> bool {
168 flavor == MarkdownFlavor::MkDocs && mkdocs_critic::contains_critic_markup(line)
169}
170
171pub fn is_in_html_tag(ctx: &LintContext, byte_pos: usize) -> bool {
173 for html_tag in ctx.html_tags().iter() {
174 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
175 return true;
176 }
177 }
178 false
179}
180
181pub fn is_in_math_context(ctx: &LintContext, byte_pos: usize) -> bool {
188 ctx.math_byte_ranges()
191 .iter()
192 .any(|&(start, end)| byte_pos >= start && byte_pos < end)
193}
194
195pub(crate) fn math_block_ranges(content: &str) -> Vec<(usize, usize)> {
206 let bytes = content.as_bytes();
207 let mut ranges = Vec::new();
208 let mut open: Option<usize> = None;
209 let mut line_start = 0usize;
210 let mut i = 0;
211 while i < bytes.len() {
212 match bytes[i] {
213 b'\n' => {
214 line_start = i + 1;
215 i += 1;
216 }
217 b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'$' => {
218 match open {
219 None => {
220 let starts_line = bytes[line_start..i]
223 .iter()
224 .all(|&b| b == b' ' || b == b'\t' || b == b'>');
225 if starts_line {
226 open = Some(i);
227 }
228 }
229 Some(start) => {
230 ranges.push((start, i + 2));
231 open = None;
232 }
233 }
234 i += 2;
235 }
236 _ => i += 1,
237 }
238 }
239 ranges
240}
241
242pub fn is_in_math_block(content: &str, byte_pos: usize) -> bool {
249 math_block_ranges(content)
250 .iter()
251 .any(|&(start, end)| byte_pos >= start && byte_pos < end)
252}
253
254pub fn is_in_inline_math(content: &str, byte_pos: usize) -> bool {
263 for m in INLINE_MATH_REGEX.find_iter(content) {
264 if content[m.start()..m.end()].starts_with("$$") {
265 continue;
266 }
267 if m.start() <= byte_pos && byte_pos < m.end() {
268 return true;
269 }
270 }
271 false
272}
273
274pub fn math_byte_ranges(content: &str) -> Vec<(usize, usize)> {
282 let mut ranges = math_block_ranges(content);
283 for m in INLINE_MATH_REGEX.find_iter(content) {
284 if content[m.start()..m.end()].starts_with("$$") {
285 continue;
286 }
287 ranges.push((m.start(), m.end()));
288 }
289 ranges
290}
291
292pub fn is_in_table_cell(ctx: &LintContext, line_num: usize, _col: usize) -> bool {
294 for table_row in ctx.table_rows().iter() {
296 if table_row.line == line_num {
297 return true;
301 }
302 }
303 false
304}
305
306pub fn is_table_line(line: &str) -> bool {
308 let trimmed = line.trim();
309
310 if trimmed
312 .chars()
313 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
314 && trimmed.contains('|')
315 && trimmed.contains('-')
316 {
317 return true;
318 }
319
320 if (trimmed.starts_with('|') || trimmed.ends_with('|')) && trimmed.matches('|').count() >= 2 {
322 return true;
323 }
324
325 false
326}
327
328pub fn is_in_icon_shortcode(line: &str, position: usize, _flavor: MarkdownFlavor) -> bool {
331 mkdocs_icons::is_in_any_shortcode(line, position)
334}
335
336pub fn is_in_pymdown_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
342 match flavor {
343 MarkdownFlavor::MkDocs => mkdocs_extensions::is_in_pymdown_markup(line, position),
344 MarkdownFlavor::Obsidian => {
345 mkdocs_extensions::is_in_mark(line, position)
347 }
348 _ => false,
349 }
350}
351
352pub fn is_in_inline_html_code(line: &str, position: usize) -> bool {
357 const TAGS: &[&str] = &["code", "pre", "samp", "kbd", "var"];
359
360 let bytes = line.as_bytes();
361
362 for tag in TAGS {
363 let open_bytes = format!("<{tag}").into_bytes();
364 let close_pattern = format!("</{tag}>").into_bytes();
365
366 let mut search_from = 0;
367 while search_from + open_bytes.len() <= bytes.len() {
368 let Some(open_abs) = find_case_insensitive(bytes, &open_bytes, search_from) else {
370 break;
371 };
372
373 let after_tag = open_abs + open_bytes.len();
374
375 if after_tag < bytes.len() {
377 let next = bytes[after_tag];
378 if next != b'>' && next != b' ' && next != b'\t' {
379 search_from = after_tag;
380 continue;
381 }
382 }
383
384 let Some(tag_close) = bytes[after_tag..].iter().position(|&b| b == b'>') else {
386 break;
387 };
388 let content_start = after_tag + tag_close + 1;
389
390 let Some(close_start) = find_case_insensitive(bytes, &close_pattern, content_start) else {
392 break;
393 };
394 let content_end = close_start;
395
396 if position >= content_start && position < content_end {
397 return true;
398 }
399
400 search_from = close_start + close_pattern.len();
401 }
402 }
403 false
404}
405
406fn find_case_insensitive(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
408 if needle.is_empty() || from + needle.len() > haystack.len() {
409 return None;
410 }
411 for i in from..=haystack.len() - needle.len() {
412 if haystack[i..i + needle.len()]
413 .iter()
414 .zip(needle.iter())
415 .all(|(h, n)| h.eq_ignore_ascii_case(n))
416 {
417 return Some(i);
418 }
419 }
420 None
421}
422
423pub fn is_in_mkdocs_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
427 if is_in_icon_shortcode(line, position, flavor) {
428 return true;
429 }
430 if is_in_pymdown_markup(line, position, flavor) {
431 return true;
432 }
433 false
434}
435
436fn is_in_inline_code_on_line(line: &str, byte_pos: usize) -> bool {
442 let bytes = line.as_bytes();
443 let mut i = 0;
444
445 while i < bytes.len() {
446 if bytes[i] == b'`' {
447 let open_start = i;
448 let mut backtick_count = 0;
449 while i < bytes.len() && bytes[i] == b'`' {
450 backtick_count += 1;
451 i += 1;
452 }
453
454 let mut j = i;
456 while j < bytes.len() {
457 if bytes[j] == b'`' {
458 let mut close_count = 0;
459 while j < bytes.len() && bytes[j] == b'`' {
460 close_count += 1;
461 j += 1;
462 }
463 if close_count == backtick_count {
464 if byte_pos >= open_start && byte_pos < j {
466 return true;
467 }
468 i = j;
469 break;
470 }
471 } else {
472 j += 1;
473 }
474 }
475
476 if j >= bytes.len() {
477 break;
479 }
480 } else {
481 i += 1;
482 }
483 }
484
485 false
486}
487
488fn is_byte_in_html_tag(html_tags: &[HtmlTag], byte_pos: usize) -> bool {
490 let idx = html_tags.partition_point(|tag| tag.byte_offset <= byte_pos);
491 idx > 0 && byte_pos < html_tags[idx - 1].byte_end
492}
493
494fn is_byte_in_html_code_content(code_ranges: &[(usize, usize)], byte_pos: usize) -> bool {
497 let idx = code_ranges.partition_point(|&(start, _)| start <= byte_pos);
498 idx > 0 && byte_pos < code_ranges[idx - 1].1
499}
500
501pub(crate) fn compute_html_code_ranges(html_tags: &[HtmlTag]) -> Vec<(usize, usize)> {
504 let mut ranges = Vec::new();
505 let mut open_code_end: Option<usize> = None;
506
507 for tag in html_tags {
508 if tag.tag_name == "code" {
509 if tag.is_self_closing {
510 continue;
511 } else if !tag.is_closing {
512 open_code_end = Some(tag.byte_end);
513 } else if tag.is_closing {
514 if let Some(start) = open_code_end {
515 ranges.push((start, tag.byte_offset));
516 }
517 open_code_end = None;
518 }
519 }
520 }
521 if let Some(start) = open_code_end {
523 ranges.push((start, usize::MAX));
524 }
525 ranges
526}
527
528pub(crate) fn should_skip_emphasis_span(
536 ctx: &LintContext,
537 html_tags: &[HtmlTag],
538 html_code_ranges: &[(usize, usize)],
539 span_start: usize,
540) -> bool {
541 let lines = ctx.raw_lines();
542 let (line_num, col) = ctx.offset_to_line_col(span_start);
543
544 if ctx
546 .line_info(line_num)
547 .is_some_and(|info| info.in_front_matter || info.in_mkdocstrings)
548 {
549 return true;
550 }
551
552 let in_mkdocs_markup = lines
554 .get(line_num.saturating_sub(1))
555 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
556
557 let in_inline_code = lines
559 .get(line_num.saturating_sub(1))
560 .is_some_and(|line| is_in_inline_code_on_line(line, col.saturating_sub(1)));
561
562 ctx.is_in_code_block_or_span(span_start)
563 || in_inline_code
564 || ctx.is_in_link(span_start)
565 || is_byte_in_html_tag(html_tags, span_start)
566 || is_byte_in_html_code_content(html_code_ranges, span_start)
567 || in_mkdocs_markup
568 || is_in_math_context(ctx, span_start)
569 || is_in_jsx_expression(ctx, span_start)
570 || is_in_mdx_comment(ctx, span_start)
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_html_comment_detection() {
579 let content = "Text <!-- comment --> more text";
580 let ranges = compute_html_comment_ranges(content);
581 assert!(is_in_html_comment_ranges(&ranges, 10)); assert!(!is_in_html_comment_ranges(&ranges, 0)); assert!(!is_in_html_comment_ranges(&ranges, 25)); }
585
586 #[test]
587 fn test_compute_html_comment_ranges_ignores_code_span_delimiters() {
588 let content = "a `<!--` b\n\nc `-->` d";
591 let open = content.find("<!--").unwrap();
592 let close = content.find("-->").unwrap();
593 let code_spans = [
595 (content.find('`').unwrap(), open + "<!--".len() + 1),
596 (content.rfind("` d").unwrap() - "-->".len(), close + "-->".len() + 1),
597 ];
598
599 assert!(
601 !compute_html_comment_ranges(content).is_empty(),
602 "sanity: raw pattern matches across the code spans"
603 );
604 assert!(
606 compute_html_comment_ranges_filtered(content, &code_spans, &[]).is_empty(),
607 "a `<!--`/`-->` pair inside code spans must not be treated as a comment"
608 );
609 }
610
611 #[test]
612 fn test_compute_html_comment_ranges_ignores_code_block_delimiters() {
613 let content = "```\n<!-- literal\n```\n\nhttps://example.com\n\n-->\n";
616 let block_end = content.find("```\n\n").unwrap() + "```".len();
617 let code_blocks = [(0usize, block_end)];
618 assert!(
619 compute_html_comment_ranges_filtered(content, &[], &code_blocks).is_empty(),
620 "a `<!--` inside a code block must not open a comment that spans to a later `-->`"
621 );
622 let real = "```\n<!-- literal\n```\n\n<!-- real --> tail";
624 let real_block_end = real.find("```\n\n").unwrap() + "```".len();
625 let ranges = compute_html_comment_ranges_filtered(real, &[], &[(0usize, real_block_end)]);
626 assert_eq!(ranges.len(), 1);
627 assert_eq!(ranges[0].start, real.find("<!-- real").unwrap());
628 }
629
630 #[test]
631 fn test_compute_html_comment_ranges_keeps_real_comments() {
632 let content = "text `code` <!-- real comment --> more";
635 let code_spans = [(content.find('`').unwrap(), content.find("` ").unwrap() + 1)];
636 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
637 assert_eq!(ranges.len(), 1, "the real comment must still be detected");
638 let comment_start = content.find("<!--").unwrap();
639 assert_eq!(ranges[0].start, comment_start);
640 }
641
642 #[test]
643 fn test_compute_html_comment_ranges_real_comment_after_code_span_opener() {
644 let content = "a `<!--` then <!-- real --> end";
648 let code_spans = [(content.find('`').unwrap(), content.find("` then").unwrap() + 1)];
649 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
650 assert_eq!(
651 ranges.len(),
652 1,
653 "the real comment after a code-span opener must be detected"
654 );
655 let real_open = content.find("<!-- real").unwrap();
656 assert_eq!(
657 ranges[0].start, real_open,
658 "range must start at the real comment, not the code-span opener"
659 );
660 assert_eq!(ranges[0].end, content.find("--> end").unwrap() + "-->".len());
661 }
662
663 #[test]
664 fn test_compute_html_comment_ranges_closer_inside_code_span_is_not_a_closer() {
665 let content = "<!-- open `-->` still open --> done";
668 let first_close = content.find("`-->`").unwrap() + 1;
669 let code_spans = [(content.find('`').unwrap(), content.find("` still").unwrap() + 1)];
670 let ranges = compute_html_comment_ranges_filtered(content, &code_spans, &[]);
671 assert_eq!(ranges.len(), 1);
672 assert_eq!(ranges[0].start, 0);
673 let real_close_end = content.find("--> done").unwrap() + "-->".len();
674 assert_eq!(
675 ranges[0].end, real_close_end,
676 "must close at the real --> ({real_close_end}), not the one in the code span ({first_close})"
677 );
678 }
679
680 #[test]
681 fn test_is_line_entirely_in_html_comment() {
682 let content = "<!--\ncomment\n--> Content after comment";
684 let ranges = compute_html_comment_ranges(content);
685 assert!(is_line_entirely_in_html_comment(&ranges, 0, 4));
687 assert!(is_line_entirely_in_html_comment(&ranges, 5, 12));
689 assert!(!is_line_entirely_in_html_comment(&ranges, 13, 38));
691
692 let content2 = "<!-- comment --> Not a comment";
694 let ranges2 = compute_html_comment_ranges(content2);
695 assert!(!is_line_entirely_in_html_comment(&ranges2, 0, 30));
697
698 let content3 = "<!-- comment -->";
700 let ranges3 = compute_html_comment_ranges(content3);
701 assert!(is_line_entirely_in_html_comment(&ranges3, 0, 16));
703
704 let content4 = "Text before <!-- comment -->";
706 let ranges4 = compute_html_comment_ranges(content4);
707 assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
709 }
710
711 #[test]
712 fn test_math_block_detection() {
713 let content = "Text\n$$\nmath content\n$$\nmore text";
714 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)); }
719
720 #[test]
721 fn test_stray_double_dollar_in_prose_is_not_math() {
722 let content = "Note: $$ is used for display math and $$ closes it";
726 let between = content.find("is used").unwrap();
727 assert!(
728 !is_in_math_block(content, between),
729 "stray paired `$$` in prose must not be treated as a math block"
730 );
731 assert!(math_block_ranges(content).is_empty());
732 }
733
734 #[test]
735 fn test_blockquoted_double_dollar_opens_block() {
736 let content = "> $$\n> x = y\n> $$\n";
738 let inside = content.find("x = y").unwrap();
739 assert!(is_in_math_block(content, inside), "blockquoted math interior");
740 }
741
742 #[test]
743 fn test_self_contained_single_line_block_leaves_trailing_prose() {
744 let content = "$$ a $$ and __not math__\n";
746 let in_math = content.find('a').unwrap();
747 assert!(is_in_math_block(content, in_math), "single-line math interior");
748 let after = content.find("not math").unwrap();
749 assert!(!is_in_math_block(content, after), "trailing prose is lintable");
750 }
751
752 #[test]
753 fn test_math_block_closes_with_content_before_fence() {
754 let content = "$$\nx = y\n\\end{x}$$\nafter __text__ here";
758
759 let inside = content.find("x = y").unwrap();
760 assert!(is_in_math_block(content, inside), "interior must be math");
761
762 let after = content.find("after").unwrap();
763 assert!(
764 !is_in_math_block(content, after),
765 "content after a content-sharing closing fence must NOT be math"
766 );
767 }
768
769 #[test]
770 fn test_inline_math_detection() {
771 let content = "Text $x + y$ and $$a^2 + b^2$$ here";
772 assert!(is_in_inline_math(content, 7), "inside the single-`$` inline span");
773 assert!(!is_in_inline_math(content, 20), "mid-line $$...$$ is not inline math");
777 assert!(
778 !is_in_math_block(content, 20),
779 "mid-line $$...$$ is not a line-start display block"
780 );
781 assert!(!is_in_inline_math(content, 0), "before any math");
782 assert!(!is_in_inline_math(content, 35), "after the spans");
783 }
784
785 #[test]
786 fn test_table_line_detection() {
787 assert!(is_table_line("| Header | Column |"));
788 assert!(is_table_line("|--------|--------|"));
789 assert!(is_table_line("| Cell 1 | Cell 2 |"));
790 assert!(!is_table_line("Regular text"));
791 assert!(!is_table_line("Just a pipe | here"));
792 }
793
794 #[test]
795 fn test_is_in_icon_shortcode() {
796 let line = "Click :material-check: to confirm";
797 assert!(!is_in_icon_shortcode(line, 0, MarkdownFlavor::MkDocs));
799 assert!(is_in_icon_shortcode(line, 6, MarkdownFlavor::MkDocs));
801 assert!(is_in_icon_shortcode(line, 15, MarkdownFlavor::MkDocs));
802 assert!(is_in_icon_shortcode(line, 21, MarkdownFlavor::MkDocs));
803 assert!(!is_in_icon_shortcode(line, 22, MarkdownFlavor::MkDocs));
805 }
806
807 #[test]
808 fn test_is_in_pymdown_markup() {
809 let line = "Press ++ctrl+c++ to copy";
811 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::MkDocs));
812 assert!(is_in_pymdown_markup(line, 6, MarkdownFlavor::MkDocs));
813 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::MkDocs));
814 assert!(!is_in_pymdown_markup(line, 17, MarkdownFlavor::MkDocs));
815
816 let line2 = "This is ==highlighted== text";
818 assert!(!is_in_pymdown_markup(line2, 0, MarkdownFlavor::MkDocs));
819 assert!(is_in_pymdown_markup(line2, 8, MarkdownFlavor::MkDocs));
820 assert!(is_in_pymdown_markup(line2, 15, MarkdownFlavor::MkDocs));
821 assert!(!is_in_pymdown_markup(line2, 23, MarkdownFlavor::MkDocs));
822
823 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Standard));
825 }
826
827 #[test]
828 fn test_is_in_mkdocs_markup() {
829 let line = ":material-check: and ++ctrl++";
831 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)); }
835
836 #[test]
839 fn test_obsidian_highlight_basic() {
840 let line = "This is ==highlighted== text";
842 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)); }
849
850 #[test]
851 fn test_obsidian_highlight_multiple() {
852 let line = "Both ==one== and ==two== here";
854 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)); }
859
860 #[test]
861 fn test_obsidian_highlight_not_standard_flavor() {
862 let line = "This is ==highlighted== text";
864 assert!(!is_in_pymdown_markup(line, 8, MarkdownFlavor::Standard));
865 assert!(!is_in_pymdown_markup(line, 15, MarkdownFlavor::Standard));
866 }
867
868 #[test]
869 fn test_obsidian_highlight_with_spaces_inside() {
870 let line = "This is ==text with spaces== here";
872 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)); }
876
877 #[test]
878 fn test_obsidian_does_not_support_keys_notation() {
879 let line = "Press ++ctrl+c++ to copy";
881 assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
882 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
883 }
884
885 #[test]
886 fn test_obsidian_mkdocs_markup_function() {
887 let line = "This is ==highlighted== text";
889 assert!(is_in_mkdocs_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(!is_in_mkdocs_markup(line, 0, MarkdownFlavor::Obsidian)); }
892
893 #[test]
894 fn test_obsidian_highlight_edge_cases() {
895 let line = "Test ==== here";
897 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
899
900 let line2 = "Test ==a== here";
902 assert!(is_in_pymdown_markup(line2, 5, MarkdownFlavor::Obsidian));
903 assert!(is_in_pymdown_markup(line2, 7, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line2, 9, MarkdownFlavor::Obsidian)); let line3 = "a === b";
908 assert!(!is_in_pymdown_markup(line3, 3, MarkdownFlavor::Obsidian));
909 }
910
911 #[test]
912 fn test_obsidian_highlight_unclosed() {
913 let line = "This ==starts but never ends";
915 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian));
916 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
917 }
918
919 #[test]
920 fn test_inline_html_code_basic() {
921 let line = "The formula is <code>a * b * c</code> in math.";
922 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)); }
929
930 #[test]
931 fn test_inline_html_code_multiple_tags() {
932 let line = "<kbd>Ctrl</kbd> + <samp>output</samp>";
933 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 24)); assert!(!is_in_inline_html_code(line, 16)); }
937
938 #[test]
939 fn test_inline_html_code_with_attributes() {
940 let line = r#"<code class="lang">x * y</code>"#;
941 assert!(is_in_inline_html_code(line, 19)); assert!(is_in_inline_html_code(line, 23)); assert!(!is_in_inline_html_code(line, 0)); }
945
946 #[test]
947 fn test_inline_html_code_case_insensitive() {
948 let line = "<CODE>a * b</CODE>";
949 assert!(is_in_inline_html_code(line, 6)); assert!(is_in_inline_html_code(line, 8)); }
952
953 #[test]
954 fn test_inline_html_code_var_and_pre() {
955 let line = "<var>x * y</var> and <pre>a * b</pre>";
956 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 26)); assert!(!is_in_inline_html_code(line, 17)); }
960
961 #[test]
962 fn test_inline_html_code_unclosed() {
963 let line = "<code>a * b without closing";
965 assert!(!is_in_inline_html_code(line, 6));
966 }
967
968 #[test]
969 fn test_inline_html_code_no_substring_match() {
970 let line = "<variable>a * b</variable>";
972 assert!(!is_in_inline_html_code(line, 11));
973
974 let line2 = "<keyboard>x * y</keyboard>";
976 assert!(!is_in_inline_html_code(line2, 11));
977 }
978}