1use crate::config::MarkdownFlavor;
5use crate::rule::{FixCapability, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
6use crate::utils::emphasis_utils::{
7 EmphasisEdges, EmphasisMarker, find_emphasis_edges, find_emphasis_markers, find_valid_emphasis_ranges,
8 has_doc_patterns, replace_inline_code_cow, replace_inline_math_cow,
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: &EmphasisEdges) -> bool {
20 span.has_leading_space || span.has_trailing_space
21}
22
23fn table_cell_ranges(line: &str, flavor: MarkdownFlavor) -> Vec<Range<usize>> {
30 let masked;
33 let scan = if line.contains('\\') || line.contains('`') {
34 let escaped = TableUtils::mask_escaped_pipes(line, flavor);
35 masked = TableUtils::mask_pipes_in_inline_code(&escaped);
36 debug_assert_eq!(
37 masked.len(),
38 line.len(),
39 "pipe masking must preserve byte offsets for cell slicing"
40 );
41 masked.as_str()
42 } else {
43 line
44 };
45
46 let mut ranges = Vec::new();
47 let mut start = 0;
48 for (pipe_pos, _) in scan.match_indices('|') {
49 ranges.push(start..pipe_pos);
50 start = pipe_pos + 1;
51 }
52 ranges.push(start..line.len());
53 ranges
54}
55
56#[inline]
59fn truncate_for_display(text: &str, max_len: usize) -> String {
60 if text.len() <= max_len {
61 return text.to_string();
62 }
63
64 let prefix_len = max_len / 2 - 2; let suffix_len = max_len / 2 - 2;
66
67 let prefix_end = text.floor_char_boundary(prefix_len.min(text.len()));
69 let suffix_start = text.floor_char_boundary(text.len().saturating_sub(suffix_len));
70
71 format!("{}...{}", &text[..prefix_end], &text[suffix_start..])
72}
73
74#[derive(Clone)]
76pub struct MD037NoSpaceInEmphasis;
77
78impl Default for MD037NoSpaceInEmphasis {
79 fn default() -> Self {
80 Self
81 }
82}
83
84impl MD037NoSpaceInEmphasis {
85 fn is_in_link(&self, ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
87 if ctx.is_range_in_rendered_link(&(byte_pos..byte_pos + 1))
88 || ctx
89 .images()
90 .iter()
91 .any(|image| image.byte_offset <= byte_pos && byte_pos < image.byte_end)
92 {
93 return true;
94 }
95
96 ctx.is_in_reference_def(byte_pos)
98 }
99
100 fn emphasis_span_ends(ctx: &crate::lint_context::LintContext) -> Vec<(usize, usize)> {
106 let mut ends: Vec<(usize, usize)> = ctx
107 .source_emphasis_spans()
108 .iter()
109 .map(|span| (span.byte_end, span.byte_offset))
110 .collect();
111 ends.sort_unstable();
112 ends
113 }
114
115 fn closes_earlier_emphasis(span_ends: &[(usize, usize)], start: usize, end: usize) -> bool {
127 let first_after_start = span_ends.partition_point(|(span_end, _)| *span_end <= start);
128 span_ends[first_after_start..]
129 .iter()
130 .take_while(|(span_end, _)| *span_end < end)
131 .any(|(_, span_start)| *span_start <= start)
132 }
133}
134
135impl Rule for MD037NoSpaceInEmphasis {
136 fn name(&self) -> &'static str {
137 "MD037"
138 }
139
140 fn description(&self) -> &'static str {
141 "Spaces inside emphasis markers"
142 }
143
144 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
145 let _timer = crate::profiling::ScopedTimer::new("MD037_check");
146 let has_html = ctx.has_char('<');
147
148 if !ctx.has_char('*') && !ctx.has_char('_') {
150 return Ok(vec![]);
151 }
152
153 let mut warnings = Vec::new();
156
157 for syntax in ctx.lexical_emphasis() {
161 let line_num = syntax.line_num;
162 let line_content = syntax.source();
163 if ctx.lines[line_num - 1].in_table_block {
164 for cell in table_cell_ranges(line_content, ctx.flavor) {
169 let Some(cell_content) = line_content.get(cell.clone()) else {
170 continue;
171 };
172 if !cell_content.contains('*') && !cell_content.contains('_') {
173 continue;
174 }
175 if has_doc_patterns(cell_content) {
176 continue;
177 }
178 self.check_line_content_for_emphasis_fast(cell_content, line_num, cell.start, &mut warnings);
179 }
180 continue;
181 }
182
183 self.check_line_for_emphasis_issues_fast(syntax, &mut warnings);
185 }
186
187 let mut filtered_warnings = Vec::new();
189 let lines = ctx.raw_lines();
190 let span_ends = if warnings.is_empty() {
191 Vec::new()
192 } else {
193 Self::emphasis_span_ends(ctx)
194 };
195
196 for warning in &warnings {
197 let line_num = warning.line;
198 let Some(line) = line_num.checked_sub(1).and_then(|index| lines.get(index)) else {
199 continue;
200 };
201 let line_start_pos = ctx.line_start_byte(line_num).unwrap_or(0);
202 let byte_pos = line_start_pos + (warning.column - 1);
206 let line_pos = warning.column - 1;
208 let char_col = byte_to_char_count(line, warning.column - 1);
209
210 let in_pandoc_construct = ctx.flavor.is_pandoc_compatible() && ctx.is_in_bracketed_span(byte_pos);
221 let byte_end = line_start_pos + (warning.end_column - 1);
222 let closing_pos = byte_end.saturating_sub(1);
223 let marker_in_html = has_html
224 && [byte_pos, closing_pos].into_iter().any(|pos| {
225 ctx.is_in_html_comment(pos) || ctx.is_in_html_tag(pos) || ctx.is_in_jsx_component_tag(pos)
226 });
227 if !in_pandoc_construct
228 && !marker_in_html
229 && !ctx.is_in_jinja_string(byte_pos)
230 && !ctx.is_in_jinja_string(closing_pos)
231 && !Self::closes_earlier_emphasis(&span_ends, byte_pos, byte_end)
232 && !self.is_in_link(ctx, byte_pos)
233 && !ctx.is_in_shortcode(byte_pos)
234 && !ctx.is_in_shortcode(closing_pos)
235 && !is_in_math_context(ctx, byte_pos)
236 && !ctx.is_in_code_span(line_num, char_col)
237 && !ctx.is_in_code_span_byte(closing_pos)
240 && !is_in_inline_html_code(line, line_pos)
241 && !ctx.overlaps_mdx_inline_code(byte_pos, byte_end)
242 && !is_in_mkdocs_markup(line, line_pos, ctx.flavor)
243 && !ctx.is_position_in_obsidian_comment(line_num, char_col)
244 {
245 let mut adjusted_warning = warning.clone();
246 adjusted_warning.column = char_col;
248 adjusted_warning.end_column = byte_to_char_count(line, warning.end_column - 1);
249 filtered_warnings.push(adjusted_warning);
250 }
251 }
252
253 Ok(filtered_warnings)
254 }
255
256 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
257 Ok(ctx.content.to_string())
258 }
259
260 fn fix_capability(&self) -> FixCapability {
261 FixCapability::Unfixable
264 }
265
266 fn category(&self) -> RuleCategory {
268 RuleCategory::Emphasis
269 }
270
271 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
273 ctx.content.is_empty() || !ctx.likely_has_emphasis()
274 }
275
276 fn as_any(&self) -> &dyn std::any::Any {
277 self
278 }
279
280 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
281 where
282 Self: Sized,
283 {
284 Box::new(MD037NoSpaceInEmphasis)
285 }
286}
287
288impl MD037NoSpaceInEmphasis {
289 #[inline]
291 fn check_line_for_emphasis_issues_fast(
292 &self,
293 syntax: crate::lint_context::EmphasisLine<'_>,
294 warnings: &mut Vec<LintWarning>,
295 ) {
296 let line = syntax.source();
297 let line_num = syntax.line_num;
298 if has_doc_patterns(line) {
300 return;
301 }
302
303 if (line.starts_with(' ') || line.starts_with('*') || line.starts_with('+') || line.starts_with('-'))
308 && UNORDERED_LIST_MARKER_REGEX.is_match(line)
309 {
310 if let Some(caps) = UNORDERED_LIST_MARKER_REGEX.captures(line)
311 && let Some(full_match) = caps.get(0)
312 {
313 let list_marker_end = full_match.end();
314 if list_marker_end < line.len() {
315 let remaining_content = &line[list_marker_end..];
316
317 self.check_line_content_for_emphasis_fast(remaining_content, line_num, list_marker_end, warnings);
320 }
321 }
322 return;
323 }
324
325 let processed = replace_inline_math_cow(syntax.without_code());
328 let fresh_markers;
329 let markers = if matches!(processed, std::borrow::Cow::Borrowed(_)) {
330 syntax.markers
331 } else {
332 fresh_markers = find_emphasis_markers(&processed);
333 &fresh_markers
334 };
335 self.check_prepared_content(line, &processed, markers, line_num, 0, warnings);
336 }
337
338 fn check_line_content_for_emphasis_fast(
340 &self,
341 content: &str,
342 line_num: usize,
343 offset: usize,
344 warnings: &mut Vec<LintWarning>,
345 ) {
346 let code_masked = replace_inline_code_cow(content);
349 let processed_content = replace_inline_math_cow(&code_masked);
350 let markers = find_emphasis_markers(&processed_content);
351 self.check_prepared_content(content, &processed_content, &markers, line_num, offset, warnings);
352 }
353
354 fn check_prepared_content(
355 &self,
356 content: &str,
357 processed_content: &str,
358 markers: &[EmphasisMarker],
359 line_num: usize,
360 offset: usize,
361 warnings: &mut Vec<LintWarning>,
362 ) {
363 if markers.is_empty() {
364 return;
365 }
366
367 let spans = find_emphasis_edges(processed_content, markers);
369
370 let valid_ranges = find_valid_emphasis_ranges(processed_content, markers);
375
376 for span in spans {
378 if has_spacing_issues(&span) {
379 let full_start = span.opening.start_pos;
380 let full_end = span.closing.end_pos();
381
382 if valid_ranges
384 .iter()
385 .any(|&(start, end)| start <= full_start && full_end <= end)
386 {
387 continue;
388 }
389
390 let full_text = &content[full_start..full_end];
391
392 if full_end < content.len() {
395 let remaining = &content[full_end..];
396 if remaining.starts_with('{') && has_span_ial(remaining.split_whitespace().next().unwrap_or("")) {
398 continue;
399 }
400 }
401
402 let display_text = truncate_for_display(full_text, 60);
404
405 let warning = LintWarning {
406 rule_name: Some(self.name().to_string()),
407 message: format!("Spaces inside emphasis markers: {display_text:?}"),
408 line: line_num,
412 column: offset + full_start + 1,
413 end_line: line_num,
414 end_column: offset + full_end + 1,
415 severity: Severity::Warning,
416 fix: None,
417 };
418
419 warnings.push(warning);
420 }
421 }
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::lint_context::LintContext;
429 use crate::utils::emphasis_utils::find_emphasis_spans;
430
431 #[test]
432 fn test_marker_inside_shortcode_cannot_close_spaced_emphasis() {
433 let rule = MD037NoSpaceInEmphasis;
434 let source = "Before * text {{< note title=\"*\" >}}\n\nBefore * visible *\n";
435 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Hugo, None);
436 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
437 assert_eq!(rule.fix(&ctx).unwrap(), source);
438 }
439
440 #[test]
441 fn test_myst_comment_contents_are_not_linted() {
442 let rule = MD037NoSpaceInEmphasis;
443 for prefix in ["% ", "%", " %\t", " % "] {
444 let source = format!("{prefix}* literal *\n\nBefore * visible *\n");
445 let ctx = crate::lint_context::LintContext::new(&source, crate::config::MarkdownFlavor::MyST, None);
446 let warnings = rule.check(&ctx).unwrap();
447 assert_eq!(warnings.len(), 1, "comments are not visible prose: {source}");
448 assert_eq!(warnings[0].line, 3);
449 assert_eq!(rule.fix(&ctx).unwrap(), source);
450 }
451 }
452
453 #[test]
454 fn test_jinja_quoted_spaced_markers_are_literal() {
455 let rule = MD037NoSpaceInEmphasis;
456 for template in [
457 r#"{{ "* literal *" }}"#,
458 "{% set text = '_ 内容 _' %}",
459 "{{\n \"** literal **\"\n}}",
460 r#"{{ {"text": "* literal *"} }}"#,
461 r#"* text {{ "*" }}"#,
462 ] {
463 let source = format!("Before {template}\n\nBefore * visible *\n");
464 let ctx = LintContext::new(&source, crate::config::MarkdownFlavor::Standard, None);
465 assert_eq!(
466 rule.check(&ctx).unwrap().len(),
467 1,
468 "quoted markers are literal: {source}"
469 );
470 assert_eq!(rule.fix(&ctx).unwrap(), source);
471 }
472 }
473
474 #[test]
475 fn test_mdx_module_spaced_markers_are_literal() {
476 let rule = MD037NoSpaceInEmphasis;
477 for module in [
478 "export const text = \"* literal *\"",
479 "export const data = {\n text: \"_ 内容 _\"\n}",
480 "import text from \"./* literal *.js\"",
481 ] {
482 let source = format!("{module}\n\nBefore * visible *\n");
483 let ctx = LintContext::new(&source, crate::config::MarkdownFlavor::MDX, None);
484 let warnings = rule.check(&ctx).unwrap();
485 assert_eq!(warnings.len(), 1, "module strings must stay literal: {source}");
486 assert!(warnings[0].line > module.lines().count());
487 assert_eq!(rule.fix(&ctx).unwrap(), source);
488 }
489 }
490
491 #[test]
492 fn test_spaced_markers_in_html_attributes_or_closing_in_comments_are_ignored() {
493 let rule = MD037NoSpaceInEmphasis;
494 for source in [
495 "Before <span title=\"* literal *\">visible</span>\n",
496 "Before <span\n title=\"_ literal _\">visible</span>\n",
497 "日本語 <span title=\"** 内容 **\">表示</span>\n",
498 "Before * text <!-- * -->\n",
499 "Before ** text <span title=\"**\">visible</span>\n",
500 ] {
501 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
502 assert!(
503 rule.check(&ctx).unwrap().is_empty(),
504 "HTML markers are literal: {source}"
505 );
506 assert_eq!(rule.fix(&ctx).unwrap(), source);
507 }
508 }
509
510 #[test]
511 fn test_spaced_emphasis_beside_html_attributes_still_reported() {
512 let rule = MD037NoSpaceInEmphasis;
513 let source = "Before <span title=\"* literal *\">* visible *</span> <!-- * hidden * -->\n";
514 let expected = "Before <span title=\"* literal *\">*visible*</span> <!-- * hidden * -->\n";
515 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
516 assert_eq!(rule.check(&ctx).unwrap().len(), 1);
517 assert_eq!(rule.fix(&ctx).unwrap(), source);
518 let fixed_ctx = LintContext::new(expected, crate::config::MarkdownFlavor::Standard, None);
519 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
520 }
521
522 #[test]
523 fn test_spaced_marker_pair_ending_inside_multiline_code_is_ignored() {
524 let rule = MD037NoSpaceInEmphasis;
525 for content in [
526 "Before * text `code *\nend`\n",
527 "_ text `code _\nend`\n",
528 "** text `code **\nend`\n",
529 "__ text ``code `tick` __\nend``\n",
530 "日本語 * 内容 `コード *\n終了`\n",
531 ] {
532 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
533 assert!(
534 rule.check(&ctx).unwrap().is_empty(),
535 "closing marker is code: {content}"
536 );
537 assert_eq!(rule.fix(&ctx).unwrap(), content);
538 }
539 }
540
541 #[test]
542 fn test_spaced_emphasis_beside_multiline_code_requires_manual_correction() {
543 let rule = MD037NoSpaceInEmphasis;
544 let source = "Before ** text `code **\nend` and * real *\n";
545 let expected = "Before ** text `code **\nend` and *real*\n";
546 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
547 let warnings = rule.check(&ctx).unwrap();
548 assert_eq!(warnings.len(), 1);
549 assert_eq!(warnings[0].line, 2);
550 assert_eq!(rule.fix(&ctx).unwrap(), source);
551 let fixed_ctx = LintContext::new(expected, crate::config::MarkdownFlavor::Standard, None);
552 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
553 assert_eq!(rule.fix(&fixed_ctx).unwrap(), expected);
554 let code_values = |content: &str| {
555 pulldown_cmark::Parser::new(content)
556 .filter_map(|event| match event {
557 pulldown_cmark::Event::Code(value) => Some(value.into_string()),
558 _ => None,
559 })
560 .collect::<Vec<_>>()
561 };
562 assert_eq!(code_values(source), vec!["code ** end"]);
563 assert_eq!(code_values(source), code_values(expected));
564 }
565
566 #[test]
567 fn table_cell_ranges_unmasked_scan_matches_masked_scan() {
568 fn always_masked(line: &str, flavor: MarkdownFlavor) -> Vec<Range<usize>> {
571 let escaped = TableUtils::mask_escaped_pipes(line, flavor);
572 let masked = TableUtils::mask_pipes_in_inline_code(&escaped);
573 let mut ranges = Vec::new();
574 let mut start = 0;
575 for (pipe_pos, _) in masked.match_indices('|') {
576 ranges.push(start..pipe_pos);
577 start = pipe_pos + 1;
578 }
579 ranges.push(start..line.len());
580 ranges
581 }
582
583 for line in [
584 "| a | b |",
585 "| a * x * | b |",
586 "a | b",
587 "no pipes at all",
588 "",
589 "|||",
590 "| naïve ünïcode | 日本語 |",
591 r"| a \| b | c |",
592 "| `a | b` | c |",
593 r"| `a \| b` | c |",
594 r"| a \\ | b |",
595 r"| a \\| b | c |",
596 "> | a * x * | b |",
597 ] {
598 for flavor in [MarkdownFlavor::Standard, MarkdownFlavor::MkDocs] {
599 assert_eq!(
600 table_cell_ranges(line, flavor),
601 always_masked(line, flavor),
602 "line: {line:?} flavor: {flavor:?}"
603 );
604 }
605 }
606 }
607
608 #[test]
609 fn test_emphasis_marker_parsing() {
610 let markers = find_emphasis_markers("This has *single* and **double** emphasis");
611 assert_eq!(markers.len(), 4); let markers = find_emphasis_markers("*start* and *end*");
614 assert_eq!(markers.len(), 4); }
616
617 #[test]
618 fn test_emphasis_span_detection() {
619 let markers = find_emphasis_markers("This has *valid* emphasis");
620 let spans = find_emphasis_spans("This has *valid* emphasis", &markers);
621 assert_eq!(spans.len(), 1);
622 assert_eq!(spans[0].content, "valid");
623 assert!(!spans[0].has_leading_space);
624 assert!(!spans[0].has_trailing_space);
625
626 let markers = find_emphasis_markers("This has * invalid * emphasis");
627 let spans = find_emphasis_spans("This has * invalid * emphasis", &markers);
628 assert_eq!(spans.len(), 1);
629 assert_eq!(spans[0].content, " invalid ");
630 assert!(spans[0].has_leading_space);
631 assert!(spans[0].has_trailing_space);
632 }
633
634 #[test]
635 fn test_with_document_structure() {
636 let rule = MD037NoSpaceInEmphasis;
637
638 let content = "This is *correct* emphasis and **strong emphasis**";
640 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
641 let result = rule.check(&ctx).unwrap();
642 assert!(result.is_empty(), "No warnings expected for correct emphasis");
643
644 let content = "This is * text with spaces * and more content";
646 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
647 let result = rule.check(&ctx).unwrap();
648 assert!(!result.is_empty(), "Expected warnings for spaces in emphasis");
649
650 let content = "This is *correct* emphasis\n```\n* incorrect * in code block\n```\nOutside block with * spaces in emphasis *";
652 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
653 let result = rule.check(&ctx).unwrap();
654 assert!(
655 !result.is_empty(),
656 "Expected warnings for spaces in emphasis outside code block"
657 );
658 }
659
660 #[test]
661 fn test_spaced_literal_markers_preserve_embedded_inline_code() {
662 let rule = MD037NoSpaceInEmphasis;
664 let content = "Set * the `id` field * below.";
665 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
666 let fixed = rule.fix(&ctx).unwrap();
667 assert_eq!(fixed, content);
668 }
669
670 #[test]
671 fn unresolved_spaced_reference_still_warns_while_rendered_links_do_not() {
672 let rule = MD037NoSpaceInEmphasis;
673 for (source, expected) in [
674 ("[ * spaced * ][unknown]\n", 1),
675 ("[ * spaced * ](url)\n", 0),
676 ("\n", 0),
677 ] {
678 let ctx = LintContext::new(source, crate::config::MarkdownFlavor::Standard, None);
679 assert_eq!(rule.check(&ctx).unwrap().len(), expected, "{source:?}");
680 }
681 }
682
683 #[test]
684 fn test_emphasis_in_links_not_flagged() {
685 let rule = MD037NoSpaceInEmphasis;
686 let content = r#"Check this [* spaced asterisk *](https://example.com/*test*) link.
687
688This has * real spaced emphasis * that should be flagged."#;
689 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
690 let result = rule.check(&ctx).unwrap();
691
692 assert_eq!(
696 result.len(),
697 1,
698 "Expected exactly 1 warning, but got: {:?}",
699 result.len()
700 );
701 assert!(result[0].message.contains("Spaces inside emphasis markers"));
702 assert!(result[0].line == 3); }
705
706 #[test]
707 fn test_emphasis_in_links_vs_outside_links() {
708 let rule = MD037NoSpaceInEmphasis;
709 let content = r#"Check [* spaced *](https://example.com/*test*) and inline * real spaced * text.
710
711[* link *]: https://example.com/*path*"#;
712 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
713 let result = rule.check(&ctx).unwrap();
714
715 assert_eq!(result.len(), 1);
717 assert!(result[0].message.contains("Spaces inside emphasis markers"));
718 assert!(result[0].line == 1);
720 }
721
722 #[test]
723 fn test_issue_49_asterisk_in_inline_code() {
724 let rule = MD037NoSpaceInEmphasis;
726
727 let content = "The `__mul__` method is needed for left-hand multiplication (`vector * 3`) and `__rmul__` is needed for right-hand multiplication (`3 * vector`).";
729 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
730 let result = rule.check(&ctx).unwrap();
731 assert!(
732 result.is_empty(),
733 "Should not flag asterisks inside inline code as emphasis (issue #49). Got: {result:?}"
734 );
735 }
736
737 #[test]
738 fn test_issue_28_inline_code_in_emphasis() {
739 let rule = MD037NoSpaceInEmphasis;
741
742 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.";
744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
745 let result = rule.check(&ctx).unwrap();
746 assert!(
747 result.is_empty(),
748 "Should not flag inline code inside emphasis as spaces (issue #28). Got: {result:?}"
749 );
750
751 let content2 = "The **`foo` and `bar`** methods are important.";
753 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
754 let result2 = rule.check(&ctx2).unwrap();
755 assert!(
756 result2.is_empty(),
757 "Should not flag multiple inline code snippets inside emphasis. Got: {result2:?}"
758 );
759
760 let content3 = "This is __inline `code`__ with underscores.";
762 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
763 let result3 = rule.check(&ctx3).unwrap();
764 assert!(
765 result3.is_empty(),
766 "Should not flag inline code with underscore emphasis. Got: {result3:?}"
767 );
768
769 let content4 = "This is *inline `test`* with single asterisks.";
771 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
772 let result4 = rule.check(&ctx4).unwrap();
773 assert!(
774 result4.is_empty(),
775 "Should not flag inline code with single asterisk emphasis. Got: {result4:?}"
776 );
777
778 let content5 = "This has * real spaces * that should be flagged.";
780 let ctx5 = LintContext::new(content5, crate::config::MarkdownFlavor::Standard, None);
781 let result5 = rule.check(&ctx5).unwrap();
782 assert!(!result5.is_empty(), "Should still flag actual spaces in emphasis");
783 assert!(result5[0].message.contains("Spaces inside emphasis markers"));
784 }
785
786 #[test]
787 fn test_multibyte_utf8_no_panic() {
788 let rule = MD037NoSpaceInEmphasis;
792
793 let greek = "Αυτό είναι ένα * τεστ με ελληνικά * και πολύ μεγάλο κείμενο που θα πρέπει να περικοπεί σωστά.";
795 let ctx = LintContext::new(greek, crate::config::MarkdownFlavor::Standard, None);
796 let result = rule.check(&ctx);
797 assert!(result.is_ok(), "Greek text should not panic");
798
799 let chinese = "这是一个 * 测试文本 * 包含中文字符,需要正确处理多字节边界。";
801 let ctx = LintContext::new(chinese, crate::config::MarkdownFlavor::Standard, None);
802 let result = rule.check(&ctx);
803 assert!(result.is_ok(), "Chinese text should not panic");
804
805 let cyrillic = "Это * тест с кириллицей * и очень длинным текстом для проверки обрезки.";
807 let ctx = LintContext::new(cyrillic, crate::config::MarkdownFlavor::Standard, None);
808 let result = rule.check(&ctx);
809 assert!(result.is_ok(), "Cyrillic text should not panic");
810
811 let mixed =
813 "日本語と * 中文と한국어が混在する非常に長いテキストでtruncate_for_displayの境界処理をテスト * します。";
814 let ctx = LintContext::new(mixed, crate::config::MarkdownFlavor::Standard, None);
815 let result = rule.check(&ctx);
816 assert!(result.is_ok(), "Mixed CJK text should not panic");
817
818 let arabic = "هذا * اختبار بالعربية * مع نص طويل جداً لاختبار معالجة حدود الأحرف.";
820 let ctx = LintContext::new(arabic, crate::config::MarkdownFlavor::Standard, None);
821 let result = rule.check(&ctx);
822 assert!(result.is_ok(), "Arabic text should not panic");
823
824 let emoji = "This has * 🎉 party 🎊 celebration 🥳 emojis * that use multi-byte sequences.";
826 let ctx = LintContext::new(emoji, crate::config::MarkdownFlavor::Standard, None);
827 let result = rule.check(&ctx);
828 assert!(result.is_ok(), "Emoji text should not panic");
829 }
830
831 #[test]
832 fn test_template_shortcode_syntax_not_flagged() {
833 let rule = MD037NoSpaceInEmphasis;
836
837 let content = "{* ../../docs_src/cookie_param_models/tutorial001.py hl[9:12,16] *}";
839 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
840 let result = rule.check(&ctx).unwrap();
841 assert!(
842 result.is_empty(),
843 "Template shortcode syntax should not be flagged. Got: {result:?}"
844 );
845
846 let content = "{* ../../docs_src/conditional_openapi/tutorial001.py hl[6,11] *}";
848 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
849 let result = rule.check(&ctx).unwrap();
850 assert!(
851 result.is_empty(),
852 "Template shortcode syntax should not be flagged. Got: {result:?}"
853 );
854
855 let content = "# Header\n\n{* file1.py *}\n\nSome text.\n\n{* file2.py hl[1-5] *}";
857 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
858 let result = rule.check(&ctx).unwrap();
859 assert!(
860 result.is_empty(),
861 "Multiple template shortcodes should not be flagged. Got: {result:?}"
862 );
863
864 let content = "This has * real spaced emphasis * here.";
866 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
867 let result = rule.check(&ctx).unwrap();
868 assert!(!result.is_empty(), "Real spaced emphasis should still be flagged");
869 }
870
871 #[test]
872 fn test_multiline_code_span_not_flagged() {
873 let rule = MD037NoSpaceInEmphasis;
876
877 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";
879 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
880 let result = rule.check(&ctx).unwrap();
881 assert!(
882 result.is_empty(),
883 "Should not flag asterisks inside multi-line code spans. Got: {result:?}"
884 );
885
886 let content2 = "Text with `code that\nspans * multiple * lines` here.";
888 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
889 let result2 = rule.check(&ctx2).unwrap();
890 assert!(
891 result2.is_empty(),
892 "Should not flag asterisks inside multi-line code spans. Got: {result2:?}"
893 );
894 }
895
896 #[test]
897 fn test_html_block_asterisks_not_flagged() {
898 let rule = MD037NoSpaceInEmphasis;
899
900 let content = r#"<table>
902<tr><td>Format</td><td>Size</td></tr>
903<tr><td>BC1</td><td><code>floor((width + 3) / 4) * floor((height + 3) / 4) * 8</code></td></tr>
904<tr><td>BC2</td><td><code>floor((width + 3) / 4) * floor((height + 3) / 4) * 16</code></td></tr>
905</table>"#;
906 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
907 let result = rule.check(&ctx).unwrap();
908 assert!(
909 result.is_empty(),
910 "Should not flag asterisks inside HTML blocks. Got: {result:?}"
911 );
912
913 let content2 = "<div>\n<p>Value is * something * here</p>\n</div>";
915 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
916 let result2 = rule.check(&ctx2).unwrap();
917 assert!(
918 result2.is_empty(),
919 "Should not flag emphasis-like patterns inside HTML div blocks. Got: {result2:?}"
920 );
921
922 let content3 = "Regular * spaced emphasis * text\n\n<div>* not emphasis *</div>";
924 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
925 let result3 = rule.check(&ctx3).unwrap();
926 assert_eq!(
927 result3.len(),
928 1,
929 "Should flag spaced emphasis in regular markdown but not inside HTML blocks. Got: {result3:?}"
930 );
931 assert_eq!(result3[0].line, 1, "Warning should be on line 1 (regular markdown)");
932 }
933
934 #[test]
935 fn test_mkdocs_icon_shortcode_not_flagged() {
936 let rule = MD037NoSpaceInEmphasis;
938
939 let content = "Click :material-check: to confirm and :fontawesome-solid-star: for favorites.";
942 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
943 let result = rule.check(&ctx).unwrap();
944 assert!(
945 result.is_empty(),
946 "Should not flag MkDocs icon shortcodes. Got: {result:?}"
947 );
948
949 let content2 = "This has * real spaced emphasis * but also :material-check: icon.";
951 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::MkDocs, None);
952 let result2 = rule.check(&ctx2).unwrap();
953 assert!(
954 !result2.is_empty(),
955 "Should still flag real spaced emphasis in MkDocs mode"
956 );
957 }
958
959 #[test]
960 fn test_mkdocs_pymdown_markup_not_flagged() {
961 let rule = MD037NoSpaceInEmphasis;
963
964 let content = "Press ++ctrl+c++ to copy.";
966 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
967 let result = rule.check(&ctx).unwrap();
968 assert!(
969 result.is_empty(),
970 "Should not flag PyMdown Keys notation. Got: {result:?}"
971 );
972
973 let content2 = "This is ==highlighted text== for emphasis.";
975 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::MkDocs, None);
976 let result2 = rule.check(&ctx2).unwrap();
977 assert!(
978 result2.is_empty(),
979 "Should not flag PyMdown Mark notation. Got: {result2:?}"
980 );
981
982 let content3 = "This is ^^inserted text^^ here.";
984 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::MkDocs, None);
985 let result3 = rule.check(&ctx3).unwrap();
986 assert!(
987 result3.is_empty(),
988 "Should not flag PyMdown Insert notation. Got: {result3:?}"
989 );
990
991 let content4 = "Press ++ctrl++ then * spaced emphasis * here.";
993 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::MkDocs, None);
994 let result4 = rule.check(&ctx4).unwrap();
995 assert!(
996 !result4.is_empty(),
997 "Should still flag real spaced emphasis alongside PyMdown markup"
998 );
999 }
1000
1001 #[test]
1004 fn test_obsidian_highlight_not_flagged() {
1005 let rule = MD037NoSpaceInEmphasis;
1007
1008 let content = "This is ==highlighted text== here.";
1010 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1011 let result = rule.check(&ctx).unwrap();
1012 assert!(
1013 result.is_empty(),
1014 "Should not flag Obsidian highlight syntax. Got: {result:?}"
1015 );
1016 }
1017
1018 #[test]
1019 fn test_obsidian_highlight_multiple_on_line() {
1020 let rule = MD037NoSpaceInEmphasis;
1022
1023 let content = "Both ==one== and ==two== are highlighted.";
1024 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1025 let result = rule.check(&ctx).unwrap();
1026 assert!(
1027 result.is_empty(),
1028 "Should not flag multiple Obsidian highlights. Got: {result:?}"
1029 );
1030 }
1031
1032 #[test]
1033 fn test_obsidian_highlight_entire_paragraph() {
1034 let rule = MD037NoSpaceInEmphasis;
1036
1037 let content = "==Entire paragraph highlighted==";
1038 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1039 let result = rule.check(&ctx).unwrap();
1040 assert!(
1041 result.is_empty(),
1042 "Should not flag entire highlighted paragraph. Got: {result:?}"
1043 );
1044 }
1045
1046 #[test]
1047 fn test_obsidian_highlight_with_emphasis() {
1048 let rule = MD037NoSpaceInEmphasis;
1050
1051 let content = "**==bold highlight==**";
1053 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1054 let result = rule.check(&ctx).unwrap();
1055 assert!(
1056 result.is_empty(),
1057 "Should not flag bold highlight combination. Got: {result:?}"
1058 );
1059
1060 let content2 = "*==italic highlight==*";
1062 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Obsidian, None);
1063 let result2 = rule.check(&ctx2).unwrap();
1064 assert!(
1065 result2.is_empty(),
1066 "Should not flag italic highlight combination. Got: {result2:?}"
1067 );
1068 }
1069
1070 #[test]
1071 fn test_obsidian_highlight_in_lists() {
1072 let rule = MD037NoSpaceInEmphasis;
1074
1075 let content = "- Item with ==highlight== text\n- Another ==highlighted== item";
1076 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1077 let result = rule.check(&ctx).unwrap();
1078 assert!(
1079 result.is_empty(),
1080 "Should not flag highlights in list items. Got: {result:?}"
1081 );
1082 }
1083
1084 #[test]
1085 fn test_obsidian_highlight_in_blockquote() {
1086 let rule = MD037NoSpaceInEmphasis;
1088
1089 let content = "> This quote has ==highlighted== text.";
1090 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1091 let result = rule.check(&ctx).unwrap();
1092 assert!(
1093 result.is_empty(),
1094 "Should not flag highlights in blockquotes. Got: {result:?}"
1095 );
1096 }
1097
1098 #[test]
1099 fn test_obsidian_highlight_in_tables() {
1100 let rule = MD037NoSpaceInEmphasis;
1102
1103 let content = "| Header | Column |\n|--------|--------|\n| ==highlighted== | text |";
1104 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1105 let result = rule.check(&ctx).unwrap();
1106 assert!(
1107 result.is_empty(),
1108 "Should not flag highlights in tables. Got: {result:?}"
1109 );
1110 }
1111
1112 #[test]
1113 fn test_obsidian_highlight_in_code_blocks_ignored() {
1114 let rule = MD037NoSpaceInEmphasis;
1116
1117 let content = "```\n==not highlight in code==\n```";
1118 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1119 let result = rule.check(&ctx).unwrap();
1120 assert!(
1121 result.is_empty(),
1122 "Should ignore highlights in code blocks. Got: {result:?}"
1123 );
1124 }
1125
1126 #[test]
1127 fn test_obsidian_highlight_edge_case_three_equals() {
1128 let rule = MD037NoSpaceInEmphasis;
1130
1131 let content = "Test === something === here";
1133 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1134 let result = rule.check(&ctx).unwrap();
1135 let _ = result;
1138 }
1139
1140 #[test]
1141 fn test_obsidian_highlight_edge_case_four_equals() {
1142 let rule = MD037NoSpaceInEmphasis;
1144
1145 let content = "Test ==== here";
1146 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1147 let result = rule.check(&ctx).unwrap();
1148 let _ = result;
1150 }
1151
1152 #[test]
1153 fn test_obsidian_highlight_adjacent() {
1154 let rule = MD037NoSpaceInEmphasis;
1156
1157 let content = "==one====two==";
1158 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1159 let result = rule.check(&ctx).unwrap();
1160 let _ = result;
1162 }
1163
1164 #[test]
1165 fn test_obsidian_highlight_with_special_chars() {
1166 let rule = MD037NoSpaceInEmphasis;
1168
1169 let content = "Test ==code: `test`== here";
1171 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1172 let result = rule.check(&ctx).unwrap();
1173 let _ = result;
1175 }
1176
1177 #[test]
1178 fn test_obsidian_highlight_unclosed() {
1179 let rule = MD037NoSpaceInEmphasis;
1181
1182 let content = "This ==starts but never ends";
1183 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1184 let result = rule.check(&ctx).unwrap();
1185 let _ = result;
1187 }
1188
1189 #[test]
1190 fn test_obsidian_highlight_still_flags_real_emphasis_issues() {
1191 let rule = MD037NoSpaceInEmphasis;
1193
1194 let content = "This has * spaced emphasis * and ==valid highlight==";
1195 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1196 let result = rule.check(&ctx).unwrap();
1197 assert!(
1198 !result.is_empty(),
1199 "Should still flag real spaced emphasis in Obsidian mode"
1200 );
1201 assert!(
1202 result.len() == 1,
1203 "Should flag exactly one issue (the spaced emphasis). Got: {result:?}"
1204 );
1205 }
1206
1207 #[test]
1208 fn test_standard_flavor_does_not_recognize_highlight() {
1209 let rule = MD037NoSpaceInEmphasis;
1212
1213 let content = "This is ==highlighted text== here.";
1214 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1215 let result = rule.check(&ctx).unwrap();
1216 let _ = result; }
1221
1222 #[test]
1223 fn test_obsidian_highlight_mixed_with_regular_emphasis() {
1224 let rule = MD037NoSpaceInEmphasis;
1226
1227 let content = "==highlighted== and *italic* and **bold** text";
1228 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1229 let result = rule.check(&ctx).unwrap();
1230 assert!(
1231 result.is_empty(),
1232 "Should not flag valid highlight and emphasis. Got: {result:?}"
1233 );
1234 }
1235
1236 #[test]
1237 fn test_obsidian_highlight_unicode() {
1238 let rule = MD037NoSpaceInEmphasis;
1240
1241 let content = "Text ==日本語 highlighted== here";
1242 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1243 let result = rule.check(&ctx).unwrap();
1244 assert!(
1245 result.is_empty(),
1246 "Should handle Unicode in highlights. Got: {result:?}"
1247 );
1248 }
1249
1250 #[test]
1251 fn test_obsidian_highlight_with_html() {
1252 let rule = MD037NoSpaceInEmphasis;
1254
1255 let content = "<!-- ==not highlight in comment== --> ==actual highlight==";
1256 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1257 let result = rule.check(&ctx).unwrap();
1258 let _ = result;
1260 }
1261
1262 #[test]
1263 fn test_obsidian_inline_comment_emphasis_ignored() {
1264 let rule = MD037NoSpaceInEmphasis;
1266
1267 let content = "Visible %%* spaced emphasis *%% still visible.";
1268 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
1269 let result = rule.check(&ctx).unwrap();
1270
1271 assert!(
1272 result.is_empty(),
1273 "Should ignore emphasis inside Obsidian comments. Got: {result:?}"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_inline_html_code_not_flagged() {
1279 let rule = MD037NoSpaceInEmphasis;
1280
1281 let content = "The formula is <code>a * b * c</code> in math.";
1283 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1284 let result = rule.check(&ctx).unwrap();
1285 assert!(
1286 result.is_empty(),
1287 "Should not flag asterisks inside inline <code> tags. Got: {result:?}"
1288 );
1289
1290 let content2 = "Use <kbd>Ctrl * A</kbd> and <samp>x * y</samp> here.";
1292 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
1293 let result2 = rule.check(&ctx2).unwrap();
1294 assert!(
1295 result2.is_empty(),
1296 "Should not flag asterisks inside inline <kbd> and <samp> tags. Got: {result2:?}"
1297 );
1298
1299 let content3 = r#"Result: <code class="math">a * b</code> done."#;
1301 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
1302 let result3 = rule.check(&ctx3).unwrap();
1303 assert!(
1304 result3.is_empty(),
1305 "Should not flag asterisks inside <code> with attributes. Got: {result3:?}"
1306 );
1307
1308 let content4 = "Text * spaced * and <code>a * b</code>.";
1310 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
1311 let result4 = rule.check(&ctx4).unwrap();
1312 assert_eq!(
1313 result4.len(),
1314 1,
1315 "Should flag real spaced emphasis but not code content. Got: {result4:?}"
1316 );
1317 assert_eq!(result4[0].column, 6);
1318 }
1319
1320 #[test]
1323 fn test_pandoc_bracketed_span_guard() {
1324 use crate::config::MarkdownFlavor;
1325 let rule = MD037NoSpaceInEmphasis;
1326 let content = "See [* important *]{.highlight} for details.\n";
1328 let ctx = LintContext::new(content, MarkdownFlavor::Pandoc, None);
1329 let result = rule.check(&ctx).unwrap();
1330 assert!(
1331 result.is_empty(),
1332 "MD037 should not flag emphasis-like patterns inside Pandoc bracketed spans: {result:?}"
1333 );
1334
1335 let ctx_std = LintContext::new(content, MarkdownFlavor::Standard, None);
1337 let result_std = rule.check(&ctx_std).unwrap();
1338 assert!(
1339 !result_std.is_empty(),
1340 "MD037 should still flag spaces in emphasis under Standard flavor: {result_std:?}"
1341 );
1342 }
1343
1344 #[test]
1345 fn test_spaced_bold_metadata_pattern_detected() {
1346 let rule = MD037NoSpaceInEmphasis;
1347
1348 let content = "# Test\n\n** Explicit Import**: Convert markdownlint configs to rumdl format:";
1350 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1351 let result = rule.check(&ctx).unwrap();
1352 assert_eq!(
1353 result.len(),
1354 1,
1355 "Should flag '** Explicit Import**' as spaced emphasis. Got: {result:?}"
1356 );
1357 assert_eq!(result[0].line, 3);
1358
1359 let content2 = "# Test\n\n**trailing only **: some text";
1361 let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
1362 let result2 = rule.check(&ctx2).unwrap();
1363 assert_eq!(
1364 result2.len(),
1365 1,
1366 "Should flag '**trailing only **' as spaced emphasis. Got: {result2:?}"
1367 );
1368
1369 let content3 = "# Test\n\n** both spaces **: some text";
1371 let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
1372 let result3 = rule.check(&ctx3).unwrap();
1373 assert_eq!(
1374 result3.len(),
1375 1,
1376 "Should flag '** both spaces **' as spaced emphasis. Got: {result3:?}"
1377 );
1378
1379 let content4 = "# Test\n\n**Key**: value";
1381 let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
1382 let result4 = rule.check(&ctx4).unwrap();
1383 assert!(
1384 result4.is_empty(),
1385 "Should not flag valid bold metadata '**Key**: value'. Got: {result4:?}"
1386 );
1387 }
1388}
1389
1390#[cfg(test)]
1391mod mdx_adjacent_prose_tests {
1392 use super::*;
1393 #[test]
1394 fn md037_mdx_code_does_not_hide_adjacent_prose() {
1395 let rule = MD037NoSpaceInEmphasis;
1396 for code in [
1397 "{value}",
1398 "{/* * literal * */}",
1399 "{\"* literal *\"}",
1400 "<Card onClick={() => null} title='* literal *'>text</Card>",
1401 ] {
1402 for ending in ["\n", "\r\n"] {
1403 let source = format!("内容 Before {code} after * visible *{ending}");
1404 let ctx = crate::lint_context::LintContext::new(&source, crate::config::MarkdownFlavor::MDX, None);
1405 assert_eq!(rule.check(&ctx).unwrap().len(), 1, "{source}");
1406 assert_eq!(rule.fix(&ctx).unwrap(), source);
1407 }
1408 }
1409 }
1410 #[test]
1411 fn md037_mdx_code_cannot_supply_the_other_emphasis_marker() {
1412 let rule = MD037NoSpaceInEmphasis;
1413 for source in ["Before * text {\"*\"} after\n", "Before * text {/* * */} after\n"] {
1414 let ctx = crate::lint_context::LintContext::new(source, crate::config::MarkdownFlavor::MDX, None);
1415 assert!(rule.check(&ctx).unwrap().is_empty(), "{source}");
1416 assert_eq!(rule.fix(&ctx).unwrap(), source);
1417 }
1418 }
1419}