From e009e1c199d78e2531bed1913a6e40a0f167c2d5 Mon Sep 17 00:00:00 2001 From: Alex Roper Date: Mon, 18 May 2020 02:21:59 -0700 Subject: [PATCH] feat: provide preserve-property-order feature using IndexMap (#25) --- Cargo.toml | 3 +- README.md | 4 +- src/elements/title.rs | 89 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a3bca2..a50f744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ travis-ci = { repository = "PoiScript/orgize" } [features] default = ["ser"] -ser = ["serde", "serde_indextree"] +ser = ["serde", "serde_indextree", "indexmap/serde-1"] [dependencies] bytecount = "0.6.0" @@ -31,6 +31,7 @@ nom = { version = "5.1.1", default-features = false, features = ["std"] } serde = { version = "1.0.106", optional = true, features = ["derive"] } serde_indextree = { version = "0.2.0", optional = true } syntect = { version = "4.1.0", optional = true } +indexmap = { version = "1.3.2", features = ["serde-1"], optional = true} [dev-dependencies] pretty_assertions = "0.6.1" diff --git a/README.md b/README.md index 2464309..8074a44 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ println!("{}", to_string(&org).unwrap()); ## Features -By now, orgize provides three features: +By now, orgize provides four features: + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default. @@ -203,6 +203,8 @@ By now, orgize provides three features: + `syntect`: provides `SyntectHtmlHandler` for highlighting code block, disabled by default. ++ `indexmap`: Uses `IndexMap` instead of `HashMap` for properties to preserve their order, disabled by default. + ## License MIT diff --git a/src/elements/title.rs b/src/elements/title.rs index b37d484..d8ccb39 100644 --- a/src/elements/title.rs +++ b/src/elements/title.rs @@ -1,7 +1,12 @@ //! Headline Title +#[cfg(not(feature = "indexmap"))] +pub type PropertiesMap = std::collections::HashMap; + +#[cfg(feature = "indexmap")] +pub type PropertiesMap = indexmap::IndexMap; + use std::borrow::Cow; -use std::collections::HashMap; use memchr::memrchr2; use nom::{ @@ -43,8 +48,11 @@ pub struct Title<'a> { #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] pub planning: Option>>, /// Property drawer associated to this headline - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "HashMap::is_empty"))] - pub properties: HashMap, Cow<'a, str>>, + #[cfg_attr( + feature = "ser", + serde(skip_serializing_if = "PropertiesMap::is_empty") + )] + pub properties: PropertiesMap, Cow<'a, str>>, /// Numbers of blank lines between last title's line and next non-blank line /// or buffer's end pub post_blank: usize, @@ -118,7 +126,7 @@ impl Default for Title<'_> { keyword: None, raw: Cow::Borrowed(""), planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, } } @@ -204,15 +212,17 @@ fn is_tag_line(input: &str) -> bool { } #[inline] -fn parse_properties_drawer(input: &str) -> IResult<&str, HashMap, Cow<'_, str>>, ()> { +fn parse_properties_drawer( + input: &str, +) -> IResult<&str, PropertiesMap, Cow<'_, str>>, ()> { let (input, (drawer, content)) = parse_drawer_without_blank(input.trim_start())?; if drawer.name != "PROPERTIES" { return Err(Err::Error(make_error(input, ErrorKind::Tag))); } let (_, map) = fold_many0( parse_node_property, - HashMap::new(), - |mut acc: HashMap<_, _>, (name, value)| { + PropertiesMap::new(), + |mut acc: PropertiesMap<_, _>, (name, value)| { acc.insert(name.into(), value.into()); acc }, @@ -247,7 +257,7 @@ fn parse_title_() { raw: "COMMENT Title".into(), tags: vec!["tag".into(), "a2%".into()], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "COMMENT Title" @@ -266,7 +276,7 @@ fn parse_title_() { raw: "ToDO [#A] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "ToDO [#A] COMMENT Title" @@ -285,7 +295,7 @@ fn parse_title_() { raw: "T0DO [#A] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "T0DO [#A] COMMENT Title" @@ -304,7 +314,7 @@ fn parse_title_() { raw: "[#1] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "[#1] COMMENT Title" @@ -323,7 +333,7 @@ fn parse_title_() { raw: "[#a] COMMENT Title".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "[#a] COMMENT Title" @@ -344,7 +354,7 @@ fn parse_title_() { raw: "[#B]::".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "[#B]::" @@ -364,7 +374,7 @@ fn parse_title_() { raw: "Title :tag:a2%".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "Title :tag:a2%" @@ -383,7 +393,7 @@ fn parse_title_() { raw: "Title tag:a2%:".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "Title tag:a2%:" @@ -409,7 +419,7 @@ fn parse_title_() { raw: "DONE Title".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "DONE Title" @@ -434,7 +444,7 @@ fn parse_title_() { raw: "Title".into(), tags: vec![], planning: None, - properties: HashMap::new(), + properties: PropertiesMap::new(), post_blank: 0, }, "Title" @@ -451,7 +461,50 @@ fn parse_properties_drawer_() { "", vec![("CUSTOM_ID".into(), "id".into())] .into_iter() - .collect::>() + .collect::>() )) ) } + +#[test] +fn preserve_properties_drawer_order() { + let mut properties = Vec::default(); + // Use a large number of properties to reduce false pass rate, since HashMap + // is non-deterministic. There are roughly 10^18 possible derangements of this sequence. + for i in 0..20 { + // Avoid alphabetic or numeric order. + let j = (i + 7) % 20; + properties.push(( + Cow::Owned(format!( + "{}{}", + if i % 3 == 0 { + "FOO" + } else if i % 3 == 1 { + "QUX" + } else { + "BAR" + }, + j + )), + Cow::Owned(i.to_string()), + )); + } + + let mut s = String::default(); + for (k, v) in &properties { + s += &format!(" :{}: {}\n", k, v); + } + let drawer = format!(" :PROPERTIES:\n{}:END:\n", &s); + let mut parsed: Vec<(_, _)> = parse_properties_drawer(&drawer) + .unwrap() + .1 + .into_iter() + .collect(); + + #[cfg(not(feature = "indexmap"))] + parsed.sort(); + #[cfg(not(feature = "indexmap"))] + properties.sort(); + + assert_eq!(parsed, properties); +}