Skip to main content

cdx_core/presentation/
continuous.rs

1//! Continuous presentation layer.
2
3use serde::{Deserialize, Serialize};
4
5use super::style::{CssValue, Style, StyleMap};
6
7/// Continuous presentation for screen reading.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct Continuous {
10    /// Format version.
11    pub version: String,
12
13    /// Presentation type (always "continuous").
14    #[serde(rename = "type")]
15    pub presentation_type: String,
16
17    /// Default settings.
18    pub defaults: ContinuousDefaults,
19
20    /// Content sections.
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub sections: Vec<Section>,
23
24    /// Style definitions.
25    #[serde(default, skip_serializing_if = "StyleMap::is_empty")]
26    pub styles: StyleMap,
27}
28
29impl Default for Continuous {
30    fn default() -> Self {
31        Self {
32            version: crate::SPEC_VERSION.to_string(),
33            presentation_type: "continuous".to_string(),
34            defaults: ContinuousDefaults::default(),
35            sections: Vec::new(),
36            styles: StyleMap::new(),
37        }
38    }
39}
40
41/// Default settings for continuous presentation.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ContinuousDefaults {
45    /// Maximum content width.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub max_width: Option<CssValue>,
48
49    /// Content padding.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub padding: Option<CssValue>,
52
53    /// Default font family.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub font_family: Option<String>,
56
57    /// Default font size.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub font_size: Option<CssValue>,
60
61    /// Default line height.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub line_height: Option<CssValue>,
64}
65
66impl Default for ContinuousDefaults {
67    fn default() -> Self {
68        Self {
69            max_width: Some(CssValue::String("800px".to_string())),
70            padding: Some(CssValue::String("24px".to_string())),
71            font_family: None,
72            font_size: None,
73            line_height: None,
74        }
75    }
76}
77
78/// A section of content in continuous presentation.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct Section {
82    /// Block references in this section.
83    pub block_refs: Vec<String>,
84
85    /// Style name to apply.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub style: Option<String>,
88
89    /// Inline style attributes.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub attributes: Option<Style>,
92}
93
94impl Section {
95    /// Create a new section with block references.
96    #[must_use]
97    pub fn new(block_refs: Vec<String>) -> Self {
98        Self {
99            block_refs,
100            style: None,
101            attributes: None,
102        }
103    }
104
105    /// Create a section with a named style.
106    #[must_use]
107    pub fn with_style(block_refs: Vec<String>, style: impl Into<String>) -> Self {
108        Self {
109            block_refs,
110            style: Some(style.into()),
111            attributes: None,
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_continuous_default() {
122        let c = Continuous::default();
123        assert_eq!(c.presentation_type, "continuous");
124        assert!(matches!(
125            c.defaults.max_width,
126            Some(CssValue::String(ref s)) if s == "800px"
127        ));
128    }
129
130    #[test]
131    fn test_section() {
132        let section = Section::with_style(vec!["block-1".to_string()], "intro");
133        assert_eq!(section.block_refs, vec!["block-1"]);
134        assert_eq!(section.style, Some("intro".to_string()));
135    }
136
137    #[test]
138    fn test_serialization() {
139        let c = Continuous::default();
140        let json = serde_json::to_string_pretty(&c).unwrap();
141        assert!(json.contains("\"type\": \"continuous\""));
142        assert!(json.contains("\"maxWidth\": \"800px\""));
143    }
144
145    #[test]
146    fn test_deserialization() {
147        let json = r#"{
148            "version": "0.1",
149            "type": "continuous",
150            "defaults": {
151                "maxWidth": "720px",
152                "padding": "20px",
153                "fontFamily": "Georgia, serif",
154                "fontSize": "18px",
155                "lineHeight": 1.7
156            },
157            "styles": {
158                "heading1": {
159                    "fontSize": "2.5rem",
160                    "fontWeight": 700
161                }
162            }
163        }"#;
164
165        let c: Continuous = serde_json::from_str(json).unwrap();
166        assert_eq!(c.defaults.font_family, Some("Georgia, serif".to_string()));
167        assert!(c.styles.contains_key("heading1"));
168    }
169}