use chromiumoxide::Page;
use crate::cli::WcagLevel;
use crate::wcag::types::{RuleMetadata, Severity, Violation};
pub const RULE_VALID_LANG: RuleMetadata = RuleMetadata {
id: "3.1.1",
name: "Valid Language Code",
level: WcagLevel::A,
severity: Severity::Medium,
description: "The lang attribute must contain a valid BCP 47 primary language subtag",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/language-of-page.html",
axe_id: "valid-lang",
tags: &["wcag2a", "wcag311", "cat.language"],
};
pub const RULE_LANG_MISMATCH: RuleMetadata = RuleMetadata {
id: "3.1.1",
name: "Language Attribute Mismatch",
level: WcagLevel::A,
severity: Severity::Medium,
description: "lang and xml:lang attributes must specify the same language",
help_url: "https://www.w3.org/WAI/WCAG21/Understanding/language-of-page.html",
axe_id: "html-xml-lang-mismatch",
tags: &["wcag2a", "wcag311", "cat.language"],
};
const LANG_ATTRS_JS: &str = "({ \
lang: document.documentElement.getAttribute('lang') || '', \
xmlLang: document.documentElement.getAttribute('xml:lang') || '' \
})";
fn primary_subtag(tag: &str) -> &str {
tag.split('-').next().unwrap_or(tag)
}
fn is_valid_primary_subtag(subtag: &str) -> bool {
let len = subtag.len();
(2..=3).contains(&len) && subtag.chars().all(|c| c.is_ascii_alphabetic())
}
pub async fn check_language_extended_with_page(page: &Page) -> Vec<Violation> {
let val = match crate::wcag::types::evaluate_or_fail_for(
page,
"language-extended",
crate::cli::WcagLevel::A,
LANG_ATTRS_JS,
)
.await
{
Ok(v) => v,
Err(violations) => return violations,
};
let lang = val.get("lang").and_then(|v| v.as_str()).unwrap_or("");
let xml_lang = val.get("xmlLang").and_then(|v| v.as_str()).unwrap_or("");
let mut violations = Vec::new();
if !lang.is_empty() && !is_valid_primary_subtag(primary_subtag(lang)) {
violations.push(
Violation::new(
RULE_VALID_LANG.id,
RULE_VALID_LANG.name,
RULE_VALID_LANG.level,
RULE_VALID_LANG.severity,
format!(
"lang=\"{}\" is not a recognised BCP 47 primary language subtag",
lang
),
"html",
)
.with_selector("html")
.with_fix("Use a valid BCP 47 primary subtag such as lang=\"en\" or lang=\"de\"")
.with_rule_id(RULE_VALID_LANG.axe_id)
.with_help_url(RULE_VALID_LANG.help_url),
);
}
if !lang.is_empty() && !xml_lang.is_empty() {
let primary_a = primary_subtag(lang).to_lowercase();
let primary_b = primary_subtag(xml_lang).to_lowercase();
if primary_a != primary_b {
violations.push(
Violation::new(
RULE_LANG_MISMATCH.id,
RULE_LANG_MISMATCH.name,
RULE_LANG_MISMATCH.level,
RULE_LANG_MISMATCH.severity,
format!(
"lang=\"{}\" and xml:lang=\"{}\" specify different primary languages",
lang, xml_lang
),
"html",
)
.with_selector("html")
.with_fix("Ensure lang and xml:lang use the same primary language subtag")
.with_rule_id(RULE_LANG_MISMATCH.axe_id)
.with_help_url(RULE_LANG_MISMATCH.help_url),
);
}
}
violations
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_primary_subtag() {
assert!(is_valid_primary_subtag("en"));
assert!(is_valid_primary_subtag("deu"));
assert!(!is_valid_primary_subtag(""));
assert!(!is_valid_primary_subtag("x"));
assert!(!is_valid_primary_subtag("toolong"));
}
#[test]
fn test_primary_subtag_extraction() {
assert_eq!(primary_subtag("en-US"), "en");
assert_eq!(primary_subtag("zh-Hans"), "zh");
assert_eq!(primary_subtag("de"), "de");
}
}