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 crate::utils::regex_cache::HTML_COMMENT_PATTERN;
16use regex::Regex;
17use std::sync::LazyLock;
18
19static INLINE_MATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$[^$]*\$\$|\$[^$\n]*\$").unwrap());
28
29#[derive(Debug, Clone, Copy)]
31pub struct ByteRange {
32 pub start: usize,
33 pub end: usize,
34}
35
36pub fn compute_html_comment_ranges(content: &str) -> Vec<ByteRange> {
39 HTML_COMMENT_PATTERN
40 .find_iter(content)
41 .map(|m| ByteRange {
42 start: m.start(),
43 end: m.end(),
44 })
45 .collect()
46}
47
48pub fn is_in_html_comment_ranges(ranges: &[ByteRange], byte_pos: usize) -> bool {
51 ranges
53 .binary_search_by(|range| {
54 if byte_pos < range.start {
55 std::cmp::Ordering::Greater
56 } else if byte_pos >= range.end {
57 std::cmp::Ordering::Less
58 } else {
59 std::cmp::Ordering::Equal
60 }
61 })
62 .is_ok()
63}
64
65pub fn is_line_entirely_in_html_comment(ranges: &[ByteRange], line_start: usize, line_end: usize) -> bool {
68 for range in ranges {
69 if line_start >= range.start && line_start < range.end {
71 return line_end <= range.end;
72 }
73 }
74 false
75}
76
77#[inline]
79pub fn is_in_jsx_expression(ctx: &LintContext, byte_pos: usize) -> bool {
80 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_jsx_expression(byte_pos)
81}
82
83#[inline]
85pub fn is_in_mdx_comment(ctx: &LintContext, byte_pos: usize) -> bool {
86 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_mdx_comment(byte_pos)
87}
88
89pub fn is_mkdocs_snippet_line(line: &str, flavor: MarkdownFlavor) -> bool {
91 flavor == MarkdownFlavor::MkDocs && mkdocs_snippets::is_snippet_marker(line)
92}
93
94pub fn is_mkdocs_admonition_line(line: &str, flavor: MarkdownFlavor) -> bool {
96 flavor == MarkdownFlavor::MkDocs && mkdocs_admonitions::is_admonition_marker(line)
97}
98
99pub fn is_mkdocs_footnote_line(line: &str, flavor: MarkdownFlavor) -> bool {
101 flavor == MarkdownFlavor::MkDocs && mkdocs_footnotes::is_footnote_definition(line)
102}
103
104pub fn is_mkdocs_tab_line(line: &str, flavor: MarkdownFlavor) -> bool {
106 flavor == MarkdownFlavor::MkDocs && mkdocs_tabs::is_tab_marker(line)
107}
108
109pub fn is_mkdocs_critic_line(line: &str, flavor: MarkdownFlavor) -> bool {
111 flavor == MarkdownFlavor::MkDocs && mkdocs_critic::contains_critic_markup(line)
112}
113
114pub fn is_in_html_comment(content: &str, byte_pos: usize) -> bool {
116 for m in HTML_COMMENT_PATTERN.find_iter(content) {
117 if m.start() <= byte_pos && byte_pos < m.end() {
118 return true;
119 }
120 }
121 false
122}
123
124pub fn is_in_html_tag(ctx: &LintContext, byte_pos: usize) -> bool {
126 for html_tag in ctx.html_tags().iter() {
127 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
128 return true;
129 }
130 }
131 false
132}
133
134pub fn is_in_math_context(ctx: &LintContext, byte_pos: usize) -> bool {
141 ctx.math_byte_ranges()
144 .iter()
145 .any(|&(start, end)| byte_pos >= start && byte_pos < end)
146}
147
148pub(crate) fn math_block_ranges(content: &str) -> Vec<(usize, usize)> {
159 let bytes = content.as_bytes();
160 let mut ranges = Vec::new();
161 let mut open: Option<usize> = None;
162 let mut line_start = 0usize;
163 let mut i = 0;
164 while i < bytes.len() {
165 match bytes[i] {
166 b'\n' => {
167 line_start = i + 1;
168 i += 1;
169 }
170 b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'$' => {
171 match open {
172 None => {
173 let starts_line = bytes[line_start..i]
176 .iter()
177 .all(|&b| b == b' ' || b == b'\t' || b == b'>');
178 if starts_line {
179 open = Some(i);
180 }
181 }
182 Some(start) => {
183 ranges.push((start, i + 2));
184 open = None;
185 }
186 }
187 i += 2;
188 }
189 _ => i += 1,
190 }
191 }
192 ranges
193}
194
195pub fn is_in_math_block(content: &str, byte_pos: usize) -> bool {
202 math_block_ranges(content)
203 .iter()
204 .any(|&(start, end)| byte_pos >= start && byte_pos < end)
205}
206
207pub fn is_in_inline_math(content: &str, byte_pos: usize) -> bool {
216 for m in INLINE_MATH_REGEX.find_iter(content) {
217 if content[m.start()..m.end()].starts_with("$$") {
218 continue;
219 }
220 if m.start() <= byte_pos && byte_pos < m.end() {
221 return true;
222 }
223 }
224 false
225}
226
227pub fn math_byte_ranges(content: &str) -> Vec<(usize, usize)> {
235 let mut ranges = math_block_ranges(content);
236 for m in INLINE_MATH_REGEX.find_iter(content) {
237 if content[m.start()..m.end()].starts_with("$$") {
238 continue;
239 }
240 ranges.push((m.start(), m.end()));
241 }
242 ranges
243}
244
245pub fn is_in_table_cell(ctx: &LintContext, line_num: usize, _col: usize) -> bool {
247 for table_row in ctx.table_rows().iter() {
249 if table_row.line == line_num {
250 return true;
254 }
255 }
256 false
257}
258
259pub fn is_table_line(line: &str) -> bool {
261 let trimmed = line.trim();
262
263 if trimmed
265 .chars()
266 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
267 && trimmed.contains('|')
268 && trimmed.contains('-')
269 {
270 return true;
271 }
272
273 if (trimmed.starts_with('|') || trimmed.ends_with('|')) && trimmed.matches('|').count() >= 2 {
275 return true;
276 }
277
278 false
279}
280
281pub fn is_in_icon_shortcode(line: &str, position: usize, _flavor: MarkdownFlavor) -> bool {
284 mkdocs_icons::is_in_any_shortcode(line, position)
287}
288
289pub fn is_in_pymdown_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
295 match flavor {
296 MarkdownFlavor::MkDocs => mkdocs_extensions::is_in_pymdown_markup(line, position),
297 MarkdownFlavor::Obsidian => {
298 mkdocs_extensions::is_in_mark(line, position)
300 }
301 _ => false,
302 }
303}
304
305pub fn is_in_inline_html_code(line: &str, position: usize) -> bool {
310 const TAGS: &[&str] = &["code", "pre", "samp", "kbd", "var"];
312
313 let bytes = line.as_bytes();
314
315 for tag in TAGS {
316 let open_bytes = format!("<{tag}").into_bytes();
317 let close_pattern = format!("</{tag}>").into_bytes();
318
319 let mut search_from = 0;
320 while search_from + open_bytes.len() <= bytes.len() {
321 let Some(open_abs) = find_case_insensitive(bytes, &open_bytes, search_from) else {
323 break;
324 };
325
326 let after_tag = open_abs + open_bytes.len();
327
328 if after_tag < bytes.len() {
330 let next = bytes[after_tag];
331 if next != b'>' && next != b' ' && next != b'\t' {
332 search_from = after_tag;
333 continue;
334 }
335 }
336
337 let Some(tag_close) = bytes[after_tag..].iter().position(|&b| b == b'>') else {
339 break;
340 };
341 let content_start = after_tag + tag_close + 1;
342
343 let Some(close_start) = find_case_insensitive(bytes, &close_pattern, content_start) else {
345 break;
346 };
347 let content_end = close_start;
348
349 if position >= content_start && position < content_end {
350 return true;
351 }
352
353 search_from = close_start + close_pattern.len();
354 }
355 }
356 false
357}
358
359fn find_case_insensitive(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
361 if needle.is_empty() || from + needle.len() > haystack.len() {
362 return None;
363 }
364 for i in from..=haystack.len() - needle.len() {
365 if haystack[i..i + needle.len()]
366 .iter()
367 .zip(needle.iter())
368 .all(|(h, n)| h.eq_ignore_ascii_case(n))
369 {
370 return Some(i);
371 }
372 }
373 None
374}
375
376pub fn is_in_mkdocs_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
380 if is_in_icon_shortcode(line, position, flavor) {
381 return true;
382 }
383 if is_in_pymdown_markup(line, position, flavor) {
384 return true;
385 }
386 false
387}
388
389fn is_in_inline_code_on_line(line: &str, byte_pos: usize) -> bool {
395 let bytes = line.as_bytes();
396 let mut i = 0;
397
398 while i < bytes.len() {
399 if bytes[i] == b'`' {
400 let open_start = i;
401 let mut backtick_count = 0;
402 while i < bytes.len() && bytes[i] == b'`' {
403 backtick_count += 1;
404 i += 1;
405 }
406
407 let mut j = i;
409 while j < bytes.len() {
410 if bytes[j] == b'`' {
411 let mut close_count = 0;
412 while j < bytes.len() && bytes[j] == b'`' {
413 close_count += 1;
414 j += 1;
415 }
416 if close_count == backtick_count {
417 if byte_pos >= open_start && byte_pos < j {
419 return true;
420 }
421 i = j;
422 break;
423 }
424 } else {
425 j += 1;
426 }
427 }
428
429 if j >= bytes.len() {
430 break;
432 }
433 } else {
434 i += 1;
435 }
436 }
437
438 false
439}
440
441fn is_byte_in_html_tag(html_tags: &[HtmlTag], byte_pos: usize) -> bool {
443 let idx = html_tags.partition_point(|tag| tag.byte_offset <= byte_pos);
444 idx > 0 && byte_pos < html_tags[idx - 1].byte_end
445}
446
447fn is_byte_in_html_code_content(code_ranges: &[(usize, usize)], byte_pos: usize) -> bool {
450 let idx = code_ranges.partition_point(|&(start, _)| start <= byte_pos);
451 idx > 0 && byte_pos < code_ranges[idx - 1].1
452}
453
454pub(crate) fn compute_html_code_ranges(html_tags: &[HtmlTag]) -> Vec<(usize, usize)> {
457 let mut ranges = Vec::new();
458 let mut open_code_end: Option<usize> = None;
459
460 for tag in html_tags {
461 if tag.tag_name == "code" {
462 if tag.is_self_closing {
463 continue;
464 } else if !tag.is_closing {
465 open_code_end = Some(tag.byte_end);
466 } else if tag.is_closing {
467 if let Some(start) = open_code_end {
468 ranges.push((start, tag.byte_offset));
469 }
470 open_code_end = None;
471 }
472 }
473 }
474 if let Some(start) = open_code_end {
476 ranges.push((start, usize::MAX));
477 }
478 ranges
479}
480
481pub(crate) fn should_skip_emphasis_span(
489 ctx: &LintContext,
490 html_tags: &[HtmlTag],
491 html_code_ranges: &[(usize, usize)],
492 span_start: usize,
493) -> bool {
494 let lines = ctx.raw_lines();
495 let (line_num, col) = ctx.offset_to_line_col(span_start);
496
497 if ctx
499 .line_info(line_num)
500 .is_some_and(|info| info.in_front_matter || info.in_mkdocstrings)
501 {
502 return true;
503 }
504
505 let in_mkdocs_markup = lines
507 .get(line_num.saturating_sub(1))
508 .is_some_and(|line| is_in_mkdocs_markup(line, col.saturating_sub(1), ctx.flavor));
509
510 let in_inline_code = lines
512 .get(line_num.saturating_sub(1))
513 .is_some_and(|line| is_in_inline_code_on_line(line, col.saturating_sub(1)));
514
515 ctx.is_in_code_block_or_span(span_start)
516 || in_inline_code
517 || ctx.is_in_link(span_start)
518 || is_byte_in_html_tag(html_tags, span_start)
519 || is_byte_in_html_code_content(html_code_ranges, span_start)
520 || in_mkdocs_markup
521 || is_in_math_context(ctx, span_start)
522 || is_in_jsx_expression(ctx, span_start)
523 || is_in_mdx_comment(ctx, span_start)
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn test_html_comment_detection() {
532 let content = "Text <!-- comment --> more text";
533 assert!(is_in_html_comment(content, 10)); assert!(!is_in_html_comment(content, 0)); assert!(!is_in_html_comment(content, 25)); }
537
538 #[test]
539 fn test_is_line_entirely_in_html_comment() {
540 let content = "<!--\ncomment\n--> Content after comment";
542 let ranges = compute_html_comment_ranges(content);
543 assert!(is_line_entirely_in_html_comment(&ranges, 0, 4));
545 assert!(is_line_entirely_in_html_comment(&ranges, 5, 12));
547 assert!(!is_line_entirely_in_html_comment(&ranges, 13, 38));
549
550 let content2 = "<!-- comment --> Not a comment";
552 let ranges2 = compute_html_comment_ranges(content2);
553 assert!(!is_line_entirely_in_html_comment(&ranges2, 0, 30));
555
556 let content3 = "<!-- comment -->";
558 let ranges3 = compute_html_comment_ranges(content3);
559 assert!(is_line_entirely_in_html_comment(&ranges3, 0, 16));
561
562 let content4 = "Text before <!-- comment -->";
564 let ranges4 = compute_html_comment_ranges(content4);
565 assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
567 }
568
569 #[test]
570 fn test_math_block_detection() {
571 let content = "Text\n$$\nmath content\n$$\nmore text";
572 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)); }
577
578 #[test]
579 fn test_stray_double_dollar_in_prose_is_not_math() {
580 let content = "Note: $$ is used for display math and $$ closes it";
584 let between = content.find("is used").unwrap();
585 assert!(
586 !is_in_math_block(content, between),
587 "stray paired `$$` in prose must not be treated as a math block"
588 );
589 assert!(math_block_ranges(content).is_empty());
590 }
591
592 #[test]
593 fn test_blockquoted_double_dollar_opens_block() {
594 let content = "> $$\n> x = y\n> $$\n";
596 let inside = content.find("x = y").unwrap();
597 assert!(is_in_math_block(content, inside), "blockquoted math interior");
598 }
599
600 #[test]
601 fn test_self_contained_single_line_block_leaves_trailing_prose() {
602 let content = "$$ a $$ and __not math__\n";
604 let in_math = content.find('a').unwrap();
605 assert!(is_in_math_block(content, in_math), "single-line math interior");
606 let after = content.find("not math").unwrap();
607 assert!(!is_in_math_block(content, after), "trailing prose is lintable");
608 }
609
610 #[test]
611 fn test_math_block_closes_with_content_before_fence() {
612 let content = "$$\nx = y\n\\end{x}$$\nafter __text__ here";
616
617 let inside = content.find("x = y").unwrap();
618 assert!(is_in_math_block(content, inside), "interior must be math");
619
620 let after = content.find("after").unwrap();
621 assert!(
622 !is_in_math_block(content, after),
623 "content after a content-sharing closing fence must NOT be math"
624 );
625 }
626
627 #[test]
628 fn test_inline_math_detection() {
629 let content = "Text $x + y$ and $$a^2 + b^2$$ here";
630 assert!(is_in_inline_math(content, 7), "inside the single-`$` inline span");
631 assert!(!is_in_inline_math(content, 20), "mid-line $$...$$ is not inline math");
635 assert!(
636 !is_in_math_block(content, 20),
637 "mid-line $$...$$ is not a line-start display block"
638 );
639 assert!(!is_in_inline_math(content, 0), "before any math");
640 assert!(!is_in_inline_math(content, 35), "after the spans");
641 }
642
643 #[test]
644 fn test_table_line_detection() {
645 assert!(is_table_line("| Header | Column |"));
646 assert!(is_table_line("|--------|--------|"));
647 assert!(is_table_line("| Cell 1 | Cell 2 |"));
648 assert!(!is_table_line("Regular text"));
649 assert!(!is_table_line("Just a pipe | here"));
650 }
651
652 #[test]
653 fn test_is_in_icon_shortcode() {
654 let line = "Click :material-check: to confirm";
655 assert!(!is_in_icon_shortcode(line, 0, MarkdownFlavor::MkDocs));
657 assert!(is_in_icon_shortcode(line, 6, MarkdownFlavor::MkDocs));
659 assert!(is_in_icon_shortcode(line, 15, MarkdownFlavor::MkDocs));
660 assert!(is_in_icon_shortcode(line, 21, MarkdownFlavor::MkDocs));
661 assert!(!is_in_icon_shortcode(line, 22, MarkdownFlavor::MkDocs));
663 }
664
665 #[test]
666 fn test_is_in_pymdown_markup() {
667 let line = "Press ++ctrl+c++ to copy";
669 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::MkDocs));
670 assert!(is_in_pymdown_markup(line, 6, MarkdownFlavor::MkDocs));
671 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::MkDocs));
672 assert!(!is_in_pymdown_markup(line, 17, MarkdownFlavor::MkDocs));
673
674 let line2 = "This is ==highlighted== text";
676 assert!(!is_in_pymdown_markup(line2, 0, MarkdownFlavor::MkDocs));
677 assert!(is_in_pymdown_markup(line2, 8, MarkdownFlavor::MkDocs));
678 assert!(is_in_pymdown_markup(line2, 15, MarkdownFlavor::MkDocs));
679 assert!(!is_in_pymdown_markup(line2, 23, MarkdownFlavor::MkDocs));
680
681 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Standard));
683 }
684
685 #[test]
686 fn test_is_in_mkdocs_markup() {
687 let line = ":material-check: and ++ctrl++";
689 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)); }
693
694 #[test]
697 fn test_obsidian_highlight_basic() {
698 let line = "This is ==highlighted== text";
700 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)); }
707
708 #[test]
709 fn test_obsidian_highlight_multiple() {
710 let line = "Both ==one== and ==two== here";
712 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)); }
717
718 #[test]
719 fn test_obsidian_highlight_not_standard_flavor() {
720 let line = "This is ==highlighted== text";
722 assert!(!is_in_pymdown_markup(line, 8, MarkdownFlavor::Standard));
723 assert!(!is_in_pymdown_markup(line, 15, MarkdownFlavor::Standard));
724 }
725
726 #[test]
727 fn test_obsidian_highlight_with_spaces_inside() {
728 let line = "This is ==text with spaces== here";
730 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)); }
734
735 #[test]
736 fn test_obsidian_does_not_support_keys_notation() {
737 let line = "Press ++ctrl+c++ to copy";
739 assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
740 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
741 }
742
743 #[test]
744 fn test_obsidian_mkdocs_markup_function() {
745 let line = "This is ==highlighted== text";
747 assert!(is_in_mkdocs_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(!is_in_mkdocs_markup(line, 0, MarkdownFlavor::Obsidian)); }
750
751 #[test]
752 fn test_obsidian_highlight_edge_cases() {
753 let line = "Test ==== here";
755 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
757
758 let line2 = "Test ==a== here";
760 assert!(is_in_pymdown_markup(line2, 5, MarkdownFlavor::Obsidian));
761 assert!(is_in_pymdown_markup(line2, 7, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line2, 9, MarkdownFlavor::Obsidian)); let line3 = "a === b";
766 assert!(!is_in_pymdown_markup(line3, 3, MarkdownFlavor::Obsidian));
767 }
768
769 #[test]
770 fn test_obsidian_highlight_unclosed() {
771 let line = "This ==starts but never ends";
773 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian));
774 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
775 }
776
777 #[test]
778 fn test_inline_html_code_basic() {
779 let line = "The formula is <code>a * b * c</code> in math.";
780 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)); }
787
788 #[test]
789 fn test_inline_html_code_multiple_tags() {
790 let line = "<kbd>Ctrl</kbd> + <samp>output</samp>";
791 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 24)); assert!(!is_in_inline_html_code(line, 16)); }
795
796 #[test]
797 fn test_inline_html_code_with_attributes() {
798 let line = r#"<code class="lang">x * y</code>"#;
799 assert!(is_in_inline_html_code(line, 19)); assert!(is_in_inline_html_code(line, 23)); assert!(!is_in_inline_html_code(line, 0)); }
803
804 #[test]
805 fn test_inline_html_code_case_insensitive() {
806 let line = "<CODE>a * b</CODE>";
807 assert!(is_in_inline_html_code(line, 6)); assert!(is_in_inline_html_code(line, 8)); }
810
811 #[test]
812 fn test_inline_html_code_var_and_pre() {
813 let line = "<var>x * y</var> and <pre>a * b</pre>";
814 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 26)); assert!(!is_in_inline_html_code(line, 17)); }
818
819 #[test]
820 fn test_inline_html_code_unclosed() {
821 let line = "<code>a * b without closing";
823 assert!(!is_in_inline_html_code(line, 6));
824 }
825
826 #[test]
827 fn test_inline_html_code_no_substring_match() {
828 let line = "<variable>a * b</variable>";
830 assert!(!is_in_inline_html_code(line, 11));
831
832 let line2 = "<keyboard>x * y</keyboard>";
834 assert!(!is_in_inline_html_code(line2, 11));
835 }
836}