use nom::{
branch::alt,
bytes::complete::{tag, take_until},
character::complete::{multispace0, space1},
combinator::{eof, map, map_opt, map_parser, opt},
multi::{many0, many1},
sequence::delimited,
IResult, ParseTo,
};
use std::{collections::HashMap, convert::TryFrom};
use thiserror::Error;
use crate::helpers::*;
#[derive(Debug, PartialEq, Copy, Clone, Eq, PartialOrd, Ord, strum::Display)]
#[strum(serialize_all = "shouty_snake_case")]
pub enum Property {
AddStyleName,
AverageWidth,
AvgCapitalWidth,
AvgLowercaseWidth,
AxisLimits,
AxisNames,
AxisTypes,
CapHeight,
CharsetEncoding,
CharsetRegistry,
Copyright,
DefaultChar,
Destination,
EndSpace,
FaceName,
FamilyName,
FigureWidth,
Font,
FontAscent,
FontDescent,
FontType,
FontVersion,
Foundry,
FullName,
ItalicAngle,
MaxSpace,
MinSpace,
NormSpace,
Notice,
PixelSize,
PointSize,
QuadWidth,
RasterizerName,
RasterizerVersion,
RawAscent,
RawDescent,
RelativeSetwidth,
RelativeWeight,
Resolution,
ResolutionX,
ResolutionY,
SetwidthName,
Slant,
SmallCapSize,
Spacing,
StrikeoutAscent,
StrikeoutDescent,
SubscriptSize,
SubscriptX,
SubscriptY,
SuperscriptSize,
SuperscriptX,
SuperscriptY,
UnderlinePosition,
UnderlineThickness,
Weight,
WeightName,
XHeight,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Properties {
properties: HashMap<String, PropertyValue>,
}
impl Properties {
pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> {
map(
opt(map_parser(
delimited(
statement("STARTPROPERTIES", parse_to_u32),
take_until("ENDPROPERTIES"),
statement("ENDPROPERTIES", eof),
),
many0(property),
)),
|properties| {
let properties = properties
.map(|p| p.iter().cloned().collect())
.unwrap_or_else(HashMap::new);
Self { properties }
},
)(input)
}
pub fn try_get<T>(&self, property: Property) -> Result<T, PropertyError>
where
T: for<'a> TryFrom<&'a PropertyValue, Error = PropertyError>,
{
self.try_get_by_name(&property.to_string())
}
pub fn try_get_by_name<T>(&self, name: &str) -> Result<T, PropertyError>
where
T: for<'a> TryFrom<&'a PropertyValue, Error = PropertyError>,
{
self.properties
.get(name)
.ok_or_else(|| PropertyError::Undefined(name.to_string()))
.and_then(TryFrom::try_from)
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
}
fn property(input: &[u8]) -> IResult<&[u8], (String, PropertyValue)> {
let (input, _) = multispace0(input)?;
let (input, key) = map_opt(take_until(" "), |s: &[u8]| s.parse_to())(input)?;
let (input, _) = space1(input)?;
let (input, value) = PropertyValue::parse(input)?;
let (input, _) = multispace0(input)?;
Ok((input, (key, value)))
}
#[derive(Debug, Clone, PartialEq)]
pub enum PropertyValue {
Text(String),
Int(i32),
}
impl PropertyValue {
pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> {
alt((Self::parse_string, Self::parse_int))(input)
}
fn parse_string(input: &[u8]) -> IResult<&[u8], PropertyValue> {
map(
many1(delimited(tag("\""), take_until("\""), tag("\""))),
|parts| {
let parts: Vec<_> = parts
.iter()
.map(|part| ascii_to_string_lossy(*part))
.collect();
PropertyValue::Text(parts.join("\""))
},
)(input)
}
fn parse_int(input: &[u8]) -> IResult<&[u8], PropertyValue> {
map(parse_to_i32, |i| PropertyValue::Int(i))(input)
}
}
impl TryFrom<&PropertyValue> for String {
type Error = PropertyError;
fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
match value {
PropertyValue::Text(text) => Ok(text.clone()),
_ => Err(PropertyError::WrongType),
}
}
}
impl TryFrom<&PropertyValue> for i32 {
type Error = PropertyError;
fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
match value {
PropertyValue::Int(int) => Ok(*int),
_ => Err(PropertyError::WrongType),
}
}
}
#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
pub enum PropertyError {
#[error("property \"{0}\" is undefined")]
Undefined(String),
#[error("wrong property type")]
WrongType,
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn parse_property_with_whitespace() {
assert_parser_ok!(
property(b"KEY \"VALUE\""),
("KEY".to_string(), PropertyValue::Text("VALUE".to_string()))
);
assert_parser_ok!(
property(b"KEY \"RANDOM WORDS AND STUFF\""),
(
"KEY".to_string(),
PropertyValue::Text("RANDOM WORDS AND STUFF".to_string())
)
);
}
#[test]
fn parse_string_property() {
assert_parser_ok!(
property(b"KEY \"VALUE\""),
("KEY".to_string(), PropertyValue::Text("VALUE".to_string()))
);
}
#[test]
fn parse_string_property_with_quote_in_value() {
assert_parser_ok!(
property(br#"WITH_QUOTE "1""23""""#),
(
"WITH_QUOTE".to_string(),
PropertyValue::Text("1\"23\"".to_string())
)
);
}
#[test]
fn parse_string_property_with_invalid_ascii() {
assert_parser_ok!(
property(b"KEY \"VALUE\xAB\""),
(
"KEY".to_string(),
PropertyValue::Text("VALUE\u{FFFD}".to_string())
)
);
}
#[test]
fn parse_integer_property() {
assert_parser_ok!(
property(b"POSITIVE_NUMBER 10"),
("POSITIVE_NUMBER".to_string(), PropertyValue::Int(10i32))
);
assert_parser_ok!(
property(b"NEGATIVE_NUMBER -10"),
("NEGATIVE_NUMBER".to_string(), PropertyValue::Int(-10i32))
);
}
#[test]
fn parse_empty_property_list() {
let input = indoc! {br#"
STARTPROPERTIES 0
ENDPROPERTIES
"#};
let (input, properties) = Properties::parse(input).unwrap();
assert_eq!(input, b"");
assert!(properties.is_empty());
}
#[test]
fn parse_properties() {
let input = indoc! {br#"
STARTPROPERTIES 2
TEXT "FONT"
INTEGER 10
ENDPROPERTIES
"#};
let (input, properties) = Properties::parse(input).unwrap();
assert_eq!(input, b"");
assert_eq!(properties.properties.len(), 2);
assert_eq!(properties.try_get_by_name("TEXT"), Ok("FONT".to_string()));
assert_eq!(properties.try_get_by_name("INTEGER"), Ok(10));
}
#[test]
fn try_get() {
let input = indoc! {br#"
STARTPROPERTIES 2
FAMILY_NAME "FAMILY"
RESOLUTION_X 100
RESOLUTION_Y 75
ENDPROPERTIES
"#};
let (input, properties) = Properties::parse(input).unwrap();
assert_eq!(input, b"");
assert_eq!(properties.properties.len(), 3);
assert_eq!(
properties.try_get(Property::FamilyName),
Ok("FAMILY".to_string())
);
assert_eq!(properties.try_get(Property::ResolutionX), Ok(100));
assert_eq!(properties.try_get(Property::ResolutionY), Ok(75));
}
#[test]
fn property_to_string() {
assert_eq!(&Property::Font.to_string(), "FONT");
assert_eq!(&Property::SuperscriptX.to_string(), "SUPERSCRIPT_X");
assert_eq!(
&Property::AvgLowercaseWidth.to_string(),
"AVG_LOWERCASE_WIDTH"
);
}
}