use crate::parser::{LangArg, PseudoClass};
use super::error::Error;
use super::xpath_expr::{XPathExpr, xpath_literal};
use super::{Kind, Translator};
const LANG_ATTRIBUTE: &str = "lang";
const TYPE_LC: &str = "translate(@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', \
'abcdefghijklmnopqrstuvwxyz')";
const FIELDSET_DISABLED: &str = "count(ancestor::fieldset[@disabled]) > \
count(ancestor::legend[not(preceding-sibling::legend)]\
[parent::fieldset[@disabled]])";
fn required_applies() -> String {
format!(
"((name(.) = 'input' and not(\
{TYPE_LC} = 'hidden' or \
{TYPE_LC} = 'range' or \
{TYPE_LC} = 'color' or \
{TYPE_LC} = 'submit' or \
{TYPE_LC} = 'image' or \
{TYPE_LC} = 'reset' or \
{TYPE_LC} = 'button')) or \
name(.) = 'select' or \
name(.) = 'textarea')"
)
}
impl Translator {
pub(crate) fn apply_pseudo_class(
&self,
xpath: &mut XPathExpr,
pc: &PseudoClass,
) -> Result<(), Error> {
match (self.kind, pc) {
(_, PseudoClass::Dir(_)) => {
xpath.add_condition("0");
}
(Kind::Generic, PseudoClass::Lang(args)) => {
self.lang_generic(xpath, args)?;
}
(Kind::Html, PseudoClass::Lang(args)) => {
self.lang_html(xpath, args)?;
}
(Kind::Html, PseudoClass::Checked) => {
xpath.add_or_condition(&format!(
"(@selected and name(.) = 'option') or \
(@checked \
and (name(.) = 'input' or name(.) = 'command')\
and ({TYPE_LC} = 'checkbox' or {TYPE_LC} = 'radio'))"
));
}
(Kind::Html, PseudoClass::Link) | (Kind::Html, PseudoClass::AnyLink) => {
xpath.add_condition(
"@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')",
);
}
(Kind::Html, PseudoClass::Required) => {
xpath.add_condition(&format!("@required and {}", required_applies()));
}
(Kind::Html, PseudoClass::Optional) => {
xpath.add_condition(&format!("not(@required) and {}", required_applies()));
}
(Kind::Html, PseudoClass::Disabled) => {
xpath.add_or_condition(&format!(
"( @disabled and ( \
(name(.) = 'input' and not({TYPE_LC} = 'hidden')) or \
name(.) = 'button' or \
name(.) = 'select' or \
name(.) = 'textarea' or \
name(.) = 'command' or \
name(.) = 'fieldset' or \
name(.) = 'optgroup' or \
name(.) = 'option' \
) ) or ( ( \
(name(.) = 'input' and not({TYPE_LC} = 'hidden')) or \
name(.) = 'button' or \
name(.) = 'select' or \
name(.) = 'textarea' \
) \
and {FIELDSET_DISABLED} \
)"
));
}
(Kind::Html, PseudoClass::Enabled) => {
xpath.add_or_condition(&format!(
"(@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')) \
or \
((name(.) = 'command' or name(.) = 'fieldset' or name(.) = 'optgroup') \
and not(@disabled)) \
or \
(((name(.) = 'input' and not({TYPE_LC} = 'hidden')) \
or name(.) = 'button' \
or name(.) = 'select' \
or name(.) = 'textarea' \
or name(.) = 'keygen') \
and not (@disabled or {FIELDSET_DISABLED})) \
or (name(.) = 'option' and not(@disabled or \
ancestor::optgroup[@disabled]))"
));
}
_ => {
xpath.add_condition("0");
}
}
Ok(())
}
fn lang_generic(&self, xpath: &mut XPathExpr, args: &[LangArg]) -> Result<(), Error> {
let mut conditions: Vec<String> = Vec::new();
for value in lang_values(args)? {
if value == "*" {
conditions.push("true()".to_owned());
} else if let Some(prefix) = value.strip_suffix('*') {
let prefix = prefix.trim_end_matches('-');
conditions.push(format!("lang({})", xpath_literal(prefix)));
} else {
conditions.push(format!("lang({})", xpath_literal(&value)));
}
}
add_lang_conditions(xpath, conditions);
Ok(())
}
fn lang_html(&self, xpath: &mut XPathExpr, args: &[LangArg]) -> Result<(), Error> {
let mut conditions: Vec<String> = Vec::new();
for value in lang_values(args)? {
if value == "*" {
conditions.push(format!("ancestor-or-self::*[@{LANG_ATTRIBUTE}]"));
} else if let Some(prefix) = value.strip_suffix('*') {
let search_prefix = if prefix.ends_with('-') {
prefix.to_lowercase()
} else {
format!("{}-", prefix.to_lowercase())
};
conditions.push(lang_ancestor_condition(&search_prefix));
} else {
conditions.push(lang_ancestor_condition(&format!(
"{}-",
value.to_lowercase()
)));
}
}
add_lang_conditions(xpath, conditions);
Ok(())
}
}
fn lang_values(args: &[LangArg]) -> Result<Vec<String>, Error> {
let mut values: Vec<String> = Vec::new();
let mut current: Option<String> = None;
for arg in args {
match arg {
LangArg::Value(v) if v.starts_with('-') && current.is_some() => {
current.as_mut().unwrap().push_str(v);
}
LangArg::Value(v) => {
if let Some(done) = current.replace(v.clone()) {
values.push(done);
}
}
LangArg::Star if current.as_deref().is_some_and(|c| c.ends_with('-')) => {
current.as_mut().unwrap().push('*');
}
LangArg::Star => {
if let Some(done) = current.replace("*".to_owned()) {
values.push(done);
}
}
}
}
values.extend(current);
for range in &values {
if let Some(pos) = range.find('*')
&& pos != range.len() - 1
{
return Err(Error::Unsupported(format!(
"the :lang() language range {range:?} \
(a wildcard outside the final subtag)"
)));
}
}
Ok(values)
}
fn add_lang_conditions(xpath: &mut XPathExpr, conditions: Vec<String>) {
match conditions.len() {
0 => {}
1 => xpath.add_condition(&conditions[0]),
_ => xpath.add_or_condition(&conditions.join(" or ")),
}
}
fn lang_ancestor_condition(search_prefix: &str) -> String {
format!(
"ancestor-or-self::*[@{LANG_ATTRIBUTE}][1][starts-with(concat(\
translate(@{LANG_ATTRIBUTE}, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', \
'abcdefghijklmnopqrstuvwxyz'), '-'), {})]",
xpath_literal(search_prefix)
)
}