Skip to main content

azul_css/codegen/
rust.rs

1//! Rust source-code emitter for parsed CSS.
2//!
3//! Produces a `const CSS: Css = ...;` literal plus a minimal `Cargo.toml` and
4//! `src/main.rs` skeleton suitable for `cargo build` against `azul`.
5
6use alloc::{format, string::String, string::ToString, vec, vec::Vec};
7use core::fmt::Write;
8
9use super::{CodegenBackend, GeneratedFile};
10use crate::{
11    css::{
12        AttributeMatchOp, Css, CssAttributeSelector, CssDeclaration, CssNthChildPattern,
13        CssNthChildSelector, CssPath, CssPathPseudoSelector, CssPathSelector, DynamicCssProperty,
14        NodeTypeTag,
15    },
16    props::property::format_static_css_prop,
17};
18
19/// Emits Rust source code for a parsed CSS stylesheet.
20#[derive(Copy, Clone, Debug)]
21pub struct RustBackend;
22
23impl CodegenBackend for RustBackend {
24    fn lang(&self) -> &'static str {
25        "rust"
26    }
27
28    fn emit_css(&self, css: &Css) -> String {
29        css_to_rust_code(css)
30    }
31
32    fn emit_project(&self, css: &Css) -> Vec<GeneratedFile> {
33        let css_literal = css_to_rust_code(css);
34        let main_rs = format!(
35            "use azul::prelude::*;\r\n\r\n{css_literal}\r\n\r\nfn main() {{\r\n    \
36             println!(\"Generated stylesheet contains {{}} rule(s)\", \
37             CSS.rules.as_ref().len());\r\n}}\r\n",
38        );
39        let cargo_toml = "[package]\r\n\
40            name = \"azul-generated-app\"\r\n\
41            version = \"0.1.0\"\r\n\
42            edition = \"2021\"\r\n\
43            \r\n\
44            [dependencies]\r\n\
45            azul = \"0.0.7\"\r\n"
46            .to_string();
47        vec![
48            GeneratedFile {
49                path: "Cargo.toml".to_string(),
50                contents: cargo_toml,
51            },
52            GeneratedFile {
53                path: "src/main.rs".to_string(),
54                contents: main_rs,
55            },
56        ]
57    }
58}
59
60/// Render a parsed [`Css`] as Rust source code (a `const CSS: Css = ...;`).
61#[must_use] pub fn css_to_rust_code(css: &Css) -> String {
62    let mut output = String::new();
63
64    output.push_str("const CSS: Css = Css {\r\n");
65    output.push_str("\trules: [\r\n");
66
67    for block in &css.rules {
68        output.push_str("\t\tCssRuleBlock {\r\n");
69        let _ = write!(output,
70            "\t\t\tpath: {},\r\n",
71            print_block_path(&block.path, 3)
72        );
73        let _ = write!(output,
74            "\t\t\tpriority: {},\r\n",
75            block.priority,
76        );
77
78        output.push_str("\t\t\tdeclarations: [\r\n");
79        for declaration in &block.declarations {
80            let _ = write!(output,
81                "\t\t\t\t{},\r\n",
82                print_declaration(declaration, 4)
83            );
84        }
85        output.push_str("\t\t\t]\r\n");
86
87        output.push_str("\t\t},\r\n");
88    }
89
90    output.push_str("\t]\r\n");
91    output.push_str("};");
92
93    output.replace('\t', "    ")
94}
95
96#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
97#[must_use] pub const fn format_node_type(n: &NodeTypeTag) -> &'static str {
98    match n {
99        // Document structure
100        NodeTypeTag::Html => "NodeTypeTag::Html",
101        NodeTypeTag::Head => "NodeTypeTag::Head",
102        NodeTypeTag::Body => "NodeTypeTag::Body",
103
104        // Block elements
105        NodeTypeTag::Div => "NodeTypeTag::Div",
106        NodeTypeTag::P => "NodeTypeTag::P",
107        NodeTypeTag::Article => "NodeTypeTag::Article",
108        NodeTypeTag::Section => "NodeTypeTag::Section",
109        NodeTypeTag::Nav => "NodeTypeTag::Nav",
110        NodeTypeTag::Aside => "NodeTypeTag::Aside",
111        NodeTypeTag::Header => "NodeTypeTag::Header",
112        NodeTypeTag::Footer => "NodeTypeTag::Footer",
113        NodeTypeTag::Main => "NodeTypeTag::Main",
114        NodeTypeTag::Figure => "NodeTypeTag::Figure",
115        NodeTypeTag::FigCaption => "NodeTypeTag::FigCaption",
116
117        // Headings
118        NodeTypeTag::H1 => "NodeTypeTag::H1",
119        NodeTypeTag::H2 => "NodeTypeTag::H2",
120        NodeTypeTag::H3 => "NodeTypeTag::H3",
121        NodeTypeTag::H4 => "NodeTypeTag::H4",
122        NodeTypeTag::H5 => "NodeTypeTag::H5",
123        NodeTypeTag::H6 => "NodeTypeTag::H6",
124
125        // Text formatting
126        NodeTypeTag::Br => "NodeTypeTag::Br",
127        NodeTypeTag::Hr => "NodeTypeTag::Hr",
128        NodeTypeTag::Pre => "NodeTypeTag::Pre",
129        NodeTypeTag::BlockQuote => "NodeTypeTag::BlockQuote",
130        NodeTypeTag::Address => "NodeTypeTag::Address",
131        NodeTypeTag::Details => "NodeTypeTag::Details",
132        NodeTypeTag::Summary => "NodeTypeTag::Summary",
133        NodeTypeTag::Dialog => "NodeTypeTag::Dialog",
134
135        // List elements
136        NodeTypeTag::Ul => "NodeTypeTag::Ul",
137        NodeTypeTag::Ol => "NodeTypeTag::Ol",
138        NodeTypeTag::Li => "NodeTypeTag::Li",
139        NodeTypeTag::Dl => "NodeTypeTag::Dl",
140        NodeTypeTag::Dt => "NodeTypeTag::Dt",
141        NodeTypeTag::Dd => "NodeTypeTag::Dd",
142        NodeTypeTag::Menu => "NodeTypeTag::Menu",
143        NodeTypeTag::MenuItem => "NodeTypeTag::MenuItem",
144        NodeTypeTag::Dir => "NodeTypeTag::Dir",
145
146        // Table elements
147        NodeTypeTag::Table => "NodeTypeTag::Table",
148        NodeTypeTag::Caption => "NodeTypeTag::Caption",
149        NodeTypeTag::THead => "NodeTypeTag::THead",
150        NodeTypeTag::TBody => "NodeTypeTag::TBody",
151        NodeTypeTag::TFoot => "NodeTypeTag::TFoot",
152        NodeTypeTag::Tr => "NodeTypeTag::Tr",
153        NodeTypeTag::Th => "NodeTypeTag::Th",
154        NodeTypeTag::Td => "NodeTypeTag::Td",
155        NodeTypeTag::ColGroup => "NodeTypeTag::ColGroup",
156        NodeTypeTag::Col => "NodeTypeTag::Col",
157
158        // Form elements
159        NodeTypeTag::Form => "NodeTypeTag::Form",
160        NodeTypeTag::FieldSet => "NodeTypeTag::FieldSet",
161        NodeTypeTag::Legend => "NodeTypeTag::Legend",
162        NodeTypeTag::Label => "NodeTypeTag::Label",
163        NodeTypeTag::Input => "NodeTypeTag::Input",
164        NodeTypeTag::Button => "NodeTypeTag::Button",
165        NodeTypeTag::Select => "NodeTypeTag::Select",
166        NodeTypeTag::OptGroup => "NodeTypeTag::OptGroup",
167        NodeTypeTag::SelectOption => "NodeTypeTag::SelectOption",
168        NodeTypeTag::TextArea => "NodeTypeTag::TextArea",
169        NodeTypeTag::Output => "NodeTypeTag::Output",
170        NodeTypeTag::Progress => "NodeTypeTag::Progress",
171        NodeTypeTag::Meter => "NodeTypeTag::Meter",
172        NodeTypeTag::DataList => "NodeTypeTag::DataList",
173
174        // Inline elements
175        NodeTypeTag::Span => "NodeTypeTag::Span",
176        NodeTypeTag::A => "NodeTypeTag::A",
177        NodeTypeTag::Em => "NodeTypeTag::Em",
178        NodeTypeTag::Strong => "NodeTypeTag::Strong",
179        NodeTypeTag::B => "NodeTypeTag::B",
180        NodeTypeTag::I => "NodeTypeTag::I",
181        NodeTypeTag::U => "NodeTypeTag::U",
182        NodeTypeTag::S => "NodeTypeTag::S",
183        NodeTypeTag::Mark => "NodeTypeTag::Mark",
184        NodeTypeTag::Del => "NodeTypeTag::Del",
185        NodeTypeTag::Ins => "NodeTypeTag::Ins",
186        NodeTypeTag::Code => "NodeTypeTag::Code",
187        NodeTypeTag::Samp => "NodeTypeTag::Samp",
188        NodeTypeTag::Kbd => "NodeTypeTag::Kbd",
189        NodeTypeTag::Var => "NodeTypeTag::Var",
190        NodeTypeTag::Cite => "NodeTypeTag::Cite",
191        NodeTypeTag::Dfn => "NodeTypeTag::Dfn",
192        NodeTypeTag::Abbr => "NodeTypeTag::Abbr",
193        NodeTypeTag::Acronym => "NodeTypeTag::Acronym",
194        NodeTypeTag::Q => "NodeTypeTag::Q",
195        NodeTypeTag::Time => "NodeTypeTag::Time",
196        NodeTypeTag::Sub => "NodeTypeTag::Sub",
197        NodeTypeTag::Sup => "NodeTypeTag::Sup",
198        NodeTypeTag::Small => "NodeTypeTag::Small",
199        NodeTypeTag::Big => "NodeTypeTag::Big",
200        NodeTypeTag::Bdo => "NodeTypeTag::Bdo",
201        NodeTypeTag::Bdi => "NodeTypeTag::Bdi",
202        NodeTypeTag::Wbr => "NodeTypeTag::Wbr",
203        NodeTypeTag::Ruby => "NodeTypeTag::Ruby",
204        NodeTypeTag::Rt => "NodeTypeTag::Rt",
205        NodeTypeTag::Rtc => "NodeTypeTag::Rtc",
206        NodeTypeTag::Rp => "NodeTypeTag::Rp",
207        NodeTypeTag::Data => "NodeTypeTag::Data",
208
209        // Embedded content
210        NodeTypeTag::Canvas => "NodeTypeTag::Canvas",
211        NodeTypeTag::Object => "NodeTypeTag::Object",
212        NodeTypeTag::Param => "NodeTypeTag::Param",
213        NodeTypeTag::Embed => "NodeTypeTag::Embed",
214        NodeTypeTag::Audio => "NodeTypeTag::Audio",
215        NodeTypeTag::Video => "NodeTypeTag::Video",
216        NodeTypeTag::Source => "NodeTypeTag::Source",
217        NodeTypeTag::Track => "NodeTypeTag::Track",
218        NodeTypeTag::Map => "NodeTypeTag::Map",
219        NodeTypeTag::Area => "NodeTypeTag::Area",
220        NodeTypeTag::Svg => "NodeTypeTag::Svg",
221        NodeTypeTag::SvgPath => "NodeTypeTag::SvgPath",
222        NodeTypeTag::SvgCircle => "NodeTypeTag::SvgCircle",
223        NodeTypeTag::SvgRect => "NodeTypeTag::SvgRect",
224        NodeTypeTag::SvgEllipse => "NodeTypeTag::SvgEllipse",
225        NodeTypeTag::SvgLine => "NodeTypeTag::SvgLine",
226        NodeTypeTag::SvgPolygon => "NodeTypeTag::SvgPolygon",
227        NodeTypeTag::SvgPolyline => "NodeTypeTag::SvgPolyline",
228        NodeTypeTag::SvgG => "NodeTypeTag::SvgG",
229
230        // SVG container elements
231        NodeTypeTag::SvgDefs => "NodeTypeTag::SvgDefs",
232        NodeTypeTag::SvgSymbol => "NodeTypeTag::SvgSymbol",
233        NodeTypeTag::SvgUse => "NodeTypeTag::SvgUse",
234        NodeTypeTag::SvgSwitch => "NodeTypeTag::SvgSwitch",
235
236        // SVG text elements
237        NodeTypeTag::SvgText => "NodeTypeTag::SvgText",
238        NodeTypeTag::SvgTspan => "NodeTypeTag::SvgTspan",
239        NodeTypeTag::SvgTextPath => "NodeTypeTag::SvgTextPath",
240
241        // SVG paint server elements
242        NodeTypeTag::SvgLinearGradient => "NodeTypeTag::SvgLinearGradient",
243        NodeTypeTag::SvgRadialGradient => "NodeTypeTag::SvgRadialGradient",
244        NodeTypeTag::SvgStop => "NodeTypeTag::SvgStop",
245        NodeTypeTag::SvgPattern => "NodeTypeTag::SvgPattern",
246
247        // SVG clipping/masking elements
248        NodeTypeTag::SvgClipPathElement => "NodeTypeTag::SvgClipPathElement",
249        NodeTypeTag::SvgMask => "NodeTypeTag::SvgMask",
250
251        // SVG filter elements
252        NodeTypeTag::SvgFilter => "NodeTypeTag::SvgFilter",
253        NodeTypeTag::SvgFeBlend => "NodeTypeTag::SvgFeBlend",
254        NodeTypeTag::SvgFeColorMatrix => "NodeTypeTag::SvgFeColorMatrix",
255        NodeTypeTag::SvgFeComponentTransfer => "NodeTypeTag::SvgFeComponentTransfer",
256        NodeTypeTag::SvgFeComposite => "NodeTypeTag::SvgFeComposite",
257        NodeTypeTag::SvgFeConvolveMatrix => "NodeTypeTag::SvgFeConvolveMatrix",
258        NodeTypeTag::SvgFeDiffuseLighting => "NodeTypeTag::SvgFeDiffuseLighting",
259        NodeTypeTag::SvgFeDisplacementMap => "NodeTypeTag::SvgFeDisplacementMap",
260        NodeTypeTag::SvgFeDistantLight => "NodeTypeTag::SvgFeDistantLight",
261        NodeTypeTag::SvgFeDropShadow => "NodeTypeTag::SvgFeDropShadow",
262        NodeTypeTag::SvgFeFlood => "NodeTypeTag::SvgFeFlood",
263        NodeTypeTag::SvgFeFuncR => "NodeTypeTag::SvgFeFuncR",
264        NodeTypeTag::SvgFeFuncG => "NodeTypeTag::SvgFeFuncG",
265        NodeTypeTag::SvgFeFuncB => "NodeTypeTag::SvgFeFuncB",
266        NodeTypeTag::SvgFeFuncA => "NodeTypeTag::SvgFeFuncA",
267        NodeTypeTag::SvgFeGaussianBlur => "NodeTypeTag::SvgFeGaussianBlur",
268        NodeTypeTag::SvgFeImage => "NodeTypeTag::SvgFeImage",
269        NodeTypeTag::SvgFeMerge => "NodeTypeTag::SvgFeMerge",
270        NodeTypeTag::SvgFeMergeNode => "NodeTypeTag::SvgFeMergeNode",
271        NodeTypeTag::SvgFeMorphology => "NodeTypeTag::SvgFeMorphology",
272        NodeTypeTag::SvgFeOffset => "NodeTypeTag::SvgFeOffset",
273        NodeTypeTag::SvgFePointLight => "NodeTypeTag::SvgFePointLight",
274        NodeTypeTag::SvgFeSpecularLighting => "NodeTypeTag::SvgFeSpecularLighting",
275        NodeTypeTag::SvgFeSpotLight => "NodeTypeTag::SvgFeSpotLight",
276        NodeTypeTag::SvgFeTile => "NodeTypeTag::SvgFeTile",
277        NodeTypeTag::SvgFeTurbulence => "NodeTypeTag::SvgFeTurbulence",
278
279        // SVG marker/image elements
280        NodeTypeTag::SvgMarker => "NodeTypeTag::SvgMarker",
281        NodeTypeTag::SvgImage => "NodeTypeTag::SvgImage",
282        NodeTypeTag::SvgForeignObject => "NodeTypeTag::SvgForeignObject",
283
284        // SVG descriptive elements
285        NodeTypeTag::SvgTitle => "NodeTypeTag::SvgTitle",
286        NodeTypeTag::SvgDesc => "NodeTypeTag::SvgDesc",
287        NodeTypeTag::SvgMetadata => "NodeTypeTag::SvgMetadata",
288        NodeTypeTag::SvgA => "NodeTypeTag::SvgA",
289        NodeTypeTag::SvgView => "NodeTypeTag::SvgView",
290        NodeTypeTag::SvgStyle => "NodeTypeTag::SvgStyle",
291        NodeTypeTag::SvgScript => "NodeTypeTag::SvgScript",
292
293        // SVG animation elements
294        NodeTypeTag::SvgAnimate => "NodeTypeTag::SvgAnimate",
295        NodeTypeTag::SvgAnimateMotion => "NodeTypeTag::SvgAnimateMotion",
296        NodeTypeTag::SvgAnimateTransform => "NodeTypeTag::SvgAnimateTransform",
297        NodeTypeTag::SvgSet => "NodeTypeTag::SvgSet",
298        NodeTypeTag::SvgMpath => "NodeTypeTag::SvgMpath",
299
300        // Metadata
301        NodeTypeTag::Title => "NodeTypeTag::Title",
302        NodeTypeTag::Meta => "NodeTypeTag::Meta",
303        NodeTypeTag::Link => "NodeTypeTag::Link",
304        NodeTypeTag::Script => "NodeTypeTag::Script",
305        NodeTypeTag::Style => "NodeTypeTag::Style",
306        NodeTypeTag::Base => "NodeTypeTag::Base",
307
308        // Content elements
309        NodeTypeTag::Text => "NodeTypeTag::Text",
310        NodeTypeTag::Img => "NodeTypeTag::Img",
311        NodeTypeTag::VirtualView => "NodeTypeTag::VirtualView",
312        NodeTypeTag::Icon => "NodeTypeTag::Icon",
313        NodeTypeTag::GeolocationProbe => "NodeTypeTag::GeolocationProbe",
314
315        // Pseudo-elements
316        NodeTypeTag::Before => "NodeTypeTag::Before",
317        NodeTypeTag::After => "NodeTypeTag::After",
318        NodeTypeTag::Marker => "NodeTypeTag::Marker",
319        NodeTypeTag::Placeholder => "NodeTypeTag::Placeholder",
320    }
321}
322
323#[must_use] pub fn print_block_path(path: &CssPath, tabs: usize) -> String {
324    let t = String::from("    ").repeat(tabs);
325    let t1 = String::from("    ").repeat(tabs + 1);
326
327    format!(
328        "CssPath {{\r\n{}selectors: {}\r\n{}}}",
329        t1,
330        format_selectors(path.selectors.as_ref(), tabs + 1),
331        t
332    )
333}
334
335#[must_use] pub fn format_selectors(selectors: &[CssPathSelector], tabs: usize) -> String {
336    let t = String::from("    ").repeat(tabs);
337    let t1 = String::from("    ").repeat(tabs + 1);
338
339    let selectors_formatted = selectors
340        .iter()
341        .map(|s| format!("{}{},", t1, format_single_selector(s, tabs + 1)))
342        .collect::<Vec<String>>()
343        .join("\r\n");
344
345    format!("vec![\r\n{selectors_formatted}\r\n{t}].into()")
346}
347
348#[must_use] pub fn format_single_selector(p: &CssPathSelector, _tabs: usize) -> String {
349    match p {
350        CssPathSelector::Global => "CssPathSelector::Global".to_string(),
351        CssPathSelector::Root(r) => format!(
352            "CssPathSelector::Root(CssScopeRange {{ start: {}, end: {} }})",
353            r.start, r.end
354        ),
355        CssPathSelector::Type(ntp) => format!("CssPathSelector::Type({})", format_node_type(ntp)),
356        CssPathSelector::Class(class) => {
357            format!("CssPathSelector::Class(String::from({class:?}))")
358        }
359        CssPathSelector::Id(id) => format!("CssPathSelector::Id(String::from({id:?}))"),
360        CssPathSelector::PseudoSelector(cps) => format!(
361            "CssPathSelector::PseudoSelector({})",
362            format_pseudo_selector_type(cps)
363        ),
364        CssPathSelector::Attribute(a) => format!(
365            "CssPathSelector::Attribute({})",
366            format_attribute_selector(a)
367        ),
368        CssPathSelector::DirectChildren => "CssPathSelector::DirectChildren".to_string(),
369        CssPathSelector::Children => "CssPathSelector::Children".to_string(),
370        CssPathSelector::AdjacentSibling => "CssPathSelector::AdjacentSibling".to_string(),
371        CssPathSelector::GeneralSibling => "CssPathSelector::GeneralSibling".to_string(),
372    }
373}
374
375#[must_use] pub fn format_pseudo_selector_type(p: &CssPathPseudoSelector) -> String {
376    match p {
377        CssPathPseudoSelector::First => "CssPathPseudoSelector::First".to_string(),
378        CssPathPseudoSelector::Last => "CssPathPseudoSelector::Last".to_string(),
379        CssPathPseudoSelector::NthChild(n) => format!(
380            "CssPathPseudoSelector::NthChild({})",
381            format_nth_child_selector(n)
382        ),
383        CssPathPseudoSelector::Hover => "CssPathPseudoSelector::Hover".to_string(),
384        CssPathPseudoSelector::Active => "CssPathPseudoSelector::Active".to_string(),
385        CssPathPseudoSelector::Focus => "CssPathPseudoSelector::Focus".to_string(),
386        CssPathPseudoSelector::Backdrop => "CssPathPseudoSelector::Backdrop".to_string(),
387        CssPathPseudoSelector::Lang(lang) => format!(
388            "CssPathPseudoSelector::Lang(AzString::from_const_str(\"{}\"))",
389            lang.as_str()
390        ),
391        CssPathPseudoSelector::Dragging => "CssPathPseudoSelector::Dragging".to_string(),
392        CssPathPseudoSelector::DragOver => "CssPathPseudoSelector::DragOver".to_string(),
393        CssPathPseudoSelector::Root => "CssPathPseudoSelector::Root".to_string(),
394    }
395}
396
397#[must_use] pub fn format_attribute_selector(a: &CssAttributeSelector) -> String {
398    let value = a.value.as_ref().map_or_else(
399        || "OptionString::None".to_string(),
400        |v| {
401            format!(
402                "OptionString::Some(AzString::from_const_str({:?}))",
403                v.as_str()
404            )
405        },
406    );
407    format!(
408        "CssAttributeSelector {{ name: AzString::from_const_str({:?}), op: {}, value: {} }}",
409        a.name.as_str(),
410        format_attribute_match_op(&a.op),
411        value
412    )
413}
414
415#[must_use] pub fn format_attribute_match_op(op: &AttributeMatchOp) -> String {
416    match op {
417        AttributeMatchOp::Exists => "AttributeMatchOp::Exists".to_string(),
418        AttributeMatchOp::Eq => "AttributeMatchOp::Eq".to_string(),
419        AttributeMatchOp::Includes => "AttributeMatchOp::Includes".to_string(),
420        AttributeMatchOp::DashMatch => "AttributeMatchOp::DashMatch".to_string(),
421        AttributeMatchOp::Prefix => "AttributeMatchOp::Prefix".to_string(),
422        AttributeMatchOp::Suffix => "AttributeMatchOp::Suffix".to_string(),
423        AttributeMatchOp::Substring => "AttributeMatchOp::Substring".to_string(),
424    }
425}
426
427#[must_use] pub fn format_nth_child_selector(n: &CssNthChildSelector) -> String {
428    match n {
429        CssNthChildSelector::Number(num) => format!("CssNthChildSelector::Number({num})"),
430        CssNthChildSelector::Even => "CssNthChildSelector::Even".to_string(),
431        CssNthChildSelector::Odd => "CssNthChildSelector::Odd".to_string(),
432        CssNthChildSelector::Pattern(CssNthChildPattern {
433            pattern_repeat,
434            offset,
435        }) => format!(
436            "CssNthChildSelector::Pattern(CssNthChildPattern {{ pattern_repeat: {pattern_repeat}, offset: {offset} }})"
437        ),
438    }
439}
440
441#[must_use] pub fn print_declaration(decl: &CssDeclaration, tabs: usize) -> String {
442    match decl {
443        CssDeclaration::Static(s) => format!(
444            "CssDeclaration::Static({})",
445            format_static_css_prop(s, tabs)
446        ),
447        CssDeclaration::Dynamic(d) => format!(
448            "CssDeclaration::Dynamic({})",
449            format_dynamic_css_prop(d, tabs)
450        ),
451    }
452}
453
454#[must_use] pub fn format_dynamic_css_prop(decl: &DynamicCssProperty, tabs: usize) -> String {
455    let t = String::from("    ").repeat(tabs);
456    format!(
457        "DynamicCssProperty {{\r\n{}    dynamic_id: {:?},\r\n{}    default_value: {},\r\n{}}}",
458        t,
459        decl.dynamic_id,
460        t,
461        format_static_css_prop(&decl.default_value, tabs + 1),
462        t
463    )
464}
465
466#[cfg(test)]
467mod tests {
468    use alloc::vec;
469
470    use super::*;
471    use crate::css::CssRuleBlock;
472
473    fn sample_css() -> Css {
474        let path = CssPath::new(vec![CssPathSelector::Type(NodeTypeTag::Div)]);
475        let block = CssRuleBlock::new(path, vec![]);
476        Css {
477            rules: vec![block].into(),
478        }
479    }
480
481    #[test]
482    fn rust_backend_emits_const_literal() {
483        let css = sample_css();
484        let rust = RustBackend.emit_css(&css);
485        assert!(rust.contains("const CSS: Css"));
486        assert!(rust.contains("NodeTypeTag::Div"));
487    }
488
489    #[test]
490    fn rust_backend_emits_project_files() {
491        let css = sample_css();
492        let files = RustBackend.emit_project(&css);
493        let paths: Vec<_> = files.iter().map(|f| f.path.as_str()).collect();
494        assert!(paths.contains(&"Cargo.toml"));
495        assert!(paths.contains(&"src/main.rs"));
496        let main_rs = files
497            .iter()
498            .find(|f| f.path == "src/main.rs")
499            .expect("main.rs missing");
500        assert!(main_rs.contains_const_literal());
501    }
502
503    impl GeneratedFile {
504        fn contains_const_literal(&self) -> bool {
505            self.contents.contains("const CSS: Css")
506        }
507    }
508}