Skip to main content

azul_css/props/style/
content.rs

1//! CSS properties for generated content (`content`, `counter-reset`,
2//! `counter-increment`, `string-set`).
3//!
4//! Defines [`Content`], [`CounterReset`], [`CounterIncrement`], and
5//! [`StringSet`], which are registered as [`CssProperty`] variants.
6
7use alloc::string::{String, ToString};
8
9use crate::{corety::AzString, props::formatter::PrintAsCssValue};
10
11/// CSS `content` property value, stored as a raw string.
12///
13/// Intentionally simplified: stores the unparsed CSS value rather than
14/// a structured `ContentPart` enum. Complex values like `counter(section) ". "`
15/// are preserved verbatim but not individually evaluated.
16///
17/// **Note:** Currently parsed and stored but not yet consumed by the layout
18/// engine (e.g., for `::before`/`::after` pseudo-element generated content).
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21pub struct Content {
22    pub inner: AzString,
23}
24
25impl Default for Content {
26    fn default() -> Self {
27        Self {
28            inner: "normal".into(),
29        }
30    }
31}
32
33impl PrintAsCssValue for Content {
34    fn print_as_css_value(&self) -> String {
35        self.inner.as_str().to_string()
36    }
37}
38
39/// CSS `counter-reset` property: resets a named counter to a given value.
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[repr(C)]
42pub struct CounterReset {
43    pub counter_name: AzString,
44    pub value: i32,
45}
46
47impl CounterReset {
48    #[must_use] pub const fn new(counter_name: AzString, value: i32) -> Self {
49        Self {
50            counter_name,
51            value,
52        }
53    }
54
55    #[must_use] pub const fn none() -> Self {
56        Self {
57            counter_name: AzString::from_const_str("none"),
58            value: 0,
59        }
60    }
61
62    #[must_use] pub const fn list_item() -> Self {
63        Self {
64            counter_name: AzString::from_const_str("list-item"),
65            value: 0,
66        }
67    }
68}
69
70impl Default for CounterReset {
71    fn default() -> Self {
72        Self::none()
73    }
74}
75
76impl PrintAsCssValue for CounterReset {
77    fn print_as_css_value(&self) -> String {
78        if self.counter_name.as_str() == "none" {
79            "none".to_string()
80        } else {
81            alloc::format!("{} {}", self.counter_name.as_str(), self.value)
82        }
83    }
84}
85
86/// CSS `counter-increment` property: increments a named counter by a given value.
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
88#[repr(C)]
89pub struct CounterIncrement {
90    pub counter_name: AzString,
91    pub value: i32,
92}
93
94impl CounterIncrement {
95    #[must_use] pub const fn new(counter_name: AzString, value: i32) -> Self {
96        Self {
97            counter_name,
98            value,
99        }
100    }
101
102    #[must_use] pub const fn none() -> Self {
103        Self {
104            counter_name: AzString::from_const_str("none"),
105            value: 0,
106        }
107    }
108
109    #[must_use] pub const fn list_item() -> Self {
110        Self {
111            counter_name: AzString::from_const_str("list-item"),
112            value: 1,
113        }
114    }
115}
116
117impl Default for CounterIncrement {
118    fn default() -> Self {
119        Self::none()
120    }
121}
122
123impl PrintAsCssValue for CounterIncrement {
124    fn print_as_css_value(&self) -> String {
125        if self.counter_name.as_str() == "none" {
126            "none".to_string()
127        } else {
128            alloc::format!("{} {}", self.counter_name.as_str(), self.value)
129        }
130    }
131}
132
133/// CSS `string-set` property value, stored as a raw string.
134///
135/// **Note:** Currently parsed and stored but not yet consumed by the layout engine.
136#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
137#[repr(C)]
138pub struct StringSet {
139    pub inner: AzString,
140}
141
142impl Default for StringSet {
143    fn default() -> Self {
144        Self {
145            inner: "none".into(),
146        }
147    }
148}
149
150impl PrintAsCssValue for StringSet {
151    fn print_as_css_value(&self) -> String {
152        self.inner.as_str().to_string()
153    }
154}
155
156// Formatting to Rust code
157impl crate::codegen::format::FormatAsRustCode for Content {
158    fn format_as_rust_code(&self, _tabs: usize) -> String {
159        format!("Content {{ inner: String::from({:?}) }}", self.inner)
160    }
161}
162
163impl crate::codegen::format::FormatAsRustCode for CounterReset {
164    fn format_as_rust_code(&self, _tabs: usize) -> String {
165        alloc::format!(
166            "CounterReset {{ counter_name: AzString::from_const_str({:?}), value: {} }}",
167            self.counter_name.as_str(),
168            self.value
169        )
170    }
171}
172
173impl crate::codegen::format::FormatAsRustCode for CounterIncrement {
174    fn format_as_rust_code(&self, _tabs: usize) -> String {
175        alloc::format!(
176            "CounterIncrement {{ counter_name: AzString::from_const_str({:?}), value: {} }}",
177            self.counter_name.as_str(),
178            self.value
179        )
180    }
181}
182
183impl crate::codegen::format::FormatAsRustCode for StringSet {
184    fn format_as_rust_code(&self, _tabs: usize) -> String {
185        format!("StringSet {{ inner: String::from({:?}) }}", self.inner)
186    }
187}
188
189// --- PARSERS ---
190
191#[cfg(feature = "parser")]
192pub mod parser {
193    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
194    use super::*;
195
196    // Simplified parsers that just take the raw string value.
197    /// # Errors
198    ///
199    /// Returns an error if `input` is not a valid CSS `content` value.
200    pub fn parse_content(input: &str) -> Result<Content, ()> {
201        Ok(Content {
202            inner: input.trim().into(),
203        })
204    }
205
206    fn parse_counter_name_value(input: &str, default_value: i32) -> Result<(AzString, i32), ()> {
207        let trimmed = input.trim();
208
209        if trimmed == "none" {
210            return Ok((AzString::from_const_str("none"), 0));
211        }
212
213        let parts: Vec<&str> = trimmed.split_whitespace().collect();
214
215        if parts.is_empty() {
216            return Err(());
217        }
218
219        let counter_name = parts[0].into();
220        let value = if parts.len() > 1 {
221            parts[1].parse::<i32>().map_err(|_| ())?
222        } else {
223            default_value
224        };
225
226        Ok((counter_name, value))
227    }
228
229    /// # Errors
230    ///
231    /// Returns an error if `input` is not a valid CSS `counter-reset` value.
232    pub fn parse_counter_reset(input: &str) -> Result<CounterReset, ()> {
233        let (counter_name, value) = parse_counter_name_value(input, 0)?;
234        Ok(CounterReset::new(counter_name, value))
235    }
236
237    /// # Errors
238    ///
239    /// Returns an error if `input` is not a valid CSS `counter-increment` value.
240    pub fn parse_counter_increment(input: &str) -> Result<CounterIncrement, ()> {
241        let (counter_name, value) = parse_counter_name_value(input, 1)?;
242        Ok(CounterIncrement::new(counter_name, value))
243    }
244
245    /// # Errors
246    ///
247    /// Returns an error if `input` is not a valid CSS `string-set` value.
248    pub fn parse_string_set(input: &str) -> Result<StringSet, ()> {
249        Ok(StringSet {
250            inner: input.trim().into(),
251        })
252    }
253}
254
255#[cfg(feature = "parser")]
256pub use parser::*;
257
258#[cfg(all(test, feature = "parser"))]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_simple_content_parser() {
264        assert_eq!(parse_content("'Hello'").unwrap().inner.as_str(), "'Hello'");
265
266        // Test counter-reset parsing
267        let reset = parse_counter_reset("page 1").unwrap();
268        assert_eq!(reset.counter_name.as_str(), "page");
269        assert_eq!(reset.value, 1);
270
271        let reset = parse_counter_reset("list-item 0").unwrap();
272        assert_eq!(reset.counter_name.as_str(), "list-item");
273        assert_eq!(reset.value, 0);
274
275        let reset = parse_counter_reset("none").unwrap();
276        assert_eq!(reset.counter_name.as_str(), "none");
277
278        // Test counter-increment parsing
279        let inc = parse_counter_increment("section").unwrap();
280        assert_eq!(inc.counter_name.as_str(), "section");
281        assert_eq!(inc.value, 1); // Default value
282
283        let inc = parse_counter_increment("list-item 2").unwrap();
284        assert_eq!(inc.counter_name.as_str(), "list-item");
285        assert_eq!(inc.value, 2);
286
287        assert_eq!(
288            parse_string_set("chapter-title content()")
289                .unwrap()
290                .inner
291                .as_str(),
292            "chapter-title content()"
293        );
294    }
295}