use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::*;
use std::panic;
const TEST_SCRIPTS: &[(&str, &str)] = &[
("bengali", "কুবারনেটিস কমিউনিটির মধ্যে ঘটে যাওয়া ঘটনাগুলির জন্য"),
("arabic", "مرحبا بكم في هذا النص العربي الطويل"),
("chinese", "这是一段很长的中文文本用于测试"),
("japanese", "日本語テキストのサンプルです"),
("korean", "한글 텍스트 샘플입니다"),
("thai", "นี่คือข้อความภาษาไทยสำหรับการทดสอบ"),
("hindi", "यह हिंदी में एक लंबा पाठ है"),
("russian", "Это длинный русский текст для тестирования"),
("greek", "Αυτό είναι ένα μεγάλο ελληνικό κείμενο"),
("emoji", "🎉🚀💻🔥✨🎊🎁🎈🎂🌟"),
("emoji_zwj", "👨👩👧👦 👩💻 🏳️🌈"),
];
fn generate_test_content(_script_name: &str, script_text: &str) -> Vec<(&'static str, String)> {
vec more text"),
),
("emphasis_multibyte", format!("*{script_text}* and **{script_text}**")),
("blockquote_multibyte", format!("> {script_text}\n> More quoted text")),
(
"table_multibyte",
format!("| Header 1 | {script_text} |\n| --- | --- |\n| {script_text} | Data |"),
),
(
"mixed_stress",
format!(
"# {script_text}\n\n{script_text} user@example.com\n\n- {script_text}\n- https://test.com\n\n> {script_text}\n\n`{script_text}`"
),
),
]
}
fn get_all_rules() -> Vec<Box<dyn Rule>> {
vec![
Box::new(MD001HeadingIncrement::default()),
Box::new(MD003HeadingStyle::default()),
Box::new(MD004UnorderedListStyle::default()),
Box::new(MD005ListIndent::default()),
Box::new(MD007ULIndent::default()),
Box::new(MD009TrailingSpaces::default()),
Box::new(MD010NoHardTabs::default()),
Box::new(MD011NoReversedLinks),
Box::new(MD012NoMultipleBlanks::default()),
Box::new(MD013LineLength::default()),
Box::new(MD014CommandsShowOutput::default()),
Box::new(MD018NoMissingSpaceAtx::new()),
Box::new(MD019NoMultipleSpaceAtx),
Box::new(MD020NoMissingSpaceClosedAtx),
Box::new(MD021NoMultipleSpaceClosedAtx),
Box::new(MD022BlanksAroundHeadings::new()),
Box::new(MD023HeadingStartLeft),
Box::new(MD024NoDuplicateHeading::default()),
Box::new(MD025SingleTitle::default()),
Box::new(MD026NoTrailingPunctuation::default()),
Box::new(MD027MultipleSpacesBlockquote::default()),
Box::new(MD028NoBlanksBlockquote),
Box::new(MD029OrderedListPrefix::default()),
Box::new(MD030ListMarkerSpace::default()),
Box::new(MD031BlanksAroundFences::default()),
Box::new(MD032BlanksAroundLists::default()),
Box::new(MD033NoInlineHtml::default()),
Box::new(MD034NoBareUrls), Box::new(MD035HRStyle::default()),
Box::new(MD036NoEmphasisAsHeading::default()),
Box::new(MD037NoSpaceInEmphasis),
Box::new(MD038NoSpaceInCode::default()),
Box::new(MD039NoSpaceInLinks),
Box::new(MD040FencedCodeLanguage::default()),
Box::new(MD041FirstLineHeading::default()),
Box::new(MD042NoEmptyLinks::default()),
Box::new(MD045NoAltText::default()),
Box::new(MD047SingleTrailingNewline),
Box::new(MD049EmphasisStyle::default()),
Box::new(MD050StrongStyle::default()),
Box::new(MD051LinkFragments::new()),
Box::new(MD052ReferenceLinkImages::default()),
Box::new(MD053LinkImageReferenceDefinitions::default()),
Box::new(MD054LinkImageStyle::default()),
Box::new(MD055TablePipeStyle::default()),
Box::new(MD056TableColumnCount),
Box::new(MD057ExistingRelativeLinks::default()),
Box::new(MD058BlanksAroundTables::default()),
]
}
#[test]
fn test_all_rules_no_panic_with_multibyte_utf8() {
let rules = get_all_rules();
let mut failures = Vec::new();
for (script_name, script_text) in TEST_SCRIPTS {
for (pattern_name, content) in generate_test_content(script_name, script_text) {
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
for rule in &rules {
let rule_name = rule.name();
let check_result = panic::catch_unwind(panic::AssertUnwindSafe(|| rule.check(&ctx)));
if check_result.is_err() {
failures.push(format!(
"PANIC in {rule_name}.check() with {script_name}/{pattern_name}"
));
continue;
}
let fix_result = panic::catch_unwind(panic::AssertUnwindSafe(|| rule.fix(&ctx)));
if fix_result.is_err() {
failures.push(format!("PANIC in {rule_name}.fix() with {script_name}/{pattern_name}"));
}
}
}
}
if !failures.is_empty() {
panic!(
"UTF-8 panics detected in {} cases:\n{}",
failures.len(),
failures.join("\n")
);
}
}
#[test]
fn test_md034_email_with_all_scripts() {
let rule = MD034NoBareUrls;
for (script_name, script_text) in TEST_SCRIPTS {
let content = format!("{script_text} user@example.com");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| rule.check(&ctx)));
assert!(
result.is_ok(),
"MD034.check() panicked with {script_name} script before email"
);
let warnings = result.unwrap().expect("check should succeed");
assert!(
!warnings.is_empty(),
"MD034 should detect email after {script_name} text"
);
}
}
#[test]
fn test_fix_ranges_are_valid_char_boundaries() {
let rules = get_all_rules();
let mut failures = Vec::new();
for (script_name, script_text) in TEST_SCRIPTS {
for (pattern_name, content) in generate_test_content(script_name, script_text) {
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
for rule in &rules {
let rule_name = rule.name();
if let Ok(warnings) = rule.check(&ctx) {
for (i, warning) in warnings.iter().enumerate() {
if let Some(fix) = &warning.fix {
if !content.is_char_boundary(fix.range.start) {
failures.push(format!(
"{rule_name} warning {i}: fix.range.start {} is not a char boundary ({script_name}/{pattern_name})",
fix.range.start
));
}
if !content.is_char_boundary(fix.range.end) {
failures.push(format!(
"{rule_name} warning {i}: fix.range.end {} is not a char boundary ({script_name}/{pattern_name})",
fix.range.end
));
}
}
}
}
}
}
}
if !failures.is_empty() {
panic!(
"Invalid fix byte ranges detected in {} cases:\n{}",
failures.len(),
failures.join("\n")
);
}
}
#[test]
fn test_md034_email_at_various_byte_offsets() {
let rule = MD034NoBareUrls;
let test_cases = [
"user@example.com",
"a user@example.com",
"ab user@example.com",
"abc user@example.com",
"abcd user@example.com",
"abcde user@example.com",
"abcdef user@example.com",
"日 user@example.com",
"日本 user@example.com",
"xmpp:user@example.com",
];
for content in test_cases {
let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| rule.check(&ctx)));
assert!(result.is_ok(), "MD034.check() panicked with content: {content}");
}
}