1
//! Azul-specific CSS properties for advanced layout features
2
//!
3
//! Defines `StyleExclusionMargin` (spacing between text and shape exclusions)
4
//! and `StyleHyphenationLanguage` (BCP 47 language code for automatic hyphenation).
5

            
6
use std::num::ParseFloatError;
7

            
8
#[cfg(feature = "parser")]
9
use crate::macros::*;
10
use crate::{
11
    corety::AzString,
12
    format_rust_code::FormatAsRustCode,
13
    props::{
14
        basic::{length::parse_float_value, FloatValue},
15
        formatter::{FormatAsCssValue, PrintAsCssValue},
16
    },
17
};
18

            
19
/// `-azul-exclusion-margin` property: defines margin around shape exclusions
20
///
21
/// This property controls the spacing between text and shapes that text flows around.
22
/// It's similar to `shape-margin` but specifically for exclusions (text wrapping).
23
///
24
/// # Example
25
/// ```css
26
/// .element {
27
///     -azul-exclusion-margin: 10.5;
28
/// }
29
/// ```
30
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31
#[repr(C)]
32
pub struct StyleExclusionMargin {
33
    pub inner: FloatValue,
34
}
35

            
36
impl Default for StyleExclusionMargin {
37
1
    fn default() -> Self {
38
1
        Self {
39
1
            inner: FloatValue::const_new(0),
40
1
        }
41
1
    }
42
}
43

            
44
impl StyleExclusionMargin {
45
1
    pub fn is_initial(&self) -> bool {
46
1
        self.inner.number == 0
47
1
    }
48
}
49

            
50
impl PrintAsCssValue for StyleExclusionMargin {
51
    fn print_as_css_value(&self) -> String {
52
        format!("{}", self.inner.get())
53
    }
54
}
55

            
56
impl FormatAsCssValue for StyleExclusionMargin {
57
    fn format_as_css_value(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
58
        write!(f, "{}", self.inner.get())
59
    }
60
}
61

            
62
impl FormatAsRustCode for StyleExclusionMargin {
63
    fn format_as_rust_code(&self, _tabs: usize) -> String {
64
        format!(
65
            "StyleExclusionMargin {{ inner: FloatValue::const_new({}) }}",
66
            self.inner.get()
67
        )
68
    }
69
}
70

            
71
#[cfg(feature = "parser")]
72
#[derive(Clone, PartialEq)]
73
pub enum StyleExclusionMarginParseError {
74
    FloatValue(ParseFloatError),
75
}
76

            
77
#[cfg(feature = "parser")]
78
impl_debug_as_display!(StyleExclusionMarginParseError);
79

            
80
#[cfg(feature = "parser")]
81
impl_display! { StyleExclusionMarginParseError, {
82
    FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
83
}}
84

            
85
#[cfg(feature = "parser")]
86
impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);
87

            
88
#[cfg(feature = "parser")]
89
#[derive(Debug, Clone, PartialEq)]
90
#[repr(C, u8)]
91
pub enum StyleExclusionMarginParseErrorOwned {
92
    FloatValue(AzString),
93
}
94

            
95
#[cfg(feature = "parser")]
96
impl StyleExclusionMarginParseError {
97
    pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
98
        match self {
99
            Self::FloatValue(e) => {
100
                StyleExclusionMarginParseErrorOwned::FloatValue(format!("{}", e).into())
101
            }
102
        }
103
    }
104
}
105

            
106
#[cfg(feature = "parser")]
107
impl StyleExclusionMarginParseErrorOwned {
108
    pub fn to_shared(&self) -> StyleExclusionMarginParseError {
109
        match self {
110
            Self::FloatValue(_) => {
111
                // ParseFloatError can't be reconstructed from its display string,
112
                // so we create one by parsing a known-invalid string
113
                StyleExclusionMarginParseError::FloatValue("".parse::<f32>().unwrap_err())
114
            }
115
        }
116
    }
117
}
118

            
119
#[cfg(feature = "parser")]
120
2
pub fn parse_style_exclusion_margin(
121
2
    input: &str,
122
2
) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
123
2
    parse_float_value(input)
124
2
        .map(|inner| StyleExclusionMargin { inner })
125
2
        .map_err(StyleExclusionMarginParseError::FloatValue)
126
2
}
127

            
128
/// `-azul-hyphenation-language` property: specifies language for hyphenation
129
///
130
/// This property defines the language code (BCP 47 format) used for automatic
131
/// hyphenation. Examples: "en-US", "de-DE", "fr-FR"
132
///
133
/// # Example
134
/// ```css
135
/// .element {
136
///     -azul-hyphenation-language: "en-US";
137
/// }
138
/// ```
139
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
140
#[repr(C)]
141
pub struct StyleHyphenationLanguage {
142
    pub inner: AzString,
143
}
144

            
145
impl Default for StyleHyphenationLanguage {
146
1
    fn default() -> Self {
147
1
        Self {
148
1
            inner: AzString::from_const_str("en-US"),
149
1
        }
150
1
    }
151
}
152

            
153
impl StyleHyphenationLanguage {
154
    pub fn is_initial(&self) -> bool {
155
        self.inner.as_str() == "en-US"
156
    }
157
}
158

            
159
impl PrintAsCssValue for StyleHyphenationLanguage {
160
    fn print_as_css_value(&self) -> String {
161
        format!("\"{}\"", self.inner.as_str())
162
    }
163
}
164

            
165
impl FormatAsCssValue for StyleHyphenationLanguage {
166
    fn format_as_css_value(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
167
        write!(f, "\"{}\"", self.inner.as_str())
168
    }
169
}
170

            
171
impl FormatAsRustCode for StyleHyphenationLanguage {
172
    fn format_as_rust_code(&self, _tabs: usize) -> String {
173
        format!(
174
            "StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
175
            self.inner.as_str()
176
        )
177
    }
178
}
179

            
180
#[cfg(feature = "parser")]
181
#[derive(Clone, PartialEq)]
182
pub enum StyleHyphenationLanguageParseError {
183
    InvalidString(String),
184
}
185

            
186
#[cfg(feature = "parser")]
187
impl_debug_as_display!(StyleHyphenationLanguageParseError);
188

            
189
#[cfg(feature = "parser")]
190
impl_display! { StyleHyphenationLanguageParseError, {
191
    InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
192
}}
193

            
194
#[cfg(feature = "parser")]
195
#[derive(Debug, Clone, PartialEq)]
196
#[repr(C, u8)]
197
pub enum StyleHyphenationLanguageParseErrorOwned {
198
    InvalidString(AzString),
199
}
200

            
201
#[cfg(feature = "parser")]
202
impl StyleHyphenationLanguageParseError {
203
    pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
204
        match self {
205
            Self::InvalidString(e) => {
206
                StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
207
            }
208
        }
209
    }
210
}
211

            
212
#[cfg(feature = "parser")]
213
impl StyleHyphenationLanguageParseErrorOwned {
214
    pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
215
        match self {
216
            Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
217
        }
218
    }
219
}
220

            
221
#[cfg(feature = "parser")]
222
11
pub fn parse_style_hyphenation_language(
223
11
    input: &str,
224
11
) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
225
    // Remove quotes if present
226
11
    let trimmed = input.trim();
227
11
    let unquoted = if (trimmed.starts_with('"') && trimmed.ends_with('"'))
228
10
        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
229
    {
230
2
        &trimmed[1..trimmed.len() - 1]
231
    } else {
232
9
        trimmed
233
    };
234

            
235
    // Basic BCP 47 validation: non-empty, ASCII alphanumeric + hyphens, no leading/trailing hyphens
236
11
    if unquoted.is_empty()
237
43
        || !unquoted.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
238
8
        || unquoted.starts_with('-')
239
7
        || unquoted.ends_with('-')
240
    {
241
5
        return Err(StyleHyphenationLanguageParseError::InvalidString(
242
5
            unquoted.to_string(),
243
5
        ));
244
6
    }
245

            
246
6
    Ok(StyleHyphenationLanguage {
247
6
        inner: AzString::from_string(unquoted.to_string()),
248
6
    })
249
11
}
250

            
251
#[cfg(test)]
252
mod tests {
253
    use super::*;
254

            
255
    #[test]
256
1
    fn test_parse_exclusion_margin() {
257
1
        let margin = parse_style_exclusion_margin("10.5").unwrap();
258
1
        assert_eq!(margin.inner.get(), 10.5);
259

            
260
1
        let margin = parse_style_exclusion_margin("0").unwrap();
261
1
        assert_eq!(margin.inner.get(), 0.0);
262
1
    }
263

            
264
    #[test]
265
1
    fn test_parse_hyphenation_language() {
266
1
        let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
267
1
        assert_eq!(lang.inner.as_str(), "en-US");
268

            
269
1
        let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
270
1
        assert_eq!(lang.inner.as_str(), "de-DE");
271

            
272
1
        let lang = parse_style_hyphenation_language("fr-FR").unwrap();
273
1
        assert_eq!(lang.inner.as_str(), "fr-FR");
274

            
275
1
        let lang = parse_style_hyphenation_language("zh").unwrap();
276
1
        assert_eq!(lang.inner.as_str(), "zh");
277

            
278
1
        let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
279
1
        assert_eq!(lang.inner.as_str(), "sr-Latn-RS");
280

            
281
        // Double hyphen is permitted by the current ASCII/format rules.
282
1
        let lang = parse_style_hyphenation_language("en--US").unwrap();
283
1
        assert_eq!(lang.inner.as_str(), "en--US");
284
1
    }
285

            
286
    #[test]
287
1
    fn test_parse_hyphenation_language_invalid() {
288
1
        assert!(matches!(
289
1
            parse_style_hyphenation_language(""),
290
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
291
        ));
292
1
        assert!(matches!(
293
1
            parse_style_hyphenation_language("-en"),
294
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
295
        ));
296
1
        assert!(matches!(
297
1
            parse_style_hyphenation_language("en-"),
298
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
299
        ));
300
1
        assert!(matches!(
301
1
            parse_style_hyphenation_language("en_US"),
302
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
303
        ));
304
1
        assert!(matches!(
305
1
            parse_style_hyphenation_language("日本語"),
306
            Err(StyleHyphenationLanguageParseError::InvalidString(_))
307
        ));
308
1
    }
309

            
310
    #[test]
311
1
    fn test_exclusion_margin_default() {
312
1
        let margin = StyleExclusionMargin::default();
313
1
        assert_eq!(margin.inner.get(), 0.0);
314
1
        assert!(margin.is_initial());
315
1
    }
316

            
317
    #[test]
318
1
    fn test_hyphenation_language_default() {
319
1
        let lang = StyleHyphenationLanguage::default();
320
1
        assert_eq!(lang.inner.as_str(), "en-US");
321
1
    }
322
}