mirror of
https://github.com/typst/typst
synced 2025-05-15 17:45:27 +08:00
Parse tuples and objects 🍒
Generalizes the parsing of tuples, objects and function arguments into generic comma-separated collections.
This commit is contained in:
parent
5badb4e8ff
commit
60099aed50
@ -1,6 +1,7 @@
|
|||||||
//! Expressions in function headers.
|
//! Expressions in function headers.
|
||||||
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Write, Debug, Formatter};
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
use crate::error::Errors;
|
use crate::error::Errors;
|
||||||
use crate::size::Size;
|
use crate::size::Size;
|
||||||
@ -90,7 +91,9 @@ impl Ident {
|
|||||||
|
|
||||||
impl Debug for Ident {
|
impl Debug for Ident {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str(&self.0)
|
f.write_char('`')?;
|
||||||
|
f.write_str(&self.0)?;
|
||||||
|
f.write_char('`')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,15 +146,42 @@ impl Tuple {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate over the items of this tuple.
|
||||||
|
pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, Spanned<Expr>> {
|
||||||
|
self.items.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Tuple {
|
||||||
|
type Item = Spanned<Expr>;
|
||||||
|
type IntoIter = std::vec::IntoIter<Spanned<Expr>>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.items.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a Tuple {
|
||||||
|
type Item = &'a Spanned<Expr>;
|
||||||
|
type IntoIter = std::slice::Iter<'a, Spanned<Expr>>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<Spanned<Expr>> for Tuple {
|
||||||
|
fn from_iter<I: IntoIterator<Item=Spanned<Expr>>>(iter: I) -> Self {
|
||||||
|
Tuple { items: iter.into_iter().collect() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Tuple {
|
impl Debug for Tuple {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
let mut tuple = f.debug_tuple("");
|
f.debug_list()
|
||||||
for item in &self.items {
|
.entries(&self.items)
|
||||||
tuple.field(item);
|
.finish()
|
||||||
}
|
|
||||||
tuple.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,6 +306,35 @@ impl Object {
|
|||||||
Err(err) => { errors.push(Spanned { v: err, span }); None }
|
Err(err) => { errors.push(Spanned { v: err, span }); None }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate over the pairs of this object.
|
||||||
|
pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, Pair> {
|
||||||
|
self.pairs.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Object {
|
||||||
|
type Item = Pair;
|
||||||
|
type IntoIter = std::vec::IntoIter<Pair>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.pairs.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a Object {
|
||||||
|
type Item = &'a Pair;
|
||||||
|
type IntoIter = std::slice::Iter<'a, Pair>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<Pair> for Object {
|
||||||
|
fn from_iter<I: IntoIterator<Item=Pair>>(iter: I) -> Self {
|
||||||
|
Object { pairs: iter.into_iter().collect() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Object {
|
impl Debug for Object {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
//! Primitives for argument parsing in library functions.
|
//! Primitives for argument parsing in library functions.
|
||||||
|
|
||||||
|
use std::iter::FromIterator;
|
||||||
use crate::error::{Error, Errors};
|
use crate::error::{Error, Errors};
|
||||||
use super::expr::{Expr, Ident, Tuple, Object, Pair};
|
use super::expr::{Expr, Ident, Tuple, Object, Pair};
|
||||||
use super::span::{Span, Spanned};
|
use super::span::{Span, Spanned};
|
||||||
@ -55,6 +56,16 @@ impl FuncArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromIterator<FuncArg> for FuncArgs {
|
||||||
|
fn from_iter<I: IntoIterator<Item=FuncArg>>(iter: I) -> Self {
|
||||||
|
let mut args = FuncArgs::new();
|
||||||
|
for item in iter.into_iter() {
|
||||||
|
args.add(item);
|
||||||
|
}
|
||||||
|
args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Either a positional or keyword argument.
|
/// Either a positional or keyword argument.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum FuncArg {
|
pub enum FuncArg {
|
||||||
|
@ -107,19 +107,23 @@ pub enum Decoration {
|
|||||||
/// ^^^^^^
|
/// ^^^^^^
|
||||||
/// ```
|
/// ```
|
||||||
InvalidFuncName,
|
InvalidFuncName,
|
||||||
|
/// A key of a keyword argument:
|
||||||
/// The key of a keyword argument:
|
|
||||||
/// ```typst
|
/// ```typst
|
||||||
/// [box: width=5cm]
|
/// [box: width=5cm]
|
||||||
/// ^^^^^
|
/// ^^^^^
|
||||||
/// ```
|
/// ```
|
||||||
ArgumentKey,
|
ArgumentKey,
|
||||||
|
/// A key in an object.
|
||||||
/// Italic.
|
/// ```typst
|
||||||
|
/// [box: padding={ left: 1cm, right: 2cm}]
|
||||||
|
/// ^^^^ ^^^^^
|
||||||
|
/// ```
|
||||||
|
ObjectKey,
|
||||||
|
/// An italic word.
|
||||||
Italic,
|
Italic,
|
||||||
/// Bold.
|
/// A bold word.
|
||||||
Bold,
|
Bold,
|
||||||
/// Monospace.
|
/// A monospace word.
|
||||||
Monospace,
|
Monospace,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
//! Parsing of source code into syntax models.
|
//! Parsing of source code into syntax models.
|
||||||
|
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
use crate::{Pass, Feedback};
|
use crate::{Pass, Feedback};
|
||||||
use super::func::{FuncHeader, FuncArgs, FuncArg};
|
use super::func::{FuncHeader, FuncArgs, FuncArg};
|
||||||
use super::expr::*;
|
use super::expr::*;
|
||||||
@ -145,11 +147,10 @@ impl<'s> FuncParser<'s> {
|
|||||||
let start = self.pos();
|
let start = self.pos();
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
|
|
||||||
let name = match self.eat() {
|
let name = match self.parse_ident() {
|
||||||
Some(Spanned { v: Token::ExprIdent(ident), span }) => {
|
Some(ident) => ident,
|
||||||
Spanned { v: Ident(ident.to_string()), span }
|
None => {
|
||||||
}
|
let other = self.eat();
|
||||||
other => {
|
|
||||||
self.expected_found_or_at("identifier", other, start);
|
self.expected_found_or_at("identifier", other, start);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -168,88 +169,62 @@ impl<'s> FuncParser<'s> {
|
|||||||
Some(FuncHeader { name, args })
|
Some(FuncHeader { name, args })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the function arguments after a colon.
|
/// Parse the argument list between colons and end of the header.
|
||||||
fn parse_func_args(&mut self) -> FuncArgs {
|
fn parse_func_args(&mut self) -> FuncArgs {
|
||||||
let mut args = FuncArgs::new();
|
// Parse a collection until the token is `None`, that is, the end of the
|
||||||
|
// header.
|
||||||
|
self.parse_collection(None, |p| {
|
||||||
|
// If we have an identifier we might have a keyword argument,
|
||||||
|
// otherwise its for sure a postional argument.
|
||||||
|
if let Some(ident) = p.parse_ident() {
|
||||||
|
p.skip_whitespace();
|
||||||
|
|
||||||
self.skip_whitespace();
|
if let Some(Token::Equals) = p.peekv() {
|
||||||
while self.peek().is_some() {
|
p.eat();
|
||||||
match self.parse_arg() {
|
p.skip_whitespace();
|
||||||
Some(arg) => args.add(arg),
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.skip_whitespace();
|
// Semantic highlighting for argument keys.
|
||||||
}
|
p.feedback.decos.push(
|
||||||
|
Spanned::new(Decoration::ArgumentKey, ident.span));
|
||||||
|
|
||||||
args
|
let value = p.parse_expr().ok_or(("value", None))?;
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a positional or keyword argument.
|
// Add a keyword argument.
|
||||||
fn parse_arg(&mut self) -> Option<FuncArg> {
|
Ok(FuncArg::Key(Pair { key: ident, value }))
|
||||||
let first = self.peek()?;
|
} else {
|
||||||
let span = first.span;
|
// Add a positional argument because there was no equals
|
||||||
|
// sign after the identifier that could have been a key.
|
||||||
let arg = if let Token::ExprIdent(ident) = first.v {
|
Ok(FuncArg::Pos(ident.map(|id| Expr::Ident(id))))
|
||||||
self.eat();
|
}
|
||||||
self.skip_whitespace();
|
|
||||||
|
|
||||||
let ident = Ident(ident.to_string());
|
|
||||||
if let Some(Token::Equals) = self.peekv() {
|
|
||||||
self.eat();
|
|
||||||
self.skip_whitespace();
|
|
||||||
|
|
||||||
self.feedback.decos.push(Spanned::new(Decoration::ArgumentKey, span));
|
|
||||||
|
|
||||||
self.parse_expr().map(|value| {
|
|
||||||
FuncArg::Key(Pair {
|
|
||||||
key: Spanned { v: ident, span },
|
|
||||||
value,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Some(FuncArg::Pos(Spanned::new(Expr::Ident(ident), span)))
|
// Add a positional argument because we haven't got an
|
||||||
|
// identifier that could be an argument key.
|
||||||
|
p.parse_expr().map(|expr| FuncArg::Pos(expr))
|
||||||
|
.ok_or(("argument", None))
|
||||||
}
|
}
|
||||||
} else {
|
}).v
|
||||||
self.parse_expr().map(|expr| FuncArg::Pos(expr))
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(arg) = &arg {
|
|
||||||
self.skip_whitespace();
|
|
||||||
match self.peekv() {
|
|
||||||
Some(Token::Comma) => { self.eat(); }
|
|
||||||
Some(_) => self.expected_at("comma", arg.span().end),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let found = self.eat();
|
|
||||||
self.expected_found_or_at("value", found, self.pos());
|
|
||||||
}
|
|
||||||
|
|
||||||
arg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an atomic or compound (tuple / object) expression.
|
/// Parse an atomic or compound (tuple / object) expression.
|
||||||
fn parse_expr(&mut self) -> Option<Spanned<Expr>> {
|
fn parse_expr(&mut self) -> Option<Spanned<Expr>> {
|
||||||
let first = self.peek()?;
|
let first = self.peek()?;
|
||||||
let spanned = |v| Spanned { v, span: first.span };
|
macro_rules! take {
|
||||||
|
($v:expr) => ({ self.eat(); Spanned { v: $v, span: first.span } });
|
||||||
|
}
|
||||||
|
|
||||||
Some(match first.v {
|
Some(match first.v {
|
||||||
Token::ExprIdent(i) => {
|
Token::ExprIdent(i) => take!((Expr::Ident(Ident(i.to_string())))),
|
||||||
self.eat();
|
|
||||||
spanned(Expr::Ident(Ident(i.to_string())))
|
|
||||||
}
|
|
||||||
Token::ExprStr { string, terminated } => {
|
Token::ExprStr { string, terminated } => {
|
||||||
if !terminated {
|
if !terminated {
|
||||||
self.expected_at("quote", first.span.end);
|
self.expected_at("quote", first.span.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.eat();
|
take!(Expr::Str(unescape(string)))
|
||||||
spanned(Expr::Str(unescape(string)))
|
|
||||||
}
|
}
|
||||||
Token::ExprNumber(n) => { self.eat(); spanned(Expr::Number(n)) }
|
|
||||||
Token::ExprSize(s) => { self.eat(); spanned(Expr::Size(s)) }
|
Token::ExprNumber(n) => take!(Expr::Number(n)),
|
||||||
Token::ExprBool(b) => { self.eat(); spanned(Expr::Bool(b)) }
|
Token::ExprSize(s) => take!(Expr::Size(s)),
|
||||||
|
Token::ExprBool(b) => take!(Expr::Bool(b)),
|
||||||
|
|
||||||
Token::LeftParen => self.parse_tuple(),
|
Token::LeftParen => self.parse_tuple(),
|
||||||
Token::LeftBrace => self.parse_object(),
|
Token::LeftBrace => self.parse_object(),
|
||||||
@ -258,30 +233,126 @@ impl<'s> FuncParser<'s> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a tuple expression.
|
/// Parse a tuple expression: `(<expr>, ...)`.
|
||||||
fn parse_tuple(&mut self) -> Spanned<Expr> {
|
fn parse_tuple(&mut self) -> Spanned<Expr> {
|
||||||
let start = self.pos();
|
let token = self.eat();
|
||||||
|
debug_assert_eq!(token.map(Spanned::value), Some(Token::LeftParen));
|
||||||
|
|
||||||
// TODO: Do the thing.
|
// Parse a collection until a right paren appears and complain about
|
||||||
self.eat_until(|t| t == Token::RightParen, true);
|
// missing a `value` when an invalid token is encoutered.
|
||||||
|
self.parse_collection(Some(Token::RightParen),
|
||||||
let end = self.pos();
|
|p| p.parse_expr().ok_or(("value", None)))
|
||||||
let span = Span { start, end };
|
.map(|tuple| Expr::Tuple(tuple))
|
||||||
|
|
||||||
Spanned { v: Expr::Tuple(Tuple::new()), span }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an object expression.
|
/// Parse an object expression: `{ <key>: <value>, ... }`.
|
||||||
fn parse_object(&mut self) -> Spanned<Expr> {
|
fn parse_object(&mut self) -> Spanned<Expr> {
|
||||||
|
let token = self.eat();
|
||||||
|
debug_assert_eq!(token.map(Spanned::value), Some(Token::LeftBrace));
|
||||||
|
|
||||||
|
// Parse a collection until a right brace appears.
|
||||||
|
self.parse_collection(Some(Token::RightBrace), |p| {
|
||||||
|
// Expect an identifier as the key.
|
||||||
|
let key = p.parse_ident().ok_or(("key", None))?;
|
||||||
|
|
||||||
|
// Expect a colon behind the key (only separated by whitespace).
|
||||||
|
let behind_key = p.pos();
|
||||||
|
p.skip_whitespace();
|
||||||
|
if p.peekv() != Some(Token::Colon) {
|
||||||
|
return Err(("colon", Some(behind_key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
p.eat();
|
||||||
|
p.skip_whitespace();
|
||||||
|
|
||||||
|
// Semantic highlighting for object keys.
|
||||||
|
p.feedback.decos.push(
|
||||||
|
Spanned::new(Decoration::ObjectKey, key.span));
|
||||||
|
|
||||||
|
let value = p.parse_expr().ok_or(("value", None))?;
|
||||||
|
|
||||||
|
Ok(Pair { key, value })
|
||||||
|
}).map(|object| Expr::Object(object))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a comma-separated collection where each item is parsed through
|
||||||
|
/// `parse_item` until the `end` token is met.
|
||||||
|
fn parse_collection<C, I, F>(
|
||||||
|
&mut self,
|
||||||
|
end: Option<Token>,
|
||||||
|
mut parse_item: F
|
||||||
|
) -> Spanned<C>
|
||||||
|
where
|
||||||
|
C: FromIterator<I>,
|
||||||
|
F: FnMut(&mut Self) -> Result<I, (&'static str, Option<Position>)>,
|
||||||
|
{
|
||||||
let start = self.pos();
|
let start = self.pos();
|
||||||
|
|
||||||
// TODO: Do the thing.
|
// Parse the comma separated items.
|
||||||
self.eat_until(|t| t == Token::RightBrace, true);
|
let collection = std::iter::from_fn(|| {
|
||||||
|
self.skip_whitespace();
|
||||||
|
let peeked = self.peekv();
|
||||||
|
|
||||||
|
// We finished as expected.
|
||||||
|
if peeked == end {
|
||||||
|
self.eat();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We finished without the expected end token (which has to be a
|
||||||
|
// `Some` value at this point since otherwise we would have already
|
||||||
|
// returned in the previous case).
|
||||||
|
if peeked == None {
|
||||||
|
self.eat();
|
||||||
|
self.expected_at(end.unwrap().name(), self.pos());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse a collection item.
|
||||||
|
match parse_item(self) {
|
||||||
|
Ok(item) => {
|
||||||
|
// Expect a comma behind the item (only separated by
|
||||||
|
// whitespace).
|
||||||
|
let behind_item = self.pos();
|
||||||
|
self.skip_whitespace();
|
||||||
|
match self.peekv() {
|
||||||
|
Some(Token::Comma) => { self.eat(); }
|
||||||
|
t @ Some(_) if t != end => self.expected_at("comma", behind_item),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(Some(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The item parser expected something different at either some
|
||||||
|
// given position or instead of the currently peekable token.
|
||||||
|
Err((expected, Some(pos))) => self.expected_at(expected, pos),
|
||||||
|
Err((expected, None)) => {
|
||||||
|
let token = self.peek();
|
||||||
|
if token.map(Spanned::value) != end {
|
||||||
|
self.eat();
|
||||||
|
}
|
||||||
|
self.expected_found_or_at(expected, token, self.pos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(None)
|
||||||
|
}).filter_map(|x| x).collect();
|
||||||
|
|
||||||
let end = self.pos();
|
let end = self.pos();
|
||||||
let span = Span { start, end };
|
Spanned::new(collection, Span { start, end })
|
||||||
|
}
|
||||||
|
|
||||||
Spanned { v: Expr::Object(Object::new()), span }
|
/// Try to parse an identifier and do nothing if the peekable token is no
|
||||||
|
/// identifier.
|
||||||
|
fn parse_ident(&mut self) -> Option<Spanned<Ident>> {
|
||||||
|
match self.peek() {
|
||||||
|
Some(Spanned { v: Token::ExprIdent(s), span }) => {
|
||||||
|
self.eat();
|
||||||
|
Some(Spanned { v: Ident(s.to_string()), span })
|
||||||
|
}
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Skip all whitespace/comment tokens.
|
/// Skip all whitespace/comment tokens.
|
||||||
@ -432,53 +503,48 @@ mod tests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test whether the given string parses into the given transform pass.
|
/// Test whether the given string parses into
|
||||||
macro_rules! test {
|
/// - the given node list (required).
|
||||||
($source:expr => [$($model:tt)*], $transform:expr) => {
|
/// - the given error list (optional, if omitted checks against empty list).
|
||||||
let (exp, cmp) = spanned![vec $($model)*];
|
/// - the given decoration list (optional, if omitted it is not tested).
|
||||||
|
macro_rules! p {
|
||||||
|
($source:expr => [$($model:tt)*]) => {
|
||||||
|
p!($source => [$($model)*], []);
|
||||||
|
};
|
||||||
|
|
||||||
|
($source:expr => [$($model:tt)*], [$($errors:tt)*] $(, [$($decos:tt)*])? $(,)?) => {
|
||||||
let mut scope = Scope::new::<DebugFn>();
|
let mut scope = Scope::new::<DebugFn>();
|
||||||
scope.add::<DebugFn>("f");
|
scope.add::<DebugFn>("f");
|
||||||
scope.add::<DebugFn>("n");
|
scope.add::<DebugFn>("n");
|
||||||
scope.add::<DebugFn>("box");
|
scope.add::<DebugFn>("box");
|
||||||
scope.add::<DebugFn>("val");
|
scope.add::<DebugFn>("val");
|
||||||
|
|
||||||
let ctx = ParseContext { scope: &scope };
|
let ctx = ParseContext { scope: &scope };
|
||||||
|
let pass = parse(Position::ZERO, $source, ctx);
|
||||||
|
|
||||||
let found = parse(Position::ZERO, $source, ctx);
|
// Test model
|
||||||
let (exp, found) = $transform(exp, found);
|
let (exp, cmp) = spanned![vec $($model)*];
|
||||||
|
check($source, exp, pass.output.nodes, cmp);
|
||||||
|
|
||||||
|
// Test errors
|
||||||
|
let (exp, cmp) = spanned![vec $($errors)*];
|
||||||
|
let exp = exp.into_iter()
|
||||||
|
.map(|s: Spanned<&str>| s.map(|e| e.to_string()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let found = pass.feedback.errors.into_iter()
|
||||||
|
.map(|s| s.map(|e| e.message))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
check($source, exp, found, cmp);
|
check($source, exp, found, cmp);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test whether the given string parses into the given node list.
|
// Test decos
|
||||||
macro_rules! p {
|
$(let (exp, cmp) = spanned![vec $($decos)*];
|
||||||
($($tts:tt)*) => {
|
check($source, exp, pass.feedback.decos, cmp);)?
|
||||||
test!($($tts)*, |exp, found: Pass<SyntaxModel>| (exp, found.output.nodes));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test whether the given string yields the given parse errors.
|
|
||||||
macro_rules! e {
|
|
||||||
($($tts:tt)*) => {
|
|
||||||
test!($($tts)*, |exp: Vec<Spanned<&str>>, found: Pass<SyntaxModel>| (
|
|
||||||
exp.into_iter().map(|s| s.map(|e| e.to_string())).collect::<Vec<_>>(),
|
|
||||||
found.feedback.errors.into_iter().map(|s| s.map(|e| e.message))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test whether the given string yields the given decorations.
|
|
||||||
macro_rules! d {
|
|
||||||
($($tts:tt)*) => {
|
|
||||||
test!($($tts)*, |exp, found: Pass<SyntaxModel>| (exp, found.feedback.decos));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write down a `DebugFn` function model compactly.
|
/// Write down a `DebugFn` function model compactly.
|
||||||
macro_rules! func {
|
macro_rules! func {
|
||||||
($name:tt $(, ($($pos:tt)*), { $($key:tt)* } )? $(; $($body:tt)*)?) => ({
|
($name:tt $(: ($($pos:tt)*), { $($key:tt)* } )? $(; $($body:tt)*)?) => ({
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut args = FuncArgs::new();
|
let mut args = FuncArgs::new();
|
||||||
$(args.pos = Tuple::parse(zspan(tuple!($($pos)*))).unwrap();)?
|
$(args.pos = Tuple::parse(zspan(tuple!($($pos)*))).unwrap();)?
|
||||||
@ -516,6 +582,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_flat_nodes() {
|
fn parse_flat_nodes() {
|
||||||
|
// Basic nodes
|
||||||
p!("" => []);
|
p!("" => []);
|
||||||
p!("hi" => [T("hi")]);
|
p!("hi" => [T("hi")]);
|
||||||
p!("*hi" => [Bold, T("hi")]);
|
p!("*hi" => [Bold, T("hi")]);
|
||||||
@ -529,204 +596,305 @@ mod tests {
|
|||||||
p!("first/*\n \n*/second" => [T("first"), T("second")]);
|
p!("first/*\n \n*/second" => [T("first"), T("second")]);
|
||||||
p!("💜\n\n 🌍" => [T("💜"), N, T("🌍")]);
|
p!("💜\n\n 🌍" => [T("💜"), N, T("🌍")]);
|
||||||
|
|
||||||
p!("Hi" => [(0:0, 0:2, T("Hi"))]);
|
// Spanned nodes
|
||||||
p!("*Hi*" => [(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)]);
|
p!("Hi" => [(0:0, 0:2, T("Hi"))]);
|
||||||
p!("🌎*/[n]" => [(0:0, 0:1, T("🌎")), (0:3, 0:6, func!((0:1, 0:2, "n")))]);
|
p!("*Hi*" => [(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)]);
|
||||||
|
p!("🌎\n*/[n]" =>
|
||||||
e!("hi\n */" => [(1:1, 1:3, "unexpected end of block comment")]);
|
[(0:0, 0:1, T("🌎")), (0:1, 1:0, S), (1:2, 1:5, func!((0:1, 0:2, "n")))],
|
||||||
|
[(1:0, 1:2, "unexpected end of block comment")],
|
||||||
|
[(1:3, 1:4, ValidFuncName)],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_function_names() {
|
fn parse_function_names() {
|
||||||
// No closing bracket
|
// No closing bracket
|
||||||
p!("[" => [func!("")]);
|
p!("[" => [func!("")], [
|
||||||
e!("[" => [
|
|
||||||
(0:1, 0:1, "expected identifier"),
|
(0:1, 0:1, "expected identifier"),
|
||||||
(0:1, 0:1, "expected closing bracket")
|
(0:1, 0:1, "expected closing bracket")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// No name
|
// No name
|
||||||
p!("[]" => [func!("")]);
|
p!("[]" => [func!("")], [(0:1, 0:1, "expected identifier")]);
|
||||||
e!("[]" => [(0:1, 0:1, "expected identifier")]);
|
p!("[\"]" => [func!("")], [
|
||||||
|
|
||||||
p!("[\"]" => [func!("")]);
|
|
||||||
e!("[\"]" => [
|
|
||||||
(0:1, 0:3, "expected identifier, found string"),
|
(0:1, 0:3, "expected identifier, found string"),
|
||||||
(0:3, 0:3, "expected closing bracket"),
|
(0:3, 0:3, "expected closing bracket"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// A valid name
|
|
||||||
p!("[f]" => [func!("f")]);
|
|
||||||
e!("[f]" => []);
|
|
||||||
d!("[f]" => [(0:1, 0:2, ValidFuncName)]);
|
|
||||||
p!("[ f]" => [func!("f")]);
|
|
||||||
e!("[ f]" => []);
|
|
||||||
d!("[ f]" => [(0:3, 0:4, ValidFuncName)]);
|
|
||||||
|
|
||||||
// An unknown name
|
// An unknown name
|
||||||
p!("[hi]" => [func!("hi")]);
|
p!("[hi]" =>
|
||||||
e!("[hi]" => [(0:1, 0:3, "unknown function")]);
|
[func!("hi")],
|
||||||
d!("[hi]" => [(0:1, 0:3, InvalidFuncName)]);
|
[(0:1, 0:3, "unknown function")],
|
||||||
|
[(0:1, 0:3, InvalidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// An invalid token
|
// A valid name
|
||||||
p!("[🌎]" => [func!("")]);
|
p!("[f]" => [func!("f")], [], [(0:1, 0:2, ValidFuncName)]);
|
||||||
e!("[🌎]" => [(0:1, 0:2, "expected identifier, found invalid token")]);
|
p!("[ f]" => [func!("f")], [], [(0:3, 0:4, ValidFuncName)]);
|
||||||
d!("[🌎]" => []);
|
|
||||||
p!("[ 🌎]" => [func!("")]);
|
// An invalid token for a name
|
||||||
e!("[ 🌎]" => [(0:3, 0:4, "expected identifier, found invalid token")]);
|
p!("[12]" => [func!("")], [(0:1, 0:3, "expected identifier, found number")], []);
|
||||||
d!("[ 🌎]" => []);
|
p!("[🌎]" => [func!("")], [(0:1, 0:2, "expected identifier, found invalid token")], []);
|
||||||
|
p!("[ 🌎]" => [func!("")], [(0:3, 0:4, "expected identifier, found invalid token")], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_colon_starting_function_arguments() {
|
fn parse_colon_starting_function_arguments() {
|
||||||
// No colon before arg
|
// No colon before arg
|
||||||
p!("[val\"s\"]" => [func!("val")]);
|
p!("[val\"s\"]" => [func!("val")], [(0:4, 0:4, "expected colon")]);
|
||||||
e!("[val\"s\"]" => [(0:4, 0:4, "expected colon")]);
|
|
||||||
|
|
||||||
// No colon before valid, but wrong token
|
// No colon before valid, but wrong token
|
||||||
p!("[val=]" => [func!("val")]);
|
p!("[val=]" => [func!("val")], [(0:4, 0:4, "expected colon")]);
|
||||||
e!("[val=]" => [(0:4, 0:4, "expected colon")]);
|
|
||||||
|
|
||||||
// No colon before invalid tokens, which are ignored
|
// No colon before invalid tokens, which are ignored
|
||||||
p!("[val/🌎:$]" => [func!("val")]);
|
p!("[val/🌎:$]" =>
|
||||||
e!("[val/🌎:$]" => [(0:4, 0:4, "expected colon")]);
|
[func!("val")],
|
||||||
d!("[val/🌎:$]" => [(0:1, 0:4, ValidFuncName)]);
|
[(0:4, 0:4, "expected colon")],
|
||||||
|
[(0:1, 0:4, ValidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// String in invalid header without colon still parsed as string
|
// String in invalid header without colon still parsed as string
|
||||||
// Note: No "expected quote" error because not even the string was
|
// Note: No "expected quote" error because not even the string was
|
||||||
// expected.
|
// expected.
|
||||||
e!("[val/\"]" => [
|
p!("[val/\"]" => [func!("val")], [
|
||||||
(0:4, 0:4, "expected colon"),
|
(0:4, 0:4, "expected colon"),
|
||||||
(0:7, 0:7, "expected closing bracket"),
|
(0:7, 0:7, "expected closing bracket"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Just colon without args
|
// Just colon without args
|
||||||
p!("[val:]" => [func!("val")]);
|
p!("[val:]" => [func!("val")]);
|
||||||
e!("[val:]" => []);
|
|
||||||
p!("[val:/*12pt*/]" => [func!("val")]);
|
p!("[val:/*12pt*/]" => [func!("val")]);
|
||||||
|
|
||||||
// Whitespace / comments around colon
|
// Whitespace / comments around colon
|
||||||
p!("[val\n:\ntrue]" => [func!("val", (Bool(true)), {})]);
|
p!("[val\n:\ntrue]" => [func!("val": (Bool(true)), {})]);
|
||||||
p!("[val/*:*/://\ntrue]" => [func!("val", (Bool(true)), {})]);
|
p!("[val/*:*/://\ntrue]" => [func!("val": (Bool(true)), {})]);
|
||||||
e!("[val/*:*/://\ntrue]" => []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_one_positional_argument() {
|
fn parse_one_positional_argument() {
|
||||||
// Different expressions
|
// Different expressions
|
||||||
d!("[val: true]" => [(0:1, 0:4, ValidFuncName)]);
|
p!("[val: true]" =>
|
||||||
p!("[val: true]" => [func!("val", (Bool(true)), {})]);
|
[func!("val": (Bool(true)), {})], [],
|
||||||
p!("[val: _]" => [func!("val", (Id("_")), {})]);
|
[(0:1, 0:4, ValidFuncName)],
|
||||||
p!("[val: name]" => [func!("val", (Id("name")), {})]);
|
);
|
||||||
p!("[val: \"hi\"]" => [func!("val", (Str("hi")), {})]);
|
p!("[val: _]" => [func!("val": (Id("_")), {})]);
|
||||||
p!("[val: \"a\n[]\\\"string\"]" => [func!("val", (Str("a\n[]\"string")), {})]);
|
p!("[val: name]" => [func!("val": (Id("name")), {})]);
|
||||||
p!("[val: 3.14]" => [func!("val", (Num(3.14)), {})]);
|
p!("[val: \"hi\"]" => [func!("val": (Str("hi")), {})]);
|
||||||
p!("[val: 4.5cm]" => [func!("val", (Sz(Size::cm(4.5))), {})]);
|
p!("[val: \"a\n[]\\\"string\"]" => [func!("val": (Str("a\n[]\"string")), {})]);
|
||||||
p!("[val: 12e1pt]" => [func!("val", (Pt(12e1)), {})]);
|
p!("[val: 3.14]" => [func!("val": (Num(3.14)), {})]);
|
||||||
|
p!("[val: 4.5cm]" => [func!("val": (Sz(Size::cm(4.5))), {})]);
|
||||||
|
p!("[val: 12e1pt]" => [func!("val": (Pt(12e1)), {})]);
|
||||||
|
|
||||||
// Unclosed string.
|
// Unclosed string.
|
||||||
p!("[val: \"hello]" => [func!("val", (Str("hello]")), {})]);
|
p!("[val: \"hello]" => [func!("val": (Str("hello]")), {})], [
|
||||||
e!("[val: \"hello]" => [
|
|
||||||
(0:13, 0:13, "expected quote"),
|
(0:13, 0:13, "expected quote"),
|
||||||
(0:13, 0:13, "expected closing bracket"),
|
(0:13, 0:13, "expected closing bracket"),
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Tuple: unimplemented
|
#[test]
|
||||||
p!("[val: ()]" => [func!("val", (tuple!()), {})]);
|
fn parse_tuples() {
|
||||||
|
// Empty tuple
|
||||||
|
p!("[val: ()]" => [func!("val": (tuple!()), {})]);
|
||||||
|
|
||||||
// Object: unimplemented
|
// Invalid value
|
||||||
p!("[val: {}]" => [func!("val", (object! {}), {})]);
|
p!("[val: (🌎)]" =>
|
||||||
|
[func!("val": (tuple!()), {})],
|
||||||
|
[(0:7, 0:8, "expected value, found invalid token")],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unclosed tuple
|
||||||
|
p!("[val: (hello]" =>
|
||||||
|
[func!("val": (tuple!(Id("hello"))), {})],
|
||||||
|
[(0:12, 0:12, "expected closing paren")],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Valid values
|
||||||
|
p!("[val: (1, 2)]" => [func!("val": (tuple!(Num(1.0), Num(2.0))), {})]);
|
||||||
|
p!("[val: (\"s\",)]" => [func!("val": (tuple!(Str("s"))), {})]);
|
||||||
|
|
||||||
|
// Nested tuples
|
||||||
|
p!("[val: (1, (2))]" => [func!("val": (tuple!(Num(1.0), tuple!(Num(2.0)))), {})]);
|
||||||
|
|
||||||
|
// Invalid commas
|
||||||
|
p!("[val: (,)]" =>
|
||||||
|
[func!("val": (tuple!()), {})],
|
||||||
|
[(0:7, 0:8, "expected value, found comma")],
|
||||||
|
);
|
||||||
|
p!("[val: (true false)]" =>
|
||||||
|
[func!("val": (tuple!(Bool(true), Bool(false))), {})],
|
||||||
|
[(0:11, 0:11, "expected comma")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_objects() {
|
||||||
|
let f = || func!("val": (object! {}), {});
|
||||||
|
|
||||||
|
// Okay objects
|
||||||
|
p!("[val: {}]" => [f()]);
|
||||||
|
p!("[val: { key: value }]" =>
|
||||||
|
[func!("val": (object! { "key" => Id("value") }), {})]);
|
||||||
|
|
||||||
|
// Unclosed object
|
||||||
|
p!("[val: {hello: world]" =>
|
||||||
|
[func!("val": (object! { "hello" => Id("world") }), {})],
|
||||||
|
[(0:19, 0:19, "expected closing brace")],
|
||||||
|
);
|
||||||
|
p!("[val: { a]" =>
|
||||||
|
[func!("val": (object! {}), {})],
|
||||||
|
[(0:9, 0:9, "expected colon"), (0:9, 0:9, "expected closing brace")],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Missing key
|
||||||
|
p!("[val: {,}]" => [f()], [(0:7, 0:8, "expected key, found comma")]);
|
||||||
|
p!("[val: { 12pt }]" => [f()], [(0:8, 0:12, "expected key, found size")]);
|
||||||
|
p!("[val: { : }]" => [f()], [(0:8, 0:9, "expected key, found colon")]);
|
||||||
|
|
||||||
|
// Missing colon
|
||||||
|
p!("[val: { key }]" => [f()], [(0:11, 0:11, "expected colon")]);
|
||||||
|
p!("[val: { key false }]" => [f()], [
|
||||||
|
(0:11, 0:11, "expected colon"),
|
||||||
|
(0:12, 0:17, "expected key, found bool"),
|
||||||
|
]);
|
||||||
|
p!("[val: { a b:c }]" =>
|
||||||
|
[func!("val": (object! { "b" => Id("c") }), {})],
|
||||||
|
[(0:9, 0:9, "expected colon")],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Missing value
|
||||||
|
p!("[val: { key: : }]" => [f()], [(0:13, 0:14, "expected value, found colon")]);
|
||||||
|
p!("[val: { key: , k: \"s\" }]" =>
|
||||||
|
[func!("val": (object! { "k" => Str("s") }), {})],
|
||||||
|
[(0:13, 0:14, "expected value, found comma")],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Missing comma, invalid token
|
||||||
|
p!("[val: left={ a: 2, b: false 🌎 }]" =>
|
||||||
|
[func!("val": (), {
|
||||||
|
"left" => object! {
|
||||||
|
"a" => Num(2.0),
|
||||||
|
"b" => Bool(false),
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
[(0:27, 0:27, "expected comma"),
|
||||||
|
(0:28, 0:29, "expected key, found invalid token")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nested_tuples_and_objects() {
|
||||||
|
p!("[val: (1, { ab: (), d: (3, 14pt) }), false]" => [func!("val": (
|
||||||
|
tuple!(
|
||||||
|
Num(1.0),
|
||||||
|
object!(
|
||||||
|
"ab" => tuple!(),
|
||||||
|
"d" => tuple!(Num(3.0), Pt(14.0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Bool(false),
|
||||||
|
), {})]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_one_keyword_argument() {
|
fn parse_one_keyword_argument() {
|
||||||
// Correct
|
// Correct
|
||||||
p!("[val: x=true]" => [func!("val", (), { "x" => Bool(true) })]);
|
p!("[val: x=true]" =>
|
||||||
d!("[val: x=true]" => [(0:6, 0:7, ArgumentKey), (0:1, 0:4, ValidFuncName)]);
|
[func!("val": (), { "x" => Bool(true) })], [],
|
||||||
|
[(0:6, 0:7, ArgumentKey), (0:1, 0:4, ValidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// Spacing around keyword arguments
|
// Spacing around keyword arguments
|
||||||
p!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => [S, func!("val", (), { "hi" => Str("s\n") })]);
|
p!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" =>
|
||||||
d!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => [(2:1, 2:3, ArgumentKey), (1:2, 1:5, ValidFuncName)]);
|
[S, func!("val": (), { "hi" => Str("s\n") })], [],
|
||||||
e!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => []);
|
[(2:1, 2:3, ArgumentKey), (1:2, 1:5, ValidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// Missing value
|
// Missing value
|
||||||
p!("[val: x=]" => [func!("val")]);
|
p!("[val: x=]" =>
|
||||||
e!("[val: x=]" => [(0:8, 0:8, "expected value")]);
|
[func!("val")],
|
||||||
d!("[val: x=]" => [(0:6, 0:7, ArgumentKey), (0:1, 0:4, ValidFuncName)]);
|
[(0:8, 0:8, "expected value")],
|
||||||
|
[(0:6, 0:7, ArgumentKey), (0:1, 0:4, ValidFuncName)],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_multiple_mixed_arguments() {
|
fn parse_multiple_mixed_arguments() {
|
||||||
p!("[val: a,]" => [func!("val", (Id("a")), {})]);
|
p!("[val: a,]" => [func!("val": (Id("a")), {})]);
|
||||||
e!("[val: a,]" => []);
|
p!("[val: 12pt, key=value]" =>
|
||||||
p!("[val: 12pt, key=value]" => [func!("val", (Pt(12.0)), { "key" => Id("value") })]);
|
[func!("val": (Pt(12.0)), { "key" => Id("value") })], [],
|
||||||
d!("[val: 12pt, key=value]" => [(0:12, 0:15, ArgumentKey), (0:1, 0:4, ValidFuncName)]);
|
[(0:12, 0:15, ArgumentKey), (0:1, 0:4, ValidFuncName)],
|
||||||
e!("[val: 12pt, key=value]" => []);
|
);
|
||||||
p!("[val: a , \"b\" , c]" => [func!("val", (Id("a"), Str("b"), Id("c")), {})]);
|
p!("[val: a , \"b\" , c]" => [func!("val": (Id("a"), Str("b"), Id("c")), {})]);
|
||||||
e!("[val: a , \"b\" , c]" => []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_invalid_values() {
|
fn parse_invalid_values() {
|
||||||
e!("[val: )]" => [(0:6, 0:7, "expected value, found closing paren")]);
|
p!("[val: )]" => [func!("val")], [(0:6, 0:7, "expected argument, found closing paren")]);
|
||||||
e!("[val: }]" => [(0:6, 0:7, "expected value, found closing brace")]);
|
p!("[val: }]" => [func!("val")], [(0:6, 0:7, "expected argument, found closing brace")]);
|
||||||
e!("[val: :]" => [(0:6, 0:7, "expected value, found colon")]);
|
p!("[val: :]" => [func!("val")], [(0:6, 0:7, "expected argument, found colon")]);
|
||||||
e!("[val: ,]" => [(0:6, 0:7, "expected value, found comma")]);
|
p!("[val: ,]" => [func!("val")], [(0:6, 0:7, "expected argument, found comma")]);
|
||||||
e!("[val: =]" => [(0:6, 0:7, "expected value, found equals sign")]);
|
p!("[val: =]" => [func!("val")], [(0:6, 0:7, "expected argument, found equals sign")]);
|
||||||
e!("[val: 🌎]" => [(0:6, 0:7, "expected value, found invalid token")]);
|
p!("[val: 🌎]" => [func!("val")], [(0:6, 0:7, "expected argument, found invalid token")]);
|
||||||
e!("[val: 12ept]" => [(0:6, 0:11, "expected value, found invalid token")]);
|
p!("[val: 12ept]" => [func!("val")], [(0:6, 0:11, "expected argument, found invalid token")]);
|
||||||
e!("[val: [hi]]" => [(0:6, 0:10, "expected value, found function")]);
|
p!("[val: [hi]]" =>
|
||||||
d!("[val: [hi]]" => [(0:1, 0:4, ValidFuncName)]);
|
[func!("val")],
|
||||||
|
[(0:6, 0:10, "expected argument, found function")],
|
||||||
|
[(0:1, 0:4, ValidFuncName)],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_invalid_key_value_pairs() {
|
fn parse_invalid_key_value_pairs() {
|
||||||
// Invalid keys
|
// Invalid keys
|
||||||
p!("[val: true=you]" => [func!("val", (Bool(true), Id("you")), {})]);
|
p!("[val: true=you]" =>
|
||||||
e!("[val: true=you]" => [
|
[func!("val": (Bool(true), Id("you")), {})],
|
||||||
(0:10, 0:10, "expected comma"),
|
[(0:10, 0:10, "expected comma"),
|
||||||
(0:10, 0:11, "expected value, found equals sign"),
|
(0:10, 0:11, "expected argument, found equals sign")],
|
||||||
]);
|
[(0:1, 0:4, ValidFuncName)],
|
||||||
d!("[val: true=you]" => [(0:1, 0:4, ValidFuncName)]);
|
);
|
||||||
|
|
||||||
p!("[box: z=y=4]" => [func!("box", (Num(4.0)), { "z" => Id("y") })]);
|
p!("[box: z=y=4]" =>
|
||||||
e!("[box: z=y=4]" => [
|
[func!("box": (Num(4.0)), { "z" => Id("y") })],
|
||||||
(0:9, 0:9, "expected comma"),
|
[(0:9, 0:9, "expected comma"),
|
||||||
(0:9, 0:10, "expected value, found equals sign"),
|
(0:9, 0:10, "expected argument, found equals sign")],
|
||||||
]);
|
);
|
||||||
|
|
||||||
// Invalid colon after keyable positional argument
|
// Invalid colon after keyable positional argument
|
||||||
p!("[val: key:12]" => [func!("val", (Id("key"), Num(12.0)), {})]);
|
p!("[val: key:12]" =>
|
||||||
e!("[val: key:12]" => [
|
[func!("val": (Id("key"), Num(12.0)), {})],
|
||||||
(0:9, 0:9, "expected comma"),
|
[(0:9, 0:9, "expected comma"),
|
||||||
(0:9, 0:10, "expected value, found colon"),
|
(0:9, 0:10, "expected argument, found colon")],
|
||||||
]);
|
[(0:1, 0:4, ValidFuncName)],
|
||||||
d!("[val: key:12]" => [(0:1, 0:4, ValidFuncName)]);
|
);
|
||||||
|
|
||||||
// Invalid colon after non-keyable positional argument
|
// Invalid colon after non-keyable positional argument
|
||||||
p!("[val: true:12]" => [func!("val", (Bool(true), Num(12.0)), {})]);
|
p!("[val: true:12]" => [func!("val": (Bool(true), Num(12.0)), {})],
|
||||||
e!("[val: true:12]" => [
|
[(0:10, 0:10, "expected comma"),
|
||||||
(0:10, 0:10, "expected comma"),
|
(0:10, 0:11, "expected argument, found colon")],
|
||||||
(0:10, 0:11, "expected value, found colon"),
|
[(0:1, 0:4, ValidFuncName)],
|
||||||
]);
|
);
|
||||||
d!("[val: true:12]" => [(0:1, 0:4, ValidFuncName)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_invalid_commas() {
|
fn parse_invalid_commas() {
|
||||||
// Missing commas
|
// Missing commas
|
||||||
p!("[val: 1pt 1]" => [func!("val", (Pt(1.0), Num(1.0)), {})]);
|
p!("[val: 1pt 1]" =>
|
||||||
e!("[val: 1pt 1]" => [(0:9, 0:9, "expected comma")]);
|
[func!("val": (Pt(1.0), Num(1.0)), {})],
|
||||||
p!(r#"[val: _"s"]"# => [func!("val", (Id("_"), Str("s")), {})]);
|
[(0:9, 0:9, "expected comma")],
|
||||||
e!(r#"[val: _"s"]"# => [(0:7, 0:7, "expected comma")]);
|
);
|
||||||
|
p!(r#"[val: _"s"]"# =>
|
||||||
|
[func!("val": (Id("_"), Str("s")), {})],
|
||||||
|
[(0:7, 0:7, "expected comma")],
|
||||||
|
);
|
||||||
|
|
||||||
// Unexpected commas
|
// Unexpected commas
|
||||||
p!("[val:,]" => [func!("val")]);
|
p!("[val:,]" => [func!("val")], [(0:5, 0:6, "expected argument, found comma")]);
|
||||||
e!("[val:,]" => [(0:5, 0:6, "expected value, found comma")]);
|
p!("[val: key=,]" => [func!("val")], [(0:10, 0:11, "expected value, found comma")]);
|
||||||
p!("[val:, true]" => [func!("val", (Bool(true)), {})]);
|
p!("[val:, true]" =>
|
||||||
e!("[val:, true]" => [(0:5, 0:6, "expected value, found comma")]);
|
[func!("val": (Bool(true)), {})],
|
||||||
p!("[val: key=,]" => [func!("val")]);
|
[(0:5, 0:6, "expected argument, found comma")],
|
||||||
e!("[val: key=,]" => [(0:10, 0:11, "expected value, found comma")]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -734,48 +902,56 @@ mod tests {
|
|||||||
p!("[val][Hi]" => [func!("val"; [T("Hi")])]);
|
p!("[val][Hi]" => [func!("val"; [T("Hi")])]);
|
||||||
|
|
||||||
// Body nodes in bodies.
|
// Body nodes in bodies.
|
||||||
p!("[val:*][*Hi*]" => [func!("val"; [Bold, T("Hi"), Bold])]);
|
p!("[val:*][*Hi*]" =>
|
||||||
e!("[val:*][*Hi*]" => [(0:5, 0:6, "expected value, found invalid token")]);
|
[func!("val"; [Bold, T("Hi"), Bold])],
|
||||||
|
[(0:5, 0:6, "expected argument, found invalid token")],
|
||||||
|
);
|
||||||
|
|
||||||
// Errors in bodies.
|
// Errors in bodies.
|
||||||
p!(" [val][ */ ]" => [S, func!("val"; [S, S])]);
|
p!(" [val][ */ ]" =>
|
||||||
e!(" [val][ */ ]" => [(0:8, 0:10, "unexpected end of block comment")]);
|
[S, func!("val"; [S, S])],
|
||||||
|
[(0:8, 0:10, "unexpected end of block comment")],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_spanned_functions() {
|
fn parse_spanned_functions() {
|
||||||
// Space before function
|
// Space before function
|
||||||
p!(" [val]" => [(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))]);
|
p!(" [val]" =>
|
||||||
d!(" [val]" => [(0:2, 0:5, ValidFuncName)]);
|
[(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))], [],
|
||||||
|
[(0:2, 0:5, ValidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// Newline before function
|
// Newline before function
|
||||||
p!(" \n\r\n[val]" => [(0:0, 2:0, N), (2:0, 2:5, func!((0:1, 0:4, "val")))]);
|
p!(" \n\r\n[val]" =>
|
||||||
d!(" \n\r\n[val]" => [(2:1, 2:4, ValidFuncName)]);
|
[(0:0, 2:0, N), (2:0, 2:5, func!((0:1, 0:4, "val")))], [],
|
||||||
|
[(2:1, 2:4, ValidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// Content before function
|
// Content before function
|
||||||
p!("hello [val][world] 🌎" => [
|
p!("hello [val][world] 🌎" =>
|
||||||
(0:0, 0:5, T("hello")),
|
[
|
||||||
(0:5, 0:6, S),
|
(0:0, 0:5, T("hello")),
|
||||||
(0:6, 0:18, func!((0:1, 0:4, "val"); [(0:6, 0:11, T("world"))])),
|
(0:5, 0:6, S),
|
||||||
(0:18, 0:19, S),
|
(0:6, 0:18, func!((0:1, 0:4, "val"); [(0:6, 0:11, T("world"))])),
|
||||||
(0:19, 0:20, T("🌎")),
|
(0:18, 0:19, S),
|
||||||
]);
|
(0:19, 0:20, T("🌎"))
|
||||||
d!("hello [val][world] 🌎" => [(0:7, 0:10, ValidFuncName)]);
|
], [],
|
||||||
e!("hello [val][world] 🌎" => []);
|
[(0:7, 0:10, ValidFuncName)],
|
||||||
|
);
|
||||||
|
|
||||||
// Nested function
|
// Nested function
|
||||||
p!(" [val][\nbody[ box]\n ]" => [
|
p!(" [val][\nbody[ box]\n ]" =>
|
||||||
(0:0, 0:1, S),
|
[
|
||||||
(0:1, 2:2, func!((0:1, 0:4, "val"); [
|
(0:0, 0:1, S),
|
||||||
(0:6, 1:0, S),
|
(0:1, 2:2, func!((0:1, 0:4, "val"); [
|
||||||
(1:0, 1:4, T("body")),
|
(0:6, 1:0, S),
|
||||||
(1:4, 1:10, func!((0:2, 0:5, "box"))),
|
(1:0, 1:4, T("body")),
|
||||||
(1:10, 2:1, S),
|
(1:4, 1:10, func!((0:2, 0:5, "box"))),
|
||||||
]))
|
(1:10, 2:1, S),
|
||||||
]);
|
]))
|
||||||
d!(" [val][\nbody[ box]\n ]" => [
|
], [],
|
||||||
(0:2, 0:5, ValidFuncName),
|
[(0:2, 0:5, ValidFuncName), (1:6, 1:9, ValidFuncName)],
|
||||||
(1:6, 1:9, ValidFuncName)
|
);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,9 +149,9 @@ impl<T> Spanned<T> {
|
|||||||
|
|
||||||
impl<T: Debug> Debug for Spanned<T> {
|
impl<T: Debug> Debug for Spanned<T> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
self.span.fmt(f)?;
|
self.v.fmt(f)?;
|
||||||
f.write_str(": ")?;
|
f.write_str(" ")?;
|
||||||
self.v.fmt(f)
|
self.span.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ impl<'s> Token<'s> {
|
|||||||
ExprStr { .. } => "string",
|
ExprStr { .. } => "string",
|
||||||
ExprNumber(_) => "number",
|
ExprNumber(_) => "number",
|
||||||
ExprSize(_) => "size",
|
ExprSize(_) => "size",
|
||||||
ExprBool(_) => "boolean",
|
ExprBool(_) => "bool",
|
||||||
Star => "star",
|
Star => "star",
|
||||||
Underscore => "underscore",
|
Underscore => "underscore",
|
||||||
Backtick => "backtick",
|
Backtick => "backtick",
|
||||||
@ -209,7 +209,7 @@ impl<'s> Iterator for Tokens<'s> {
|
|||||||
'`' if self.mode == Body => Backtick,
|
'`' if self.mode == Body => Backtick,
|
||||||
|
|
||||||
// An escaped thing.
|
// An escaped thing.
|
||||||
'\\' => self.parse_escaped(),
|
'\\' if self.mode == Body => self.parse_escaped(),
|
||||||
|
|
||||||
// Expressions or just strings.
|
// Expressions or just strings.
|
||||||
c => {
|
c => {
|
||||||
@ -217,9 +217,10 @@ impl<'s> Iterator for Tokens<'s> {
|
|||||||
let text = self.read_string_until(|n| {
|
let text = self.read_string_until(|n| {
|
||||||
match n {
|
match n {
|
||||||
c if c.is_whitespace() => true,
|
c if c.is_whitespace() => true,
|
||||||
'\\' | '[' | ']' | '/' => true,
|
'[' | ']' | '/' => true,
|
||||||
'*' | '_' | '`' if body => true,
|
'\\' | '*' | '_' | '`' if body => true,
|
||||||
':' | '=' | ',' | '"' if !body => true,
|
':' | '=' | ',' | '"' |
|
||||||
|
'(' | ')' | '{' | '}' if !body => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}, false, -(c.len_utf8() as isize), 0).0;
|
}, false, -(c.len_utf8() as isize), 0).0;
|
||||||
@ -340,24 +341,19 @@ impl<'s> Tokens<'s> {
|
|||||||
fn parse_escaped(&mut self) -> Token<'s> {
|
fn parse_escaped(&mut self) -> Token<'s> {
|
||||||
fn is_escapable(c: char) -> bool {
|
fn is_escapable(c: char) -> bool {
|
||||||
match c {
|
match c {
|
||||||
'\\' | '[' | ']' | '*' | '_' | '`' | '/' => true,
|
'[' | ']' | '\\' | '/' | '*' | '_' | '`' => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = self.peek().unwrap_or('n');
|
Text(match self.peek() {
|
||||||
let string = if is_escapable(c) {
|
Some(c) if is_escapable(c) => {
|
||||||
let index = self.index();
|
let index = self.index();
|
||||||
self.eat();
|
self.eat();
|
||||||
&self.src[index .. index + c.len_utf8()]
|
&self.src[index .. index + c.len_utf8()]
|
||||||
} else {
|
}
|
||||||
"\\"
|
_ => "\\"
|
||||||
};
|
})
|
||||||
|
|
||||||
match self.mode {
|
|
||||||
Header => Invalid(string),
|
|
||||||
Body => Text(string),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expr(&mut self, text: &'s str) -> Token<'s> {
|
fn parse_expr(&mut self, text: &'s str) -> Token<'s> {
|
||||||
@ -570,6 +566,8 @@ mod tests {
|
|||||||
t!(Header, "2.3cm" => [Sz(Size::cm(2.3))]);
|
t!(Header, "2.3cm" => [Sz(Size::cm(2.3))]);
|
||||||
t!(Header, "02.4mm" => [Sz(Size::mm(2.4))]);
|
t!(Header, "02.4mm" => [Sz(Size::mm(2.4))]);
|
||||||
t!(Header, "2.4.cm" => [Invalid("2.4.cm")]);
|
t!(Header, "2.4.cm" => [Invalid("2.4.cm")]);
|
||||||
|
t!(Header, "(1,2)" => [LP, Num(1.0), Comma, Num(2.0), RP]);
|
||||||
|
t!(Header, "{abc}" => [LB, Id("abc"), RB]);
|
||||||
t!(Header, "🌓, 🌍," => [Invalid("🌓"), Comma, S(0), Invalid("🌍"), Comma]);
|
t!(Header, "🌓, 🌍," => [Invalid("🌓"), Comma, S(0), Invalid("🌍"), Comma]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,8 +614,8 @@ mod tests {
|
|||||||
t!(Body, r"\a" => [T("\\"), T("a")]);
|
t!(Body, r"\a" => [T("\\"), T("a")]);
|
||||||
t!(Body, r"\:" => [T(r"\"), T(":")]);
|
t!(Body, r"\:" => [T(r"\"), T(":")]);
|
||||||
t!(Body, r"\=" => [T(r"\"), T("=")]);
|
t!(Body, r"\=" => [T(r"\"), T("=")]);
|
||||||
t!(Header, r"\\\\" => [Invalid("\\"), Invalid("\\")]);
|
t!(Header, r"\\\\" => [Invalid(r"\\\\")]);
|
||||||
t!(Header, r"\a" => [Invalid("\\"), Id("a")]);
|
t!(Header, r"\a" => [Invalid(r"\a")]);
|
||||||
t!(Header, r"\:" => [Invalid(r"\"), Colon]);
|
t!(Header, r"\:" => [Invalid(r"\"), Colon]);
|
||||||
t!(Header, r"\=" => [Invalid(r"\"), Equals]);
|
t!(Header, r"\=" => [Invalid(r"\"), Equals]);
|
||||||
t!(Header, r"\," => [Invalid(r"\"), Comma]);
|
t!(Header, r"\," => [Invalid(r"\"), Comma]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user