feat(format_string): Add syntax for positional segments

This commit is contained in:
heyrict 2020-04-24 20:51:45 +08:00
parent d81c353124
commit 71020b0397
No known key found for this signature in database
GPG Key ID: 803E2EACED3FF3AA
4 changed files with 114 additions and 64 deletions

View File

@ -1,4 +1,9 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
pub trait VariableHolder<T> {
fn get_variables(&self) -> BTreeSet<T>;
}
pub struct TextGroup<'a> {
pub format: Vec<FormatElement<'a>>,
@ -9,9 +14,34 @@ pub enum FormatElement<'a> {
Text(Cow<'a, str>),
Variable(Cow<'a, str>),
TextGroup(TextGroup<'a>),
Positional(Vec<FormatElement<'a>>),
}
pub enum StyleElement<'a> {
Text(Cow<'a, str>),
Variable(Cow<'a, str>),
}
impl<'a> VariableHolder<Cow<'a, str>> for FormatElement<'a> {
fn get_variables(&self) -> BTreeSet<Cow<'a, str>> {
match self {
FormatElement::Variable(var) => {
let mut variables = BTreeSet::new();
variables.insert(var.clone());
variables
}
FormatElement::TextGroup(textgroup) => textgroup.format.get_variables(),
FormatElement::Positional(format) => format.get_variables(),
_ => Default::default(),
}
}
}
impl<'a> VariableHolder<Cow<'a, str>> for Vec<FormatElement<'a>> {
fn get_variables(&self) -> BTreeSet<Cow<'a, str>> {
self.iter().fold(BTreeSet::new(), |mut acc, el| {
acc.extend(el.get_variables());
acc
})
}
}

View File

@ -6,6 +6,18 @@ use super::model::*;
#[grammar = "formatter/spec.pest"]
struct IdentParser;
fn _parse_value(value: Pair<Rule>) -> FormatElement {
match value.as_rule() {
Rule::text => FormatElement::Text(_parse_text(value).into()),
Rule::variable => FormatElement::Variable(_parse_variable(value).into()),
Rule::textgroup => FormatElement::TextGroup(_parse_textgroup(value)),
Rule::positional => {
FormatElement::Positional(_parse_format(value.into_inner().next().unwrap()))
}
_ => unreachable!(),
}
}
fn _parse_textgroup(textgroup: Pair<Rule>) -> TextGroup {
let mut inner_rules = textgroup.into_inner();
let format = inner_rules.next().unwrap();
@ -29,15 +41,7 @@ fn _parse_text(text: Pair<Rule>) -> String {
}
fn _parse_format(format: Pair<Rule>) -> Vec<FormatElement> {
format
.into_inner()
.map(|pair| match pair.as_rule() {
Rule::text => FormatElement::Text(_parse_text(pair).into()),
Rule::variable => FormatElement::Variable(_parse_variable(pair).into()),
Rule::textgroup => FormatElement::TextGroup(_parse_textgroup(pair)),
_ => unreachable!(),
})
.collect()
format.into_inner().map(_parse_value).collect()
}
fn _parse_style(style: Pair<Rule>) -> Vec<StyleElement> {
@ -55,12 +59,7 @@ pub fn parse(format: &str) -> Result<Vec<FormatElement>, Error<Rule>> {
IdentParser::parse(Rule::expression, format).map(|pairs| {
pairs
.take_while(|pair| pair.as_rule() != Rule::EOI)
.map(|pair| match pair.as_rule() {
Rule::text => FormatElement::Text(_parse_text(pair).into()),
Rule::variable => FormatElement::Variable(_parse_variable(pair).into()),
Rule::textgroup => FormatElement::TextGroup(_parse_textgroup(pair)),
_ => unreachable!(),
})
.map(_parse_value)
.collect()
})
}

View File

@ -4,7 +4,7 @@
//
// Should be started with SOI and ended with EOI, with a format string in it.
expression = _{ SOI ~ value* ~ EOI }
value = _{ text | variable | textgroup }
value = _{ text | variable | textgroup | positional }
// Variable
//
@ -44,3 +44,8 @@ escaped_char = { "[" | "]" | "(" | ")" | "\\" | "$" }
textgroup = { "[" ~ format ~ "]" ~ "(" ~ style ~ ")" }
format = { value* }
style = { (variable | string)* }
// Positional
//
// A positional format string that won't render if all the containing variables are empty.
positional = { "(" ~ format ~ ")" }

View File

@ -2,6 +2,7 @@ use ansi_term::Style;
use pest::error::Error;
use rayon::prelude::*;
use std::collections::BTreeMap;
use std::iter::FromIterator;
use crate::config::parse_style_string;
use crate::segment::Segment;
@ -33,16 +34,22 @@ impl<'a> StringFormatter<'a> {
pub fn new(format: &'a str) -> Result<Self, Error<Rule>> {
parse(format)
.map(|format| {
let variables = _get_variables(&format);
let variables = VariableMapType::from_iter(
format
.get_variables()
.into_iter()
.map(|key| (key.to_string(), None))
.collect::<Vec<(String, Option<_>)>>(),
);
(format, variables)
})
.map(|(format, variables)| Self { format, variables })
}
/// Maps variable name to its value
pub fn map(mut self, mapper: impl Fn(&str) -> Option<String> + Sync) -> Self {
pub fn map<T: Into<String>>(mut self, mapper: impl Fn(&str) -> Option<T> + Sync) -> Self {
self.variables.par_iter_mut().for_each(|(key, value)| {
*value = mapper(key).map(VariableValue::Plain);
*value = mapper(key).map(|var| var.into()).map(VariableValue::Plain);
});
self
}
@ -105,9 +112,9 @@ impl<'a> StringFormatter<'a> {
}
FormatElement::Variable(name) => variables
.get(name.as_ref())
.map(|segments| {
let value = segments.clone().unwrap_or_default();
match value {
.and_then(|segments| {
let value = segments.clone()?;
Some(match value {
VariableValue::Styled(segments) => segments
.into_iter()
.map(|mut segment| {
@ -122,9 +129,23 @@ impl<'a> StringFormatter<'a> {
VariableValue::Plain(text) => {
vec![_new_segment(name.to_string(), text, style)]
}
}
})
})
.unwrap_or_default(),
FormatElement::Positional(format) => {
let should_show: bool = format.get_variables().iter().any(|var| {
variables
.get(var.as_ref())
.map(|segments| segments.is_some())
.unwrap_or(false)
});
if should_show {
_parse_format(format, style, variables)
} else {
Vec::new()
}
}
})
.collect()
}
@ -133,47 +154,6 @@ impl<'a> StringFormatter<'a> {
}
}
/// Extract variable names from an array of `FormatElement` into a `BTreeMap`
fn _get_variables<'a>(format: &[FormatElement<'a>]) -> VariableMapType {
let mut variables: VariableMapType = Default::default();
fn _push_variables_from_textgroup<'a>(
variables: &mut VariableMapType,
textgroup: &'a TextGroup<'a>,
) {
for el in &textgroup.format {
match el {
FormatElement::Variable(name) => _push_variable(variables, name.as_ref()),
FormatElement::TextGroup(textgroup) => {
_push_variables_from_textgroup(variables, &textgroup)
}
_ => {}
}
}
for el in &textgroup.style {
if let StyleElement::Variable(name) = el {
_push_variable(variables, name.as_ref())
}
}
}
fn _push_variable<'a>(variables: &mut VariableMapType, name: &'a str) {
variables.insert(name.to_owned(), None);
}
for el in format {
match el {
FormatElement::Variable(name) => _push_variable(&mut variables, name.as_ref()),
FormatElement::TextGroup(textgroup) => {
_push_variables_from_textgroup(&mut variables, &textgroup)
}
_ => {}
}
}
variables
}
/// Helper function to create a new segment
fn _new_segment(name: String, value: String, style: Option<Style>) -> Segment {
Segment {
@ -317,6 +297,42 @@ mod tests {
match_next!(result_iter, "styled_no_modifier", styled_no_modifier_style);
}
#[test]
fn test_positional() {
const FORMAT_STR: &str = "($some) should render but ($none) shouldn't";
let formatter = StringFormatter::new(FORMAT_STR)
.unwrap()
.map(|var| match var {
"some" => Some("$some"),
_ => None,
});
let result = formatter.parse(None);
let mut result_iter = result.iter();
match_next!(result_iter, "$some", None);
match_next!(result_iter, " should render but ", None);
match_next!(result_iter, " shouldn't", None);
}
#[test]
fn test_nested_positional() {
const FORMAT_STR: &str = "($some ($none)) and ($none ($some))";
let formatter = StringFormatter::new(FORMAT_STR)
.unwrap()
.map(|var| match var {
"some" => Some("$some"),
_ => None,
});
let result = formatter.parse(None);
let mut result_iter = result.iter();
match_next!(result_iter, "$some", None);
match_next!(result_iter, " ", None);
match_next!(result_iter, " and ", None);
match_next!(result_iter, " ", None);
match_next!(result_iter, "$some", None);
}
#[test]
fn test_parse_error() {
// brackets without escape