bdf_parser/
properties.rs

1use nom::{
2    branch::alt,
3    bytes::complete::{tag, take_until},
4    character::complete::{multispace0, space1},
5    combinator::{eof, map, map_opt, map_parser, opt},
6    multi::{many0, many1},
7    sequence::delimited,
8    IResult, ParseTo,
9};
10use std::{collections::HashMap, convert::TryFrom};
11use thiserror::Error;
12
13use crate::helpers::*;
14
15/// BDF file property.
16///
17/// Source: https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/XLFD/xlfd.html
18#[derive(Debug, PartialEq, Copy, Clone, Eq, PartialOrd, Ord, strum::Display)]
19#[strum(serialize_all = "shouty_snake_case")]
20pub enum Property {
21    /// ADD_STYLE_NAME
22    AddStyleName,
23    /// AVERAGE_WIDTH
24    AverageWidth,
25    /// AVG_CAPITAL_WIDTH
26    AvgCapitalWidth,
27    /// AVG_LOWERCASE_WIDTH
28    AvgLowercaseWidth,
29    /// AXIS_LIMITS
30    AxisLimits,
31    /// AXIS_NAMES
32    AxisNames,
33    /// AXIS_TYPES
34    AxisTypes,
35    /// CAP_HEIGHT
36    CapHeight,
37    /// CHARSET_ENCODING
38    CharsetEncoding,
39    /// CHARSET_REGISTRY
40    CharsetRegistry,
41    /// COPYRIGHT
42    Copyright,
43    /// DEFAULT_CHAR
44    DefaultChar,
45    /// DESTINATION
46    Destination,
47    /// END_SPACE
48    EndSpace,
49    /// FACE_NAME
50    FaceName,
51    /// FAMILY_NAME
52    FamilyName,
53    /// FIGURE_WIDTH
54    FigureWidth,
55    /// FONT
56    Font,
57    /// FONT_ASCENT
58    FontAscent,
59    /// FONT_DESCENT
60    FontDescent,
61    /// FONT_TYPE
62    FontType,
63    /// FONT_VERSION
64    FontVersion,
65    /// FOUNDRY
66    Foundry,
67    /// FULL_NAME
68    FullName,
69    /// ITALIC_ANGLE
70    ItalicAngle,
71    /// MAX_SPACE
72    MaxSpace,
73    /// MIN_SPACE
74    MinSpace,
75    /// NORM_SPACE
76    NormSpace,
77    /// NOTICE
78    Notice,
79    /// PIXEL_SIZE
80    PixelSize,
81    /// POINT_SIZE
82    PointSize,
83    /// QUAD_WIDTH
84    QuadWidth,
85    /// RASTERIZER_NAME
86    RasterizerName,
87    /// RASTERIZER_VERSION
88    RasterizerVersion,
89    /// RAW_ASCENT
90    RawAscent,
91    /// RAW_DESCENT
92    RawDescent,
93    /// RELATIVE_SETWIDTH
94    RelativeSetwidth,
95    /// RELATIVE_WEIGHT
96    RelativeWeight,
97    /// RESOLUTION
98    Resolution,
99    /// RESOLUTION_X
100    ResolutionX,
101    /// RESOLUTION_Y
102    ResolutionY,
103    /// SETWIDTH_NAME
104    SetwidthName,
105    /// SLANT
106    Slant,
107    /// SMALL_CAP_SIZE
108    SmallCapSize,
109    /// SPACING
110    Spacing,
111    /// STRIKEOUT_ASCENT
112    StrikeoutAscent,
113    /// STRIKEOUT_DESCENT
114    StrikeoutDescent,
115    /// SUBSCRIPT_SIZE
116    SubscriptSize,
117    /// SUBSCRIPT_X
118    SubscriptX,
119    /// SUBSCRIPT_Y
120    SubscriptY,
121    /// SUPERSCRIPT_SIZE
122    SuperscriptSize,
123    /// SUPERSCRIPT_X
124    SuperscriptX,
125    /// SUPERSCRIPT_Y
126    SuperscriptY,
127    /// UNDERLINE_POSITION
128    UnderlinePosition,
129    /// UNDERLINE_THICKNESS
130    UnderlineThickness,
131    /// WEIGHT
132    Weight,
133    /// WEIGHT_NAME
134    WeightName,
135    /// X_HEIGHT
136    XHeight,
137}
138
139/// BDF file properties.
140#[derive(Debug, Clone, PartialEq)]
141pub struct Properties {
142    properties: HashMap<String, PropertyValue>,
143}
144
145impl Properties {
146    pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> {
147        map(
148            opt(map_parser(
149                delimited(
150                    statement("STARTPROPERTIES", parse_to_u32),
151                    take_until("ENDPROPERTIES"),
152                    statement("ENDPROPERTIES", eof),
153                ),
154                many0(property),
155            )),
156            |properties| {
157                // Convert vector of properties into a HashMap
158                let properties = properties
159                    .map(|p| p.iter().cloned().collect())
160                    .unwrap_or_else(HashMap::new);
161
162                Self { properties }
163            },
164        )(input)
165    }
166
167    /// Tries to get a property.
168    ///
169    /// Returns an error if the property doesn't exist or the value has the wrong type.
170    pub fn try_get<T>(&self, property: Property) -> Result<T, PropertyError>
171    where
172        T: for<'a> TryFrom<&'a PropertyValue, Error = PropertyError>,
173    {
174        self.try_get_by_name(&property.to_string())
175    }
176
177    /// Tries to get a property by name.
178    ///
179    /// Returns an error if the property doesn't exist or the value has the wrong type.
180    pub fn try_get_by_name<T>(&self, name: &str) -> Result<T, PropertyError>
181    where
182        T: for<'a> TryFrom<&'a PropertyValue, Error = PropertyError>,
183    {
184        self.properties
185            .get(name)
186            .ok_or_else(|| PropertyError::Undefined(name.to_string()))
187            .and_then(TryFrom::try_from)
188    }
189
190    /// Returns `true` if no properties exist.
191    pub fn is_empty(&self) -> bool {
192        self.properties.is_empty()
193    }
194}
195
196fn property(input: &[u8]) -> IResult<&[u8], (String, PropertyValue)> {
197    let (input, _) = multispace0(input)?;
198    let (input, key) = map_opt(take_until(" "), |s: &[u8]| s.parse_to())(input)?;
199    let (input, _) = space1(input)?;
200    let (input, value) = PropertyValue::parse(input)?;
201    let (input, _) = multispace0(input)?;
202
203    Ok((input, (key, value)))
204}
205
206#[derive(Debug, Clone, PartialEq)]
207pub enum PropertyValue {
208    Text(String),
209    Int(i32),
210}
211
212impl PropertyValue {
213    pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> {
214        alt((Self::parse_string, Self::parse_int))(input)
215    }
216
217    fn parse_string(input: &[u8]) -> IResult<&[u8], PropertyValue> {
218        map(
219            many1(delimited(tag("\""), take_until("\""), tag("\""))),
220            |parts| {
221                let parts: Vec<_> = parts
222                    .iter()
223                    .map(|part| ascii_to_string_lossy(*part))
224                    .collect();
225                PropertyValue::Text(parts.join("\""))
226            },
227        )(input)
228    }
229
230    fn parse_int(input: &[u8]) -> IResult<&[u8], PropertyValue> {
231        map(parse_to_i32, |i| PropertyValue::Int(i))(input)
232    }
233}
234
235impl TryFrom<&PropertyValue> for String {
236    type Error = PropertyError;
237
238    fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
239        match value {
240            PropertyValue::Text(text) => Ok(text.clone()),
241            _ => Err(PropertyError::WrongType),
242        }
243    }
244}
245
246impl TryFrom<&PropertyValue> for i32 {
247    type Error = PropertyError;
248
249    fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
250        match value {
251            PropertyValue::Int(int) => Ok(*int),
252            _ => Err(PropertyError::WrongType),
253        }
254    }
255}
256
257/// Error returned by property getters.
258#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
259pub enum PropertyError {
260    /// Undefined property.
261    #[error("property \"{0}\" is undefined")]
262    Undefined(String),
263    /// Wrong property type.
264    #[error("wrong property type")]
265    WrongType,
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use indoc::indoc;
272
273    #[test]
274    fn parse_property_with_whitespace() {
275        assert_parser_ok!(
276            property(b"KEY   \"VALUE\""),
277            ("KEY".to_string(), PropertyValue::Text("VALUE".to_string()))
278        );
279
280        assert_parser_ok!(
281            property(b"KEY   \"RANDOM WORDS AND STUFF\""),
282            (
283                "KEY".to_string(),
284                PropertyValue::Text("RANDOM WORDS AND STUFF".to_string())
285            )
286        );
287    }
288
289    #[test]
290    fn parse_string_property() {
291        assert_parser_ok!(
292            property(b"KEY \"VALUE\""),
293            ("KEY".to_string(), PropertyValue::Text("VALUE".to_string()))
294        );
295    }
296
297    #[test]
298    fn parse_string_property_with_quote_in_value() {
299        assert_parser_ok!(
300            property(br#"WITH_QUOTE "1""23""""#),
301            (
302                "WITH_QUOTE".to_string(),
303                PropertyValue::Text("1\"23\"".to_string())
304            )
305        );
306    }
307
308    #[test]
309    fn parse_string_property_with_invalid_ascii() {
310        assert_parser_ok!(
311            property(b"KEY \"VALUE\xAB\""),
312            (
313                "KEY".to_string(),
314                PropertyValue::Text("VALUE\u{FFFD}".to_string())
315            )
316        );
317    }
318
319    #[test]
320    fn parse_integer_property() {
321        assert_parser_ok!(
322            property(b"POSITIVE_NUMBER 10"),
323            ("POSITIVE_NUMBER".to_string(), PropertyValue::Int(10i32))
324        );
325
326        assert_parser_ok!(
327            property(b"NEGATIVE_NUMBER -10"),
328            ("NEGATIVE_NUMBER".to_string(), PropertyValue::Int(-10i32))
329        );
330    }
331
332    #[test]
333    fn parse_empty_property_list() {
334        let input = indoc! {br#"
335            STARTPROPERTIES 0
336            ENDPROPERTIES
337        "#};
338
339        let (input, properties) = Properties::parse(input).unwrap();
340        assert_eq!(input, b"");
341        assert!(properties.is_empty());
342    }
343
344    #[test]
345    fn parse_properties() {
346        let input = indoc! {br#"
347            STARTPROPERTIES 2
348            TEXT "FONT"
349            INTEGER 10
350            ENDPROPERTIES
351        "#};
352
353        let (input, properties) = Properties::parse(input).unwrap();
354        assert_eq!(input, b"");
355
356        assert_eq!(properties.properties.len(), 2);
357        assert_eq!(properties.try_get_by_name("TEXT"), Ok("FONT".to_string()));
358        assert_eq!(properties.try_get_by_name("INTEGER"), Ok(10));
359    }
360
361    #[test]
362    fn try_get() {
363        let input = indoc! {br#"
364            STARTPROPERTIES 2
365            FAMILY_NAME "FAMILY"
366            RESOLUTION_X 100
367            RESOLUTION_Y 75
368            ENDPROPERTIES
369        "#};
370
371        let (input, properties) = Properties::parse(input).unwrap();
372        assert_eq!(input, b"");
373
374        assert_eq!(properties.properties.len(), 3);
375        assert_eq!(
376            properties.try_get(Property::FamilyName),
377            Ok("FAMILY".to_string())
378        );
379        assert_eq!(properties.try_get(Property::ResolutionX), Ok(100));
380        assert_eq!(properties.try_get(Property::ResolutionY), Ok(75));
381    }
382
383    #[test]
384    fn property_to_string() {
385        assert_eq!(&Property::Font.to_string(), "FONT");
386        assert_eq!(&Property::SuperscriptX.to_string(), "SUPERSCRIPT_X");
387        assert_eq!(
388            &Property::AvgLowercaseWidth.to_string(),
389            "AVG_LOWERCASE_WIDTH"
390        );
391    }
392}