dampen_core/parser/
theme_parser.rs

1//! Theme and style class parsing
2//!
3//! This module provides parsers for theme definitions and style classes.
4
5use crate::ir::layout::LayoutConstraints;
6use crate::ir::style::{Color, StyleProperties};
7use crate::ir::theme::{
8    FontWeight, SpacingScale, StyleClass, Theme, ThemeDocument, ThemeError, ThemeErrorKind,
9    ThemePalette, Typography, WidgetState,
10};
11use std::collections::HashMap;
12
13/// Parse a complete theme.dampen document
14pub fn parse_theme_document(xml: &str) -> Result<ThemeDocument, ThemeError> {
15    let doc = roxmltree::Document::parse(xml).map_err(|e| ThemeError {
16        kind: ThemeErrorKind::MissingPaletteColor,
17        message: format!("THEME_003: Failed to parse XML: {}", e),
18    })?;
19
20    // Get the first child element (the <dampen> root)
21    let root = doc.root().first_child().ok_or_else(|| ThemeError {
22        kind: ThemeErrorKind::MissingPaletteColor,
23        message: "THEME_003: No root element found".to_string(),
24    })?;
25
26    // Verify root element
27    if root.tag_name().name() != "dampen" {
28        return Err(ThemeError {
29            kind: ThemeErrorKind::MissingPaletteColor,
30            message: "THEME_003: Root element must be <dampen>".to_string(),
31        });
32    }
33
34    let mut themes = HashMap::new();
35    let mut default_theme = None;
36    let mut follow_system = true;
37
38    // Parse child elements
39    for child in root.children() {
40        if child.node_type() != roxmltree::NodeType::Element {
41            continue;
42        }
43
44        let tag = child.tag_name().name();
45
46        match tag {
47            "themes" => {
48                // Parse each theme
49                for grandchild in child.children() {
50                    if grandchild.node_type() != roxmltree::NodeType::Element {
51                        continue;
52                    }
53
54                    if grandchild.tag_name().name() == "theme" {
55                        let theme = parse_theme_from_node_simple(grandchild)?;
56                        if themes.contains_key(&theme.name) {
57                            return Err(ThemeError {
58                                kind: ThemeErrorKind::DuplicateThemeName,
59                                message: format!(
60                                    "THEME_005: Duplicate theme name: '{}'",
61                                    theme.name
62                                ),
63                            });
64                        }
65                        themes.insert(theme.name.clone(), theme);
66                    }
67                }
68            }
69            "default_theme" => {
70                if let Some(name) = child.attribute("name") {
71                    default_theme = Some(name.to_string());
72                }
73            }
74            "follow_system" => {
75                if let Some(enabled) = child.attribute("enabled") {
76                    follow_system = enabled.parse::<bool>().unwrap_or(true);
77                }
78            }
79            _ => {}
80        }
81    }
82
83    let document = ThemeDocument {
84        themes,
85        default_theme,
86        follow_system,
87    };
88
89    document.validate()?;
90    Ok(document)
91}
92
93/// Parse a theme node (simplified version for ThemeDocument parsing)
94fn parse_theme_from_node_simple(node: roxmltree::Node) -> Result<Theme, ThemeError> {
95    let name = node
96        .attribute("name")
97        .map(|s| s.to_string())
98        .unwrap_or_else(|| "default".to_string());
99
100    let extends = node.attribute("extends").map(|s| s.to_string());
101
102    let mut palette_attrs = HashMap::new();
103    let mut typography_attrs = HashMap::new();
104    let mut spacing_unit = None;
105
106    // Parse child elements
107    for child in node.children() {
108        if child.node_type() != roxmltree::NodeType::Element {
109            continue;
110        }
111
112        let tag = child.tag_name().name();
113
114        match tag {
115            "palette" => {
116                for attr in child.attributes() {
117                    palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
118                }
119            }
120            "typography" => {
121                for attr in child.attributes() {
122                    typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
123                }
124            }
125            "spacing" => {
126                if let Some(unit) = child.attribute("unit") {
127                    spacing_unit = unit.parse::<f32>().ok();
128                }
129            }
130            _ => {}
131        }
132    }
133
134    let palette = parse_palette(&palette_attrs).map_err(|e| ThemeError {
135        kind: ThemeErrorKind::MissingPaletteColor,
136        message: format!("THEME_003: Invalid palette: {}", e),
137    })?;
138
139    let typography = parse_typography(&typography_attrs).map_err(|e| ThemeError {
140        kind: ThemeErrorKind::MissingPaletteColor,
141        message: format!("THEME_003: Invalid typography: {}", e),
142    })?;
143
144    let spacing = SpacingScale { unit: spacing_unit };
145
146    spacing.validate().map_err(|e| ThemeError {
147        kind: ThemeErrorKind::MissingPaletteColor,
148        message: format!("THEME_003: Invalid spacing: {}", e),
149    })?;
150
151    let theme = Theme {
152        name,
153        palette,
154        typography,
155        spacing,
156        base_styles: HashMap::new(),
157        extends,
158    };
159
160    Ok(theme)
161}
162
163/// Parse a theme definition from XML attributes
164pub fn parse_theme(
165    name: String,
166    palette_attrs: &HashMap<String, String>,
167    typography_attrs: &HashMap<String, String>,
168    spacing_unit: Option<f32>,
169    extends: Option<String>,
170) -> Result<Theme, String> {
171    let palette = parse_palette(palette_attrs)?;
172    let typography = parse_typography(typography_attrs)?;
173    let spacing = SpacingScale { unit: spacing_unit };
174
175    let theme = Theme {
176        name,
177        palette,
178        typography,
179        spacing,
180        base_styles: HashMap::new(),
181        extends: extends.clone(),
182    };
183
184    theme.validate(extends.is_some())?;
185    Ok(theme)
186}
187
188/// Parse theme palette from attributes
189pub fn parse_palette(attrs: &HashMap<String, String>) -> Result<ThemePalette, String> {
190    let get_color = |key: &str| -> Result<Option<Color>, String> {
191        if let Some(value) = attrs.get(key) {
192            Ok(Some(Color::parse(value)?))
193        } else {
194            Ok(None)
195        }
196    };
197
198    Ok(ThemePalette {
199        primary: get_color("primary")?,
200        secondary: get_color("secondary")?,
201        success: get_color("success")?,
202        warning: get_color("warning")?,
203        danger: get_color("danger")?,
204        background: get_color("background")?,
205        surface: get_color("surface")?,
206        text: get_color("text")?,
207        text_secondary: get_color("text_secondary")?,
208    })
209}
210
211/// Parse typography from attributes
212pub fn parse_typography(attrs: &HashMap<String, String>) -> Result<Typography, String> {
213    let font_family = attrs.get("font_family").cloned();
214
215    let font_size_base = if let Some(s) = attrs.get("font_size_base") {
216        Some(s.parse().map_err(|_| "Invalid font_size_base")?)
217    } else {
218        None
219    };
220
221    let font_size_small = if let Some(s) = attrs.get("font_size_small") {
222        Some(s.parse().map_err(|_| "Invalid font_size_small")?)
223    } else {
224        None
225    };
226
227    let font_size_large = if let Some(s) = attrs.get("font_size_large") {
228        Some(s.parse().map_err(|_| "Invalid font_size_large")?)
229    } else {
230        None
231    };
232
233    let font_weight = match attrs.get("font_weight") {
234        Some(w) => FontWeight::parse(w)?,
235        None => FontWeight::Normal,
236    };
237
238    let line_height = if let Some(s) = attrs.get("line_height") {
239        Some(s.parse().map_err(|_| "Invalid line_height")?)
240    } else {
241        None
242    };
243
244    Ok(Typography {
245        font_family,
246        font_size_base,
247        font_size_small,
248        font_size_large,
249        font_weight,
250        line_height,
251    })
252}
253
254/// Parse a style class definition
255pub fn parse_style_class(
256    name: String,
257    base_attrs: &HashMap<String, String>,
258    extends: Vec<String>,
259    state_variants: HashMap<WidgetState, StyleProperties>,
260    combined_state_variants: HashMap<crate::ir::theme::StateSelector, StyleProperties>,
261    layout: Option<LayoutConstraints>,
262) -> Result<StyleClass, String> {
263    let style = parse_style_properties_from_attrs(base_attrs)?;
264
265    let class = StyleClass {
266        name,
267        style,
268        layout,
269        extends,
270        state_variants,
271        combined_state_variants,
272    };
273
274    Ok(class)
275}
276
277/// Parse style properties from a map of attributes
278pub fn parse_style_properties_from_attrs(
279    attrs: &HashMap<String, String>,
280) -> Result<StyleProperties, String> {
281    use crate::parser::style_parser::*;
282
283    let mut background = None;
284    let mut color = None;
285    let mut shadow = None;
286    let mut opacity = None;
287    let mut transform = None;
288
289    // Parse background
290    if let Some(value) = attrs.get("background") {
291        background = Some(parse_background_attr(value)?);
292    }
293
294    // Parse color
295    if let Some(value) = attrs.get("color") {
296        color = Some(parse_color_attr(value)?);
297    }
298
299    // Parse border properties
300    let border_width = attrs
301        .get("border_width")
302        .map(|v| parse_border_width(v))
303        .transpose()?;
304    let border_color = attrs
305        .get("border_color")
306        .map(|v| parse_border_color(v))
307        .transpose()?;
308    let border_radius = attrs
309        .get("border_radius")
310        .map(|v| parse_border_radius(v))
311        .transpose()?;
312    let border_style = attrs
313        .get("border_style")
314        .map(|v| parse_border_style(v))
315        .transpose()?;
316
317    let border = build_border(border_width, border_color, border_radius, border_style)?;
318
319    // Parse shadow
320    if let Some(value) = attrs.get("shadow") {
321        shadow = Some(parse_shadow_attr(value)?);
322    }
323
324    // Parse opacity
325    if let Some(value) = attrs.get("opacity") {
326        opacity = Some(parse_opacity(value)?);
327    }
328
329    // Parse transform
330    if let Some(value) = attrs.get("transform") {
331        transform = Some(parse_transform(value)?);
332    }
333
334    build_style_properties(background, color, border, shadow, opacity, transform)
335}
336
337/// Parse layout constraints from attributes
338pub fn parse_layout_constraints(
339    attrs: &HashMap<String, String>,
340) -> Result<Option<LayoutConstraints>, String> {
341    use crate::parser::style_parser::*;
342
343    let mut constraints = LayoutConstraints::default();
344    let mut has_any = false;
345
346    // Parse sizing
347    if let Some(value) = attrs.get("width") {
348        constraints.width = Some(parse_length_attr(value)?);
349        has_any = true;
350    }
351
352    if let Some(value) = attrs.get("height") {
353        constraints.height = Some(parse_length_attr(value)?);
354        has_any = true;
355    }
356
357    // Parse constraints
358    if let Some(value) = attrs.get("min_width") {
359        constraints.min_width = Some(parse_constraint(value)?);
360        has_any = true;
361    }
362
363    if let Some(value) = attrs.get("max_width") {
364        constraints.max_width = Some(parse_constraint(value)?);
365        has_any = true;
366    }
367
368    if let Some(value) = attrs.get("min_height") {
369        constraints.min_height = Some(parse_constraint(value)?);
370        has_any = true;
371    }
372
373    if let Some(value) = attrs.get("max_height") {
374        constraints.max_height = Some(parse_constraint(value)?);
375        has_any = true;
376    }
377
378    // Parse layout
379    if let Some(value) = attrs.get("padding") {
380        constraints.padding = Some(parse_padding_attr(value)?);
381        has_any = true;
382    }
383
384    if let Some(value) = attrs.get("spacing") {
385        constraints.spacing = Some(parse_spacing(value)?);
386        has_any = true;
387    }
388
389    // Parse alignment
390    if let Some(value) = attrs.get("align_items") {
391        constraints.align_items = Some(parse_alignment(value)?);
392        has_any = true;
393    }
394
395    if let Some(value) = attrs.get("justify_content") {
396        constraints.justify_content = Some(parse_justification(value)?);
397        has_any = true;
398    }
399
400    if let Some(value) = attrs.get("align_self") {
401        constraints.align_self = Some(parse_alignment(value)?);
402        has_any = true;
403    }
404
405    // Parse direction
406    if let Some(value) = attrs.get("direction") {
407        constraints.direction = Some(crate::ir::layout::Direction::parse(value)?);
408        has_any = true;
409    }
410
411    if has_any {
412        constraints.validate()?;
413        Ok(Some(constraints))
414    } else {
415        Ok(None)
416    }
417}
418
419/// Parse state-prefixed attributes into state variants
420/// Type alias for state variant maps
421pub type StateVariantMaps = (
422    HashMap<WidgetState, StyleProperties>,
423    HashMap<crate::ir::theme::StateSelector, StyleProperties>,
424);
425
426/// Returns both single and combined state variants
427pub fn parse_state_variants(attrs: &HashMap<String, String>) -> Result<StateVariantMaps, String> {
428    use crate::ir::theme::StateSelector;
429
430    let mut single_variants: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
431    let mut combined_variants: HashMap<StateSelector, HashMap<String, String>> = HashMap::new();
432
433    for (key, value) in attrs {
434        // Check if key has state prefix
435        if let Some((prefix, attr_name)) = split_state_prefix(key) {
436            // Try to parse as combined states first
437            if let Some(states) = parse_combined_states(prefix) {
438                if states.len() == 1 {
439                    // Single state
440                    single_variants
441                        .entry(states[0])
442                        .or_default()
443                        .insert(attr_name.to_string(), value.to_string());
444                } else {
445                    // Combined states
446                    let selector = StateSelector::combined(states);
447                    combined_variants
448                        .entry(selector)
449                        .or_default()
450                        .insert(attr_name.to_string(), value.to_string());
451                }
452            } else {
453                return Err(format!("Invalid state prefix: {}", prefix));
454            }
455        }
456    }
457
458    // Parse each single state's properties
459    let mut single_result = HashMap::new();
460    for (state, state_attrs) in single_variants {
461        let style = parse_style_properties_from_attrs(&state_attrs)?;
462        single_result.insert(state, style);
463    }
464
465    // Parse each combined state's properties
466    let mut combined_result = HashMap::new();
467    for (selector, state_attrs) in combined_variants {
468        let style = parse_style_properties_from_attrs(&state_attrs)?;
469        combined_result.insert(selector, style);
470    }
471
472    Ok((single_result, combined_result))
473}
474
475/// Split a state-prefixed attribute name
476/// e.g., "hover:background" -> Some(("hover", "background"))
477/// Also handles combined states: "hover:active:background" -> Some(("hover:active", "background"))
478fn split_state_prefix(key: &str) -> Option<(&str, &str)> {
479    // Find all colons
480    let colons: Vec<usize> = key.match_indices(':').map(|(i, _)| i).collect();
481
482    // The attribute name is after the last colon
483    let last_colon = match colons.last() {
484        Some(&pos) => pos,
485        None => return None,
486    };
487    let attr_name = &key[last_colon + 1..];
488
489    // Check if what comes after the last colon looks like an attribute name
490    // (not a state name like "hover", "active", etc.)
491    let potential_states = &key[..last_colon];
492
493    // Split potential states by ':'
494    let state_parts: Vec<&str> = potential_states.split(':').collect();
495
496    // Verify all parts except the last are valid state names
497    let all_valid_states = state_parts.iter().all(|&s| {
498        matches!(
499            s.trim().to_lowercase().as_str(),
500            "hover" | "focus" | "active" | "disabled"
501        )
502    });
503
504    if all_valid_states && !state_parts.is_empty() {
505        // Return the combined state prefix and attribute name
506        return Some((potential_states, attr_name));
507    }
508
509    None
510}
511
512/// Parse combined state prefix into individual states
513/// e.g., "hover:active" -> vec![WidgetState::Hover, WidgetState::Active]
514fn parse_combined_states(prefix: &str) -> Option<Vec<WidgetState>> {
515    let parts: Vec<&str> = prefix.split(':').collect();
516    let mut states = Vec::new();
517
518    for part in parts {
519        if let Some(state) = WidgetState::from_prefix(part) {
520            // Avoid duplicates
521            if !states.contains(&state) {
522                states.push(state);
523            }
524        } else {
525            return None;
526        }
527    }
528
529    if states.is_empty() {
530        None
531    } else {
532        Some(states)
533    }
534}
535
536/// Parse a theme node from XML
537pub fn parse_theme_from_node(
538    node: roxmltree::Node,
539    _source: &str,
540) -> Result<Theme, crate::parser::error::ParseError> {
541    use crate::parser::error::{ParseError, ParseErrorKind};
542
543    let name = node
544        .attribute("name")
545        .map(|s| s.to_string())
546        .unwrap_or_else(|| "default".to_string());
547
548    let extends = node.attribute("extends").map(|s| s.to_string());
549
550    let mut palette_attrs = HashMap::new();
551    let mut typography_attrs = HashMap::new();
552    let mut spacing_unit = None;
553
554    // Parse child elements
555    for child in node.children() {
556        if child.node_type() != roxmltree::NodeType::Element {
557            continue;
558        }
559
560        let tag = child.tag_name().name();
561
562        if tag == "palette" {
563            for attr in child.attributes() {
564                palette_attrs.insert(attr.name().to_string(), attr.value().to_string());
565            }
566        } else if tag == "typography" {
567            for attr in child.attributes() {
568                typography_attrs.insert(attr.name().to_string(), attr.value().to_string());
569            }
570        } else if tag == "spacing"
571            && let Some(unit) = child.attribute("unit")
572        {
573            spacing_unit = unit.parse::<f32>().ok();
574        }
575    }
576
577    // Parse using existing function
578    let theme = parse_theme(
579        name,
580        &palette_attrs,
581        &typography_attrs,
582        spacing_unit,
583        extends,
584    )
585    .map_err(|e| ParseError {
586        kind: ParseErrorKind::InvalidValue,
587        message: format!("Failed to parse theme: {}", e),
588        span: crate::ir::Span::default(),
589        suggestion: None,
590    })?;
591
592    Ok(theme)
593}
594
595/// Parse a style class node from XML
596pub fn parse_style_class_from_node(
597    node: roxmltree::Node,
598    _source: &str,
599) -> Result<StyleClass, crate::parser::error::ParseError> {
600    use crate::parser::error::{ParseError, ParseErrorKind};
601
602    let name = node
603        .attribute("name")
604        .map(|s| s.to_string())
605        .unwrap_or_default();
606
607    if name.is_empty() {
608        return Err(ParseError {
609            kind: ParseErrorKind::InvalidValue,
610            message: "Style class must have a name".to_string(),
611            span: crate::ir::Span::default(),
612            suggestion: None,
613        });
614    }
615
616    // Collect all attributes
617    let mut base_attrs = HashMap::new();
618    let mut extends = Vec::new();
619    let mut state_variants_raw: HashMap<WidgetState, HashMap<String, String>> = HashMap::new();
620    let mut combined_state_variants_raw: HashMap<
621        crate::ir::theme::StateSelector,
622        HashMap<String, String>,
623    > = HashMap::new();
624    let mut layout = None;
625
626    for attr in node.attributes() {
627        let key = attr.name();
628        let value = attr.value();
629
630        // Check for extends
631        if key == "extends" {
632            extends = value.split_whitespace().map(|s| s.to_string()).collect();
633            continue;
634        }
635
636        // Check for state variants (prefixed attributes)
637        if let Some((prefix, attr_name)) = split_state_prefix(key) {
638            // Try to parse as combined states
639            if let Some(states) = parse_combined_states(prefix) {
640                if states.len() == 1 {
641                    // Single state
642                    let state_attr = state_variants_raw.entry(states[0]).or_default();
643                    state_attr.insert(attr_name.to_string(), value.to_string());
644                } else {
645                    // Combined states
646                    let selector = crate::ir::theme::StateSelector::combined(states);
647                    let state_attr = combined_state_variants_raw.entry(selector).or_default();
648                    state_attr.insert(attr_name.to_string(), value.to_string());
649                }
650            } else {
651                return Err(ParseError {
652                    kind: ParseErrorKind::InvalidValue,
653                    message: format!("Invalid state prefix: {}", prefix),
654                    span: crate::ir::Span::default(),
655                    suggestion: None,
656                });
657            }
658            continue;
659        }
660
661        // Check for layout attributes
662        let layout_attr_names = [
663            "width",
664            "height",
665            "min_width",
666            "max_width",
667            "min_height",
668            "max_height",
669            "padding",
670            "spacing",
671            "align_items",
672            "justify_content",
673            "align_self",
674            "direction",
675        ];
676
677        if layout_attr_names.contains(&key) {
678            base_attrs.insert(key.to_string(), value.to_string());
679            continue;
680        }
681
682        // Regular style attribute
683        base_attrs.insert(key.to_string(), value.to_string());
684    }
685
686    // Parse child elements for state variants and base styles
687    for child in node.children() {
688        if child.node_type() != roxmltree::NodeType::Element {
689            continue;
690        }
691
692        let tag = child.tag_name().name();
693
694        // Handle state variant child elements
695        if let Some(state) = WidgetState::from_prefix(tag) {
696            let state_attr = state_variants_raw.entry(state).or_default();
697            for attr in child.attributes() {
698                state_attr.insert(attr.name().to_string(), attr.value().to_string());
699            }
700            continue;
701        }
702
703        // Handle base element
704        if tag == "base" {
705            for attr in child.attributes() {
706                base_attrs.insert(attr.name().to_string(), attr.value().to_string());
707            }
708            continue;
709        }
710
711        // Handle layout child element
712        if tag == "layout" {
713            let mut layout_attrs = HashMap::new();
714            for attr in child.attributes() {
715                layout_attrs.insert(attr.name().to_string(), attr.value().to_string());
716            }
717            layout = parse_layout_constraints(&layout_attrs).map_err(|e| ParseError {
718                kind: ParseErrorKind::InvalidValue,
719                message: format!("Failed to parse layout: {}", e),
720                span: crate::ir::Span::default(),
721                suggestion: None,
722            })?;
723            continue;
724        }
725    }
726
727    // Parse layout if any layout attributes present
728    if base_attrs.keys().any(|k| {
729        matches!(
730            k.as_str(),
731            "width"
732                | "height"
733                | "min_width"
734                | "max_width"
735                | "min_height"
736                | "max_height"
737                | "padding"
738                | "spacing"
739                | "align_items"
740                | "justify_content"
741                | "align_self"
742                | "direction"
743        )
744    }) {
745        layout = parse_layout_constraints(&base_attrs).map_err(|e| ParseError {
746            kind: ParseErrorKind::InvalidValue,
747            message: format!("Failed to parse layout: {}", e),
748            span: crate::ir::Span::default(),
749            suggestion: None,
750        })?;
751
752        // Remove layout attributes from base_attrs
753        let layout_keys: Vec<String> = base_attrs
754            .keys()
755            .filter(|k| {
756                matches!(
757                    k.as_str(),
758                    "width"
759                        | "height"
760                        | "min_width"
761                        | "max_width"
762                        | "min_height"
763                        | "max_height"
764                        | "padding"
765                        | "spacing"
766                        | "align_items"
767                        | "justify_content"
768                        | "align_self"
769                        | "direction"
770                )
771            })
772            .cloned()
773            .collect();
774
775        for key in layout_keys {
776            base_attrs.remove(&key);
777        }
778    }
779
780    // Parse state variants into StyleProperties
781    let mut state_variants = HashMap::new();
782    for (state, state_attrs) in state_variants_raw {
783        let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
784            kind: ParseErrorKind::InvalidValue,
785            message: format!("Failed to parse state variant for {:?}: {}", state, e),
786            span: crate::ir::Span::default(),
787            suggestion: None,
788        })?;
789        state_variants.insert(state, style);
790    }
791
792    // Parse combined state variants into StyleProperties
793    let mut combined_state_variants = HashMap::new();
794    for (selector, state_attrs) in combined_state_variants_raw {
795        let style = parse_style_properties_from_attrs(&state_attrs).map_err(|e| ParseError {
796            kind: ParseErrorKind::InvalidValue,
797            message: format!(
798                "Failed to parse combined state variant for {:?}: {}",
799                selector, e
800            ),
801            span: crate::ir::Span::default(),
802            suggestion: None,
803        })?;
804        combined_state_variants.insert(selector, style);
805    }
806
807    // Parse using existing function
808    let class = parse_style_class(
809        name,
810        &base_attrs,
811        extends,
812        state_variants,
813        combined_state_variants,
814        layout,
815    )
816    .map_err(|e| ParseError {
817        kind: ParseErrorKind::InvalidValue,
818        message: format!("Failed to parse style class: {}", e),
819        span: crate::ir::Span::default(),
820        suggestion: None,
821    })?;
822
823    Ok(class)
824}