1use crate::config::MarkdownFlavor;
7use crate::lint_context::LintContext;
8use crate::utils::kramdown_utils::is_math_block_delimiter;
9use crate::utils::mkdocs_admonitions;
10use crate::utils::mkdocs_critic;
11use crate::utils::mkdocs_extensions;
12use crate::utils::mkdocs_footnotes;
13use crate::utils::mkdocs_icons;
14use crate::utils::mkdocs_snippets;
15use crate::utils::mkdocs_tabs;
16use crate::utils::regex_cache::HTML_COMMENT_PATTERN;
17use regex::Regex;
18use std::sync::LazyLock;
19
20static INLINE_MATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$[^$]*\$\$|\$[^$\n]*\$").unwrap());
29
30#[derive(Debug, Clone, Copy)]
32pub struct ByteRange {
33 pub start: usize,
34 pub end: usize,
35}
36
37pub fn compute_html_comment_ranges(content: &str) -> Vec<ByteRange> {
40 HTML_COMMENT_PATTERN
41 .find_iter(content)
42 .map(|m| ByteRange {
43 start: m.start(),
44 end: m.end(),
45 })
46 .collect()
47}
48
49pub fn is_in_html_comment_ranges(ranges: &[ByteRange], byte_pos: usize) -> bool {
52 ranges
54 .binary_search_by(|range| {
55 if byte_pos < range.start {
56 std::cmp::Ordering::Greater
57 } else if byte_pos >= range.end {
58 std::cmp::Ordering::Less
59 } else {
60 std::cmp::Ordering::Equal
61 }
62 })
63 .is_ok()
64}
65
66pub fn is_line_entirely_in_html_comment(ranges: &[ByteRange], line_start: usize, line_end: usize) -> bool {
69 for range in ranges {
70 if line_start >= range.start && line_start < range.end {
72 return line_end <= range.end;
73 }
74 }
75 false
76}
77
78#[inline]
80pub fn is_in_jsx_expression(ctx: &LintContext, byte_pos: usize) -> bool {
81 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_jsx_expression(byte_pos)
82}
83
84#[inline]
86pub fn is_in_mdx_comment(ctx: &LintContext, byte_pos: usize) -> bool {
87 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_mdx_comment(byte_pos)
88}
89
90pub fn is_mkdocs_snippet_line(line: &str, flavor: MarkdownFlavor) -> bool {
92 flavor == MarkdownFlavor::MkDocs && mkdocs_snippets::is_snippet_marker(line)
93}
94
95pub fn is_mkdocs_admonition_line(line: &str, flavor: MarkdownFlavor) -> bool {
97 flavor == MarkdownFlavor::MkDocs && mkdocs_admonitions::is_admonition_marker(line)
98}
99
100pub fn is_mkdocs_footnote_line(line: &str, flavor: MarkdownFlavor) -> bool {
102 flavor == MarkdownFlavor::MkDocs && mkdocs_footnotes::is_footnote_definition(line)
103}
104
105pub fn is_mkdocs_tab_line(line: &str, flavor: MarkdownFlavor) -> bool {
107 flavor == MarkdownFlavor::MkDocs && mkdocs_tabs::is_tab_marker(line)
108}
109
110pub fn is_mkdocs_critic_line(line: &str, flavor: MarkdownFlavor) -> bool {
112 flavor == MarkdownFlavor::MkDocs && mkdocs_critic::contains_critic_markup(line)
113}
114
115pub fn is_in_html_comment(content: &str, byte_pos: usize) -> bool {
117 for m in HTML_COMMENT_PATTERN.find_iter(content) {
118 if m.start() <= byte_pos && byte_pos < m.end() {
119 return true;
120 }
121 }
122 false
123}
124
125pub fn is_in_html_tag(ctx: &LintContext, byte_pos: usize) -> bool {
127 for html_tag in ctx.html_tags().iter() {
128 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
129 return true;
130 }
131 }
132 false
133}
134
135pub fn is_in_math_context(ctx: &LintContext, byte_pos: usize) -> bool {
137 let content = ctx.content;
138
139 if is_in_math_block(content, byte_pos) {
141 return true;
142 }
143
144 if is_in_inline_math(content, byte_pos) {
146 return true;
147 }
148
149 false
150}
151
152pub fn is_in_math_block(content: &str, byte_pos: usize) -> bool {
154 let mut in_math_block = false;
155 let mut current_pos = 0;
156
157 for line in content.lines() {
158 let line_start = current_pos;
159 let line_end = current_pos + line.len();
160
161 if is_math_block_delimiter(line) {
163 if byte_pos >= line_start && byte_pos <= line_end {
164 return true;
166 }
167 in_math_block = !in_math_block;
168 } else if in_math_block && byte_pos >= line_start && byte_pos <= line_end {
169 return true;
171 }
172
173 current_pos = line_end + 1; }
175
176 false
177}
178
179pub fn is_in_inline_math(content: &str, byte_pos: usize) -> bool {
181 for m in INLINE_MATH_REGEX.find_iter(content) {
183 if m.start() <= byte_pos && byte_pos < m.end() {
184 return true;
185 }
186 }
187 false
188}
189
190pub fn is_in_table_cell(ctx: &LintContext, line_num: usize, _col: usize) -> bool {
192 for table_row in ctx.table_rows().iter() {
194 if table_row.line == line_num {
195 return true;
199 }
200 }
201 false
202}
203
204pub fn is_table_line(line: &str) -> bool {
206 let trimmed = line.trim();
207
208 if trimmed
210 .chars()
211 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
212 && trimmed.contains('|')
213 && trimmed.contains('-')
214 {
215 return true;
216 }
217
218 if (trimmed.starts_with('|') || trimmed.ends_with('|')) && trimmed.matches('|').count() >= 2 {
220 return true;
221 }
222
223 false
224}
225
226pub fn is_in_icon_shortcode(line: &str, position: usize, _flavor: MarkdownFlavor) -> bool {
229 mkdocs_icons::is_in_any_shortcode(line, position)
232}
233
234pub fn is_in_pymdown_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
240 match flavor {
241 MarkdownFlavor::MkDocs => mkdocs_extensions::is_in_pymdown_markup(line, position),
242 MarkdownFlavor::Obsidian => {
243 mkdocs_extensions::is_in_mark(line, position)
245 }
246 _ => false,
247 }
248}
249
250pub fn is_in_inline_html_code(line: &str, position: usize) -> bool {
255 const TAGS: &[&str] = &["code", "pre", "samp", "kbd", "var"];
257
258 let bytes = line.as_bytes();
259
260 for tag in TAGS {
261 let open_bytes = format!("<{tag}").into_bytes();
262 let close_pattern = format!("</{tag}>").into_bytes();
263
264 let mut search_from = 0;
265 while search_from + open_bytes.len() <= bytes.len() {
266 let Some(open_abs) = find_case_insensitive(bytes, &open_bytes, search_from) else {
268 break;
269 };
270
271 let after_tag = open_abs + open_bytes.len();
272
273 if after_tag < bytes.len() {
275 let next = bytes[after_tag];
276 if next != b'>' && next != b' ' && next != b'\t' {
277 search_from = after_tag;
278 continue;
279 }
280 }
281
282 let Some(tag_close) = bytes[after_tag..].iter().position(|&b| b == b'>') else {
284 break;
285 };
286 let content_start = after_tag + tag_close + 1;
287
288 let Some(close_start) = find_case_insensitive(bytes, &close_pattern, content_start) else {
290 break;
291 };
292 let content_end = close_start;
293
294 if position >= content_start && position < content_end {
295 return true;
296 }
297
298 search_from = close_start + close_pattern.len();
299 }
300 }
301 false
302}
303
304fn find_case_insensitive(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
306 if needle.is_empty() || from + needle.len() > haystack.len() {
307 return None;
308 }
309 for i in from..=haystack.len() - needle.len() {
310 if haystack[i..i + needle.len()]
311 .iter()
312 .zip(needle.iter())
313 .all(|(h, n)| h.eq_ignore_ascii_case(n))
314 {
315 return Some(i);
316 }
317 }
318 None
319}
320
321pub fn is_in_mkdocs_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
325 if is_in_icon_shortcode(line, position, flavor) {
326 return true;
327 }
328 if is_in_pymdown_markup(line, position, flavor) {
329 return true;
330 }
331 false
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_html_comment_detection() {
340 let content = "Text <!-- comment --> more text";
341 assert!(is_in_html_comment(content, 10)); assert!(!is_in_html_comment(content, 0)); assert!(!is_in_html_comment(content, 25)); }
345
346 #[test]
347 fn test_is_line_entirely_in_html_comment() {
348 let content = "<!--\ncomment\n--> Content after comment";
350 let ranges = compute_html_comment_ranges(content);
351 assert!(is_line_entirely_in_html_comment(&ranges, 0, 4));
353 assert!(is_line_entirely_in_html_comment(&ranges, 5, 12));
355 assert!(!is_line_entirely_in_html_comment(&ranges, 13, 38));
357
358 let content2 = "<!-- comment --> Not a comment";
360 let ranges2 = compute_html_comment_ranges(content2);
361 assert!(!is_line_entirely_in_html_comment(&ranges2, 0, 30));
363
364 let content3 = "<!-- comment -->";
366 let ranges3 = compute_html_comment_ranges(content3);
367 assert!(is_line_entirely_in_html_comment(&ranges3, 0, 16));
369
370 let content4 = "Text before <!-- comment -->";
372 let ranges4 = compute_html_comment_ranges(content4);
373 assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
375 }
376
377 #[test]
378 fn test_math_block_detection() {
379 let content = "Text\n$$\nmath content\n$$\nmore text";
380 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)); }
385
386 #[test]
387 fn test_inline_math_detection() {
388 let content = "Text $x + y$ and $$a^2 + b^2$$ here";
389 assert!(is_in_inline_math(content, 7)); assert!(is_in_inline_math(content, 20)); assert!(!is_in_inline_math(content, 0)); assert!(!is_in_inline_math(content, 35)); }
394
395 #[test]
396 fn test_table_line_detection() {
397 assert!(is_table_line("| Header | Column |"));
398 assert!(is_table_line("|--------|--------|"));
399 assert!(is_table_line("| Cell 1 | Cell 2 |"));
400 assert!(!is_table_line("Regular text"));
401 assert!(!is_table_line("Just a pipe | here"));
402 }
403
404 #[test]
405 fn test_is_in_icon_shortcode() {
406 let line = "Click :material-check: to confirm";
407 assert!(!is_in_icon_shortcode(line, 0, MarkdownFlavor::MkDocs));
409 assert!(is_in_icon_shortcode(line, 6, MarkdownFlavor::MkDocs));
411 assert!(is_in_icon_shortcode(line, 15, MarkdownFlavor::MkDocs));
412 assert!(is_in_icon_shortcode(line, 21, MarkdownFlavor::MkDocs));
413 assert!(!is_in_icon_shortcode(line, 22, MarkdownFlavor::MkDocs));
415 }
416
417 #[test]
418 fn test_is_in_pymdown_markup() {
419 let line = "Press ++ctrl+c++ to copy";
421 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::MkDocs));
422 assert!(is_in_pymdown_markup(line, 6, MarkdownFlavor::MkDocs));
423 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::MkDocs));
424 assert!(!is_in_pymdown_markup(line, 17, MarkdownFlavor::MkDocs));
425
426 let line2 = "This is ==highlighted== text";
428 assert!(!is_in_pymdown_markup(line2, 0, MarkdownFlavor::MkDocs));
429 assert!(is_in_pymdown_markup(line2, 8, MarkdownFlavor::MkDocs));
430 assert!(is_in_pymdown_markup(line2, 15, MarkdownFlavor::MkDocs));
431 assert!(!is_in_pymdown_markup(line2, 23, MarkdownFlavor::MkDocs));
432
433 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Standard));
435 }
436
437 #[test]
438 fn test_is_in_mkdocs_markup() {
439 let line = ":material-check: and ++ctrl++";
441 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)); }
445
446 #[test]
449 fn test_obsidian_highlight_basic() {
450 let line = "This is ==highlighted== text";
452 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)); }
459
460 #[test]
461 fn test_obsidian_highlight_multiple() {
462 let line = "Both ==one== and ==two== here";
464 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)); }
469
470 #[test]
471 fn test_obsidian_highlight_not_standard_flavor() {
472 let line = "This is ==highlighted== text";
474 assert!(!is_in_pymdown_markup(line, 8, MarkdownFlavor::Standard));
475 assert!(!is_in_pymdown_markup(line, 15, MarkdownFlavor::Standard));
476 }
477
478 #[test]
479 fn test_obsidian_highlight_with_spaces_inside() {
480 let line = "This is ==text with spaces== here";
482 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)); }
486
487 #[test]
488 fn test_obsidian_does_not_support_keys_notation() {
489 let line = "Press ++ctrl+c++ to copy";
491 assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
492 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
493 }
494
495 #[test]
496 fn test_obsidian_mkdocs_markup_function() {
497 let line = "This is ==highlighted== text";
499 assert!(is_in_mkdocs_markup(line, 10, MarkdownFlavor::Obsidian)); assert!(!is_in_mkdocs_markup(line, 0, MarkdownFlavor::Obsidian)); }
502
503 #[test]
504 fn test_obsidian_highlight_edge_cases() {
505 let line = "Test ==== here";
507 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian)); assert!(!is_in_pymdown_markup(line, 6, MarkdownFlavor::Obsidian));
509
510 let line2 = "Test ==a== here";
512 assert!(is_in_pymdown_markup(line2, 5, MarkdownFlavor::Obsidian));
513 assert!(is_in_pymdown_markup(line2, 7, MarkdownFlavor::Obsidian)); assert!(is_in_pymdown_markup(line2, 9, MarkdownFlavor::Obsidian)); let line3 = "a === b";
518 assert!(!is_in_pymdown_markup(line3, 3, MarkdownFlavor::Obsidian));
519 }
520
521 #[test]
522 fn test_obsidian_highlight_unclosed() {
523 let line = "This ==starts but never ends";
525 assert!(!is_in_pymdown_markup(line, 5, MarkdownFlavor::Obsidian));
526 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Obsidian));
527 }
528
529 #[test]
530 fn test_inline_html_code_basic() {
531 let line = "The formula is <code>a * b * c</code> in math.";
532 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)); }
539
540 #[test]
541 fn test_inline_html_code_multiple_tags() {
542 let line = "<kbd>Ctrl</kbd> + <samp>output</samp>";
543 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 24)); assert!(!is_in_inline_html_code(line, 16)); }
547
548 #[test]
549 fn test_inline_html_code_with_attributes() {
550 let line = r#"<code class="lang">x * y</code>"#;
551 assert!(is_in_inline_html_code(line, 19)); assert!(is_in_inline_html_code(line, 23)); assert!(!is_in_inline_html_code(line, 0)); }
555
556 #[test]
557 fn test_inline_html_code_case_insensitive() {
558 let line = "<CODE>a * b</CODE>";
559 assert!(is_in_inline_html_code(line, 6)); assert!(is_in_inline_html_code(line, 8)); }
562
563 #[test]
564 fn test_inline_html_code_var_and_pre() {
565 let line = "<var>x * y</var> and <pre>a * b</pre>";
566 assert!(is_in_inline_html_code(line, 5)); assert!(is_in_inline_html_code(line, 26)); assert!(!is_in_inline_html_code(line, 17)); }
570
571 #[test]
572 fn test_inline_html_code_unclosed() {
573 let line = "<code>a * b without closing";
575 assert!(!is_in_inline_html_code(line, 6));
576 }
577
578 #[test]
579 fn test_inline_html_code_no_substring_match() {
580 let line = "<variable>a * b</variable>";
582 assert!(!is_in_inline_html_code(line, 11));
583
584 let line2 = "<keyboard>x * y</keyboard>";
586 assert!(!is_in_inline_html_code(line2, 11));
587 }
588}