feat: provide preserve-property-order feature using IndexMap (#25)
This commit is contained in:
parent
efdcb4e73a
commit
e009e1c199
|
@ -17,7 +17,7 @@ travis-ci = { repository = "PoiScript/orgize" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ser"]
|
default = ["ser"]
|
||||||
ser = ["serde", "serde_indextree"]
|
ser = ["serde", "serde_indextree", "indexmap/serde-1"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytecount = "0.6.0"
|
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 = { version = "1.0.106", optional = true, features = ["derive"] }
|
||||||
serde_indextree = { version = "0.2.0", optional = true }
|
serde_indextree = { version = "0.2.0", optional = true }
|
||||||
syntect = { version = "4.1.0", optional = true }
|
syntect = { version = "4.1.0", optional = true }
|
||||||
|
indexmap = { version = "1.3.2", features = ["serde-1"], optional = true}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "0.6.1"
|
pretty_assertions = "0.6.1"
|
||||||
|
|
|
@ -195,7 +195,7 @@ println!("{}", to_string(&org).unwrap());
|
||||||
|
|
||||||
## Features
|
## 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.
|
+ `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.
|
+ `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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
//! Headline Title
|
//! 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::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use memchr::memrchr2;
|
use memchr::memrchr2;
|
||||||
use nom::{
|
use nom::{
|
||||||
|
@ -43,8 +48,11 @@ pub struct Title<'a> {
|
||||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub planning: Option<Box<Planning<'a>>>,
|
pub planning: Option<Box<Planning<'a>>>,
|
||||||
/// Property drawer associated to this headline
|
/// Property drawer associated to this headline
|
||||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "HashMap::is_empty"))]
|
#[cfg_attr(
|
||||||
pub properties: HashMap<Cow<'a, str>, Cow<'a, str>>,
|
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
|
/// Numbers of blank lines between last title's line and next non-blank line
|
||||||
/// or buffer's end
|
/// or buffer's end
|
||||||
pub post_blank: usize,
|
pub post_blank: usize,
|
||||||
|
@ -118,7 +126,7 @@ impl Default for Title<'_> {
|
||||||
keyword: None,
|
keyword: None,
|
||||||
raw: Cow::Borrowed(""),
|
raw: Cow::Borrowed(""),
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,15 +212,17 @@ fn is_tag_line(input: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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())?;
|
let (input, (drawer, content)) = parse_drawer_without_blank(input.trim_start())?;
|
||||||
if drawer.name != "PROPERTIES" {
|
if drawer.name != "PROPERTIES" {
|
||||||
return Err(Err::Error(make_error(input, ErrorKind::Tag)));
|
return Err(Err::Error(make_error(input, ErrorKind::Tag)));
|
||||||
}
|
}
|
||||||
let (_, map) = fold_many0(
|
let (_, map) = fold_many0(
|
||||||
parse_node_property,
|
parse_node_property,
|
||||||
HashMap::new(),
|
PropertiesMap::new(),
|
||||||
|mut acc: HashMap<_, _>, (name, value)| {
|
|mut acc: PropertiesMap<_, _>, (name, value)| {
|
||||||
acc.insert(name.into(), value.into());
|
acc.insert(name.into(), value.into());
|
||||||
acc
|
acc
|
||||||
},
|
},
|
||||||
|
@ -247,7 +257,7 @@ fn parse_title_() {
|
||||||
raw: "COMMENT Title".into(),
|
raw: "COMMENT Title".into(),
|
||||||
tags: vec!["tag".into(), "a2%".into()],
|
tags: vec!["tag".into(), "a2%".into()],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"COMMENT Title"
|
"COMMENT Title"
|
||||||
|
@ -266,7 +276,7 @@ fn parse_title_() {
|
||||||
raw: "ToDO [#A] COMMENT Title".into(),
|
raw: "ToDO [#A] COMMENT Title".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"ToDO [#A] COMMENT Title"
|
"ToDO [#A] COMMENT Title"
|
||||||
|
@ -285,7 +295,7 @@ fn parse_title_() {
|
||||||
raw: "T0DO [#A] COMMENT Title".into(),
|
raw: "T0DO [#A] COMMENT Title".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"T0DO [#A] COMMENT Title"
|
"T0DO [#A] COMMENT Title"
|
||||||
|
@ -304,7 +314,7 @@ fn parse_title_() {
|
||||||
raw: "[#1] COMMENT Title".into(),
|
raw: "[#1] COMMENT Title".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"[#1] COMMENT Title"
|
"[#1] COMMENT Title"
|
||||||
|
@ -323,7 +333,7 @@ fn parse_title_() {
|
||||||
raw: "[#a] COMMENT Title".into(),
|
raw: "[#a] COMMENT Title".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"[#a] COMMENT Title"
|
"[#a] COMMENT Title"
|
||||||
|
@ -344,7 +354,7 @@ fn parse_title_() {
|
||||||
raw: "[#B]::".into(),
|
raw: "[#B]::".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"[#B]::"
|
"[#B]::"
|
||||||
|
@ -364,7 +374,7 @@ fn parse_title_() {
|
||||||
raw: "Title :tag:a2%".into(),
|
raw: "Title :tag:a2%".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"Title :tag:a2%"
|
"Title :tag:a2%"
|
||||||
|
@ -383,7 +393,7 @@ fn parse_title_() {
|
||||||
raw: "Title tag:a2%:".into(),
|
raw: "Title tag:a2%:".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"Title tag:a2%:"
|
"Title tag:a2%:"
|
||||||
|
@ -409,7 +419,7 @@ fn parse_title_() {
|
||||||
raw: "DONE Title".into(),
|
raw: "DONE Title".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"DONE Title"
|
"DONE Title"
|
||||||
|
@ -434,7 +444,7 @@ fn parse_title_() {
|
||||||
raw: "Title".into(),
|
raw: "Title".into(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
planning: None,
|
planning: None,
|
||||||
properties: HashMap::new(),
|
properties: PropertiesMap::new(),
|
||||||
post_blank: 0,
|
post_blank: 0,
|
||||||
},
|
},
|
||||||
"Title"
|
"Title"
|
||||||
|
@ -451,7 +461,50 @@ fn parse_properties_drawer_() {
|
||||||
"",
|
"",
|
||||||
vec![("CUSTOM_ID".into(), "id".into())]
|
vec![("CUSTOM_ID".into(), "id".into())]
|
||||||
.into_iter()
|
.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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue