mirror of
https://github.com/zaphar/ucg.git
synced 2025-07-22 18:19:54 -04:00
Add format string support.
This commit is contained in:
parent
200db01b13
commit
9df7d57f69
@ -47,6 +47,15 @@ concatenation using `+`. The expressions enforce the same type between operands.
|
||||
|
||||
"foo" + "bar";
|
||||
|
||||
### String formatting
|
||||
|
||||
UCG supports some string interpolation using format strings. The syntax is
|
||||
shamelessly ripped off from python.
|
||||
|
||||
"foo @ @ \@" % (1, "bar")
|
||||
|
||||
This gets turned into "foo 1 bar {"
|
||||
|
||||
### Bindings and Tuples.
|
||||
|
||||
Let statements introduce a new name in a UCG file. Most configurations
|
||||
|
27
src/build.rs
27
src/build.rs
@ -25,6 +25,8 @@ use std::rc::Rc;
|
||||
|
||||
use nom;
|
||||
|
||||
use format;
|
||||
|
||||
quick_error! {
|
||||
#[derive(Debug,PartialEq)]
|
||||
pub enum BuildError {
|
||||
@ -52,6 +54,10 @@ quick_error! {
|
||||
description("Eval Error")
|
||||
display("Bad Argument Length {}", msg)
|
||||
}
|
||||
FormatError(msg: String) {
|
||||
description("String Format Error")
|
||||
display("String format Error {}", msg)
|
||||
}
|
||||
TODO(msg: String) {
|
||||
description("TODO Error")
|
||||
display("TODO Error {}", msg)
|
||||
@ -112,7 +118,6 @@ pub enum Val {
|
||||
}
|
||||
|
||||
impl Val {
|
||||
|
||||
pub fn type_name(&self) -> String {
|
||||
match self {
|
||||
&Val::Int(_) => "Integer".to_string(),
|
||||
@ -177,6 +182,17 @@ impl Display for Val {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Val> for String {
|
||||
fn from(v: Val) -> String {
|
||||
match v {
|
||||
Val::Int(ref i) => format!("{}", i),
|
||||
Val::Float(ref f) => format!("{}", f),
|
||||
Val::String(ref s) => s.to_string(),
|
||||
val => format!("<{}>", val),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ValueMap defines a set of values in a parsed file.
|
||||
type ValueMap = HashMap<String, Rc<Val>>;
|
||||
|
||||
@ -480,6 +496,15 @@ impl Builder {
|
||||
Expression::Grouped(expr) => {
|
||||
return self.eval_expr(*expr);
|
||||
},
|
||||
Expression::Format(tmpl, mut args) => {
|
||||
let mut vals = Vec::new();
|
||||
for v in args.drain(0..) {
|
||||
let rcv = try!(self.eval_expr(v));
|
||||
vals.push(rcv.deref().clone());
|
||||
}
|
||||
let formatter = format::Formatter::new(tmpl, vals);
|
||||
Ok(Rc::new(Val::String(try!(formatter.render()))))
|
||||
},
|
||||
Expression::Call{macroref: sel, arglist: mut args} => {
|
||||
let v = try!(self.lookup_selector(sel));
|
||||
if let &Val::Macro(ref m) = v.deref() {
|
||||
|
84
src/format.rs
Normal file
84
src/format.rs
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2017 Jeremy Wall <jeremy@marzhillstudios.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::clone::Clone;
|
||||
use std::error::Error;
|
||||
|
||||
use build::BuildError;
|
||||
|
||||
pub struct Formatter<V: Into<String> + Clone> {
|
||||
tmpl: String,
|
||||
args: Vec<V>,
|
||||
}
|
||||
|
||||
impl<V: Into<String> + Clone> Formatter<V> {
|
||||
pub fn new<S: Into<String>>(tmpl: S, args: Vec<V>) -> Self {
|
||||
Formatter{
|
||||
tmpl: tmpl.into(),
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self) -> Result<String, Box<Error>> {
|
||||
let mut buf = String::new();
|
||||
let mut should_escape = false;
|
||||
let mut count = 0;
|
||||
for c in self.tmpl.chars() {
|
||||
if c == '@' && !should_escape {
|
||||
if count == self.args.len() {
|
||||
return Err(Box::new(
|
||||
BuildError::FormatError(
|
||||
"Too few arguments to string formatter.".to_string())))
|
||||
}
|
||||
let arg = self.args[count].clone();
|
||||
let strval = arg.into();
|
||||
buf.push_str(&strval);
|
||||
count += 1;
|
||||
} else if c == '\\' && !should_escape {
|
||||
should_escape = true;
|
||||
} else {
|
||||
buf.push(c);
|
||||
}
|
||||
}
|
||||
if self.args.len() != count {
|
||||
return Err(Box::new(
|
||||
BuildError::FormatError(
|
||||
"Too many arguments to string formatter.".to_string())))
|
||||
}
|
||||
return Ok(buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Formatter;
|
||||
|
||||
#[test]
|
||||
fn test_format_happy_path() {
|
||||
let formatter = Formatter::new("foo @ @ \\@", vec!["bar", "quux"]);
|
||||
assert_eq!(formatter.render().unwrap(), "foo bar quux @");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_happy_wrong_too_few_args() {
|
||||
let formatter = Formatter::new("foo @ @ \\@", vec!["bar"]);
|
||||
assert!(formatter.render().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_happy_wrong_too_many_args() {
|
||||
let formatter = Formatter::new("foo @ @ \\@", vec!["bar", "quux", "baz"]);
|
||||
assert!(formatter.render().is_err());
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ extern crate quick_error;
|
||||
pub mod parse;
|
||||
pub mod build;
|
||||
|
||||
mod format;
|
||||
|
||||
pub use parse::Value;
|
||||
pub use parse::Expression;
|
||||
pub use parse::Statement;
|
||||
|
47
src/parse.rs
47
src/parse.rs
@ -21,6 +21,9 @@ quick_error! {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jwall): Convert to tokenizer steps followed by parser steps.
|
||||
// TODO(jwall): Error Reporting with Line and Column information.
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::str::from_utf8;
|
||||
use std::error::Error;
|
||||
@ -97,14 +100,18 @@ pub enum Expression {
|
||||
Copy(SelectorList, FieldList),
|
||||
Grouped(Box<Expression>),
|
||||
|
||||
Format(String, Vec<Expression>),
|
||||
|
||||
Call {
|
||||
macroref: SelectorList,
|
||||
arglist: Vec<Expression>,
|
||||
},
|
||||
|
||||
Macro {
|
||||
arglist: Vec<String>,
|
||||
tuple: FieldList,
|
||||
},
|
||||
|
||||
Select {
|
||||
val: Box<Expression>,
|
||||
default: Box<Expression>,
|
||||
@ -449,6 +456,24 @@ named!(select_expression<Expression>,
|
||||
)
|
||||
);
|
||||
|
||||
fn tuple_to_format(t: (String, Vec<Expression>)) -> ParseResult<Expression> {
|
||||
Ok(Expression::Format(t.0, t.1))
|
||||
}
|
||||
|
||||
named!(format_expression<Expression>,
|
||||
map_res!(
|
||||
do_parse!(
|
||||
tmpl: ws!(quoted) >>
|
||||
ws!(tag!("%")) >>
|
||||
lparen >>
|
||||
args: ws!(separated_list!(ws!(comma), expression)) >>
|
||||
rparen >>
|
||||
(tmpl, args)
|
||||
),
|
||||
tuple_to_format
|
||||
)
|
||||
);
|
||||
|
||||
fn tuple_to_call(t: (Value, Vec<Expression>)) -> ParseResult<Expression> {
|
||||
if let Value::Selector(sl) = t.0 {
|
||||
Ok(Expression::Call {
|
||||
@ -502,6 +527,7 @@ named!(expression<Expression>,
|
||||
complete!(div_expression) |
|
||||
complete!(grouped_expression) |
|
||||
complete!(macro_expression) |
|
||||
complete!(format_expression) |
|
||||
complete!(select_expression) |
|
||||
complete!(call_expression) |
|
||||
complete!(copy_expression) |
|
||||
@ -576,8 +602,9 @@ named!(pub parse<Vec<Statement> >, many1!(ws!(statement)));
|
||||
mod test {
|
||||
use std::str::from_utf8;
|
||||
use super::{Statement, Expression, Value};
|
||||
use super::{number, symbol, parse, field_value, tuple, grouped_expression, copy_expression};
|
||||
use super::{arglist, macro_expression, select_expression, call_expression, expression};
|
||||
use super::{number, symbol, parse, field_value, tuple, grouped_expression};
|
||||
use super::{arglist, copy_expression, macro_expression, select_expression};
|
||||
use super::{format_expression, call_expression, expression};
|
||||
use super::{expression_statement, let_statement, import_statement, statement};
|
||||
use nom::IResult;
|
||||
|
||||
@ -774,6 +801,22 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_parse() {
|
||||
assert!(format_expression(&b"\"foo"[..]).is_err() );
|
||||
assert!(format_expression(&b"\"foo\""[..]).is_incomplete() );
|
||||
assert!(format_expression(&b"\"foo\" %"[..]).is_incomplete() );
|
||||
assert!(format_expression(&b"\"foo\" % (1, 2"[..]).is_incomplete() );
|
||||
|
||||
assert_eq!(format_expression(&b"\"foo @ @\" % (1, 2)"[..]),
|
||||
IResult::Done(&b""[..],
|
||||
Expression::Format("foo @ @".to_string(),
|
||||
vec![Expression::Simple(Value::Int(1)),
|
||||
Expression::Simple(Value::Int(2))])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_parse() {
|
||||
assert!(call_expression(&b"foo"[..]).is_incomplete() );
|
||||
|
Loading…
x
Reference in New Issue
Block a user