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#[derive(Debug, PartialEq, Copy, Clone, Eq, PartialOrd, Ord, strum::Display)]
19#[strum(serialize_all = "shouty_snake_case")]
20pub enum Property {
21 AddStyleName,
23 AverageWidth,
25 AvgCapitalWidth,
27 AvgLowercaseWidth,
29 AxisLimits,
31 AxisNames,
33 AxisTypes,
35 CapHeight,
37 CharsetEncoding,
39 CharsetRegistry,
41 Copyright,
43 DefaultChar,
45 Destination,
47 EndSpace,
49 FaceName,
51 FamilyName,
53 FigureWidth,
55 Font,
57 FontAscent,
59 FontDescent,
61 FontType,
63 FontVersion,
65 Foundry,
67 FullName,
69 ItalicAngle,
71 MaxSpace,
73 MinSpace,
75 NormSpace,
77 Notice,
79 PixelSize,
81 PointSize,
83 QuadWidth,
85 RasterizerName,
87 RasterizerVersion,
89 RawAscent,
91 RawDescent,
93 RelativeSetwidth,
95 RelativeWeight,
97 Resolution,
99 ResolutionX,
101 ResolutionY,
103 SetwidthName,
105 Slant,
107 SmallCapSize,
109 Spacing,
111 StrikeoutAscent,
113 StrikeoutDescent,
115 SubscriptSize,
117 SubscriptX,
119 SubscriptY,
121 SuperscriptSize,
123 SuperscriptX,
125 SuperscriptY,
127 UnderlinePosition,
129 UnderlineThickness,
131 Weight,
133 WeightName,
135 XHeight,
137}
138
139#[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 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 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 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 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#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
259pub enum PropertyError {
260 #[error("property \"{0}\" is undefined")]
262 Undefined(String),
263 #[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}