html-cat 0.1.0

HTML5 parser: tokenizer + tree builder producing a Document tree of Element/Text/Comment nodes. No mut, no Rc/Arc, no interior mutability, no panics, exhaustive matches. First sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! Element attributes.

/// A name/value attribute pair on an element.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Attribute {
    name: String,
    value: String,
}

impl Attribute {
    /// Build an attribute.  Names are stored as-is (HTML attributes are
    /// ASCII-case-insensitive but we preserve the source form).
    #[must_use]
    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            value: value.into(),
        }
    }

    /// The attribute name.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// The attribute value.  Empty for valueless attributes (`<input disabled>`).
    #[must_use]
    pub fn value(&self) -> &str {
        &self.value
    }
}

impl std::fmt::Display for Attribute {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.value.is_empty() {
            write!(f, "{}", self.name)
        } else {
            write!(f, "{}=\"{}\"", self.name, self.value)
        }
    }
}

/// An ordered collection of attributes on an element.  Order is
/// preserved (source order); duplicate names keep the first occurrence
/// per the HTML5 spec.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Attributes {
    items: Vec<Attribute>,
}

impl Attributes {
    /// An empty attribute set.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Build from a vector, dropping duplicate names (later entries).
    #[must_use]
    pub fn from_iter_dedup<I: IntoIterator<Item = Attribute>>(items: I) -> Self {
        let mut seen = std::collections::BTreeSet::new();
        let deduped = items
            .into_iter()
            .filter(|a| seen.insert(a.name().to_owned()))
            .collect();
        Self { items: deduped }
    }

    /// Append `attribute`; no-op if the name already exists (first wins
    /// per HTML5 attribute-parsing rules).
    #[must_use]
    pub fn with(self, attribute: Attribute) -> Self {
        if self.items.iter().any(|a| a.name() == attribute.name()) {
            self
        } else {
            let extended: Vec<Attribute> = self
                .items
                .into_iter()
                .chain(std::iter::once(attribute))
                .collect();
            Self { items: extended }
        }
    }

    /// All attributes in source order.
    pub fn iter(&self) -> std::slice::Iter<'_, Attribute> {
        self.items.iter()
    }

    /// Look up an attribute by name.  HTML attribute names are
    /// case-insensitive; this comparison is ASCII-case-insensitive.
    #[must_use]
    pub fn get(&self, name: &str) -> Option<&Attribute> {
        self.items
            .iter()
            .find(|a| a.name().eq_ignore_ascii_case(name))
    }

    /// Whether the set is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// Number of attributes.
    #[must_use]
    pub fn len(&self) -> usize {
        self.items.len()
    }
}

impl<'a> IntoIterator for &'a Attributes {
    type Item = &'a Attribute;
    type IntoIter = std::slice::Iter<'a, Attribute>;
    fn into_iter(self) -> Self::IntoIter {
        self.items.iter()
    }
}

impl std::fmt::Display for Attributes {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let body = self
            .items
            .iter()
            .map(|a| format!("{a}"))
            .collect::<Vec<_>>()
            .join(" ");
        f.write_str(&body)
    }
}