use super::MD093NoFormattingInHeadings;
use super::md093_config::MD093Config;
use crate::config::MarkdownFlavor;
use crate::lint_context::LintContext;
use crate::rule::{LintWarning, Rule};
fn rule() -> MD093NoFormattingInHeadings {
MD093NoFormattingInHeadings::default()
}
fn with_config(code: bool, strong: bool, emphasis: bool) -> MD093NoFormattingInHeadings {
MD093NoFormattingInHeadings::from_config_struct(MD093Config { code, strong, emphasis })
}
fn check_with(rule: &MD093NoFormattingInHeadings, content: &str) -> Vec<LintWarning> {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
rule.check(&ctx).unwrap()
}
fn check(content: &str) -> Vec<LintWarning> {
check_with(&rule(), content)
}
fn assert_messages(content: &str, expected: &[&str]) {
let warnings = check(content);
let messages: Vec<&str> = warnings.iter().map(|warning| warning.message.as_str()).collect();
assert_eq!(messages, expected, "messages for {content:?}");
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
assert_eq!(
rule().fix(&ctx).unwrap(),
content,
"MD093 reports only, so fix must never rewrite {content:?}"
);
}
fn assert_silent(content: &str) {
assert_messages(content, &[]);
}
#[test]
fn code_span_in_an_atx_heading() {
assert_messages("## Method `map()`\n", &["Inline code in heading: `map()`"]);
}
#[test]
fn strong_in_an_atx_heading() {
assert_messages("### **Practice**\n", &["Strong emphasis in heading: **Practice**"]);
}
#[test]
fn emphasis_in_an_atx_heading() {
assert_messages("## The _.env_ file\n", &["Emphasis in heading: _.env_"]);
}
#[test]
fn a_path_that_became_strong_emphasis_by_itself() {
assert_messages("## __tests__/gt.test.js\n", &["Strong emphasis in heading: __tests__"]);
}
#[test]
fn code_span_in_a_setext_heading() {
assert_messages(
"Heading with `code`\n-------------------\n",
&["Inline code in heading: `code`"],
);
}
#[test]
fn several_constructs_in_one_heading_are_reported_in_source_order() {
assert_messages(
"## `first` and **second** and _third_\n",
&[
"Inline code in heading: `first`",
"Strong emphasis in heading: **second**",
"Emphasis in heading: _third_",
],
);
}
#[test]
fn the_position_points_at_the_span() {
let warnings = check("## Method `map()`\n");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].line, 1);
assert_eq!(warnings[0].column, 11);
assert_eq!(warnings[0].end_column, 18);
}
#[test]
fn a_multibyte_heading_keeps_character_columns() {
let warnings = check("## Метод `map()`\n");
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].column, 10);
}
#[test]
fn markup_inside_a_link_in_a_heading_is_reported() {
assert_messages(
"## [The `map()` docs](https://example.com)\n",
&["Inline code in heading: `map()`"],
);
}
#[test]
fn a_plain_link_in_a_heading_is_silent() {
assert_silent("## [The docs](https://example.com)\n");
}
#[test]
fn a_heading_inside_a_blockquote_is_a_heading() {
assert_messages(
"> ## **Bold** and `code`\n",
&["Strong emphasis in heading: **Bold**", "Inline code in heading: `code`"],
);
}
#[test]
fn formatting_in_body_text_is_not_a_heading() {
assert_silent("Text with `code`, **bold** and _italic_.\n");
}
#[test]
fn a_heading_inside_a_code_block_is_content() {
assert_silent("```markdown\n## Method `map()`\n```\n");
}
#[test]
fn a_line_missing_the_space_after_its_hashes_is_not_a_heading() {
assert_silent("##Method `map()`\n");
}
#[test]
fn a_plain_heading_is_silent() {
assert_silent("## Method map()\n");
}
#[test]
fn each_construct_has_its_own_switch() {
let content = "## `code` and **strong** and _emphasis_\n";
let only_strong = check_with(&with_config(false, true, false), content);
let messages: Vec<&str> = only_strong.iter().map(|w| w.message.as_str()).collect();
assert_eq!(messages, ["Strong emphasis in heading: **strong**"]);
let only_code = check_with(&with_config(true, false, false), content);
let messages: Vec<&str> = only_code.iter().map(|w| w.message.as_str()).collect();
assert_eq!(messages, ["Inline code in heading: `code`"]);
assert!(check_with(&with_config(false, false, false), content).is_empty());
}
#[test]
fn a_long_span_is_shortened_in_the_message() {
let warnings = check("## `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`\n");
assert_eq!(warnings.len(), 1);
assert!(
warnings[0].message.ends_with("..."),
"message was not shortened: {}",
warnings[0].message
);
}
#[test]
fn cached_warning_ranges_match_scalar_positions() {
use super::Construct;
use crate::lint_context::SourcePositions;
for newline in ["\n", "\r\n"] {
let text = format!("{}**日本🙂** `écode`", "é日本🙂 ".repeat(150));
for content in [format!("# {text}{newline}"), format!("{text}{newline}===")] {
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let warnings = rule().check(&ctx).unwrap();
assert_eq!(warnings.len(), 2);
let mut positions = SourcePositions::new(&content, &ctx.lines);
let spans = ctx
.source_code_spans()
.iter()
.map(|s| (s.byte_offset, s.byte_end, Construct::Code))
.chain(
ctx.source_emphasis_spans()
.iter()
.map(|s| (s.byte_offset, s.byte_end, Construct::Strong)),
);
for (start, end, construct) in spans {
let warning = rule().warning(&ctx, &mut positions, (start, end), construct);
let (line, column) = ctx.offset_to_line_col(start);
assert_eq!(
(warning.line, warning.column, warning.end_line, warning.end_column),
(line, column, line, column + content[start..end].chars().count())
);
}
let start = content.find("**").unwrap();
let warning = rule().warning(&ctx, &mut positions, (start, content.len()), Construct::Strong);
let (line, column) = ctx.offset_to_line_col(start);
assert_eq!(
(warning.end_line, warning.end_column),
(line, column + content[start..].chars().count())
);
}
}
}