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::mkdocstrings_refs;
17use crate::utils::regex_cache::HTML_COMMENT_PATTERN;
18use regex::Regex;
19use std::sync::LazyLock;
20
21static INLINE_MATH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\$[^$]*\$\$|\$[^$\n]*\$").unwrap());
30
31#[derive(Debug, Clone, Copy)]
33pub struct ByteRange {
34 pub start: usize,
35 pub end: usize,
36}
37
38pub fn compute_html_comment_ranges(content: &str) -> Vec<ByteRange> {
41 HTML_COMMENT_PATTERN
42 .find_iter(content)
43 .map(|m| ByteRange {
44 start: m.start(),
45 end: m.end(),
46 })
47 .collect()
48}
49
50pub fn is_in_html_comment_ranges(ranges: &[ByteRange], byte_pos: usize) -> bool {
53 ranges
55 .binary_search_by(|range| {
56 if byte_pos < range.start {
57 std::cmp::Ordering::Greater
58 } else if byte_pos >= range.end {
59 std::cmp::Ordering::Less
60 } else {
61 std::cmp::Ordering::Equal
62 }
63 })
64 .is_ok()
65}
66
67pub fn is_line_entirely_in_html_comment(ranges: &[ByteRange], line_start: usize, line_end: usize) -> bool {
70 for range in ranges {
71 if line_start >= range.start && line_start < range.end {
73 return line_end <= range.end;
74 }
75 }
76 false
77}
78
79pub fn is_in_front_matter(content: &str, line_num: usize) -> bool {
81 let lines: Vec<&str> = content.lines().collect();
82
83 if !lines.is_empty() && lines[0] == "---" {
85 for (i, line) in lines.iter().enumerate().skip(1) {
86 if *line == "---" {
87 return line_num <= i;
88 }
89 }
90 }
91
92 if !lines.is_empty() && lines[0] == "+++" {
94 for (i, line) in lines.iter().enumerate().skip(1) {
95 if *line == "+++" {
96 return line_num <= i;
97 }
98 }
99 }
100
101 false
102}
103
104pub fn is_in_skip_context(ctx: &LintContext, byte_pos: usize) -> bool {
106 if ctx.is_in_code_block_or_span(byte_pos) {
108 return true;
109 }
110
111 if is_in_html_comment(ctx.content, byte_pos) {
113 return true;
114 }
115
116 if is_in_math_context(ctx, byte_pos) {
118 return true;
119 }
120
121 if is_in_html_tag(ctx, byte_pos) {
123 return true;
124 }
125
126 if ctx.flavor == MarkdownFlavor::MDX {
128 if ctx.is_in_jsx_expression(byte_pos) {
130 return true;
131 }
132 if ctx.is_in_mdx_comment(byte_pos) {
134 return true;
135 }
136 }
137
138 if ctx.flavor == MarkdownFlavor::MkDocs {
140 if mkdocs_snippets::is_within_snippet_section(ctx.content, byte_pos) {
141 return true;
142 }
143 if mkdocs_snippets::is_within_snippet_block(ctx.content, byte_pos) {
144 return true;
145 }
146 }
147
148 if ctx.flavor == MarkdownFlavor::MkDocs && mkdocs_admonitions::is_within_admonition(ctx.content, byte_pos) {
150 return true;
151 }
152
153 if ctx.flavor == MarkdownFlavor::MkDocs && mkdocs_footnotes::is_within_footnote_definition(ctx.content, byte_pos) {
155 return true;
156 }
157
158 if ctx.flavor == MarkdownFlavor::MkDocs && mkdocs_tabs::is_within_tab_content(ctx.content, byte_pos) {
160 return true;
161 }
162
163 if ctx.flavor == MarkdownFlavor::MkDocs && mkdocstrings_refs::is_within_autodoc_block(ctx.content, byte_pos) {
165 return true;
166 }
167
168 if ctx.flavor == MarkdownFlavor::MkDocs && mkdocs_critic::is_within_critic_markup(ctx.content, byte_pos) {
170 return true;
171 }
172
173 false
174}
175
176#[inline]
178pub fn is_in_jsx_expression(ctx: &LintContext, byte_pos: usize) -> bool {
179 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_jsx_expression(byte_pos)
180}
181
182#[inline]
184pub fn is_in_mdx_comment(ctx: &LintContext, byte_pos: usize) -> bool {
185 ctx.flavor == MarkdownFlavor::MDX && ctx.is_in_mdx_comment(byte_pos)
186}
187
188pub fn is_mkdocs_snippet_line(line: &str, flavor: MarkdownFlavor) -> bool {
190 flavor == MarkdownFlavor::MkDocs && mkdocs_snippets::is_snippet_marker(line)
191}
192
193pub fn is_mkdocs_admonition_line(line: &str, flavor: MarkdownFlavor) -> bool {
195 flavor == MarkdownFlavor::MkDocs && mkdocs_admonitions::is_admonition_marker(line)
196}
197
198pub fn is_mkdocs_footnote_line(line: &str, flavor: MarkdownFlavor) -> bool {
200 flavor == MarkdownFlavor::MkDocs && mkdocs_footnotes::is_footnote_definition(line)
201}
202
203pub fn is_mkdocs_tab_line(line: &str, flavor: MarkdownFlavor) -> bool {
205 flavor == MarkdownFlavor::MkDocs && mkdocs_tabs::is_tab_marker(line)
206}
207
208pub fn is_mkdocstrings_autodoc_line(line: &str, flavor: MarkdownFlavor) -> bool {
210 flavor == MarkdownFlavor::MkDocs && mkdocstrings_refs::is_autodoc_marker(line)
211}
212
213pub fn is_mkdocs_critic_line(line: &str, flavor: MarkdownFlavor) -> bool {
215 flavor == MarkdownFlavor::MkDocs && mkdocs_critic::contains_critic_markup(line)
216}
217
218pub fn is_in_html_comment(content: &str, byte_pos: usize) -> bool {
220 for m in HTML_COMMENT_PATTERN.find_iter(content) {
221 if m.start() <= byte_pos && byte_pos < m.end() {
222 return true;
223 }
224 }
225 false
226}
227
228pub fn is_in_html_tag(ctx: &LintContext, byte_pos: usize) -> bool {
230 for html_tag in ctx.html_tags().iter() {
231 if html_tag.byte_offset <= byte_pos && byte_pos < html_tag.byte_end {
232 return true;
233 }
234 }
235 false
236}
237
238pub fn is_in_math_context(ctx: &LintContext, byte_pos: usize) -> bool {
240 let content = ctx.content;
241
242 if is_in_math_block(content, byte_pos) {
244 return true;
245 }
246
247 if is_in_inline_math(content, byte_pos) {
249 return true;
250 }
251
252 false
253}
254
255pub fn is_in_math_block(content: &str, byte_pos: usize) -> bool {
257 let mut in_math_block = false;
258 let mut current_pos = 0;
259
260 for line in content.lines() {
261 let line_start = current_pos;
262 let line_end = current_pos + line.len();
263
264 if is_math_block_delimiter(line) {
266 if byte_pos >= line_start && byte_pos <= line_end {
267 return true;
269 }
270 in_math_block = !in_math_block;
271 } else if in_math_block && byte_pos >= line_start && byte_pos <= line_end {
272 return true;
274 }
275
276 current_pos = line_end + 1; }
278
279 false
280}
281
282pub fn is_in_inline_math(content: &str, byte_pos: usize) -> bool {
284 for m in INLINE_MATH_REGEX.find_iter(content) {
286 if m.start() <= byte_pos && byte_pos < m.end() {
287 return true;
288 }
289 }
290 false
291}
292
293pub fn is_in_table_cell(ctx: &LintContext, line_num: usize, _col: usize) -> bool {
295 for table_row in ctx.table_rows().iter() {
297 if table_row.line == line_num {
298 return true;
302 }
303 }
304 false
305}
306
307pub fn is_table_line(line: &str) -> bool {
309 let trimmed = line.trim();
310
311 if trimmed
313 .chars()
314 .all(|c| c == '|' || c == '-' || c == ':' || c.is_whitespace())
315 && trimmed.contains('|')
316 && trimmed.contains('-')
317 {
318 return true;
319 }
320
321 if (trimmed.starts_with('|') || trimmed.ends_with('|')) && trimmed.matches('|').count() >= 2 {
323 return true;
324 }
325
326 false
327}
328
329pub fn is_in_icon_shortcode(line: &str, position: usize, _flavor: MarkdownFlavor) -> bool {
332 mkdocs_icons::is_in_any_shortcode(line, position)
335}
336
337pub fn is_in_pymdown_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
340 if flavor != MarkdownFlavor::MkDocs {
341 return false;
342 }
343 mkdocs_extensions::is_in_pymdown_markup(line, position)
344}
345
346pub fn is_in_mkdocs_markup(line: &str, position: usize, flavor: MarkdownFlavor) -> bool {
349 if is_in_icon_shortcode(line, position, flavor) {
350 return true;
351 }
352 if is_in_pymdown_markup(line, position, flavor) {
353 return true;
354 }
355 false
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_html_comment_detection() {
364 let content = "Text <!-- comment --> more text";
365 assert!(is_in_html_comment(content, 10)); assert!(!is_in_html_comment(content, 0)); assert!(!is_in_html_comment(content, 25)); }
369
370 #[test]
371 fn test_is_line_entirely_in_html_comment() {
372 let content = "<!--\ncomment\n--> Content after comment";
374 let ranges = compute_html_comment_ranges(content);
375 assert!(is_line_entirely_in_html_comment(&ranges, 0, 4));
377 assert!(is_line_entirely_in_html_comment(&ranges, 5, 12));
379 assert!(!is_line_entirely_in_html_comment(&ranges, 13, 38));
381
382 let content2 = "<!-- comment --> Not a comment";
384 let ranges2 = compute_html_comment_ranges(content2);
385 assert!(!is_line_entirely_in_html_comment(&ranges2, 0, 30));
387
388 let content3 = "<!-- comment -->";
390 let ranges3 = compute_html_comment_ranges(content3);
391 assert!(is_line_entirely_in_html_comment(&ranges3, 0, 16));
393
394 let content4 = "Text before <!-- comment -->";
396 let ranges4 = compute_html_comment_ranges(content4);
397 assert!(!is_line_entirely_in_html_comment(&ranges4, 0, 28));
399 }
400
401 #[test]
402 fn test_math_block_detection() {
403 let content = "Text\n$$\nmath content\n$$\nmore text";
404 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)); }
409
410 #[test]
411 fn test_inline_math_detection() {
412 let content = "Text $x + y$ and $$a^2 + b^2$$ here";
413 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)); }
418
419 #[test]
420 fn test_table_line_detection() {
421 assert!(is_table_line("| Header | Column |"));
422 assert!(is_table_line("|--------|--------|"));
423 assert!(is_table_line("| Cell 1 | Cell 2 |"));
424 assert!(!is_table_line("Regular text"));
425 assert!(!is_table_line("Just a pipe | here"));
426 }
427
428 #[test]
429 fn test_is_in_front_matter() {
430 let yaml_content = r#"---
432title: "My Post"
433tags: ["test", "example"]
434---
435
436# Content"#;
437
438 assert!(
439 is_in_front_matter(yaml_content, 0),
440 "Line 1 should be in YAML front matter"
441 );
442 assert!(
443 is_in_front_matter(yaml_content, 2),
444 "Line 3 should be in YAML front matter"
445 );
446 assert!(
447 is_in_front_matter(yaml_content, 3),
448 "Line 4 should be in YAML front matter"
449 );
450 assert!(
451 !is_in_front_matter(yaml_content, 4),
452 "Line 5 should NOT be in front matter"
453 );
454
455 let toml_content = r#"+++
457title = "My Post"
458tags = ["test", "example"]
459+++
460
461# Content"#;
462
463 assert!(
464 is_in_front_matter(toml_content, 0),
465 "Line 1 should be in TOML front matter"
466 );
467 assert!(
468 is_in_front_matter(toml_content, 2),
469 "Line 3 should be in TOML front matter"
470 );
471 assert!(
472 is_in_front_matter(toml_content, 3),
473 "Line 4 should be in TOML front matter"
474 );
475 assert!(
476 !is_in_front_matter(toml_content, 4),
477 "Line 5 should NOT be in front matter"
478 );
479
480 let mixed_content = r#"# Content
482
483+++
484title = "Not frontmatter"
485+++
486
487More content"#;
488
489 assert!(
490 !is_in_front_matter(mixed_content, 2),
491 "TOML block not at beginning should NOT be front matter"
492 );
493 assert!(
494 !is_in_front_matter(mixed_content, 3),
495 "TOML block not at beginning should NOT be front matter"
496 );
497 assert!(
498 !is_in_front_matter(mixed_content, 4),
499 "TOML block not at beginning should NOT be front matter"
500 );
501 }
502
503 #[test]
504 fn test_is_in_icon_shortcode() {
505 let line = "Click :material-check: to confirm";
506 assert!(!is_in_icon_shortcode(line, 0, MarkdownFlavor::MkDocs));
508 assert!(is_in_icon_shortcode(line, 6, MarkdownFlavor::MkDocs));
510 assert!(is_in_icon_shortcode(line, 15, MarkdownFlavor::MkDocs));
511 assert!(is_in_icon_shortcode(line, 21, MarkdownFlavor::MkDocs));
512 assert!(!is_in_icon_shortcode(line, 22, MarkdownFlavor::MkDocs));
514 }
515
516 #[test]
517 fn test_is_in_pymdown_markup() {
518 let line = "Press ++ctrl+c++ to copy";
520 assert!(!is_in_pymdown_markup(line, 0, MarkdownFlavor::MkDocs));
521 assert!(is_in_pymdown_markup(line, 6, MarkdownFlavor::MkDocs));
522 assert!(is_in_pymdown_markup(line, 10, MarkdownFlavor::MkDocs));
523 assert!(!is_in_pymdown_markup(line, 17, MarkdownFlavor::MkDocs));
524
525 let line2 = "This is ==highlighted== text";
527 assert!(!is_in_pymdown_markup(line2, 0, MarkdownFlavor::MkDocs));
528 assert!(is_in_pymdown_markup(line2, 8, MarkdownFlavor::MkDocs));
529 assert!(is_in_pymdown_markup(line2, 15, MarkdownFlavor::MkDocs));
530 assert!(!is_in_pymdown_markup(line2, 23, MarkdownFlavor::MkDocs));
531
532 assert!(!is_in_pymdown_markup(line, 10, MarkdownFlavor::Standard));
534 }
535
536 #[test]
537 fn test_is_in_mkdocs_markup() {
538 let line = ":material-check: and ++ctrl++";
540 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)); }
544}