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, find_valid_emphasis_ranges, has_doc_patterns,
8 replace_inline_code, replace_inline_math,
9};
10use crate::utils::kramdown_utils::has_span_ial;
11use crate::utils::range_utils::byte_to_char_count;
12use crate::utils::regex_cache::UNORDERED_LIST_MARKER_REGEX;
13use crate::utils::skip_context::{is_in_inline_html_code, is_in_math_context, is_in_mkdocs_markup};
14use crate::utils::table_utils::TableUtils;
15use std::ops::Range;
16
17#[inline]
19fn has_spacing_issues(span: &EmphasisSpan) -> bool {
20 span.has_leading_space || span.has_trailing_space
21}
22
23fn table_line_flags(ctx: &crate::lint_context::LintContext) -> Vec<bool> {
26 if ctx.table_blocks.is_empty() {
27 return Vec::new();
28 }
29
30 let mut flags = vec![false; ctx.lines.len()];
31 for block in &ctx.table_blocks {
33 for idx in block.start_line..=block.end_line {
34 if let Some(flag) = flags.get_mut(idx) {
35 *flag = true;
36 }
37 }
38 }
39 flags
40}
41
42fn table_cell_ranges(line: &str) -> Vec<Range<usize>> {
49 let masked;
52 let scan = if line.contains('\\') || line.contains('`') {
53 let escaped = TableUtils::mask_pipes_for_table_parsing(line);
54 masked = TableUtils::mask_pipes_in_inline_code(&escaped);
55 debug_assert_eq!(
56 masked.len(),
57 line.len(),
58 "pipe masking must preserve byte offsets for cell slicing"
59 );
60 masked.as_str()
61 } else {
62 line
63 };
64
65 let mut ranges = Vec::new();
66 let mut start = 0;
67 for (pipe_pos, _) in scan.match_indices('|') {
68 ranges.push(start..pipe_pos);
69 start = pipe_pos + 1;
70 }
71 ranges.push(start..line.len());
72 ranges
73}
74
75#[inline]
78fn truncate_for_display(text: &str, max_len: usize) -> String {
79 if text.len() <= max_len {
80 return text.to_string();
81 }
82
83 let prefix_len = max_len / 2 - 2; let suffix_len = max_len / 2 - 2;
85
86 let prefix_end = text.floor_char_boundary(prefix_len.min(text.len()));
88 let suffix_start = text.floor_char_boundary(text.len().saturating_sub(suffix_len));
89
90 format!("{}...{}", &text[..prefix_end], &text[suffix_start..])
91}
92
93#[derive(Clone)]
95pub struct MD037NoSpaceInEmphasis;
96
97impl Default for MD037NoSpaceInEmphasis {
98 fn default() -> Self {
99 Self
100 }
101}
102
103impl MD037NoSpaceInEmphasis {
104 fn is_in_link(&self, ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
106 for link in ctx.links() {
108 if link.byte_offset <= byte_pos && byte_pos < link.byte_end {
109 if link.is_reference && link.url.is_empty() {
110 continue;
111 }
112 return true;
113 }
114 }
115
116 for image in ctx.images() {
118 if image.byte_offset <= byte_pos && byte_pos < image.byte_end {
119 return true;
120 }
121 }
122
123 ctx.is_in_reference_def(byte_pos)
125 }
126
127 fn emphasis_span_ends(ctx: &crate::lint_context::LintContext) -> Vec<(usize, usize)> {
133 let mut ends: Vec<(usize, usize)> = ctx
134 .emphasis_spans()
135 .iter()
136 .map(|span| (span.byte_end, span.byte_offset))
137 .collect();
138 ends.sort_unstable();
139 ends
140 }
141
142 fn closes_earlier_emphasis(span_ends: &[(usize, usize)], start: usize, end: usize) -> bool {
154 let first_after_start = span_ends.partition_point(|(span_end, _)| *span_end <= start);
155 span_ends[first_after_start..]
156 .iter()
157 .take_while(|(span_end, _)| *span_end < end)
158 .any(|(_, span_start)| *span_start <= start)
159 }
160}
161
162impl Rule for MD037NoSpaceInEmphasis {
163 fn name(&self) -> &'static str {
164 "MD037"
165 }
166
167 fn description(&self) -> &'static str {
168 "Spaces inside emphasis markers"
169 }
170
171 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
172 let content = ctx.content;
173 let _timer = crate::profiling::ScopedTimer::new("MD037_check");
174 let has_html = content.contains('<');
175
176 if !content.contains('*') && !content.contains('_') {
178 return Ok(vec![]);
179 }
180
181 let mut warnings = Vec::new();
184 let table_lines = table_line_flags(ctx);
185
186 for line in ctx
190 .filtered_lines()
191 .skip_front_matter()
192 .skip_code_blocks()
193 .skip_math_blocks()
194 .skip_html_blocks()
195 .skip_esm_blocks()
196 .skip_obsidian_comments()
197 .skip_mkdocstrings()
198 {
199 if line.line_info.is_myst_comment {
200 continue;
201 }
202 if !line.content.contains('*') && !line.content.contains('_') {
204 continue;
205 }
206
207 let masked =
208 crate::utils::skip_context::mask_mdx_inline_code(ctx, line.content, line.line_info.byte_offset);
209 let line_content = masked.as_ref();
210 if table_lines.get(line.line_num - 1).copied().unwrap_or(false) {
211 for cell in table_cell_ranges(line_content) {
216 let Some(cell_content) = line_content.get(cell.clone()) else {
217 continue;
218 };
219 if !cell_content.contains('*') && !cell_content.contains('_') {
220 continue;
221 }
222 if has_doc_patterns(cell_content) {
223 continue;
224 }
225 self.check_line_content_for_emphasis_fast(cell_content, line.line_num, cell.start, &mut warnings);
226 }
227 continue;
228 }
229
230 self.check_line_for_emphasis_issues_fast(line_content, line.line_num, &mut warnings);
232 }
233
234 let mut filtered_warnings = Vec::new();
236 let lines = ctx.raw_lines();
237 let span_ends = if warnings.is_empty() {
238 Vec::new()
239 } else {
240 Self::emphasis_span_ends(ctx)
241 };
242
243 for (line_idx, line) in lines.iter().enumerate() {
244 let line_num = line_idx + 1;
245 let line_start_pos = ctx.line_start_byte(line_num).unwrap_or(0);
246
247 for warning in &warnings {
249 if warning.line == line_num {
250 let byte_pos = line_start_pos + (warning.column - 1);
254 let line_pos = warning.column - 1;
256 let char_col = byte_to_char_count(line, warning.column - 1);
257
258 let in_pandoc_construct = ctx.flavor.is_pandoc_compatible() && ctx.is_in_bracketed_span(byte_pos);
269 let byte_end = line_start_pos + (warning.end_column - 1);
270 let closing_pos = byte_end.saturating_sub(1);
271 let marker_in_html = has_html
272 && [byte_pos, closing_pos].into_iter().any(|pos| {
273 ctx.is_in_html_comment(pos) || ctx.is_in_html_tag(pos) || ctx.is_in_jsx_component_tag(pos)
274 });
275 if !in_pandoc_construct
276 && !marker_in_html
277 && !ctx.is_in_jinja_string(byte_pos)
278 && !ctx.is_in_jinja_string(closing_pos)
279 && !Self::closes_earlier_emphasis(&span_ends, byte_pos, byte_end)
280 && !self.is_in_link(ctx, byte_pos)
281 && !ctx.is_in_shortcode(byte_pos)
282 && !ctx.is_in_shortcode(closing_pos)
283 && !is_in_math_context(ctx, byte_pos)
284 && !ctx.is_in_code_span(line_num, char_col)
285 && !ctx.is_in_code_span_byte(closing_pos)
288 && !is_in_inline_html_code(line, line_pos)
289 && !ctx.overlaps_mdx_inline_code(byte_pos, byte_end)
290 && !is_in_mkdocs_markup(line, line_pos, ctx.flavor)
291 && !ctx.is_position_in_obsidian_comment(line_num, char_col)
292 {
293 let mut adjusted_warning = warning.clone();
294 adjusted_warning.column = char_col;
296 adjusted_warning.end_column = byte_to_char_count(line, warning.end_column - 1);
297 if let Some(fix) = &mut adjusted_warning.fix {
298 let abs_start = line_start_pos + fix.range.start;
300 let abs_end = line_start_pos + fix.range.end;
301 fix.range = abs_start..abs_end;
302 }
303 filtered_warnings.push(adjusted_warning);
304 }
305 }
306 }
307 }
308
309 Ok(filtered_warnings)
310 }
311
312 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
313 let content = ctx.content;
314 let _timer = crate::profiling::ScopedTimer::new("MD037_fix");
315
316 if !content.contains('*') && !content.contains('_') {
318 return Ok(content.to_string());
319 }
320
321 let warnings = self.check(ctx)?;
323 let warnings =
324 crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
325
326 if warnings.is_empty() {
328 return Ok(content.to_string());
329 }
330
331 let mut result = content.to_string();
333 let mut offset: isize = 0;
334
335 let mut sorted_warnings: Vec<_> = warnings.iter().filter(|w| w.fix.is_some()).collect();
337 sorted_warnings.sort_by_key(|w| (w.line, w.column));
338
339 for warning in sorted_warnings {
340 if let Some(fix) = &warning.fix {
341 let actual_start = (fix.range.start as isize + offset) as usize;
343 let actual_end = (fix.range.end as isize + offset) as usize;
344
345 if actual_start < result.len() && actual_end <= result.len() {
347 result.replace_range(actual_start..actual_end, &fix.replacement);
349 offset += fix.replacement.len() as isize - (fix.range.end - fix.range.start) as isize;
351 }
352 }
353 }
354
355 Ok(result)
356 }
357
358 fn category(&self) -> RuleCategory {
360 RuleCategory::Emphasis
361 }
362
363 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
365 ctx.content.is_empty() || !ctx.likely_has_emphasis()
366 }
367
368 fn as_any(&self) -> &dyn std::any::Any {
369 self
370 }
371
372 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
373 where
374 Self: Sized,
375 {
376 Box::new(MD037NoSpaceInEmphasis)
377 }
378}
379
380impl MD037NoSpaceInEmphasis {
381 #[inline]
383 fn check_line_for_emphasis_issues_fast(&self, line: &str, line_num: usize, warnings: &mut Vec<LintWarning>) {
384 if has_doc_patterns(line) {
386 return;
387 }
388
389 if (line.starts_with(' ') || line.starts_with('*') || line.starts_with('+') || line.starts_with('-'))
394 && UNORDERED_LIST_MARKER_REGEX.is_match(line)
395 {
396 if let Some(caps) = UNORDERED_LIST_MARKER_REGEX.captures(line)
397 && let Some(full_match) = caps.get(0)
398 {
399 let list_marker_end = full_match.end();
400 if list_marker_end < line.len() {
401 let remaining_content = &line[list_marker_end..];
402
403 self.check_line_content_for_emphasis_fast(remaining_content, line_num, list_marker_end, warnings);
406 }
407 }
408 return;
409 }
410
411 self.check_line_content_for_emphasis_fast(line, line_num, 0, warnings);
413 }
414
415 fn check_line_content_for_emphasis_fast(
417 &self,
418 content: &str,
419 line_num: usize,
420 offset: usize,
421 warnings: &mut Vec<LintWarning>,
422 ) {
423 let processed_content = replace_inline_code(content);
426 let processed_content = replace_inline_math(&processed_content);
427
428 let markers = find_emphasis_markers(&processed_content);
430 if markers.is_empty() {
431 return;
432 }
433
434 let spans = find_emphasis_spans(&processed_content, &markers);
436
437 let valid_ranges = find_valid_emphasis_ranges(&processed_content, &markers);
442
443 for span in spans {
445 if has_spacing_issues(&span) {
446 let full_start = span.opening.start_pos;
447 let full_end = span.closing.end_pos();
448
449 if valid_ranges
451 .iter()
452 .any(|&(start, end)| start <= full_start && full_end <= end)
453 {
454 continue;
455 }
456
457 let full_text = &content[full_start..full_end];
458
459 if full_end < content.len() {
462 let remaining = &content[full_end..];
463 if remaining.starts_with('{') && has_span_ial(remaining.split_whitespace().next().unwrap_or("")) {
465 continue;
466 }
467 }
468
469 let marker_char = span.opening.as_char();
471 let marker_str = if span.opening.count == 1 {
472 marker_char.to_string()
473 } else {
474 format!("{marker_char}{marker_char}")
475 };
476
477 let original_content = &content[span.opening.end_pos()..span.closing.start_pos];
484 let trimmed_content = original_content.trim();
485 let fixed_text = format!("{marker_str}{trimmed_content}{marker_str}");
486
487 let display_text = truncate_for_display(full_text, 60);
489
490 let warning = LintWarning {
491 rule_name: Some(self.name().to_string()),
492 message: format!("Spaces inside emphasis markers: {display_text:?}"),
493 line: line_num,
497 column: offset + full_start + 1,
498 end_line: line_num,
499 end_column: offset + full_end + 1,
500 severity: Severity::Warning,
501 fix: Some(Fix::new((offset + full_start)..(offset + full_end), fixed_text)),
502 };
503
504 warnings.push(warning);
505 }
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::lint_context::LintContext;
514
515 #[test]
516 fn test_marker_inside_shortcode_cannot_close_spaced_emphasis() {
517 let rule = MD037NoSpaceInEmphasis;
518 let source = "Before * text {{< note title=\"*\" >}}\n\nBefore * visible *\n";
519 let expected = "Before * text {{< note title=\"*\" >}}\n\nBefore *visible*\n";
520 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Hugo, None);
521 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
522 assert_eq!(rule.fix(&ctx).unwrap(), expected);
523 }
524
525 #[test]
526 fn test_myst_comment_contents_are_not_linted() {
527 let rule = MD037NoSpaceInEmphasis;
528 for prefix in ["% ", "%", " %\t", " % "] {
529 let source = format!("{prefix}* literal *\n\nBefore * visible *\n");
530 let expected = format!("{prefix}* literal *\n\nBefore *visible*\n");
531 let ctx = crate::lint_context::LintContext::new(&source, crate::config::MarkdownFlavor::MyST, None);
532 let warnings = rule.check(&ctx).unwrap();
533 assert_eq!(warnings.len(), 1, "comments are not visible prose: {source}");
534 assert_eq!(warnings[0].line, 3);
535 assert_eq!(rule.fix(&ctx).unwrap(), expected);
536 }
537 }
538
539 #[test]
540 fn test_jinja_quoted_spaced_markers_are_literal() {
541 let rule = MD037NoSpaceInEmphasis;
542 for template in [
543 r#"{{ "* literal *" }}"#,
544 "{% set text = '_ 内容 _' %}",
545 "{{\n \"** literal **\"\n}}",
546 r#"{{ {"text": "* literal *"} }}"#,
547 r#"* text {{ "*" }}"#,
548 ] {
549 let source = format!("Before {template}\n\nBefore * visible *\n");
550 let expected = format!("Before {template}\n\nBefore *visible*\n");
551 let ctx = LintContext::new(&source, crate::config::MarkdownFlavor::Standard, None);
552 assert_eq!(
553 rule.check(&ctx).unwrap().len(),
554 1,
555 "quoted markers are literal: {source}"
556 );
557 assert_eq!(rule.fix(&ctx).unwrap(), expected);
558 }
559 }
560
561 #[test]
562 fn test_mdx_module_spaced_markers_are_literal() {
563 let rule = MD037NoSpaceInEmphasis;
564 for module in [
565 "export const text = \"* literal *\"",
566 "export const data = {\n text: \"_ 内容 _\"\n}",
567 "import text from \"./* literal *.js\"",
568 ] {
569 let source = format!("{module}\n\nBefore * visible *\n");
570 let expected = format!("{module}\n\nBefore *visible*\n");
571 let ctx = LintContext::new(&source, crate::config::MarkdownFlavor::MDX, None);
572 let warnings = rule.check(&ctx).unwrap();
573 assert_eq!(warnings.len(), 1, "module strings must stay literal: {source}");
574 assert!(warnings[0].line > module.lines().count());
575 assert_eq!(rule.fix(&ctx).unwrap(), expected);
576 }
577 }
578
579 #[test]
580 fn test_spaced_markers_in_html_attributes_or_closing_in_comments_are_ignored() {
581 let rule = MD037NoSpaceInEmphasis;
582 for source in [
583 "Before <span title=\"* literal *\">visible</span>\n",
584 "Before <span\n title=\"_ literal _\">visible</span>\n",
585 "日本語 <span title=\"** 内容 **\">表示</span>\n",
586 "Before * text <!-- * -->\n",
587 "Before ** text <span title=\"**\">visible</span>\n",
588 ] {
589 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
590 assert!(
591 rule.check(&ctx).unwrap().is_empty(),
592 "HTML markers are literal: {source}"
593 );
594 assert_eq!(rule.fix(&ctx).unwrap(), source);
595 }
596 }
597
598 #[test]
599 fn test_spaced_emphasis_beside_html_attributes_still_gets_fixed() {
600 let rule = MD037NoSpaceInEmphasis;
601 let source = "Before <span title=\"* literal *\">* visible *</span> <!-- * hidden * -->\n";
602 let expected = "Before <span title=\"* literal *\">*visible*</span> <!-- * hidden * -->\n";
603 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
604 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
605 assert_eq!(rule.fix(&ctx).unwrap(), expected);
606 let fixed_ctx = LintContext::new(expected, crate::config::MarkdownFlavor::Standard, None);
607 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
608 }
609
610 #[test]
611 fn test_spaced_marker_pair_ending_inside_multiline_code_is_ignored() {
612 let rule = MD037NoSpaceInEmphasis;
613 for content in [
614 "Before * text `code *\nend`\n",
615 "_ text `code _\nend`\n",
616 "** text `code **\nend`\n",
617 "__ text ``code `tick` __\nend``\n",
618 "日本語 * 内容 `コード *\n終了`\n",
619 ] {
620 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
621 assert!(
622 rule.check(&ctx).unwrap().is_empty(),
623 "closing marker is code: {content}"
624 );
625 assert_eq!(rule.fix(&ctx).unwrap(), content);
626 }
627 }
628
629 #[test]
630 fn test_real_spaced_emphasis_beside_multiline_code_is_fixed() {
631 let rule = MD037NoSpaceInEmphasis;
632 let source = "Before ** text `code **\nend` and * real *\n";
633 let expected = "Before ** text `code **\nend` and *real*\n";
634 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
635 let warnings = rule.check(&ctx).unwrap();
636 assert_eq!(warnings.len(), 1);
637 assert_eq!(warnings[0].line, 2);
638 assert_eq!(rule.fix(&ctx).unwrap(), expected);
639 let fixed_ctx = LintContext::new(expected, crate::config::MarkdownFlavor::Standard, None);
640 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
641 assert_eq!(rule.fix(&fixed_ctx).unwrap(), expected);
642 let code_values = |content: &str| {
643 pulldown_cmark::Parser::new(content)
644 .filter_map(|event| match event {
645 pulldown_cmark::Event::Code(value) => Some(value.into_string()),
646 _ => None,
647 })
648 .collect::<Vec<_>>()
649 };
650 assert_eq!(code_values(source), vec!["code ** end"]);
651 assert_eq!(code_values(source), code_values(expected));
652 }
653
654 #[test]
655 fn table_cell_ranges_unmasked_scan_matches_masked_scan() {
656 fn always_masked(line: &str) -> Vec<Range<usize>> {
659 let escaped = TableUtils::mask_pipes_for_table_parsing(line);
660 let masked = TableUtils::mask_pipes_in_inline_code(&escaped);
661 let mut ranges = Vec::new();
662 let mut start = 0;
663 for (pipe_pos, _) in masked.match_indices('|') {
664 ranges.push(start..pipe_pos);
665 start = pipe_pos + 1;
666 }
667 ranges.push(start..line.len());
668 ranges
669 }
670
671 for line in [
672 "| a | b |",
673 "| a * x * | b |",
674 "a | b",
675 "no pipes at all",
676 "",
677 "|||",
678 "| naïve ünïcode | 日本語 |",
679 r"| a \| b | c |",
680 "| `a | b` | c |",
681 r"| `a \| b` | c |",
682 r"| a \\ | b |",
683 "> | a * x * | b |",
684 ] {
685 assert_eq!(table_cell_ranges(line), always_masked(line), "line: {line:?}");
686 }
687 }
688
689 #[test]
690 fn test_emphasis_marker_parsing() {
691 let markers = find_emphasis_markers("This has *single* and **double** emphasis");
692 assert_eq!(markers.len(), 4); let markers = find_emphasis_markers("*start* and *end*");
695 assert_eq!(markers.len(), 4); }
697
698 #[test]
699 fn test_emphasis_span_detection() {
700 let markers = find_emphasis_markers("This has *valid* emphasis");
701 let spans = find_emphasis_spans("This has *valid* emphasis", &markers);
702 assert_eq!(spans.len(), 1);
703 assert_eq!(spans[0].content, "valid");
704 assert!(!spans[0].has_leading_space);
705 assert!(!spans[0].has_trailing_space);
706
707 let markers = find_emphasis_markers("This has * invalid * emphasis");
708 let spans = find_emphasis_spans("This has * invalid * emphasis", &markers);
709 assert_eq!(spans.len(), 1);
710 assert_eq!(spans[0].content, " invalid ");
711 assert!(spans[0].has_leading_space);
712 assert!(spans[0].has_trailing_space);
713 }
714
715 #[test]
716 fn test_with_document_structure() {
717 let rule = MD037NoSpaceInEmphasis;
718
719 let content = "This is *correct* emphasis and **strong emphasis**";
721 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
722 let result = rule.check(&ctx).unwrap();
723 assert!(result.is_empty(), "No warnings expected for correct emphasis");
724
725 let content = "This is * text with spaces * and more content";
727 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
728 let result = rule.check(&ctx).unwrap();
729 assert!(!result.is_empty(), "Expected warnings for spaces in emphasis");
730
731 let content = "This is *correct* emphasis\n```\n* incorrect * in code block\n```\nOutside block with * spaces in emphasis *";
733 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
734 let result = rule.check(&ctx).unwrap();
735 assert!(
736 !result.is_empty(),
737 "Expected warnings for spaces in emphasis outside code block"
738 );
739 }
740
741 #[test]
742 fn test_inline_code_inside_spaced_emphasis_preserved_on_fix() {
743 let rule = MD037NoSpaceInEmphasis;
747 let content = "Set * the `id` field * below.";
748 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
749 let fixed = rule.fix(&ctx).unwrap();
750 assert_eq!(fixed, "Set *the `id` field* below.");
751 assert!(!fixed.contains('X'), "masking placeholder leaked into fix: {fixed}");
752 }
753
754 #[test]
755 fn test_emphasis_in_links_not_flagged() {
756 let rule = MD037NoSpaceInEmphasis;
757 let content = r#"Check this [* spaced asterisk *](https://example.com/*test*) link.
758
759This has * real spaced emphasis * that should be flagged."#;
760 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
761 let result = rule.check(&ctx).unwrap();
762
763 assert_eq!(
767 result.len(),
768 1,
769 "Expected exactly 1 warning, but got: {:?}",
770 result.len()
771 );
772 assert!(result[0].message.contains("Spaces inside emphasis markers"));
773 assert!(result[0].line == 3); }
776
777 #[test]
778 fn test_emphasis_in_links_vs_outside_links() {
779 let rule = MD037NoSpaceInEmphasis;
780 let content = r#"Check [* spaced *](https://example.com/*test*) and inline * real spaced * text.
781
782[* link *]: https://example.com/*path*"#;
783 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
784 let result = rule.check(&ctx).unwrap();
785
786 assert_eq!(result.len(), 1);
788 assert!(result[0].message.contains("Spaces inside emphasis markers"));
789 assert!(result[0].line == 1);
791 }
792
793 #[test]
794 fn test_issue_49_asterisk_in_inline_code() {
795 let rule = MD037NoSpaceInEmphasis;
797
798 let content = "The `__mul__` method is needed for left-hand multiplication (`vector * 3`) and `__rmul__` is needed for right-hand multiplication (`3 * vector`).";
800 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
801 let result = rule.check(&ctx).unwrap();
802 assert!(
803 result.is_empty(),
804 "Should not flag asterisks inside inline code as emphasis (issue #49). Got: {result:?}"
805 );
806 }
807
808 #[test]
809 fn test_issue_28_inline_code_in_emphasis() {
810 let rule = MD037NoSpaceInEmphasis;
812
813 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.";
815 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
816 let result = rule.check(&ctx).unwrap();
817 assert!(
818 result.is_empty(),
819 "Should not flag inline code inside emphasis as spaces (issue #28). Got: {result:?}"
820 );
821
822 let content2 = "The **`foo` and `bar`** methods are important.";
824 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
825 let result2 = rule.check(&ctx2).unwrap();
826 assert!(
827 result2.is_empty(),
828 "Should not flag multiple inline code snippets inside emphasis. Got: {result2:?}"
829 );
830
831 let content3 = "This is __inline `code`__ with underscores.";
833 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
834 let result3 = rule.check(&ctx3).unwrap();
835 assert!(
836 result3.is_empty(),
837 "Should not flag inline code with underscore emphasis. Got: {result3:?}"
838 );
839
840 let content4 = "This is *inline `test`* with single asterisks.";
842 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
843 let result4 = rule.check(&ctx4).unwrap();
844 assert!(
845 result4.is_empty(),
846 "Should not flag inline code with single asterisk emphasis. Got: {result4:?}"
847 );
848
849 let content5 = "This has * real spaces * that should be flagged.";
851 let ctx5 = LintContext::new(content5, crate::config::MarkdownFlavor::Standard, None);
852 let result5 = rule.check(&ctx5).unwrap();
853 assert!(!result5.is_empty(), "Should still flag actual spaces in emphasis");
854 assert!(result5[0].message.contains("Spaces inside emphasis markers"));
855 }
856
857 #[test]
858 fn test_multibyte_utf8_no_panic() {
859 let rule = MD037NoSpaceInEmphasis;
863
864 let greek = "Αυτό είναι ένα * τεστ με ελληνικά * και πολύ μεγάλο κείμενο που θα πρέπει να περικοπεί σωστά.";
866 let ctx = LintContext::new(greek, crate::config::MarkdownFlavor::Standard, None);
867 let result = rule.check(&ctx);
868 assert!(result.is_ok(), "Greek text should not panic");
869
870 let chinese = "这是一个 * 测试文本 * 包含中文字符,需要正确处理多字节边界。";
872 let ctx = LintContext::new(chinese, crate::config::MarkdownFlavor::Standard, None);
873 let result = rule.check(&ctx);
874 assert!(result.is_ok(), "Chinese text should not panic");
875
876 let cyrillic = "Это * тест с кириллицей * и очень длинным текстом для проверки обрезки.";
878 let ctx = LintContext::new(cyrillic, crate::config::MarkdownFlavor::Standard, None);
879 let result = rule.check(&ctx);
880 assert!(result.is_ok(), "Cyrillic text should not panic");
881
882 let mixed =
884 "日本語と * 中文と한국어が混在する非常に長いテキストでtruncate_for_displayの境界処理をテスト * します。";
885 let ctx = LintContext::new(mixed, crate::config::MarkdownFlavor::Standard, None);
886 let result = rule.check(&ctx);
887 assert!(result.is_ok(), "Mixed CJK text should not panic");
888
889 let arabic = "هذا * اختبار بالعربية * مع نص طويل جداً لاختبار معالجة حدود الأحرف.";
891 let ctx = LintContext::new(arabic, crate::config::MarkdownFlavor::Standard, None);
892 let result = rule.check(&ctx);
893 assert!(result.is_ok(), "Arabic text should not panic");
894
895 let emoji = "This has * 🎉 party 🎊 celebration 🥳 emojis * that use multi-byte sequences.";
897 let ctx = LintContext::new(emoji, crate::config::MarkdownFlavor::Standard, None);
898 let result = rule.check(&ctx);
899 assert!(result.is_ok(), "Emoji text should not panic");
900 }
901
902 #[test]
903 fn test_template_shortcode_syntax_not_flagged() {
904 let rule = MD037NoSpaceInEmphasis;
907
908 let content = "{* ../../docs_src/cookie_param_models/tutorial001.py hl[9:12,16] *}";
910 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
911 let result = rule.check(&ctx).unwrap();
912 assert!(
913 result.is_empty(),
914 "Template shortcode syntax should not be flagged. Got: {result:?}"
915 );
916
917 let content = "{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *}";
919 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
920 let result = rule.check(&ctx).unwrap();
921 assert!(
922 result.is_empty(),
923 "Template shortcode syntax should not be flagged. Got: {result:?}"
924 );
925
926 let content = "# Header\n\n{* file1.py *}\n\nSome text.\n\n{* file2.py hl[1-5] *}";
928 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
929 let result = rule.check(&ctx).unwrap();
930 assert!(
931 result.is_empty(),
932 "Multiple template shortcodes should not be flagged. Got: {result:?}"
933 );
934
935 let content = "This has * real spaced emphasis * here.";
937 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
938 let result = rule.check(&ctx).unwrap();
939 assert!(!result.is_empty(), "Real spaced emphasis should still be flagged");
940 }
941
942 #[test]
943 fn test_multiline_code_span_not_flagged() {
944 let rule = MD037NoSpaceInEmphasis;
947
948 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";
950 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
951 let result = rule.check(&ctx).unwrap();
952 assert!(
953 result.is_empty(),
954 "Should not flag asterisks inside multi-line code spans. Got: {result:?}"
955 );
956
957 let content2 = "Text with `code that\nspans * multiple * lines` here.";
959 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
960 let result2 = rule.check(&ctx2).unwrap();
961 assert!(
962 result2.is_empty(),
963 "Should not flag asterisks inside multi-line code spans. Got: {result2:?}"
964 );
965 }
966
967 #[test]
968 fn test_html_block_asterisks_not_flagged() {
969 let rule = MD037NoSpaceInEmphasis;
970
971 let content = r#"<table>
973<tr><td>Format</td><td>Size</td></tr>
974<tr><td>BC1</td><td><code>floor((width + 3) / 4) * floor((height + 3) / 4) * 8</code></td></tr>
975<tr><td>BC2</td><td><code>floor((width + 3) / 4) * floor((height + 3) / 4) * 16</code></td></tr>
976</table>"#;
977 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
978 let result = rule.check(&ctx).unwrap();
979 assert!(
980 result.is_empty(),
981 "Should not flag asterisks inside HTML blocks. Got: {result:?}"
982 );
983
984 let content2 = "<div>\n<p>Value is * something * here</p>\n</div>";
986 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
987 let result2 = rule.check(&ctx2).unwrap();
988 assert!(
989 result2.is_empty(),
990 "Should not flag emphasis-like patterns inside HTML div blocks. Got: {result2:?}"
991 );
992
993 let content3 = "Regular * spaced emphasis * text\n\n<div>* not emphasis *</div>";
995 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
996 let result3 = rule.check(&ctx3).unwrap();
997 assert_eq!(
998 result3.len(),
999 1,
1000 "Should flag spaced emphasis in regular markdown but not inside HTML blocks. Got: {result3:?}"
1001 );
1002 assert_eq!(result3[0].line, 1, "Warning should be on line 1 (regular markdown)");
1003 }
1004
1005 #[test]
1006 fn test_mkdocs_icon_shortcode_not_flagged() {
1007 let rule = MD037NoSpaceInEmphasis;
1009
1010 let content = "Click :material-check: to confirm and :fontawesome-solid-star: for favorites.";
1013 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1014 let result = rule.check(&ctx).unwrap();
1015 assert!(
1016 result.is_empty(),
1017 "Should not flag MkDocs icon shortcodes. Got: {result:?}"
1018 );
1019
1020 let content2 = "This has * real spaced emphasis * but also :material-check: icon.";
1022 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::MkDocs, None);
1023 let result2 = rule.check(&ctx2).unwrap();
1024 assert!(
1025 !result2.is_empty(),
1026 "Should still flag real spaced emphasis in MkDocs mode"
1027 );
1028 }
1029
1030 #[test]
1031 fn test_mkdocs_pymdown_markup_not_flagged() {
1032 let rule = MD037NoSpaceInEmphasis;
1034
1035 let content = "Press ++ctrl+c++ to copy.";
1037 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1038 let result = rule.check(&ctx).unwrap();
1039 assert!(
1040 result.is_empty(),
1041 "Should not flag PyMdown Keys notation. Got: {result:?}"
1042 );
1043
1044 let content2 = "This is ==highlighted text== for emphasis.";
1046 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::MkDocs, None);
1047 let result2 = rule.check(&ctx2).unwrap();
1048 assert!(
1049 result2.is_empty(),
1050 "Should not flag PyMdown Mark notation. Got: {result2:?}"
1051 );
1052
1053 let content3 = "This is ^^inserted text^^ here.";
1055 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::MkDocs, None);
1056 let result3 = rule.check(&ctx3).unwrap();
1057 assert!(
1058 result3.is_empty(),
1059 "Should not flag PyMdown Insert notation. Got: {result3:?}"
1060 );
1061
1062 let content4 = "Press ++ctrl++ then * spaced emphasis * here.";
1064 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::MkDocs, None);
1065 let result4 = rule.check(&ctx4).unwrap();
1066 assert!(
1067 !result4.is_empty(),
1068 "Should still flag real spaced emphasis alongside PyMdown markup"
1069 );
1070 }
1071
1072 #[test]
1075 fn test_obsidian_highlight_not_flagged() {
1076 let rule = MD037NoSpaceInEmphasis;
1078
1079 let content = "This is ==highlighted text== here.";
1081 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1082 let result = rule.check(&ctx).unwrap();
1083 assert!(
1084 result.is_empty(),
1085 "Should not flag Obsidian highlight syntax. Got: {result:?}"
1086 );
1087 }
1088
1089 #[test]
1090 fn test_obsidian_highlight_multiple_on_line() {
1091 let rule = MD037NoSpaceInEmphasis;
1093
1094 let content = "Both ==one== and ==two== are highlighted.";
1095 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1096 let result = rule.check(&ctx).unwrap();
1097 assert!(
1098 result.is_empty(),
1099 "Should not flag multiple Obsidian highlights. Got: {result:?}"
1100 );
1101 }
1102
1103 #[test]
1104 fn test_obsidian_highlight_entire_paragraph() {
1105 let rule = MD037NoSpaceInEmphasis;
1107
1108 let content = "==Entire paragraph highlighted==";
1109 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1110 let result = rule.check(&ctx).unwrap();
1111 assert!(
1112 result.is_empty(),
1113 "Should not flag entire highlighted paragraph. Got: {result:?}"
1114 );
1115 }
1116
1117 #[test]
1118 fn test_obsidian_highlight_with_emphasis() {
1119 let rule = MD037NoSpaceInEmphasis;
1121
1122 let content = "**==bold highlight==**";
1124 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1125 let result = rule.check(&ctx).unwrap();
1126 assert!(
1127 result.is_empty(),
1128 "Should not flag bold highlight combination. Got: {result:?}"
1129 );
1130
1131 let content2 = "*==italic highlight==*";
1133 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Obsidian, None);
1134 let result2 = rule.check(&ctx2).unwrap();
1135 assert!(
1136 result2.is_empty(),
1137 "Should not flag italic highlight combination. Got: {result2:?}"
1138 );
1139 }
1140
1141 #[test]
1142 fn test_obsidian_highlight_in_lists() {
1143 let rule = MD037NoSpaceInEmphasis;
1145
1146 let content = "- Item with ==highlight== text\n- Another ==highlighted== item";
1147 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1148 let result = rule.check(&ctx).unwrap();
1149 assert!(
1150 result.is_empty(),
1151 "Should not flag highlights in list items. Got: {result:?}"
1152 );
1153 }
1154
1155 #[test]
1156 fn test_obsidian_highlight_in_blockquote() {
1157 let rule = MD037NoSpaceInEmphasis;
1159
1160 let content = "> This quote has ==highlighted== text.";
1161 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1162 let result = rule.check(&ctx).unwrap();
1163 assert!(
1164 result.is_empty(),
1165 "Should not flag highlights in blockquotes. Got: {result:?}"
1166 );
1167 }
1168
1169 #[test]
1170 fn test_obsidian_highlight_in_tables() {
1171 let rule = MD037NoSpaceInEmphasis;
1173
1174 let content = "| Header | Column |\n|--------|--------|\n| ==highlighted== | text |";
1175 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1176 let result = rule.check(&ctx).unwrap();
1177 assert!(
1178 result.is_empty(),
1179 "Should not flag highlights in tables. Got: {result:?}"
1180 );
1181 }
1182
1183 #[test]
1184 fn test_obsidian_highlight_in_code_blocks_ignored() {
1185 let rule = MD037NoSpaceInEmphasis;
1187
1188 let content = "```\n==not highlight in code==\n```";
1189 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1190 let result = rule.check(&ctx).unwrap();
1191 assert!(
1192 result.is_empty(),
1193 "Should ignore highlights in code blocks. Got: {result:?}"
1194 );
1195 }
1196
1197 #[test]
1198 fn test_obsidian_highlight_edge_case_three_equals() {
1199 let rule = MD037NoSpaceInEmphasis;
1201
1202 let content = "Test === something === here";
1204 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1205 let result = rule.check(&ctx).unwrap();
1206 let _ = result;
1209 }
1210
1211 #[test]
1212 fn test_obsidian_highlight_edge_case_four_equals() {
1213 let rule = MD037NoSpaceInEmphasis;
1215
1216 let content = "Test ==== here";
1217 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1218 let result = rule.check(&ctx).unwrap();
1219 let _ = result;
1221 }
1222
1223 #[test]
1224 fn test_obsidian_highlight_adjacent() {
1225 let rule = MD037NoSpaceInEmphasis;
1227
1228 let content = "==one====two==";
1229 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1230 let result = rule.check(&ctx).unwrap();
1231 let _ = result;
1233 }
1234
1235 #[test]
1236 fn test_obsidian_highlight_with_special_chars() {
1237 let rule = MD037NoSpaceInEmphasis;
1239
1240 let content = "Test ==code: `test`== here";
1242 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1243 let result = rule.check(&ctx).unwrap();
1244 let _ = result;
1246 }
1247
1248 #[test]
1249 fn test_obsidian_highlight_unclosed() {
1250 let rule = MD037NoSpaceInEmphasis;
1252
1253 let content = "This ==starts but never ends";
1254 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1255 let result = rule.check(&ctx).unwrap();
1256 let _ = result;
1258 }
1259
1260 #[test]
1261 fn test_obsidian_highlight_still_flags_real_emphasis_issues() {
1262 let rule = MD037NoSpaceInEmphasis;
1264
1265 let content = "This has * spaced emphasis * and ==valid highlight==";
1266 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1267 let result = rule.check(&ctx).unwrap();
1268 assert!(
1269 !result.is_empty(),
1270 "Should still flag real spaced emphasis in Obsidian mode"
1271 );
1272 assert!(
1273 result.len() == 1,
1274 "Should flag exactly one issue (the spaced emphasis). Got: {result:?}"
1275 );
1276 }
1277
1278 #[test]
1279 fn test_standard_flavor_does_not_recognize_highlight() {
1280 let rule = MD037NoSpaceInEmphasis;
1283
1284 let content = "This is ==highlighted text== here.";
1285 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1286 let result = rule.check(&ctx).unwrap();
1287 let _ = result; }
1292
1293 #[test]
1294 fn test_obsidian_highlight_mixed_with_regular_emphasis() {
1295 let rule = MD037NoSpaceInEmphasis;
1297
1298 let content = "==highlighted== and *italic* and **bold** text";
1299 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1300 let result = rule.check(&ctx).unwrap();
1301 assert!(
1302 result.is_empty(),
1303 "Should not flag valid highlight and emphasis. Got: {result:?}"
1304 );
1305 }
1306
1307 #[test]
1308 fn test_obsidian_highlight_unicode() {
1309 let rule = MD037NoSpaceInEmphasis;
1311
1312 let content = "Text ==日本語 highlighted== here";
1313 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1314 let result = rule.check(&ctx).unwrap();
1315 assert!(
1316 result.is_empty(),
1317 "Should handle Unicode in highlights. Got: {result:?}"
1318 );
1319 }
1320
1321 #[test]
1322 fn test_obsidian_highlight_with_html() {
1323 let rule = MD037NoSpaceInEmphasis;
1325
1326 let content = "<!-- ==not highlight in comment== --> ==actual highlight==";
1327 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1328 let result = rule.check(&ctx).unwrap();
1329 let _ = result;
1331 }
1332
1333 #[test]
1334 fn test_obsidian_inline_comment_emphasis_ignored() {
1335 let rule = MD037NoSpaceInEmphasis;
1337
1338 let content = "Visible %%* spaced emphasis *%% still visible.";
1339 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1340 let result = rule.check(&ctx).unwrap();
1341
1342 assert!(
1343 result.is_empty(),
1344 "Should ignore emphasis inside Obsidian comments. Got: {result:?}"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_inline_html_code_not_flagged() {
1350 let rule = MD037NoSpaceInEmphasis;
1351
1352 let content = "The formula is <code>a * b * c</code> in math.";
1354 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1355 let result = rule.check(&ctx).unwrap();
1356 assert!(
1357 result.is_empty(),
1358 "Should not flag asterisks inside inline <code> tags. Got: {result:?}"
1359 );
1360
1361 let content2 = "Use <kbd>Ctrl * A</kbd> and <samp>x * y</samp> here.";
1363 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
1364 let result2 = rule.check(&ctx2).unwrap();
1365 assert!(
1366 result2.is_empty(),
1367 "Should not flag asterisks inside inline <kbd> and <samp> tags. Got: {result2:?}"
1368 );
1369
1370 let content3 = r#"Result: <code class="math">a * b</code> done."#;
1372 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
1373 let result3 = rule.check(&ctx3).unwrap();
1374 assert!(
1375 result3.is_empty(),
1376 "Should not flag asterisks inside <code> with attributes. Got: {result3:?}"
1377 );
1378
1379 let content4 = "Text * spaced * and <code>a * b</code>.";
1381 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
1382 let result4 = rule.check(&ctx4).unwrap();
1383 assert_eq!(
1384 result4.len(),
1385 1,
1386 "Should flag real spaced emphasis but not code content. Got: {result4:?}"
1387 );
1388 assert_eq!(result4[0].column, 6);
1389 }
1390
1391 #[test]
1394 fn test_pandoc_bracketed_span_guard() {
1395 use crate::config::MarkdownFlavor;
1396 let rule = MD037NoSpaceInEmphasis;
1397 let content = "See [* important *]{.highlight} for details.\n";
1399 let ctx = LintContext::new(content, MarkdownFlavor::Pandoc, None);
1400 let result = rule.check(&ctx).unwrap();
1401 assert!(
1402 result.is_empty(),
1403 "MD037 should not flag emphasis-like patterns inside Pandoc bracketed spans: {result:?}"
1404 );
1405
1406 let ctx_std = LintContext::new(content, MarkdownFlavor::Standard, None);
1408 let result_std = rule.check(&ctx_std).unwrap();
1409 assert!(
1410 !result_std.is_empty(),
1411 "MD037 should still flag spaces in emphasis under Standard flavor: {result_std:?}"
1412 );
1413 }
1414
1415 #[test]
1416 fn test_spaced_bold_metadata_pattern_detected() {
1417 let rule = MD037NoSpaceInEmphasis;
1418
1419 let content = "# Test\n\n** Explicit Import**: Convert markdownlint configs to rumdl format:";
1421 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1422 let result = rule.check(&ctx).unwrap();
1423 assert_eq!(
1424 result.len(),
1425 1,
1426 "Should flag '** Explicit Import**' as spaced emphasis. Got: {result:?}"
1427 );
1428 assert_eq!(result[0].line, 3);
1429
1430 let content2 = "# Test\n\n**trailing only **: some text";
1432 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
1433 let result2 = rule.check(&ctx2).unwrap();
1434 assert_eq!(
1435 result2.len(),
1436 1,
1437 "Should flag '**trailing only **' as spaced emphasis. Got: {result2:?}"
1438 );
1439
1440 let content3 = "# Test\n\n** both spaces **: some text";
1442 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
1443 let result3 = rule.check(&ctx3).unwrap();
1444 assert_eq!(
1445 result3.len(),
1446 1,
1447 "Should flag '** both spaces **' as spaced emphasis. Got: {result3:?}"
1448 );
1449
1450 let content4 = "# Test\n\n**Key**: value";
1452 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
1453 let result4 = rule.check(&ctx4).unwrap();
1454 assert!(
1455 result4.is_empty(),
1456 "Should not flag valid bold metadata '**Key**: value'. Got: {result4:?}"
1457 );
1458 }
1459}
1460
1461#[cfg(test)]
1462mod mdx_adjacent_prose_tests {
1463 use super::*;
1464 #[test]
1465 fn md037_mdx_code_does_not_hide_adjacent_prose() {
1466 let rule = MD037NoSpaceInEmphasis;
1467 for code in [
1468 "{value}",
1469 "{/* * literal * */}",
1470 "{\"* literal *\"}",
1471 "<Card onClick={() => null} title='* literal *'>text</Card>",
1472 ] {
1473 for ending in ["\n", "\r\n"] {
1474 let source = format!("内容 Before {code} after * visible *{ending}");
1475 let expected = format!("内容 Before {code} after *visible*{ending}");
1476 let ctx = crate::lint_context::LintContext::new(&source, crate::config::MarkdownFlavor::MDX, None);
1477 assert_eq!(rule.check(&ctx).unwrap().len(), 1, "{source}");
1478 assert_eq!(rule.fix(&ctx).unwrap(), expected);
1479 }
1480 }
1481 }
1482 #[test]
1483 fn md037_mdx_code_cannot_supply_the_other_emphasis_marker() {
1484 let rule = MD037NoSpaceInEmphasis;
1485 for source in ["Before * text {\"*\"} after\n", "Before * text {/* * */} after\n"] {
1486 let ctx = crate::lint_context::LintContext::new(source, crate::config::MarkdownFlavor::MDX, None);
1487 assert!(rule.check(&ctx).unwrap().is_empty(), "{source}");
1488 assert_eq!(rule.fix(&ctx).unwrap(), source);
1489 }
1490 }
1491}