mottle 0.5.0

Create themes for VS Code with ease
Documentation
pub mod semantic;
pub mod textmate;

use indexmap::IndexMap;
use serde::Serialize;
use std::borrow::Cow;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Theme {
    pub name: String,
    #[serde(rename = "tokenColors")]
    pub textmate_rules: Vec<textmate::Rule>,
    #[serde(flatten)]
    pub semantic_highlighting: semantic::Highlighting,
    #[serde(rename = "colors")]
    pub workbench_rules: IndexMap<Cow<'static, str>, Color>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Color {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl Serialize for Color {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        if self.a == 0xFF {
            serializer.collect_str(&format_args!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b))
        } else {
            serializer.collect_str(&format_args!(
                "#{:02X}{:02X}{:02X}{:02X}",
                self.r, self.g, self.b, self.a
            ))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use expect_test::{expect, Expect};
    use indexmap::IndexMap;

    fn check(theme: Theme, expect: Expect) {
        expect.assert_eq(&crate::serialize_theme(&theme));
    }

    #[test]
    fn completely_empty() {
        check(
            Theme {
                name: "My cool theme".to_string(),
                textmate_rules: Vec::new(),
                semantic_highlighting: semantic::Highlighting::Off,
                workbench_rules: IndexMap::new(),
            },
            expect![[r#"
                // Do not edit directly; this file is generated.
                {
                    "name": "My cool theme",
                    "tokenColors": [],
                    "semanticHighlighting": false,
                    "colors": {}
                }
            "#]],
        );
    }

    #[test]
    fn empty_semantic() {
        check(
            Theme {
                name: "My cool theme".to_string(),
                textmate_rules: Vec::new(),
                semantic_highlighting: semantic::Highlighting::On { rules: IndexMap::new() },
                workbench_rules: IndexMap::new(),
            },
            expect![[r#"
                // Do not edit directly; this file is generated.
                {
                    "name": "My cool theme",
                    "tokenColors": [],
                    "semanticHighlighting": true,
                    "semanticTokenColors": {},
                    "colors": {}
                }
            "#]],
        );
    }

    #[test]
    fn basic_textmate() {
        check(
            Theme {
                name: "My cool theme".to_string(),
                textmate_rules: vec![textmate::Rule {
                    scope: vec!["entity.function.name".to_string()],
                    settings: textmate::RuleSettings {
                        foreground: Some(Color { r: 156, g: 219, b: 222, a: 255 }),
                        font_style: textmate::FontStyle::Inherit,
                    },
                }],
                semantic_highlighting: semantic::Highlighting::Off,
                workbench_rules: IndexMap::new(),
            },
            expect![[r##"
                // Do not edit directly; this file is generated.
                {
                    "name": "My cool theme",
                    "tokenColors": [
                        {
                            "scope": [
                                "entity.function.name"
                            ],
                            "settings": {
                                "foreground": "#9CDBDE"
                            }
                        }
                    ],
                    "semanticHighlighting": false,
                    "colors": {}
                }
            "##]],
        );
    }

    #[test]
    fn textmate_with_font_styles() {
        check(
            Theme {
                name: "My cool theme".to_string(),
                textmate_rules: vec![
                    textmate::Rule {
                        scope: vec!["storage".to_string()],
                        settings: textmate::RuleSettings {
                            foreground: Some(Color { r: 0, g: 0, b: 0, a: 255 }),
                            font_style: textmate::FontStyle::Set {
                                bold: true,
                                italic: true,
                                underline: false,
                            },
                        },
                    },
                    textmate::Rule {
                        scope: vec!["entity".to_string()],
                        settings: textmate::RuleSettings {
                            foreground: None,
                            font_style: textmate::FontStyle::Set {
                                bold: false,
                                italic: true,
                                underline: false,
                            },
                        },
                    },
                ],
                semantic_highlighting: semantic::Highlighting::Off,
                workbench_rules: IndexMap::new(),
            },
            expect![[r##"
                // Do not edit directly; this file is generated.
                {
                    "name": "My cool theme",
                    "tokenColors": [
                        {
                            "scope": [
                                "storage"
                            ],
                            "settings": {
                                "foreground": "#000000",
                                "fontStyle": "italic bold"
                            }
                        },
                        {
                            "scope": [
                                "entity"
                            ],
                            "settings": {
                                "fontStyle": "italic"
                            }
                        }
                    ],
                    "semanticHighlighting": false,
                    "colors": {}
                }
            "##]],
        );
    }

    #[test]
    fn semantic() {
        let mut rules = IndexMap::new();

        rules.insert(
            semantic::Selector {
                kind: semantic::TokenKind::Wildcard,
                modifiers: vec![semantic::Identifier::new("unsafe").unwrap()],
                language: None,
            },
            semantic::Style {
                foreground: Some(Color { r: 255, g: 0, b: 0, a: 255 }),
                font_style: semantic::FontStyle {
                    bold: semantic::FontStyleSetting::True,
                    italic: semantic::FontStyleSetting::Inherit,
                    underline: semantic::FontStyleSetting::Inherit,
                },
            },
        );

        rules.insert(
            semantic::Selector {
                kind: semantic::TokenKind::Wildcard,
                modifiers: vec![semantic::Identifier::new("mutable").unwrap()],
                language: None,
            },
            semantic::Style {
                foreground: None,
                font_style: semantic::FontStyle {
                    bold: semantic::FontStyleSetting::Inherit,
                    italic: semantic::FontStyleSetting::Inherit,
                    underline: semantic::FontStyleSetting::True,
                },
            },
        );

        rules.insert(
            semantic::Selector {
                kind: semantic::TokenKind::Specific(semantic::Identifier::new("variable").unwrap()),
                modifiers: Vec::new(),
                language: Some(semantic::Identifier::new("rust").unwrap()),
            },
            semantic::Style {
                foreground: Some(Color { r: 224, g: 224, b: 201, a: 255 }),
                font_style: semantic::FontStyle {
                    bold: semantic::FontStyleSetting::Inherit,
                    italic: semantic::FontStyleSetting::Inherit,
                    underline: semantic::FontStyleSetting::Inherit,
                },
            },
        );

        rules.insert(
            semantic::Selector {
                kind: semantic::TokenKind::Specific(semantic::Identifier::new("function").unwrap()),
                modifiers: vec![
                    semantic::Identifier::new("declaration").unwrap(),
                    semantic::Identifier::new("public").unwrap(),
                ],
                language: None,
            },
            semantic::Style {
                foreground: Some(Color { r: 156, g: 219, b: 222, a: 255 }),
                font_style: semantic::FontStyle {
                    bold: semantic::FontStyleSetting::Inherit,
                    italic: semantic::FontStyleSetting::Inherit,
                    underline: semantic::FontStyleSetting::Inherit,
                },
            },
        );

        check(
            Theme {
                name: "My cool theme".to_string(),
                textmate_rules: Vec::new(),
                semantic_highlighting: semantic::Highlighting::On { rules },
                workbench_rules: IndexMap::new(),
            },
            expect![[r##"
                // Do not edit directly; this file is generated.
                {
                    "name": "My cool theme",
                    "tokenColors": [],
                    "semanticHighlighting": true,
                    "semanticTokenColors": {
                        "*.unsafe": {
                            "foreground": "#FF0000",
                            "bold": true
                        },
                        "*.mutable": {
                            "underline": true
                        },
                        "variable:rust": {
                            "foreground": "#E0E0C9"
                        },
                        "function.declaration.public": {
                            "foreground": "#9CDBDE"
                        }
                    },
                    "colors": {}
                }
            "##]],
        );
    }

    #[test]
    fn workbench_rules() {
        let mut workbench_rules = IndexMap::new();
        workbench_rules
            .insert(Cow::Borrowed("editor.foreground"), Color { r: 255, g: 0, b: 0, a: 255 });

        check(
            Theme {
                name: "My cool theme".to_string(),
                textmate_rules: Vec::new(),
                semantic_highlighting: semantic::Highlighting::Off,
                workbench_rules,
            },
            expect![[r##"
                // Do not edit directly; this file is generated.
                {
                    "name": "My cool theme",
                    "tokenColors": [],
                    "semanticHighlighting": false,
                    "colors": {
                        "editor.foreground": "#FF0000"
                    }
                }
            "##]],
        );
    }
}