use indexmap::IndexMap;
pub type AttrMap = IndexMap<String, String>;
#[derive(Debug, Clone)]
pub struct Document {
pub doctype: Option<String>,
pub children: Vec<Node>,
}
#[derive(Debug, Clone)]
pub enum Node {
Element(Element),
Text(String),
Comment(String),
}
#[derive(Debug, Clone)]
pub struct Element {
pub name: String,
pub attrs: AttrMap,
pub children: Vec<Node>,
pub void: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlotKind {
Named(String),
Default,
FragmentMount(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EachDirective(String);
impl EachDirective {
#[must_use]
pub fn new(expr: impl Into<String>) -> Option<Self> {
let expr = expr.into();
if expr.trim().is_empty() {
None
} else {
Some(Self(expr))
}
}
#[must_use]
pub fn expr(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaticaLinkRel {
Data,
Fragment,
Font,
}
impl Document {
#[must_use]
pub fn new() -> Self {
Self {
doctype: None,
children: Vec::new(),
}
}
pub fn walk_mut(&mut self, f: &mut impl FnMut(&mut Node)) {
for child in &mut self.children {
walk_node_mut(child, f);
}
}
pub fn find(&self, mut pred: impl FnMut(&Element) -> bool) -> Vec<&Element> {
let mut out = Vec::new();
for child in &self.children {
find_in_node(child, &mut pred, &mut out);
}
out
}
}
impl Default for Document {
fn default() -> Self {
Self::new()
}
}
impl Element {
#[must_use]
pub fn attr(&self, name: &str) -> Option<&str> {
self.attrs.get(name).map(String::as_str)
}
#[must_use]
pub fn has_attr_value(&self, name: &str, value: &str) -> bool {
self.attr(name).is_some_and(|v| v == value)
}
#[must_use]
pub fn is_slot(&self) -> bool {
self.name.eq_ignore_ascii_case("slot")
}
#[must_use]
pub fn slot_kind(&self) -> Option<SlotKind> {
if !self.is_slot() {
return None;
}
match (self.attr("name"), self.attr("id")) {
(Some(name), None) => Some(SlotKind::Named(name.to_string())),
(None, None) => Some(SlotKind::Default),
(None, Some(id)) => Some(SlotKind::FragmentMount(id.to_string())),
(Some(_), Some(_)) => None,
}
}
#[must_use]
pub fn each_directive(&self) -> Option<EachDirective> {
self.attr("data-each")
.and_then(|expr| EachDirective::new(expr.to_string()))
}
#[must_use]
pub fn bind_directive(&self) -> Option<&str> {
self.attr("data-bind")
.map(str::trim)
.filter(|expr| !expr.is_empty())
}
#[must_use]
pub fn text_directive(&self) -> Option<&str> {
self.attr("data-t")
}
#[must_use]
pub fn translated_attr_target(name: &str) -> Option<&str> {
name.strip_prefix("data-t-")
}
#[must_use]
pub fn is_translation_attr(name: &str) -> bool {
name == "data-t" || Self::translated_attr_target(name).is_some()
}
#[must_use]
pub fn statica_link_rel(&self) -> Option<StaticaLinkRel> {
if !self.is_link() {
return None;
}
let rel = self.attr("rel")?;
if rel.split_whitespace().any(|part| part == "statica/data") {
return Some(StaticaLinkRel::Data);
}
if rel
.split_whitespace()
.any(|part| part == "statica/fragment")
{
return Some(StaticaLinkRel::Fragment);
}
if rel.split_whitespace().any(|part| part == "statica/font") {
return Some(StaticaLinkRel::Font);
}
None
}
#[must_use]
pub fn is_template(&self) -> bool {
self.name.eq_ignore_ascii_case("template")
}
#[must_use]
pub fn is_script(&self) -> bool {
self.name.eq_ignore_ascii_case("script")
}
#[must_use]
pub fn is_style(&self) -> bool {
self.name.eq_ignore_ascii_case("style")
}
#[must_use]
pub fn is_link(&self) -> bool {
self.name.eq_ignore_ascii_case("link")
}
}
fn walk_node_mut(node: &mut Node, f: &mut impl FnMut(&mut Node)) {
f(node);
if let Node::Element(el) = node {
for child in &mut el.children {
walk_node_mut(child, f);
}
}
}
fn find_in_node<'a>(
node: &'a Node,
pred: &mut impl FnMut(&Element) -> bool,
out: &mut Vec<&'a Element>,
) {
if let Node::Element(el) = node {
if pred(el) {
out.push(el);
}
for child in &el.children {
find_in_node(child, pred, out);
}
}
}
#[must_use]
pub fn is_void_element(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}