termimad/serde/
serde_skin.rs

1use {
2    super::ScrollBarStyleDef,
3    crate::{
4        minimad::Alignment,
5        parse_compound_style,
6        parse_line_style,
7        parse_styled_char,
8        LineStyle,
9        MadSkin,
10        TableBorderChars,
11        ATTRIBUTES,
12    },
13    serde::{
14        de,
15        ser::SerializeMap,
16        Deserialize,
17        Serialize,
18        Serializer,
19    },
20    std::fmt,
21};
22
23impl<'de> de::Deserialize<'de> for MadSkin {
24    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
25    where
26        D: de::Deserializer<'de>,
27    {
28        struct SkinVisitor;
29
30        impl<'de> de::Visitor<'de> for SkinVisitor {
31            type Value = MadSkin;
32
33            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
34                formatter.write_str("MadSkin")
35            }
36
37            fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
38            where
39                V: de::MapAccess<'de>,
40            {
41                let mut skin = MadSkin::default();
42                while let Some(key) = map.next_key::<String>()? {
43                    match key.as_str() {
44                        // inline styles
45                        "bold" => {
46                            let value = map.next_value::<String>()?;
47                            let cs = parse_compound_style(&value).map_err(de::Error::custom)?;
48                            skin.bold = cs;
49                        }
50                        "italic" => {
51                            let value = map.next_value::<String>()?;
52                            let cs = parse_compound_style(&value).map_err(de::Error::custom)?;
53                            skin.italic = cs;
54                        }
55                        "strikeout" => {
56                            let value = map.next_value::<String>()?;
57                            let cs = parse_compound_style(&value).map_err(de::Error::custom)?;
58                            skin.strikeout = cs;
59                        }
60                        "inline_code" | "inline-code" => {
61                            let value = map.next_value::<String>()?;
62                            let cs = parse_compound_style(&value).map_err(de::Error::custom)?;
63                            skin.inline_code = cs;
64                        }
65                        "ellipsis" => {
66                            let value = map.next_value::<String>()?;
67                            let cs = parse_compound_style(&value).map_err(de::Error::custom)?;
68                            skin.ellipsis = cs;
69                        }
70
71                        // marker chars
72                        "bullet" => {
73                            let value = map.next_value::<String>()?;
74                            let sc = parse_styled_char(&value, '*').map_err(de::Error::custom)?;
75                            skin.bullet = sc;
76                        }
77                        "quote_mark" | "quote" | "quote-mark" => {
78                            let value = map.next_value::<String>()?;
79                            let sc = parse_styled_char(&value, '*').map_err(de::Error::custom)?;
80                            skin.quote_mark = sc;
81                        }
82                        "horizontal_rule" | "horizontal-rule" | "rule" => {
83                            let value = map.next_value::<String>()?;
84                            let sc = parse_styled_char(&value, '*').map_err(de::Error::custom)?;
85                            skin.horizontal_rule = sc;
86                        }
87
88                        // scrollbar
89                        "scrollbar" => {
90                            let def: ScrollBarStyleDef = map.next_value()?;
91                            skin.scrollbar = def.into_scrollbar_style();
92                        }
93
94                        // line styles
95                        "paragraph" => {
96                            let value = map.next_value::<String>()?;
97                            let ls = parse_line_style(&value).map_err(de::Error::custom)?;
98                            skin.paragraph = ls;
99                        }
100                        "code_block" | "code-block" => {
101                            let value = map.next_value::<String>()?;
102                            let ls = parse_line_style(&value).map_err(de::Error::custom)?;
103                            skin.code_block = ls;
104                        }
105                        "table" => {
106                            let value = map.next_value::<String>()?;
107                            let ls = parse_line_style(&value).map_err(de::Error::custom)?;
108                            skin.table = ls;
109                        }
110
111                        // headers
112                        "headers" => match map.next_value::<HeadersStyleInfo>()? {
113                            HeadersStyleInfo::Add(ls) => {
114                                for h in &mut skin.headers {
115                                    if let Some(fg) = ls.compound_style.get_fg() {
116                                        h.compound_style.set_fg(fg);
117                                    }
118                                    if let Some(bg) = ls.compound_style.get_bg() {
119                                        h.compound_style.set_bg(bg);
120                                    }
121                                    for &attr in ATTRIBUTES {
122                                        if ls.compound_style.has_attr(attr) {
123                                            h.compound_style.add_attr(attr);
124                                        }
125                                    }
126                                    if ls.align != Alignment::Unspecified {
127                                        h.align = ls.align;
128                                    }
129                                }
130                            }
131                            HeadersStyleInfo::Levels(mut vls) => {
132                                for (lvl, h) in vls.drain(..).enumerate() {
133                                    if lvl < skin.headers.len() {
134                                        skin.headers[lvl] = h;
135                                    }
136                                }
137                            }
138                        },
139
140                        // table border chars
141                        // There's currently no way to allow custom table border
142                        // chars. It would require a change in MadSkin: either
143                        // make it require a lifetime, or use a Cow for the border
144                        // chars
145                        "table_border_chars" | "table-border-chars" => {
146                            let key = map.next_value::<String>()?;
147                            if let Some(chars) = TableBorderChars::by_key(&key) {
148                                skin.table_border_chars = chars;
149                            }
150                        }
151
152                        _ => {
153                            let _ = map.next_value::<String>()?;
154                            println!("unknown key: {key}");
155                        }
156                    }
157                }
158                Ok(skin)
159            }
160        }
161
162        deserializer.deserialize_map(SkinVisitor {})
163    }
164}
165
166impl Serialize for MadSkin {
167    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
168    where
169        S: Serializer,
170    {
171        let mut skin = serializer.serialize_map(None)?;
172
173        // inline styles
174        skin.serialize_entry("bold", &self.bold)?;
175        skin.serialize_entry("italic", &self.italic)?;
176        skin.serialize_entry("strikeout", &self.strikeout)?;
177        skin.serialize_entry("inline_code", &self.inline_code)?;
178        skin.serialize_entry("ellipsis", &self.ellipsis)?;
179
180        // marker chars
181        skin.serialize_entry("bullet", &self.bullet)?;
182        skin.serialize_entry("quote", &self.quote_mark)?;
183        skin.serialize_entry("horizontal_rule", &self.horizontal_rule)?;
184
185        // scrollbar
186        let def: ScrollBarStyleDef = (&self.scrollbar).into();
187        skin.serialize_entry("scrollbar", &def)?;
188
189        // line styles
190        skin.serialize_entry("paragraph", &self.paragraph)?;
191        skin.serialize_entry("code_block", &self.code_block)?;
192        skin.serialize_entry("table", &self.table)?;
193
194        // headers
195        skin.serialize_entry("headers", &self.headers)?;
196
197        // table border chars
198        // There's currently no way to allow custom
199        if let Some(key) = self.table_border_chars.key() {
200            skin.serialize_entry("table_border_chars", key)?;
201        }
202
203        skin.end()
204    }
205}
206
207#[derive(Deserialize)]
208#[serde(untagged)]
209enum HeadersStyleInfo {
210    Add(LineStyle),
211    Levels(Vec<LineStyle>),
212}
213
214/// Check that serializing a skin in JSON, then deserializing this
215/// JSON into a new skin, results in an identical skin.
216#[test]
217fn skin_json_roundtrip() {
218    use {
219        crate::{
220            crossterm::style::{
221                Attribute,
222                Color::*,
223            },
224            gray,
225            rgb,
226            StyledChar,
227            ROUNDED_TABLE_BORDER_CHARS,
228        },
229        pretty_assertions::assert_eq,
230    };
231
232    let skin = MadSkin::default();
233    let serialized = serde_json::to_string_pretty(&skin).unwrap();
234    let deserialized = serde_json::from_str(&serialized).unwrap();
235    assert_eq!(skin, deserialized);
236
237    let mut skin = MadSkin::no_style();
238    skin.limit_to_ascii();
239    let serialized = serde_json::to_string_pretty(&skin).unwrap();
240    let deserialized = serde_json::from_str(&serialized).unwrap();
241    assert_eq!(skin, deserialized);
242
243    let skin = MadSkin::default_dark();
244    let serialized = serde_json::to_string_pretty(&skin).unwrap();
245    let deserialized = serde_json::from_str(&serialized).unwrap();
246    assert_eq!(skin, deserialized);
247
248    let skin = MadSkin::default_light();
249    let serialized = serde_json::to_string_pretty(&skin).unwrap();
250    let deserialized = serde_json::from_str(&serialized).unwrap();
251    assert_eq!(skin, deserialized);
252
253    let mut skin = MadSkin::default();
254    skin.set_headers_fg(AnsiValue(178));
255    skin.headers[2].set_fg(gray(22));
256    skin.bold.set_fg(Yellow);
257    skin.italic.set_fgbg(Magenta, rgb(30, 30, 40));
258    skin.bullet = StyledChar::from_fg_char(Yellow, '⟡');
259    skin.quote_mark.set_fg(Yellow);
260    skin.italic.set_fg(Magenta);
261    skin.scrollbar.thumb.set_fg(AnsiValue(178));
262    skin.table_border_chars = ROUNDED_TABLE_BORDER_CHARS;
263    skin.paragraph.align = Alignment::Center;
264    skin.table.align = Alignment::Center;
265    skin.inline_code.add_attr(Attribute::Reverse);
266    skin.paragraph.set_fgbg(Magenta, rgb(30, 30, 40));
267    skin.italic.add_attr(Attribute::Underlined);
268    skin.italic.add_attr(Attribute::OverLined);
269    let serialized = serde_json::to_string_pretty(&skin).unwrap();
270    let deserialized = serde_json::from_str(&serialized).unwrap();
271    assert_eq!(skin, deserialized);
272}