feat: provide preserve-property-order feature using IndexMap (#25)

This commit is contained in:
Alex Roper 2020-05-18 02:21:59 -07:00 committed by GitHub
parent efdcb4e73a
commit e009e1c199
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 20 deletions

View file

@ -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"

View file

@ -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

View file

@ -1,7 +1,12 @@
//! Headline Title
#[cfg(not(feature = "indexmap"))]
pub type PropertiesMap<K, V> = std::collections::HashMap<K, V>;
#[cfg(feature = "indexmap")]
pub type PropertiesMap<K, V> = indexmap::IndexMap<K, V>;
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<Box<Planning<'a>>>,
/// Property drawer associated to this headline
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "HashMap::is_empty"))]
pub properties: HashMap<Cow<'a, str>, Cow<'a, str>>,
#[cfg_attr(
feature = "ser",
serde(skip_serializing_if = "PropertiesMap::is_empty")
)]
pub properties: PropertiesMap<Cow<'a, str>, 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>, Cow<'_, str>>, ()> {
fn parse_properties_drawer(
input: &str,
) -> IResult<&str, PropertiesMap<Cow<'_, str>, 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::<HashMap<_, _>>()
.collect::<PropertiesMap<_, _>>()
))
)
}
#[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);
}