winged_rust/core/
attribute.rs1use crate::core::escape::write_escaped_attribute;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Attribute {
20 key: String,
21 value: String,
22 is_boolean: bool,
23}
24
25impl Attribute {
26 pub fn new(key: impl Into<String>, value: impl AsRef<str>) -> Self {
28 let raw = value.as_ref();
29 let mut escaped = String::with_capacity(raw.len());
30 write_escaped_attribute(&mut escaped, raw);
31 Self {
32 key: key.into(),
33 value: escaped,
34 is_boolean: false,
35 }
36 }
37
38 pub fn raw(key: impl Into<String>, value: impl Into<String>) -> Self {
44 Self {
45 key: key.into(),
46 value: value.into(),
47 is_boolean: false,
48 }
49 }
50
51 pub fn boolean(key: impl Into<String>) -> Self {
55 Self {
56 key: key.into(),
57 value: String::new(),
58 is_boolean: true,
59 }
60 }
61
62 #[must_use]
64 pub fn key(&self) -> &str {
65 &self.key
66 }
67
68 #[must_use]
70 pub fn value(&self) -> &str {
71 &self.value
72 }
73
74 #[must_use]
76 pub fn is_boolean(&self) -> bool {
77 self.is_boolean
78 }
79
80 pub(crate) fn write_into(&self, out: &mut String) {
82 out.push(' ');
83 out.push_str(&self.key);
84 if !self.is_boolean {
85 out.push_str("=\"");
86 out.push_str(&self.value);
87 out.push('"');
88 }
89 }
90}
91
92impl std::fmt::Display for Attribute {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 let mut out = String::new();
95 self.write_into(&mut out);
96 f.write_str(&out)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn value_is_escaped_at_construction() {
106 let attr = Attribute::new("title", r#"a "quoted" & <tagged> value"#);
107 assert_eq!(attr.value(), "a "quoted" & <tagged> value");
108 }
109
110 #[test]
112 fn boolean_attributes_render_as_a_bare_key() {
113 assert_eq!(Attribute::boolean("required").to_string(), " required");
114 assert!(Attribute::boolean("open").is_boolean());
115 }
116
117 #[test]
118 fn raw_attributes_are_not_escaped() {
119 assert_eq!(Attribute::raw("property", "og:title").value(), "og:title");
120 }
121
122 #[test]
123 fn non_boolean_attributes_render_with_a_quoted_value() {
124 assert_eq!(
125 Attribute::new("class", "card p-4").to_string(),
126 r#" class="card p-4""#
127 );
128 }
129
130 #[test]
131 fn a_quote_in_the_value_cannot_break_out_of_the_attribute() {
132 let attr = Attribute::new("class", r#"a" onload="alert(1)"#);
133 assert!(!attr.value().contains('"'));
134 }
135}