use crate::error::Result;
use crate::rule::{AstRule, RuleCategory, RuleMetadata};
use crate::{
Document,
violation::{Severity, Violation},
};
use comrak::nodes::{AstNode, NodeValue};
pub struct MD045;
impl MD045 {
fn is_empty_alt_text<'a>(&self, node: &'a AstNode<'a>) -> bool {
let text_content = Self::extract_text_content(node);
text_content.trim().is_empty()
}
fn extract_text_content<'a>(node: &'a AstNode<'a>) -> String {
let mut content = String::new();
match &node.data.borrow().value {
NodeValue::Text(text) => {
content.push_str(text);
}
NodeValue::Code(code) => {
content.push_str(&code.literal);
}
_ => {}
}
for child in node.children() {
content.push_str(&Self::extract_text_content(child));
}
content
}
fn get_position<'a>(&self, node: &'a AstNode<'a>) -> (usize, usize) {
let data = node.data.borrow();
let pos = data.sourcepos;
(pos.start.line, pos.start.column)
}
fn check_node<'a>(&self, node: &'a AstNode<'a>, violations: &mut Vec<Violation>) {
if let NodeValue::Image(_) = &node.data.borrow().value {
if self.is_empty_alt_text(node) {
let (line, column) = self.get_position(node);
violations.push(self.create_violation(
"Images should have alternate text".to_string(),
line,
column,
Severity::Warning,
));
}
}
for child in node.children() {
self.check_node(child, violations);
}
}
}
impl AstRule for MD045 {
fn id(&self) -> &'static str {
"MD045"
}
fn name(&self) -> &'static str {
"no-alt-text"
}
fn description(&self) -> &'static str {
"Images should have alternate text"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Content).introduced_in("mdbook-lint v0.1.0")
}
fn check_ast<'a>(&self, _document: &Document, ast: &'a AstNode<'a>) -> Result<Vec<Violation>> {
let mut violations = Vec::new();
self.check_node(ast, &mut violations);
Ok(violations)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rule::Rule;
use std::path::PathBuf;
fn create_test_document(content: &str) -> Document {
Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
}
#[test]
fn test_md045_images_with_alt_text_valid() {
let content = r#"Here is an image with alt text: .
Another  here.
And a reference image: ![alt text][ref]
[ref]: image3.gif
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md045_images_without_alt_text_violation() {
let content = r#"Here is an image without alt text: .
Another  here.
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
assert_eq!(violations[0].rule_id, "MD045");
assert!(
violations[0]
.message
.contains("Images should have alternate text")
);
assert_eq!(violations[0].line, 1);
assert_eq!(violations[1].line, 3);
}
#[test]
fn test_md045_images_with_whitespace_only_alt_text() {
let content = r#"Image with spaces: .
Image with tabs: .
Image with mixed whitespace: .
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 3);
assert_eq!(violations[0].line, 1);
assert_eq!(violations[1].line, 3);
assert_eq!(violations[2].line, 5);
}
#[test]
fn test_md045_images_with_code_alt_text_valid() {
let content = r#"Image with code alt text: .
Another with inline code: .
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md045_images_with_emphasis_alt_text_valid() {
let content = r#"Image with emphasis: .
Image with strong: .
Image with mixed: .
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md045_reference_images() {
let content = r#"Good reference image: ![Good alt text][good].
Bad reference image: ![][bad].
Another bad one: ![ ][also-bad].
[good]: image1.png
[bad]: image2.png
[also-bad]: image3.png
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
assert_eq!(violations[0].line, 3);
assert_eq!(violations[1].line, 5);
}
#[test]
fn test_md045_links_ignored() {
let content = r#"This is a [link without text]() which should not be flagged.
This is a [](http://example.com) empty link, also not flagged by this rule.
But this  empty image should be flagged.
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 5);
}
#[test]
fn test_md045_mixed_images_and_links() {
let content = r#"Good image:  and good [link](http://example.com).
Bad image:  and empty [](http://example.com) link.
Another good image:  here.
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 3);
}
#[test]
fn test_md045_nested_formatting_in_alt_text() {
let content = r#"Complex alt text: .
Simple alt text: .
Empty alt text: .
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 5);
}
#[test]
fn test_md045_inline_images() {
let content = r#"Text with inline  image.
Text with inline  empty image.
More text with  alt text.
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].line, 3);
}
#[test]
fn test_md045_multiple_images_per_line() {
let content = r#"Multiple images:  and  and .
All good:  and .
All bad:  and .
"#;
let document = create_test_document(content);
let rule = MD045;
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 3);
assert_eq!(violations[0].line, 1); assert_eq!(violations[1].line, 5); assert_eq!(violations[2].line, 5); }
}