Skip to main content

palette_core/
style.rs

1//! Text style modifiers for syntax tokens.
2//!
3//! [`SyntaxStyles`](crate::style::SyntaxStyles) mirrors the field names of
4//! [`SyntaxColors`](crate::palette::SyntaxColors), each slot an
5//! `Option<StyleModifiers>`. Resolution through
6//! [`ResolvedSyntaxStyles`](crate::style::ResolvedSyntaxStyles) applies the
7//! same parent→child fallback chain as colors, defaulting to no modifiers.
8
9use std::sync::Arc;
10
11use crate::error::PaletteError;
12use crate::manifest::ManifestSection;
13
14/// Text style modifiers for a single syntax token.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16#[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
17pub struct StyleModifiers {
18    /// Render the token in bold weight.
19    pub bold: bool,
20    /// Render the token in italic style.
21    pub italic: bool,
22    /// Render the token with an underline.
23    pub underline: bool,
24}
25
26impl StyleModifiers {
27    /// Parse a comma-separated modifier string (e.g. `"bold,italic"`).
28    ///
29    /// Whitespace around each modifier name is trimmed. An empty string
30    /// or unknown modifier name returns [`PaletteError::InvalidStyle`].
31    pub fn parse(s: &str, section: &str, field: &str) -> Result<Self, PaletteError> {
32        let trimmed = s.trim();
33        if trimmed.is_empty() {
34            return Err(PaletteError::InvalidStyle {
35                section: Arc::from(section),
36                field: Arc::from(field),
37                value: Arc::from(s),
38            });
39        }
40
41        let mut result = Self::default();
42        for part in trimmed.split(',') {
43            match part.trim() {
44                "bold" => result.bold = true,
45                "italic" => result.italic = true,
46                "underline" => result.underline = true,
47                _ => {
48                    return Err(PaletteError::InvalidStyle {
49                        section: Arc::from(section),
50                        field: Arc::from(field),
51                        value: Arc::from(s),
52                    });
53                }
54            }
55        }
56        Ok(result)
57    }
58
59    /// True when no modifiers are set.
60    pub fn is_empty(self) -> bool {
61        !self.bold && !self.italic && !self.underline
62    }
63
64    /// CSS value string for the combined modifiers.
65    ///
66    /// Returns `"normal"` when empty, otherwise space-separated names
67    /// like `"bold"`, `"italic"`, or `"bold italic"`.
68    pub fn to_css_value(self) -> &'static str {
69        match (self.bold, self.italic, self.underline) {
70            (false, false, false) => "normal",
71            (true, false, false) => "bold",
72            (false, true, false) => "italic",
73            (false, false, true) => "underline",
74            (true, true, false) => "bold italic",
75            (true, false, true) => "bold underline",
76            (false, true, true) => "italic underline",
77            (true, true, true) => "bold italic underline",
78        }
79    }
80}
81
82impl std::fmt::Display for StyleModifiers {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.write_str(self.to_css_value())
85    }
86}
87
88fn resolve_style(
89    section: &ManifestSection,
90    section_name: &str,
91    field: &str,
92) -> Result<Option<StyleModifiers>, PaletteError> {
93    match section.get(field) {
94        None => Ok(None),
95        Some(val) => StyleModifiers::parse(val, section_name, field).map(Some),
96    }
97}
98
99macro_rules! style_group {
100    ($(#[$_meta:meta])* $_color_type:ident { $($field:ident),+ $(,)? }) => {
101        /// Unresolved syntax style modifiers — each slot is `Option<StyleModifiers>`.
102        ///
103        /// Field names match [`SyntaxColors`](crate::palette::SyntaxColors) exactly.
104        #[derive(Debug, Clone, Default, PartialEq, Eq)]
105        #[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
106        pub struct SyntaxStyles {
107            $(
108                #[doc = concat!("`", stringify!($field), "` slot.")]
109                pub $field: Option<StyleModifiers>,
110            )+
111        }
112
113        impl SyntaxStyles {
114            /// Parse a `[syntax_style]` manifest section into style modifiers.
115            pub fn from_section(
116                section: &ManifestSection,
117                section_name: &str,
118            ) -> Result<Self, PaletteError> {
119                Ok(Self {
120                    $($field: resolve_style(section, section_name, stringify!($field))?,)+
121                })
122            }
123
124            /// Merge two style groups, preferring `self` values over `fallback`.
125            pub fn merge(&self, fallback: &Self) -> Self {
126                Self {
127                    $($field: self.$field.or(fallback.$field),)+
128                }
129            }
130
131            /// Iterate over slots that have a style assigned.
132            pub fn populated_slots(&self) -> impl Iterator<Item = (&'static str, &StyleModifiers)> {
133                [$(
134                    (stringify!($field), self.$field.as_ref()),
135                )+]
136                .into_iter()
137                .filter_map(|(name, style)| style.map(|s| (name, s)))
138            }
139        }
140    };
141}
142
143crate::palette::syntax_fields!(style_group);
144
145macro_rules! resolved_style_group {
146    ($(#[$_meta:meta])* $_color_type:ident { $($field:ident),+ $(,)? }) => {
147        /// Resolved syntax styles where every slot is a concrete [`StyleModifiers`].
148        ///
149        /// Built via [`ResolvedSyntaxStyles::from_group_with_fallback`], which applies
150        /// the same parent→child fallback chain as
151        /// [`ResolvedSyntaxColors`](crate::resolved::ResolvedSyntaxColors).
152        /// Absent slots default to [`StyleModifiers::default`] (no modifiers).
153        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
154        #[cfg_attr(feature = "snapshot", derive(serde::Serialize))]
155        pub struct ResolvedSyntaxStyles {
156            $(
157                #[doc = concat!("`", stringify!($field), "` slot.")]
158                pub $field: StyleModifiers,
159            )+
160        }
161
162        impl ResolvedSyntaxStyles {
163            fn from_group(group: &SyntaxStyles) -> Self {
164                Self {
165                    $($field: group.$field.unwrap_or_default(),)+
166                }
167            }
168
169            /// Iterate over all slots as `(name, &StyleModifiers)` pairs.
170            pub fn all_slots(&self) -> impl Iterator<Item = (&'static str, &StyleModifiers)> {
171                [$(
172                    (stringify!($field), &self.$field),
173                )+]
174                .into_iter()
175            }
176        }
177    };
178}
179
180crate::palette::syntax_fields!(resolved_style_group);
181
182impl ResolvedSyntaxStyles {
183    /// Resolve styles with parent→child fallback, mirroring
184    /// [`ResolvedSyntaxColors::from_group_with_fallback`](crate::resolved::ResolvedSyntaxColors::from_group_with_fallback).
185    pub fn from_group_with_fallback(group: &SyntaxStyles) -> Self {
186        let mut resolved = Self::from_group(group);
187        crate::palette::resolve_syntax_fallback!(resolved, group);
188        resolved
189    }
190}