diff --git a/Cargo.toml b/Cargo.toml index 4261fe2..8e59ac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ edition = "2018" [dependencies] nom = "6.0.1" +phf = { version = "0.8.0", features = ["macros"] } thiserror = "1.0.22" diff --git a/src/node.rs b/src/node.rs index 87ca110..ff49d9b 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, convert::TryFrom}; +use std::{collections::HashMap, convert::TryFrom, fmt}; use crate::TryFromKdlNodeValueError; @@ -19,6 +19,73 @@ pub enum KdlValue { Null, } +impl fmt::Display for KdlNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.write(f, 0) + } +} + +impl KdlNode { + fn write(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result { + write!(f, "{:indent$}", "", indent = indent)?; + + display_identifier(f, &self.name)?; + for arg in &self.values { + write!(f, " {}", arg)?; + } + for (prop, value) in &self.properties { + write!(f, " ")?; + display_identifier(f, prop)?; + write!(f, "={}", value)?; + } + + if self.children.is_empty() { + return Ok(()); + } + + writeln!(f, " {{")?; + for child in &self.children { + child.write(f, indent + 4)?; + writeln!(f)?; + } + write!(f, "{:indent$}}}", "", indent = indent)?; + + Ok(()) + } +} +impl fmt::Display for KdlValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use KdlValue::*; + match self { + Int(x) => write!(f, "{}", x), + Float(x) => write!(f, "{}", x), + String(x) => display_string(f, x), + Boolean(x) => write!(f, "{}", x), + Null => write!(f, "null"), + } + } +} + +fn display_identifier(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result { + if let Ok(("", identifier)) = crate::parser::bare_identifier(s) { + write!(f, "{}", identifier) + } else { + display_string(f, s) + } +} + +fn display_string(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result { + write!(f, "\"")?; + for c in s.chars() { + match crate::parser::ESCAPE_CHARS.1.get(&c) { + None => write!(f, "{}", c)?, + Some(c) => write!(f, "\\{}", c)?, + } + } + write!(f, "\"")?; + Ok(()) +} + // Support conversions from base types into KdlNodeValue impl From for KdlValue { @@ -120,6 +187,83 @@ impl_try_from!(&, bool, KdlValue::Boolean(v) => *v; Int, Float, String); mod tests { use super::*; + #[test] + fn display_value() { + assert_eq!("1", format!("{}", KdlValue::Int(1))); + assert_eq!("1.5", format!("{}", KdlValue::Float(1.5))); + assert_eq!("true", format!("{}", KdlValue::Boolean(true))); + assert_eq!("false", format!("{}", KdlValue::Boolean(false))); + assert_eq!("null", format!("{}", KdlValue::Null)); + assert_eq!( + r#""foo""#, + format!("{}", KdlValue::String("foo".to_owned())) + ); + assert_eq!( + r#""foo \"bar\" baz""#, + format!("{}", KdlValue::String(r#"foo "bar" baz"#.to_owned())) + ); + } + + #[test] + fn display_node() { + let mut value = KdlNode { + name: "foo".into(), + values: vec![1.into(), "two".into()], + properties: HashMap::new(), + children: vec![], + }; + + value.properties.insert("three".to_owned(), 3.into()); + + assert_eq!(r#"foo 1 "two" three=3"#, format!("{}", value)); + } + + #[test] + fn display_nested_node() { + let value = KdlNode { + name: "a1".into(), + values: vec!["a".into(), 1.into()], + properties: HashMap::new(), + children: vec![ + KdlNode { + name: "b1".into(), + values: vec!["b".into(), 1.into()], + properties: HashMap::new(), + children: vec![KdlNode { + name: "c1".into(), + values: vec!["c".into(), 1.into()], + properties: HashMap::new(), + children: vec![], + }], + }, + KdlNode { + name: "b2".into(), + values: vec!["b".into(), 2.into()], + properties: HashMap::new(), + children: vec![KdlNode { + name: "c2".into(), + values: vec!["c".into(), 2.into()], + properties: HashMap::new(), + children: vec![], + }], + }, + ], + }; + + assert_eq!( + r#" +a1 "a" 1 { + b1 "b" 1 { + c1 "c" 1 + } + b2 "b" 2 { + c2 "c" 2 + } +}"#, + format!("\n{}", value) + ); + } + #[test] fn from() { assert_eq!(KdlValue::from(1), KdlValue::Int(1)); diff --git a/src/parser.rs b/src/parser.rs index ebad30c..5c94d7a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -112,20 +112,19 @@ pub(crate) fn node(input: &str) -> IResult<&str, Option, KdlParseError< } } -/// `identifier := [a-zA-Z_] [a-zA-Z0-9!$%&'*+\-./:<>?@\^_|~]* | string` -fn identifier(input: &str) -> IResult<&str, String, KdlParseError<&str>> { - alt(( - map( - recognize(pair( - alt((alpha1, tag("_"))), - many0(alt((alphanumeric1, recognize(one_of("~!@$%^&*-_+./:<>?"))))), - )), - String::from, - ), - string, +/// `bare_identifier := [a-zA-Z_] [a-zA-Z0-9!$%&'*+\-./:<>?@\^_|~]*` +pub(crate) fn bare_identifier(input: &str) -> IResult<&str, &str, KdlParseError<&str>> { + recognize(pair( + alt((alpha1, tag("_"))), + many0(alt((alphanumeric1, recognize(one_of("~!@$%^&*-_+./:<>?"))))), ))(input) } +/// `identifier := bare_identifier | string` +fn identifier(input: &str) -> IResult<&str, String, KdlParseError<&str>> { + alt((map(bare_identifier, String::from), string))(input) +} + /// `node-props-and-args := ('/-' ws*)? (prop | value)` fn node_prop_or_arg(input: &str) -> IResult<&str, Option, KdlParseError<&str>> { let (input, comment) = opt(terminated(tag("/-"), many0(whitespace)))(input)?; @@ -197,18 +196,30 @@ fn character(input: &str) -> IResult<&str, char, KdlParseError<&str>> { alt((preceded(char('\\'), escape), none_of("\\\"")))(input) } +// creates a (map, inverse map) tuple +macro_rules! bimap { + ($($x:expr => $y:expr),+) => { + (phf::phf_map!($($x => $y),+), phf::phf_map!($($y => $x),+)) + } +} + +/// a map and its inverse of escape-sequence<->char +pub(crate) static ESCAPE_CHARS: (phf::Map, phf::Map) = bimap! { + '"' => '"', + '\\' => '\\', + '/' => '/', + 'b' => '\u{08}', + 'f' => '\u{0C}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t' +}; + /// `escape := ["\\/bfnrt] | 'u{' hex-digit{1, 6} '}'` fn escape(input: &str) -> IResult<&str, char, KdlParseError<&str>> { alt(( delimited(tag("u{"), unicode, char('}')), - value('"', char('"')), - value('\\', char('\\')), - value('/', char('/')), - value('\u{08}', char('b')), - value('\u{0C}', char('f')), - value('\n', char('n')), - value('\r', char('r')), - value('\t', char('t')), + map_opt(anychar, |c| ESCAPE_CHARS.0.get(&c).copied()), ))(input) }