use crate::rule::{LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
use pulldown_cmark::LinkType;
mod md054_config;
use md054_config::MD054Config;
#[derive(Debug, Default, Clone)]
pub struct MD054LinkImageStyle {
config: MD054Config,
}
impl MD054LinkImageStyle {
pub fn new(autolink: bool, collapsed: bool, full: bool, inline: bool, shortcut: bool, url_inline: bool) -> Self {
Self {
config: MD054Config {
autolink,
collapsed,
full,
inline,
shortcut,
url_inline,
},
}
}
pub fn from_config_struct(config: MD054Config) -> Self {
Self { config }
}
fn byte_to_char_col(content: &str, byte_offset: usize) -> usize {
let before = &content[..byte_offset];
let last_newline = before.rfind('\n').map(|i| i + 1).unwrap_or(0);
before[last_newline..].chars().count() + 1
}
fn is_style_allowed(&self, style: &str) -> bool {
match style {
"autolink" => self.config.autolink,
"collapsed" => self.config.collapsed,
"full" => self.config.full,
"inline" => self.config.inline,
"shortcut" => self.config.shortcut,
"url-inline" => self.config.url_inline,
_ => false,
}
}
}
impl Rule for MD054LinkImageStyle {
fn name(&self) -> &'static str {
"MD054"
}
fn description(&self) -> &'static str {
"Link and image style should be consistent"
}
fn category(&self) -> RuleCategory {
RuleCategory::Link
}
fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
let content = ctx.content;
let mut warnings = Vec::new();
for link in &ctx.links {
if matches!(
link.link_type,
LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut
) && link.url.is_empty()
{
continue;
}
let style = match link.link_type {
LinkType::Autolink | LinkType::Email => "autolink",
LinkType::Inline => {
if link.text == link.url {
"url-inline"
} else {
"inline"
}
}
LinkType::Reference => "full",
LinkType::Collapsed => "collapsed",
LinkType::Shortcut => "shortcut",
_ => continue,
};
if ctx
.line_info(link.line)
.is_some_and(|info| info.in_front_matter || info.in_code_block)
{
continue;
}
if !self.is_style_allowed(style) {
let start_col = Self::byte_to_char_col(content, link.byte_offset);
let (end_line, _) = ctx.offset_to_line_col(link.byte_end);
let end_col = Self::byte_to_char_col(content, link.byte_end);
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
line: link.line,
column: start_col,
end_line,
end_column: end_col,
message: format!("Link/image style '{style}' is not allowed"),
severity: Severity::Warning,
fix: None,
});
}
}
for image in &ctx.images {
if matches!(
image.link_type,
LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut
) && image.url.is_empty()
{
continue;
}
let style = match image.link_type {
LinkType::Autolink | LinkType::Email => "autolink",
LinkType::Inline => {
if image.alt_text == image.url {
"url-inline"
} else {
"inline"
}
}
LinkType::Reference => "full",
LinkType::Collapsed => "collapsed",
LinkType::Shortcut => "shortcut",
_ => continue,
};
if ctx
.line_info(image.line)
.is_some_and(|info| info.in_front_matter || info.in_code_block)
{
continue;
}
if !self.is_style_allowed(style) {
let start_col = Self::byte_to_char_col(content, image.byte_offset);
let (end_line, _) = ctx.offset_to_line_col(image.byte_end);
let end_col = Self::byte_to_char_col(content, image.byte_end);
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
line: image.line,
column: start_col,
end_line,
end_column: end_col,
message: format!("Link/image style '{style}' is not allowed"),
severity: Severity::Warning,
fix: None,
});
}
}
Ok(warnings)
}
fn fix(&self, _ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
Err(LintError::FixFailed(
"MD054 does not support automatic fixing of link/image style consistency.".to_string(),
))
}
fn fix_capability(&self) -> crate::rule::FixCapability {
crate::rule::FixCapability::Unfixable
}
fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
ctx.content.is_empty() || (!ctx.likely_has_links_or_images() && !ctx.likely_has_html())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn default_config_section(&self) -> Option<(String, toml::Value)> {
let json_value = serde_json::to_value(&self.config).ok()?;
Some((
self.name().to_string(),
crate::rule_config_serde::json_to_toml_value(&json_value)?,
))
}
fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
where
Self: Sized,
{
let rule_config = crate::rule_config_serde::load_rule_config::<MD054Config>(config);
Box::new(Self::from_config_struct(rule_config))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint_context::LintContext;
#[test]
fn test_all_styles_allowed_by_default() {
let rule = MD054LinkImageStyle::new(true, true, true, true, true, true);
let content = "[inline](url) [ref][] [ref] <https://autolink.com> [full][ref] [url](url)\n\n[ref]: url";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_only_inline_allowed() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[allowed](url) [not][ref] <https://bad.com> [collapsed][] [shortcut]\n\n[ref]: url\n[shortcut]: url\n[collapsed]: url";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 4, "Expected 4 warnings, got: {result:?}");
assert!(result[0].message.contains("'full'"));
assert!(result[1].message.contains("'autolink'"));
assert!(result[2].message.contains("'collapsed'"));
assert!(result[3].message.contains("'shortcut'"));
}
#[test]
fn test_only_autolink_allowed() {
let rule = MD054LinkImageStyle::new(true, false, false, false, false, false);
let content = "<https://good.com> [bad](url) [bad][ref]\n\n[ref]: url";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Expected 2 warnings, got: {result:?}");
assert!(result[0].message.contains("'inline'"));
assert!(result[1].message.contains("'full'"));
}
#[test]
fn test_url_inline_detection() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, true);
let content = "[https://example.com](https://example.com) [text](https://example.com)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_url_inline_not_allowed() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[https://example.com](https://example.com)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].message.contains("'url-inline'"));
}
#[test]
fn test_shortcut_vs_full_detection() {
let rule = MD054LinkImageStyle::new(false, false, true, false, false, false);
let content = "[shortcut] [full][ref]\n\n[shortcut]: url\n[ref]: url2";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
assert!(result[0].message.contains("'shortcut'"));
}
#[test]
fn test_collapsed_reference() {
let rule = MD054LinkImageStyle::new(false, true, false, false, false, false);
let content = "[collapsed][] [bad][ref]\n\n[collapsed]: url\n[ref]: url2";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Expected 1 warning, got: {result:?}");
assert!(result[0].message.contains("'full'"));
}
#[test]
fn test_code_blocks_ignored() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "```\n[ignored](url) <https://ignored.com>\n```\n\n[checked](url)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_code_spans_ignored() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "`[ignored](url)` and `<https://ignored.com>` but [checked](url)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_reference_definitions_ignored() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[ref]: https://example.com\n[ref2]: <https://example2.com>";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_html_comments_ignored() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "<!-- [ignored](url) -->\n <!-- <https://ignored.com> -->";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_unicode_support() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[cafe](https://cafe.com) [emoji](url) [korean](url) [hebrew](url)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_line_positions() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "Line 1\n\nLine 3 with <https://bad.com> here";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 3);
assert_eq!(result[0].column, 13); }
#[test]
fn test_multiple_links_same_line() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[ok](url) but <https://good.com> and [also][bad]\n\n[bad]: url";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Expected 2 warnings, got: {result:?}");
assert!(result[0].message.contains("'autolink'"));
assert!(result[1].message.contains("'full'"));
}
#[test]
fn test_empty_content() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_no_links() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "Just plain text without any links";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_fix_returns_error() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[link](url)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.fix(&ctx);
assert!(result.is_err());
if let Err(LintError::FixFailed(msg)) = result {
assert!(msg.contains("does not support automatic fixing"));
}
}
#[test]
fn test_priority_order() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[text][ref] not detected as [shortcut]\n\n[ref]: url\n[shortcut]: url2";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Expected 2 warnings, got: {result:?}");
assert!(result[0].message.contains("'full'"));
assert!(result[1].message.contains("'shortcut'"));
}
#[test]
fn test_not_shortcut_when_followed_by_bracket() {
let rule = MD054LinkImageStyle::new(false, false, false, true, true, false);
let content = "[text][ more text\n[text](url) is inline";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_cjk_correct_column_positions() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "日本語テスト <https://example.com>";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].message.contains("'autolink'"));
assert_eq!(
result[0].column, 8,
"Column should be 1-indexed character position of '<'"
);
}
#[test]
fn test_code_span_detection_with_cjk_prefix() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "日本語 `[link](url)` text";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0, "Link inside code span should not be flagged");
}
#[test]
fn test_complex_unicode_with_zwj() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "[family](url) [cafe](https://cafe.com)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_gfm_alert_not_flagged_as_shortcut() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "> [!NOTE]\n> This is a note.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"GFM alert should not be flagged as shortcut link, got: {result:?}"
);
}
#[test]
fn test_various_alert_types_not_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
for alert_type in ["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION", "note", "info"] {
let content = format!("> [!{alert_type}]\n> Content.\n");
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Alert type {alert_type} should not be flagged, got: {result:?}"
);
}
}
#[test]
fn test_shortcut_link_still_flagged_when_disallowed() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "See [reference] for details.\n\n[reference]: https://example.com\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "Regular shortcut links should still be flagged");
}
#[test]
fn test_alert_with_frontmatter_not_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "---\ntitle: heading\n---\n\n> [!note]\n> Content for the note.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Alert in blockquote with frontmatter should not be flagged, got: {result:?}"
);
}
#[test]
fn test_alert_without_blockquote_prefix_not_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "[!NOTE]\nSome content\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"[!NOTE] without blockquote prefix should not be flagged, got: {result:?}"
);
}
#[test]
fn test_alert_custom_types_not_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
for alert_type in ["bug", "example", "quote", "abstract", "todo", "faq"] {
let content = format!("> [!{alert_type}]\n> Content.\n");
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Custom alert type {alert_type} should not be flagged, got: {result:?}"
);
}
}
#[test]
fn test_code_span_with_brackets_in_inline_link() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "Link to [`[myArray]`](#info).";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Code span with brackets in inline link should not be flagged, got: {result:?}"
);
}
#[test]
fn test_code_span_with_array_index_in_inline_link() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "See [`item[0]`](#info) for details.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Array index in code span should not be flagged, got: {result:?}"
);
}
#[test]
fn test_code_span_with_hash_brackets_in_inline_link() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = r#"See [`hash["key"]`](#info) for details."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Hash access in code span should not be flagged, got: {result:?}"
);
}
#[test]
fn test_issue_488_full_reproduction() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "---\ntitle: heading\n---\n\nLink to information about [`[myArray]`](#information-on-myarray).\n\n## Information on `[myArray]`\n\nSome section content.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Issue #488 reproduction case should produce no warnings, got: {result:?}"
);
}
#[test]
fn test_bracket_text_without_definition_not_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "Some [noref] text without a definition.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Bracket text without definition should not be flagged as a link, got: {result:?}"
);
}
#[test]
fn test_array_index_notation_not_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "Access `arr[0]` and use [1] or [optional] in your code.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Array indices and bracket text should not be flagged, got: {result:?}"
);
}
#[test]
fn test_real_shortcut_reference_still_flagged() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "See [example] for details.\n\n[example]: https://example.com\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Real shortcut reference with definition should be flagged, got: {result:?}"
);
assert!(result[0].message.contains("'shortcut'"));
}
#[test]
fn test_footnote_syntax_not_flagged_as_shortcut() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = "See [^1] for details.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Footnote syntax should not be flagged as shortcut, got: {result:?}"
);
}
#[test]
fn test_inline_link_with_code_span_detected_as_inline() {
let rule = MD054LinkImageStyle::new(true, true, true, false, true, true);
let content = "See [`[myArray]`](#info) for details.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Inline link with code span should be flagged when inline is disallowed"
);
assert!(
result[0].message.contains("'inline'"),
"Should be flagged as 'inline' style, got: {}",
result[0].message
);
}
#[test]
fn test_autolink_only_document_not_skipped() {
let rule = MD054LinkImageStyle::new(false, false, false, true, false, false);
let content = "Visit <https://example.com> for more info.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
assert!(
!rule.should_skip(&ctx),
"should_skip must return false for autolink-only documents"
);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Autolink should be flagged when disallowed");
assert!(result[0].message.contains("'autolink'"));
}
#[test]
fn test_nested_image_in_link() {
let rule = MD054LinkImageStyle::new(false, false, false, false, false, false);
let content = "[](https://example.com)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.len() >= 2,
"Nested image-in-link should detect both elements, got: {result:?}"
);
}
#[test]
fn test_multi_line_link() {
let rule = MD054LinkImageStyle::new(false, false, false, false, false, false);
let content = "[long link\ntext](url)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Multi-line inline link should be detected");
assert!(result[0].message.contains("'inline'"));
}
#[test]
fn test_link_with_title() {
let rule = MD054LinkImageStyle::new(false, false, false, false, false, false);
let content = r#"[text](url "title")"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Link with title should be detected as inline");
assert!(result[0].message.contains("'inline'"));
}
#[test]
fn test_empty_link_text() {
let rule = MD054LinkImageStyle::new(false, false, false, false, false, false);
let content = "[](url)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Empty link text should be detected");
assert!(result[0].message.contains("'inline'"));
}
#[test]
fn test_escaped_brackets_not_detected() {
let rule = MD054LinkImageStyle::new(true, true, true, true, false, true);
let content = r"\[not a link\] and also \[not this either\]";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Escaped brackets should not be flagged, got: {result:?}"
);
}
#[test]
fn test_links_in_blockquotes() {
let rule = MD054LinkImageStyle::new(false, false, false, false, false, false);
let content = "> [link](url) in a blockquote";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Links in blockquotes should be detected");
assert!(result[0].message.contains("'inline'"));
}
#[test]
fn test_image_detection() {
let rule = MD054LinkImageStyle::new(false, false, false, false, false, false);
let content = "";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Inline image should be detected");
assert!(result[0].message.contains("'inline'"));
}
}