use super::{ConvertError, ConvertErrorKind};
use crate::html_parser::Tag;
const F_REPEAT_NAME: &str = "f-repeat";
const F_WHEN_NAME: &str = "f-when";
pub(super) fn parse_directive<'a>(
tag: &Tag<'a>,
kind: DirectiveKind,
tag_offset: usize,
) -> Result<&'a str, ConvertError<'a>> {
let mut value: Option<(Option<&'a str>, usize)> = None;
for attr in tag.attrs() {
if attr.name.eq_ignore_ascii_case("value") {
if value.is_none() {
value = Some((attr.value, attr.raw_range.start));
}
continue;
}
let error_kind = if has_f_prefix(attr.name) {
ConvertErrorKind::UnsupportedFAttribute {
attribute: attr.name,
}
} else {
ConvertErrorKind::UnexpectedDirectiveAttribute {
tag: kind.tag_name(),
attribute: attr.name,
}
};
return Err(ConvertError::new(
error_kind,
tag_offset + attr.raw_range.start,
));
}
let Some((value, value_raw_start)) = value else {
return Err(ConvertError::new(
ConvertErrorKind::MissingValueAttribute {
tag: kind.tag_name(),
},
tag_offset,
));
};
let Some(value) = value else {
return Err(ConvertError::new(
ConvertErrorKind::MissingValueAttribute {
tag: kind.tag_name(),
},
tag_offset + value_raw_start,
));
};
let value_offset = tag_offset + value_raw_start;
let trimmed = value.trim();
if !trimmed.starts_with("{{") || !trimmed.ends_with("}}") || trimmed.len() <= 4 {
return Err(ConvertError::new(
ConvertErrorKind::InvalidDirectiveValue {
tag: kind.tag_name(),
value,
},
value_offset,
));
}
let expression = trimmed[2..trimmed.len() - 2].trim();
if expression.is_empty() {
return Err(ConvertError::new(
ConvertErrorKind::InvalidDirectiveValue {
tag: kind.tag_name(),
value,
},
value_offset,
));
}
Ok(expression)
}
pub(super) fn validate_attributes<'a>(
tag: &Tag<'a>,
tag_offset: usize,
) -> Result<(), ConvertError<'a>> {
for attr in tag.attrs() {
if has_f_prefix(attr.name) && !is_supported_f_attribute(attr.name) {
return Err(ConvertError::new(
ConvertErrorKind::UnsupportedFAttribute {
attribute: attr.name,
},
tag_offset + attr.raw_range.start,
));
}
}
Ok(())
}
pub(super) fn is_repeat_expression(expression: &str) -> bool {
let mut parts = expression.split_whitespace();
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(alias), Some("in"), Some(path), None) => is_identifier(alias) && is_path(path),
_ => false,
}
}
#[inline]
fn is_supported_f_attribute(name: &str) -> bool {
name.eq_ignore_ascii_case("f-ref")
|| name.eq_ignore_ascii_case("f-children")
|| name.eq_ignore_ascii_case("f-slotted")
}
#[inline]
pub(super) fn has_f_prefix(name: &str) -> bool {
name.as_bytes()
.get(..2)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"f-"))
}
fn is_path(path: &str) -> bool {
let mut parts = path.split('.');
parts.next().is_some_and(is_identifier) && parts.all(is_identifier)
}
fn is_identifier(identifier: &str) -> bool {
let mut chars = identifier.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first == '$' || first.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DirectiveKind {
Repeat,
When,
}
impl DirectiveKind {
#[inline]
pub(super) fn from_tag_name(name: &str) -> Option<Self> {
if name.eq_ignore_ascii_case(F_REPEAT_NAME) {
Some(Self::Repeat)
} else if name.eq_ignore_ascii_case(F_WHEN_NAME) {
Some(Self::When)
} else {
None
}
}
#[inline]
pub(super) fn tag_name(self) -> &'static str {
match self {
Self::Repeat => F_REPEAT_NAME,
Self::When => F_WHEN_NAME,
}
}
#[inline]
pub(super) fn output_close(self) -> &'static str {
match self {
Self::Repeat => "</for>",
Self::When => "</if>",
}
}
}