use crate::core::escape::write_escaped_attribute;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attribute {
key: String,
value: String,
is_boolean: bool,
}
impl Attribute {
pub fn new(key: impl Into<String>, value: impl AsRef<str>) -> Self {
let raw = value.as_ref();
let mut escaped = String::with_capacity(raw.len());
write_escaped_attribute(&mut escaped, raw);
Self {
key: key.into(),
value: escaped,
is_boolean: false,
}
}
pub fn raw(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
is_boolean: false,
}
}
pub fn boolean(key: impl Into<String>) -> Self {
Self {
key: key.into(),
value: String::new(),
is_boolean: true,
}
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub fn is_boolean(&self) -> bool {
self.is_boolean
}
pub(crate) fn write_into(&self, out: &mut String) {
out.push(' ');
out.push_str(&self.key);
if !self.is_boolean {
out.push_str("=\"");
out.push_str(&self.value);
out.push('"');
}
}
}
impl std::fmt::Display for Attribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut out = String::new();
self.write_into(&mut out);
f.write_str(&out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_is_escaped_at_construction() {
let attr = Attribute::new("title", r#"a "quoted" & <tagged> value"#);
assert_eq!(attr.value(), "a "quoted" & <tagged> value");
}
#[test]
fn boolean_attributes_render_as_a_bare_key() {
assert_eq!(Attribute::boolean("required").to_string(), " required");
assert!(Attribute::boolean("open").is_boolean());
}
#[test]
fn raw_attributes_are_not_escaped() {
assert_eq!(Attribute::raw("property", "og:title").value(), "og:title");
}
#[test]
fn non_boolean_attributes_render_with_a_quoted_value() {
assert_eq!(
Attribute::new("class", "card p-4").to_string(),
r#" class="card p-4""#
);
}
#[test]
fn a_quote_in_the_value_cannot_break_out_of_the_attribute() {
let attr = Attribute::new("class", r#"a" onload="alert(1)"#);
assert!(!attr.value().contains('"'));
}
}