feat(formatter): Allow scoped variables (#1094)

* feat: Allow scoped variables

, with the following improvements to the format string parser.

- Add documentation to spec
- Simplify some syntax in the spec
- Rewrite for loop with iterators
This commit is contained in:
Zhenhui Xie 2020-04-17 11:52:50 +08:00 committed by GitHub
parent 1ab2ba9e4d
commit 610e63ce4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 85 additions and 58 deletions

View File

@ -22,55 +22,45 @@ fn _parse_variable(variable: Pair<Rule>) -> &str {
}
fn _parse_text(text: Pair<Rule>) -> String {
let mut result = String::new();
for pair in text.into_inner() {
result.push_str(pair.as_str());
}
result
text.into_inner()
.map(|pair| pair.as_str().chars())
.flatten()
.collect()
}
fn _parse_format(format: Pair<Rule>) -> Vec<FormatElement> {
let mut result: Vec<FormatElement> = Vec::new();
for pair in format.into_inner() {
match pair.as_rule() {
Rule::text => result.push(FormatElement::Text(_parse_text(pair).into())),
Rule::variable => result.push(FormatElement::Variable(_parse_variable(pair).into())),
Rule::textgroup => result.push(FormatElement::TextGroup(_parse_textgroup(pair))),
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!(),
}
}
result
})
.collect()
}
fn _parse_style(style: Pair<Rule>) -> Vec<StyleElement> {
let mut result: Vec<StyleElement> = Vec::new();
for pair in style.into_inner() {
match pair.as_rule() {
Rule::text => result.push(StyleElement::Text(_parse_text(pair).into())),
Rule::variable => result.push(StyleElement::Variable(_parse_variable(pair).into())),
style
.into_inner()
.map(|pair| match pair.as_rule() {
Rule::string => StyleElement::Text(pair.as_str().into()),
Rule::variable => StyleElement::Variable(_parse_variable(pair).into()),
_ => unreachable!(),
}
}
result
})
.collect()
}
pub fn parse(format: &str) -> Result<Vec<FormatElement>, Error<Rule>> {
let pairs = IdentParser::parse(Rule::expression, format)?;
let mut result: Vec<FormatElement> = Vec::new();
// Lifetime of Segment is the same as result
for pair in pairs.take_while(|pair| pair.as_rule() != Rule::EOI) {
match pair.as_rule() {
Rule::text => result.push(FormatElement::Text(_parse_text(pair).into())),
Rule::variable => result.push(FormatElement::Variable(_parse_variable(pair).into())),
Rule::textgroup => result.push(FormatElement::TextGroup(_parse_textgroup(pair))),
_ => unreachable!(),
}
}
Ok(result)
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!(),
})
.collect()
})
}

View File

@ -1,16 +1,46 @@
// Expression
//
// The expression of the format string.
//
// Should be started with SOI and ended with EOI, with a format string in it.
expression = _{ SOI ~ value* ~ EOI }
value = _{ text | variable | textgroup }
variable = { "$" ~ variable_name }
variable_name = @{ char+ }
// Variable
//
// A variable is defined as one of the following:
//
// - A valid variable name followed by a `$` character (`$[a-zA-Z_][a-zA-Z0-9_]*`),
// e.g. `$variable`.
//
// - Some texts wrapped in a curly bracket (`${[^\(\)\[\]\\\${}]+}`),
// e.g. `${env:HOST}`.
variable = { "$" ~ (variable_name | variable_scope) }
variable_name = @{ ('a'..'z' | 'A'..'Z' | "_") ~ char* }
char = _{ 'a'..'z' | 'A'..'Z' | '0'..'9' | "_" }
text = { text_inner+ }
text_inner = _{ text_inner_char | escape }
text_inner_char = { !("[" | "]" | "(" | ")" | "$" | "\\") ~ ANY }
variable_scope = _{ "{" ~ variable_scoped_name ~ "}" }
variable_scoped_name = { scoped_char+ }
scoped_char = _{ !(escaped_char | "{" | "}") ~ ANY }
// Text
//
// Texts can be one of `string` or `escaped_char`, where string is one or more of
// unescapable chars.
//
// This is implemented so as to ensure all functional characters are escaped.
text = { (string | escape)+ }
string = @{ text_inner_char+ }
text_inner_char = { !escaped_char ~ ANY }
escape = _{ "\\" ~ escaped_char }
escaped_char = { "[" | "]" | "(" | ")" | "\\" | "$" }
// TextGroup
//
// A textgroup is a pair of `format` and `style` (`[format](style)`)
//
// - `format`: A format string, can contain any number of variables, texts or textgroups.
// - `style`: A style string, can contain any number of variables or texts.
textgroup = { "[" ~ format ~ "]" ~ "(" ~ style ~ ")" }
format = { (variable | text | textgroup)* }
style = { (variable | text)* }
format = { value* }
style = { (variable | string)* }

View File

@ -86,15 +86,13 @@ impl<'a> StringFormatter<'a> {
}
fn _parse_format<'a>(
mut format: Vec<FormatElement<'a>>,
format: Vec<FormatElement<'a>>,
style: Option<Style>,
variables: &'a VariableMapType,
) -> Vec<Segment> {
let mut result: Vec<Segment> = Vec::new();
format.reverse();
while let Some(el) = format.pop() {
let mut segments = match el {
format
.into_iter()
.flat_map(|el| match el {
FormatElement::Text(text) => {
vec![_new_segment("_text".into(), text.into_owned(), style)]
}
@ -127,11 +125,8 @@ impl<'a> StringFormatter<'a> {
}
})
.unwrap_or_default(),
};
result.append(&mut segments);
}
result
})
.collect()
}
_parse_format(self.format, default_style, &self.variables)
@ -241,6 +236,18 @@ mod tests {
match_next!(result_iter, "text1", None);
}
#[test]
fn test_scoped_variable() {
const FORMAT_STR: &str = "${env:PWD}";
let formatter = StringFormatter::new(FORMAT_STR)
.unwrap()
.map(|variable| Some(format!("${{{}}}", variable)));
let result = formatter.parse(None);
let mut result_iter = result.iter();
match_next!(result_iter, "${env:PWD}", None);
}
#[test]
fn test_escaped_chars() {
const FORMAT_STR: &str = r#"\\\[\$text\]\(red bold\)"#;