1use crate::filtered_lines::FilteredLinesExt;
5use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
6use crate::utils::emphasis_utils::{
7 EmphasisSpan, find_emphasis_markers, find_emphasis_spans, has_doc_patterns, replace_inline_code,
8 replace_inline_math,
9};
10use crate::utils::kramdown_utils::has_span_ial;
11use crate::utils::regex_cache::UNORDERED_LIST_MARKER_REGEX;
12use crate::utils::skip_context::{
13 is_in_html_comment, is_in_jsx_expression, is_in_math_context, is_in_mdx_comment, is_in_mkdocs_markup,
14 is_in_table_cell,
15};
16
17#[inline]
19fn has_spacing_issues(span: &EmphasisSpan) -> bool {
20 span.has_leading_space || span.has_trailing_space
21}
22
23#[inline]
26fn truncate_for_display(text: &str, max_len: usize) -> String {
27 if text.len() <= max_len {
28 return text.to_string();
29 }
30
31 let prefix_len = max_len / 2 - 2; let suffix_len = max_len / 2 - 2;
33
34 let prefix_end = text.floor_char_boundary(prefix_len.min(text.len()));
36 let suffix_start = text.floor_char_boundary(text.len().saturating_sub(suffix_len));
37
38 format!("{}...{}", &text[..prefix_end], &text[suffix_start..])
39}
40
41#[derive(Clone)]
43pub struct MD037NoSpaceInEmphasis;
44
45impl Default for MD037NoSpaceInEmphasis {
46 fn default() -> Self {
47 Self
48 }
49}
50
51impl MD037NoSpaceInEmphasis {
52 fn is_in_link(&self, ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
54 for link in &ctx.links {
56 if link.byte_offset <= byte_pos && byte_pos < link.byte_end {
57 return true;
58 }
59 }
60
61 for image in &ctx.images {
63 if image.byte_offset <= byte_pos && byte_pos < image.byte_end {
64 return true;
65 }
66 }
67
68 ctx.is_in_reference_def(byte_pos)
70 }
71}
72
73impl Rule for MD037NoSpaceInEmphasis {
74 fn name(&self) -> &'static str {
75 "MD037"
76 }
77
78 fn description(&self) -> &'static str {
79 "Spaces inside emphasis markers"
80 }
81
82 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
83 let content = ctx.content;
84 let _timer = crate::profiling::ScopedTimer::new("MD037_check");
85
86 if !content.contains('*') && !content.contains('_') {
88 return Ok(vec![]);
89 }
90
91 let line_index = &ctx.line_index;
93
94 let mut warnings = Vec::new();
95
96 for line in ctx
100 .filtered_lines()
101 .skip_front_matter()
102 .skip_code_blocks()
103 .skip_math_blocks()
104 .skip_obsidian_comments()
105 {
106 if !line.content.contains('*') && !line.content.contains('_') {
108 continue;
109 }
110
111 self.check_line_for_emphasis_issues_fast(line.content, line.line_num, &mut warnings);
113 }
114
115 let mut filtered_warnings = Vec::new();
117 let lines = ctx.raw_lines();
118
119 for (line_idx, line) in lines.iter().enumerate() {
120 let line_num = line_idx + 1;
121 let line_start_pos = line_index.get_line_start_byte(line_num).unwrap_or(0);
122
123 for warning in &warnings {
125 if warning.line == line_num {
126 let byte_pos = line_start_pos + (warning.column - 1);
128 let line_pos = warning.column - 1;
130
131 if !self.is_in_link(ctx, byte_pos)
134 && !is_in_html_comment(content, byte_pos)
135 && !is_in_math_context(ctx, byte_pos)
136 && !is_in_table_cell(ctx, line_num, warning.column)
137 && !ctx.is_in_code_span(line_num, warning.column)
138 && !is_in_jsx_expression(ctx, byte_pos)
139 && !is_in_mdx_comment(ctx, byte_pos)
140 && !is_in_mkdocs_markup(line, line_pos, ctx.flavor)
141 && !ctx.is_position_in_obsidian_comment(line_num, warning.column)
142 {
143 let mut adjusted_warning = warning.clone();
144 if let Some(fix) = &mut adjusted_warning.fix {
145 let abs_start = line_start_pos + fix.range.start;
147 let abs_end = line_start_pos + fix.range.end;
148 fix.range = abs_start..abs_end;
149 }
150 filtered_warnings.push(adjusted_warning);
151 }
152 }
153 }
154 }
155
156 Ok(filtered_warnings)
157 }
158
159 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
160 let content = ctx.content;
161 let _timer = crate::profiling::ScopedTimer::new("MD037_fix");
162
163 if !content.contains('*') && !content.contains('_') {
165 return Ok(content.to_string());
166 }
167
168 let warnings = self.check(ctx)?;
170
171 if warnings.is_empty() {
173 return Ok(content.to_string());
174 }
175
176 let mut result = content.to_string();
178 let mut offset: isize = 0;
179
180 let mut sorted_warnings: Vec<_> = warnings.iter().filter(|w| w.fix.is_some()).collect();
182 sorted_warnings.sort_by_key(|w| (w.line, w.column));
183
184 for warning in sorted_warnings {
185 if let Some(fix) = &warning.fix {
186 let actual_start = (fix.range.start as isize + offset) as usize;
188 let actual_end = (fix.range.end as isize + offset) as usize;
189
190 if actual_start < result.len() && actual_end <= result.len() {
192 result.replace_range(actual_start..actual_end, &fix.replacement);
194 offset += fix.replacement.len() as isize - (fix.range.end - fix.range.start) as isize;
196 }
197 }
198 }
199
200 Ok(result)
201 }
202
203 fn category(&self) -> RuleCategory {
205 RuleCategory::Emphasis
206 }
207
208 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
210 ctx.content.is_empty() || !ctx.likely_has_emphasis()
211 }
212
213 fn as_any(&self) -> &dyn std::any::Any {
214 self
215 }
216
217 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
218 where
219 Self: Sized,
220 {
221 Box::new(MD037NoSpaceInEmphasis)
222 }
223}
224
225impl MD037NoSpaceInEmphasis {
226 #[inline]
228 fn check_line_for_emphasis_issues_fast(&self, line: &str, line_num: usize, warnings: &mut Vec<LintWarning>) {
229 if has_doc_patterns(line) {
231 return;
232 }
233
234 if (line.starts_with(' ') || line.starts_with('*') || line.starts_with('+') || line.starts_with('-'))
239 && UNORDERED_LIST_MARKER_REGEX.is_match(line)
240 {
241 if let Some(caps) = UNORDERED_LIST_MARKER_REGEX.captures(line)
242 && let Some(full_match) = caps.get(0)
243 {
244 let list_marker_end = full_match.end();
245 if list_marker_end < line.len() {
246 let remaining_content = &line[list_marker_end..];
247
248 self.check_line_content_for_emphasis_fast(remaining_content, line_num, list_marker_end, warnings);
251 }
252 }
253 return;
254 }
255
256 self.check_line_content_for_emphasis_fast(line, line_num, 0, warnings);
258 }
259
260 fn check_line_content_for_emphasis_fast(
262 &self,
263 content: &str,
264 line_num: usize,
265 offset: usize,
266 warnings: &mut Vec<LintWarning>,
267 ) {
268 let processed_content = replace_inline_code(content);
271 let processed_content = replace_inline_math(&processed_content);
272
273 let markers = find_emphasis_markers(&processed_content);
275 if markers.is_empty() {
276 return;
277 }
278
279 let spans = find_emphasis_spans(&processed_content, markers);
281
282 for span in spans {
284 if has_spacing_issues(&span) {
285 let full_start = span.opening.start_pos;
287 let full_end = span.closing.end_pos();
288 let full_text = &content[full_start..full_end];
289
290 if full_end < content.len() {
293 let remaining = &content[full_end..];
294 if remaining.starts_with('{') && has_span_ial(remaining.split_whitespace().next().unwrap_or("")) {
296 continue;
297 }
298 }
299
300 let marker_char = span.opening.as_char();
302 let marker_str = if span.opening.count == 1 {
303 marker_char.to_string()
304 } else {
305 format!("{marker_char}{marker_char}")
306 };
307
308 let trimmed_content = span.content.trim();
310 let fixed_text = format!("{marker_str}{trimmed_content}{marker_str}");
311
312 let display_text = truncate_for_display(full_text, 60);
314
315 let warning = LintWarning {
316 rule_name: Some(self.name().to_string()),
317 message: format!("Spaces inside emphasis markers: {display_text:?}"),
318 line: line_num,
319 column: offset + full_start + 1, end_line: line_num,
321 end_column: offset + full_end + 1,
322 severity: Severity::Warning,
323 fix: Some(Fix {
324 range: (offset + full_start)..(offset + full_end),
325 replacement: fixed_text,
326 }),
327 };
328
329 warnings.push(warning);
330 }
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::lint_context::LintContext;
339
340 #[test]
341 fn test_emphasis_marker_parsing() {
342 let markers = find_emphasis_markers("This has *single* and **double** emphasis");
343 assert_eq!(markers.len(), 4); let markers = find_emphasis_markers("*start* and *end*");
346 assert_eq!(markers.len(), 4); }
348
349 #[test]
350 fn test_emphasis_span_detection() {
351 let markers = find_emphasis_markers("This has *valid* emphasis");
352 let spans = find_emphasis_spans("This has *valid* emphasis", markers);
353 assert_eq!(spans.len(), 1);
354 assert_eq!(spans[0].content, "valid");
355 assert!(!spans[0].has_leading_space);
356 assert!(!spans[0].has_trailing_space);
357
358 let markers = find_emphasis_markers("This has * invalid * emphasis");
359 let spans = find_emphasis_spans("This has * invalid * emphasis", markers);
360 assert_eq!(spans.len(), 1);
361 assert_eq!(spans[0].content, " invalid ");
362 assert!(spans[0].has_leading_space);
363 assert!(spans[0].has_trailing_space);
364 }
365
366 #[test]
367 fn test_with_document_structure() {
368 let rule = MD037NoSpaceInEmphasis;
369
370 let content = "This is *correct* emphasis and **strong emphasis**";
372 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
373 let result = rule.check(&ctx).unwrap();
374 assert!(result.is_empty(), "No warnings expected for correct emphasis");
375
376 let content = "This is * text with spaces * and more content";
378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
379 let result = rule.check(&ctx).unwrap();
380 assert!(!result.is_empty(), "Expected warnings for spaces in emphasis");
381
382 let content = "This is *correct* emphasis\n```\n* incorrect * in code block\n```\nOutside block with * spaces in emphasis *";
384 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
385 let result = rule.check(&ctx).unwrap();
386 assert!(
387 !result.is_empty(),
388 "Expected warnings for spaces in emphasis outside code block"
389 );
390 }
391
392 #[test]
393 fn test_emphasis_in_links_not_flagged() {
394 let rule = MD037NoSpaceInEmphasis;
395 let content = r#"Check this [* spaced asterisk *](https://example.com/*test*) link.
396
397This has * real spaced emphasis * that should be flagged."#;
398 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
399 let result = rule.check(&ctx).unwrap();
400
401 assert_eq!(
405 result.len(),
406 1,
407 "Expected exactly 1 warning, but got: {:?}",
408 result.len()
409 );
410 assert!(result[0].message.contains("Spaces inside emphasis markers"));
411 assert!(result[0].line == 3); }
414
415 #[test]
416 fn test_emphasis_in_links_vs_outside_links() {
417 let rule = MD037NoSpaceInEmphasis;
418 let content = r#"Check [* spaced *](https://example.com/*test*) and inline * real spaced * text.
419
420[* link *]: https://example.com/*path*"#;
421 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
422 let result = rule.check(&ctx).unwrap();
423
424 assert_eq!(result.len(), 1);
426 assert!(result[0].message.contains("Spaces inside emphasis markers"));
427 assert!(result[0].line == 1);
429 }
430
431 #[test]
432 fn test_issue_49_asterisk_in_inline_code() {
433 let rule = MD037NoSpaceInEmphasis;
435
436 let content = "The `__mul__` method is needed for left-hand multiplication (`vector * 3`) and `__rmul__` is needed for right-hand multiplication (`3 * vector`).";
438 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
439 let result = rule.check(&ctx).unwrap();
440 assert!(
441 result.is_empty(),
442 "Should not flag asterisks inside inline code as emphasis (issue #49). Got: {result:?}"
443 );
444 }
445
446 #[test]
447 fn test_issue_28_inline_code_in_emphasis() {
448 let rule = MD037NoSpaceInEmphasis;
450
451 let content = "Though, we often call this an **inline `if`** because it looks sort of like an `if`-`else` statement all in *one line* of code.";
453 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
454 let result = rule.check(&ctx).unwrap();
455 assert!(
456 result.is_empty(),
457 "Should not flag inline code inside emphasis as spaces (issue #28). Got: {result:?}"
458 );
459
460 let content2 = "The **`foo` and `bar`** methods are important.";
462 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
463 let result2 = rule.check(&ctx2).unwrap();
464 assert!(
465 result2.is_empty(),
466 "Should not flag multiple inline code snippets inside emphasis. Got: {result2:?}"
467 );
468
469 let content3 = "This is __inline `code`__ with underscores.";
471 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
472 let result3 = rule.check(&ctx3).unwrap();
473 assert!(
474 result3.is_empty(),
475 "Should not flag inline code with underscore emphasis. Got: {result3:?}"
476 );
477
478 let content4 = "This is *inline `test`* with single asterisks.";
480 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
481 let result4 = rule.check(&ctx4).unwrap();
482 assert!(
483 result4.is_empty(),
484 "Should not flag inline code with single asterisk emphasis. Got: {result4:?}"
485 );
486
487 let content5 = "This has * real spaces * that should be flagged.";
489 let ctx5 = LintContext::new(content5, crate::config::MarkdownFlavor::Standard, None);
490 let result5 = rule.check(&ctx5).unwrap();
491 assert!(!result5.is_empty(), "Should still flag actual spaces in emphasis");
492 assert!(result5[0].message.contains("Spaces inside emphasis markers"));
493 }
494
495 #[test]
496 fn test_multibyte_utf8_no_panic() {
497 let rule = MD037NoSpaceInEmphasis;
501
502 let greek = "Αυτό είναι ένα * τεστ με ελληνικά * και πολύ μεγάλο κείμενο που θα πρέπει να περικοπεί σωστά.";
504 let ctx = LintContext::new(greek, crate::config::MarkdownFlavor::Standard, None);
505 let result = rule.check(&ctx);
506 assert!(result.is_ok(), "Greek text should not panic");
507
508 let chinese = "这是一个 * 测试文本 * 包含中文字符,需要正确处理多字节边界。";
510 let ctx = LintContext::new(chinese, crate::config::MarkdownFlavor::Standard, None);
511 let result = rule.check(&ctx);
512 assert!(result.is_ok(), "Chinese text should not panic");
513
514 let cyrillic = "Это * тест с кириллицей * и очень длинным текстом для проверки обрезки.";
516 let ctx = LintContext::new(cyrillic, crate::config::MarkdownFlavor::Standard, None);
517 let result = rule.check(&ctx);
518 assert!(result.is_ok(), "Cyrillic text should not panic");
519
520 let mixed =
522 "日本語と * 中文と한국어が混在する非常に長いテキストでtruncate_for_displayの境界処理をテスト * します。";
523 let ctx = LintContext::new(mixed, crate::config::MarkdownFlavor::Standard, None);
524 let result = rule.check(&ctx);
525 assert!(result.is_ok(), "Mixed CJK text should not panic");
526
527 let arabic = "هذا * اختبار بالعربية * مع نص طويل جداً لاختبار معالجة حدود الأحرف.";
529 let ctx = LintContext::new(arabic, crate::config::MarkdownFlavor::Standard, None);
530 let result = rule.check(&ctx);
531 assert!(result.is_ok(), "Arabic text should not panic");
532
533 let emoji = "This has * 🎉 party 🎊 celebration 🥳 emojis * that use multi-byte sequences.";
535 let ctx = LintContext::new(emoji, crate::config::MarkdownFlavor::Standard, None);
536 let result = rule.check(&ctx);
537 assert!(result.is_ok(), "Emoji text should not panic");
538 }
539
540 #[test]
541 fn test_template_shortcode_syntax_not_flagged() {
542 let rule = MD037NoSpaceInEmphasis;
545
546 let content = "{* ../../docs_src/cookie_param_models/tutorial001.py hl[9:12,16] *}";
548 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
549 let result = rule.check(&ctx).unwrap();
550 assert!(
551 result.is_empty(),
552 "Template shortcode syntax should not be flagged. Got: {result:?}"
553 );
554
555 let content = "{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *}";
557 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
558 let result = rule.check(&ctx).unwrap();
559 assert!(
560 result.is_empty(),
561 "Template shortcode syntax should not be flagged. Got: {result:?}"
562 );
563
564 let content = "# Header\n\n{* file1.py *}\n\nSome text.\n\n{* file2.py hl[1-5] *}";
566 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
567 let result = rule.check(&ctx).unwrap();
568 assert!(
569 result.is_empty(),
570 "Multiple template shortcodes should not be flagged. Got: {result:?}"
571 );
572
573 let content = "This has * real spaced emphasis * here.";
575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
576 let result = rule.check(&ctx).unwrap();
577 assert!(!result.is_empty(), "Real spaced emphasis should still be flagged");
578 }
579
580 #[test]
581 fn test_multiline_code_span_not_flagged() {
582 let rule = MD037NoSpaceInEmphasis;
585
586 let content = "# Test\n\naffects the structure. `1 + 0 + 0` is parsed as `(1 + 0) +\n0` while `1 + 0 * 0` is parsed as `1 + (0 * 0)`. Since the pattern";
588 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
589 let result = rule.check(&ctx).unwrap();
590 assert!(
591 result.is_empty(),
592 "Should not flag asterisks inside multi-line code spans. Got: {result:?}"
593 );
594
595 let content2 = "Text with `code that\nspans * multiple * lines` here.";
597 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
598 let result2 = rule.check(&ctx2).unwrap();
599 assert!(
600 result2.is_empty(),
601 "Should not flag asterisks inside multi-line code spans. Got: {result2:?}"
602 );
603 }
604
605 #[test]
606 fn test_mkdocs_icon_shortcode_not_flagged() {
607 let rule = MD037NoSpaceInEmphasis;
609
610 let content = "Click :material-check: to confirm and :fontawesome-solid-star: for favorites.";
613 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
614 let result = rule.check(&ctx).unwrap();
615 assert!(
616 result.is_empty(),
617 "Should not flag MkDocs icon shortcodes. Got: {result:?}"
618 );
619
620 let content2 = "This has * real spaced emphasis * but also :material-check: icon.";
622 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::MkDocs, None);
623 let result2 = rule.check(&ctx2).unwrap();
624 assert!(
625 !result2.is_empty(),
626 "Should still flag real spaced emphasis in MkDocs mode"
627 );
628 }
629
630 #[test]
631 fn test_mkdocs_pymdown_markup_not_flagged() {
632 let rule = MD037NoSpaceInEmphasis;
634
635 let content = "Press ++ctrl+c++ to copy.";
637 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
638 let result = rule.check(&ctx).unwrap();
639 assert!(
640 result.is_empty(),
641 "Should not flag PyMdown Keys notation. Got: {result:?}"
642 );
643
644 let content2 = "This is ==highlighted text== for emphasis.";
646 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::MkDocs, None);
647 let result2 = rule.check(&ctx2).unwrap();
648 assert!(
649 result2.is_empty(),
650 "Should not flag PyMdown Mark notation. Got: {result2:?}"
651 );
652
653 let content3 = "This is ^^inserted text^^ here.";
655 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::MkDocs, None);
656 let result3 = rule.check(&ctx3).unwrap();
657 assert!(
658 result3.is_empty(),
659 "Should not flag PyMdown Insert notation. Got: {result3:?}"
660 );
661
662 let content4 = "Press ++ctrl++ then * spaced emphasis * here.";
664 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::MkDocs, None);
665 let result4 = rule.check(&ctx4).unwrap();
666 assert!(
667 !result4.is_empty(),
668 "Should still flag real spaced emphasis alongside PyMdown markup"
669 );
670 }
671
672 #[test]
675 fn test_obsidian_highlight_not_flagged() {
676 let rule = MD037NoSpaceInEmphasis;
678
679 let content = "This is ==highlighted text== here.";
681 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
682 let result = rule.check(&ctx).unwrap();
683 assert!(
684 result.is_empty(),
685 "Should not flag Obsidian highlight syntax. Got: {result:?}"
686 );
687 }
688
689 #[test]
690 fn test_obsidian_highlight_multiple_on_line() {
691 let rule = MD037NoSpaceInEmphasis;
693
694 let content = "Both ==one== and ==two== are highlighted.";
695 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
696 let result = rule.check(&ctx).unwrap();
697 assert!(
698 result.is_empty(),
699 "Should not flag multiple Obsidian highlights. Got: {result:?}"
700 );
701 }
702
703 #[test]
704 fn test_obsidian_highlight_entire_paragraph() {
705 let rule = MD037NoSpaceInEmphasis;
707
708 let content = "==Entire paragraph highlighted==";
709 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
710 let result = rule.check(&ctx).unwrap();
711 assert!(
712 result.is_empty(),
713 "Should not flag entire highlighted paragraph. Got: {result:?}"
714 );
715 }
716
717 #[test]
718 fn test_obsidian_highlight_with_emphasis() {
719 let rule = MD037NoSpaceInEmphasis;
721
722 let content = "**==bold highlight==**";
724 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
725 let result = rule.check(&ctx).unwrap();
726 assert!(
727 result.is_empty(),
728 "Should not flag bold highlight combination. Got: {result:?}"
729 );
730
731 let content2 = "*==italic highlight==*";
733 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Obsidian, None);
734 let result2 = rule.check(&ctx2).unwrap();
735 assert!(
736 result2.is_empty(),
737 "Should not flag italic highlight combination. Got: {result2:?}"
738 );
739 }
740
741 #[test]
742 fn test_obsidian_highlight_in_lists() {
743 let rule = MD037NoSpaceInEmphasis;
745
746 let content = "- Item with ==highlight== text\n- Another ==highlighted== item";
747 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
748 let result = rule.check(&ctx).unwrap();
749 assert!(
750 result.is_empty(),
751 "Should not flag highlights in list items. Got: {result:?}"
752 );
753 }
754
755 #[test]
756 fn test_obsidian_highlight_in_blockquote() {
757 let rule = MD037NoSpaceInEmphasis;
759
760 let content = "> This quote has ==highlighted== text.";
761 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
762 let result = rule.check(&ctx).unwrap();
763 assert!(
764 result.is_empty(),
765 "Should not flag highlights in blockquotes. Got: {result:?}"
766 );
767 }
768
769 #[test]
770 fn test_obsidian_highlight_in_tables() {
771 let rule = MD037NoSpaceInEmphasis;
773
774 let content = "| Header | Column |\n|--------|--------|\n| ==highlighted== | text |";
775 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
776 let result = rule.check(&ctx).unwrap();
777 assert!(
778 result.is_empty(),
779 "Should not flag highlights in tables. Got: {result:?}"
780 );
781 }
782
783 #[test]
784 fn test_obsidian_highlight_in_code_blocks_ignored() {
785 let rule = MD037NoSpaceInEmphasis;
787
788 let content = "```\n==not highlight in code==\n```";
789 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
790 let result = rule.check(&ctx).unwrap();
791 assert!(
792 result.is_empty(),
793 "Should ignore highlights in code blocks. Got: {result:?}"
794 );
795 }
796
797 #[test]
798 fn test_obsidian_highlight_edge_case_three_equals() {
799 let rule = MD037NoSpaceInEmphasis;
801
802 let content = "Test === something === here";
804 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
805 let result = rule.check(&ctx).unwrap();
806 let _ = result;
809 }
810
811 #[test]
812 fn test_obsidian_highlight_edge_case_four_equals() {
813 let rule = MD037NoSpaceInEmphasis;
815
816 let content = "Test ==== here";
817 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
818 let result = rule.check(&ctx).unwrap();
819 let _ = result;
821 }
822
823 #[test]
824 fn test_obsidian_highlight_adjacent() {
825 let rule = MD037NoSpaceInEmphasis;
827
828 let content = "==one====two==";
829 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
830 let result = rule.check(&ctx).unwrap();
831 let _ = result;
833 }
834
835 #[test]
836 fn test_obsidian_highlight_with_special_chars() {
837 let rule = MD037NoSpaceInEmphasis;
839
840 let content = "Test ==code: `test`== here";
842 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
843 let result = rule.check(&ctx).unwrap();
844 let _ = result;
846 }
847
848 #[test]
849 fn test_obsidian_highlight_unclosed() {
850 let rule = MD037NoSpaceInEmphasis;
852
853 let content = "This ==starts but never ends";
854 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
855 let result = rule.check(&ctx).unwrap();
856 let _ = result;
858 }
859
860 #[test]
861 fn test_obsidian_highlight_still_flags_real_emphasis_issues() {
862 let rule = MD037NoSpaceInEmphasis;
864
865 let content = "This has * spaced emphasis * and ==valid highlight==";
866 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
867 let result = rule.check(&ctx).unwrap();
868 assert!(
869 !result.is_empty(),
870 "Should still flag real spaced emphasis in Obsidian mode"
871 );
872 assert!(
873 result.len() == 1,
874 "Should flag exactly one issue (the spaced emphasis). Got: {result:?}"
875 );
876 }
877
878 #[test]
879 fn test_standard_flavor_does_not_recognize_highlight() {
880 let rule = MD037NoSpaceInEmphasis;
883
884 let content = "This is ==highlighted text== here.";
885 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
886 let result = rule.check(&ctx).unwrap();
887 let _ = result; }
892
893 #[test]
894 fn test_obsidian_highlight_mixed_with_regular_emphasis() {
895 let rule = MD037NoSpaceInEmphasis;
897
898 let content = "==highlighted== and *italic* and **bold** text";
899 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
900 let result = rule.check(&ctx).unwrap();
901 assert!(
902 result.is_empty(),
903 "Should not flag valid highlight and emphasis. Got: {result:?}"
904 );
905 }
906
907 #[test]
908 fn test_obsidian_highlight_unicode() {
909 let rule = MD037NoSpaceInEmphasis;
911
912 let content = "Text ==日本語 highlighted== here";
913 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
914 let result = rule.check(&ctx).unwrap();
915 assert!(
916 result.is_empty(),
917 "Should handle Unicode in highlights. Got: {result:?}"
918 );
919 }
920
921 #[test]
922 fn test_obsidian_highlight_with_html() {
923 let rule = MD037NoSpaceInEmphasis;
925
926 let content = "<!-- ==not highlight in comment== --> ==actual highlight==";
927 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
928 let result = rule.check(&ctx).unwrap();
929 let _ = result;
931 }
932
933 #[test]
934 fn test_obsidian_inline_comment_emphasis_ignored() {
935 let rule = MD037NoSpaceInEmphasis;
937
938 let content = "Visible %%* spaced emphasis *%% still visible.";
939 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
940 let result = rule.check(&ctx).unwrap();
941
942 assert!(
943 result.is_empty(),
944 "Should ignore emphasis inside Obsidian comments. Got: {result:?}"
945 );
946 }
947}