diff --git a/src/func.rs b/src/func.rs index 88d08d2dd..2cdd026c2 100644 --- a/src/func.rs +++ b/src/func.rs @@ -166,24 +166,22 @@ macro_rules! function { /// Parse the body of a function. /// -/// - If the function does not expect a body, use `body!(nope: body, errors)`. -/// - If the function can have a body, use `body!(opt: body, ctx, errors, decos)`. +/// - If the function does not expect a body, use `body!(nope: body, feedback)`. +/// - If the function can have a body, use `body!(opt: body, ctx, feedback, +/// decos)`. /// /// # Arguments /// - The `$body` should be of type `Option>`. -/// - The `$ctx` is the [`ParseContext`](crate::syntax::ParseContext) to use for parsing. -/// - The `$errors` and `$decos` should be mutable references to vectors of spanned -/// errors / decorations which are filled with the errors and decorations arising -/// from parsing. +/// - The `$ctx` is the [`ParseContext`](crate::syntax::ParseContext) to use for +/// parsing. +/// - The `$feedback` should be a mutable references to a +/// [`Feedback`](crate::Feedback) struct which is filled with the feedback +/// information arising from parsing. #[macro_export] macro_rules! body { (opt: $body:expr, $ctx:expr, $feedback:expr) => ({ $body.map(|body| { - // Since the body span starts at the opening bracket of the body, we - // need to add 1 column to find out the start position of body - // content. - let start = body.span.start + $crate::syntax::span::Position::new(0, 1); - let parsed = $crate::syntax::parse(start, body.v, $ctx); + let parsed = $crate::syntax::parse(body.span.start, body.v, $ctx); $feedback.extend(parsed.feedback); parsed.output }) diff --git a/src/layout/actions.rs b/src/layout/actions.rs index 0abef5f9e..8b50edfa3 100644 --- a/src/layout/actions.rs +++ b/src/layout/actions.rs @@ -60,7 +60,7 @@ impl Debug for LayoutAction { use LayoutAction::*; match self { MoveAbsolute(s) => write!(f, "move {} {}", s.x, s.y), - SetFont(i, s) => write!(f, "font {}_{} {}", i.id, i.variant, s), + SetFont(i, s) => write!(f, "font {}-{} {}", i.id, i.variant, s), WriteText(s) => write!(f, "write {:?}", s), DebugBox(s) => write!(f, "box {} {}", s.x, s.y), } diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 57a24e610..1f3072728 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -1,8 +1,8 @@ //! Parsing of source code into syntax models. use crate::{Pass, Feedback}; -use super::expr::*; use super::func::{FuncHeader, FuncArgs, FuncArg}; +use super::expr::*; use super::scope::Scope; use super::span::{Position, Span, Spanned}; use super::tokens::{Token, Tokens, TokenizationMode}; @@ -82,11 +82,10 @@ struct FuncParser<'s> { tokens: Tokens<'s>, peeked: Option>>>, - /// The spanned body string if there is a body. The string itself is just - /// the parsed without the brackets, while the span includes the brackets. + /// The spanned body string if there is a body. /// ```typst /// [tokens][body] - /// ^^^^^^ + /// ^^^^ /// ``` body: Option>, } @@ -398,7 +397,8 @@ fn unescape(string: &str) -> String { #[allow(non_snake_case)] mod tests { use crate::size::Size; - use super::super::test::{DebugFn, check, zspan}; + use crate::syntax::test::{DebugFn, check, zspan}; + use crate::syntax::func::Value; use super::*; use Decoration::*; @@ -407,11 +407,31 @@ mod tests { ToggleItalic as Italic, ToggleBolder as Bold, ToggleMonospace as Mono, }; - use Expr::{/*Number as Num,*/ Bool}; + use Expr::{Number as Num, Size as Sz, Bool}; fn Id(text: &str) -> Expr { Expr::Ident(Ident(text.to_string())) } fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) } + fn Pt(points: f32) -> Expr { Expr::Size(Size::pt(points)) } fn T(text: &str) -> Node { Node::Text(text.to_string()) } + /// Create a tuple expression. + macro_rules! tuple { + ($($items:expr),* $(,)?) => { + Expr::Tuple(Tuple { items: spanned![vec $($items),*].0 }) + }; + } + + /// Create an object expression. + macro_rules! object { + ($($key:expr => $value:expr),* $(,)?) => { + Expr::Object(Object { + pairs: vec![$(Pair { + key: zspan(Ident($key.to_string())), + value: zspan($value), + }),*] + }) + }; + } + /// Test whether the given string parses into the given transform pass. macro_rules! test { ($source:expr => [$($model:tt)*], $transform:expr) => { @@ -421,6 +441,7 @@ mod tests { scope.add::("f"); scope.add::("n"); scope.add::("box"); + scope.add::("val"); let ctx = ParseContext { scope: &scope }; let found = parse(Position::ZERO, $source, ctx); @@ -457,34 +478,25 @@ mod tests { /// Write down a `DebugFn` function model compactly. macro_rules! func { - ($name:expr - $(,pos: [$($item:expr),* $(,)?])? - $(,key: [$($key:expr => $value:expr),* $(,)?])?; - $($b:tt)*) => ({ + ($name:tt $(, ($($pos:tt)*), { $($key:tt)* } )? $(; $($body:tt)*)?) => ({ #[allow(unused_mut)] let mut args = FuncArgs::new(); - $(args.pos = Tuple { items: spanned![vec $($item),*].0 };)? - $(args.key = Object { - pairs: vec![$(Pair { - key: zspan(Ident($key.to_string())), - value: zspan($value), - }),*] - };)? + $(args.pos = Tuple::parse(zspan(tuple!($($pos)*))).unwrap();)? + $(args.key = Object::parse(zspan(object! { $($key)* })).unwrap();)? Node::Model(Box::new(DebugFn { header: FuncHeader { - name: zspan(Ident($name.to_string())), + name: spanned!(item $name).map(|s| Ident(s.to_string())), args, }, - body: func!(@body $($b)*), + body: func!(@body $($($body)*)?), })) }); - (@body Some([$($body:tt)*])) => ({ + (@body [$($body:tt)*]) => ({ Some(SyntaxModel { nodes: spanned![vec $($body)*].0 }) }); - - (@body None) => (None); + (@body) => (None); } #[test] @@ -504,70 +516,266 @@ mod tests { #[test] fn parse_flat_nodes() { - p!("" => []); - p!("hi" => [T("hi")]); - p!("*hi" => [Bold, T("hi")]); - p!("hi_" => [T("hi"), Italic]); - p!("`py`" => [Mono, T("py"), Mono]); - p!("hi you" => [T("hi"), S, T("you")]); - p!("πŸ’œ\n\n 🌍" => [T("πŸ’œ"), N, T("🌍")]); + p!("" => []); + p!("hi" => [T("hi")]); + p!("*hi" => [Bold, T("hi")]); + p!("hi_" => [T("hi"), Italic]); + p!("`py`" => [Mono, T("py"), Mono]); + p!("hi you" => [T("hi"), S, T("you")]); + p!("hi// you\nw" => [T("hi"), S, T("w")]); + p!("\n\n\nhello" => [N, T("hello")]); + p!("first//\n//\nsecond" => [T("first"), S, S, T("second")]); + p!("first//\n \nsecond" => [T("first"), N, T("second")]); + p!("first/*\n \n*/second" => [T("first"), T("second")]); + p!("πŸ’œ\n\n 🌍" => [T("πŸ’œ"), N, T("🌍")]); + + p!("Hi" => [(0:0, 0:2, T("Hi"))]); + p!("*Hi*" => [(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)]); + p!("🌎*/[n]" => [(0:0, 0:1, T("🌎")), (0:3, 0:6, func!((0:1, 0:2, "n")))]); + + e!("hi\n */" => [(1:1, 1:3, "unexpected end of block comment")]); } #[test] - fn parse_functions() { - p!("[func]" => [func!("func"; None)]); - p!("[tree][hi *you*]" => [func!("tree"; Some([T("hi"), S, Bold, T("you"), Bold]))]); - p!("[f: , hi, * \"du\"]" => [func!("f", pos: [Id("hi"), Str("du")]; None)]); - p!("from [align: left] to" => [ - T("from"), S, func!("align", pos: [Id("left")]; None), S, T("to") + fn parse_function_names() { + // No closing bracket + p!("[" => [func!("")]); + e!("[" => [ + (0:1, 0:1, "expected identifier"), + (0:1, 0:1, "expected closing bracket") ]); - p!("[f: left, 12pt, false]" => [ - func!("f", pos: [Id("left"), Expr::Size(Size::pt(12.0)), Bool(false)]; None) + // No name + p!("[]" => [func!("")]); + e!("[]" => [(0:1, 0:1, "expected identifier")]); + + p!("[\"]" => [func!("")]); + e!("[\"]" => [ + (0:1, 0:3, "expected identifier, found string"), + (0:3, 0:3, "expected closing bracket"), ]); - p!("[box: x=1.2pt, false][a b c] bye" => [ - func!( - "box", - pos: [Bool(false)], - key: ["x" => Expr::Size(Size::pt(1.2))]; - Some([T("a"), S, T("b"), S, T("c")]) - ), - S, T("bye"), - ]); + // 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 + p!("[hi]" => [func!("hi")]); + e!("[hi]" => [(0:1, 0:3, "unknown function")]); + d!("[hi]" => [(0:1, 0:3, InvalidFuncName)]); + + // An invalid token + p!("[🌎]" => [func!("")]); + e!("[🌎]" => [(0:1, 0:2, "expected identifier, found invalid token")]); + d!("[🌎]" => []); + p!("[ 🌎]" => [func!("")]); + e!("[ 🌎]" => [(0:3, 0:4, "expected identifier, found invalid token")]); + d!("[ 🌎]" => []); } #[test] - fn parse_spanned() { - p!("hi you" => [(0:0, 0:2, T("hi")), (0:2, 0:3, S), (0:3, 0:6, T("you"))]); + fn parse_colon_starting_function_arguments() { + // No colon before arg + p!("[val\"s\"]" => [func!("val")]); + e!("[val\"s\"]" => [(0:4, 0:4, "expected colon")]); + + // No colon before valid, but wrong token + p!("[val=]" => [func!("val")]); + e!("[val=]" => [(0:4, 0:4, "expected colon")]); + + // No colon before invalid tokens, which are ignored + p!("[val/🌎:$]" => [func!("val")]); + e!("[val/🌎:$]" => [(0:4, 0:4, "expected colon")]); + d!("[val/🌎:$]" => [(0:1, 0:4, ValidFuncName)]); + + // String in invalid header without colon still parsed as string + // Note: No "expected quote" error because not even the string was + // expected. + e!("[val/\"]" => [ + (0:4, 0:4, "expected colon"), + (0:7, 0:7, "expected closing bracket"), + ]); + + // Just colon without args + p!("[val:]" => [func!("val")]); + e!("[val:]" => []); + p!("[val:/*12pt*/]" => [func!("val")]); + + // Whitespace / comments around colon + p!("[val\n:\ntrue]" => [func!("val", (Bool(true)), {})]); + p!("[val/*:*/://\ntrue]" => [func!("val", (Bool(true)), {})]); + e!("[val/*:*/://\ntrue]" => []); } #[test] - fn parse_errors() { - e!("[f: , hi, * \"du\"]" => [ - (0:4, 0:5, "expected value, found comma"), - (0:10, 0:11, "expected value, found invalid token"), - ]); - e!("[f:, , ,]" => [ - (0:3, 0:4, "expected value, found comma"), - (0:5, 0:6, "expected value, found comma"), - (0:7, 0:8, "expected value, found comma"), - ]); - e!("[f:" => [(0:3, 0:3, "expected closing bracket")]); - e!("[f: hi" => [(0:6, 0:6, "expected closing bracket")]); - e!("[f: hey 12pt]" => [(0:7, 0:7, "expected comma")]); - e!("[box: x=, false z=y=4" => [ - (0:8, 0:9, "expected value, found comma"), - (0:15, 0:15, "expected comma"), - (0:19, 0:19, "expected comma"), - (0:19, 0:20, "expected value, found equals sign"), - (0:21, 0:21, "expected closing bracket"), + fn parse_one_positional_argument() { + // Different expressions + d!("[val: true]" => [(0:1, 0:4, ValidFuncName)]); + p!("[val: true]" => [func!("val", (Bool(true)), {})]); + p!("[val: _]" => [func!("val", (Id("_")), {})]); + p!("[val: name]" => [func!("val", (Id("name")), {})]); + p!("[val: \"hi\"]" => [func!("val", (Str("hi")), {})]); + p!("[val: \"a\n[]\\\"string\"]" => [func!("val", (Str("a\n[]\"string")), {})]); + 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. + p!("[val: \"hello]" => [func!("val", (Str("hello]")), {})]); + e!("[val: \"hello]" => [ + (0:13, 0:13, "expected quote"), + (0:13, 0:13, "expected closing bracket"), ]); + + // Tuple: unimplemented + p!("[val: ()]" => [func!("val", (tuple!()), {})]); + + // Object: unimplemented + p!("[val: {}]" => [func!("val", (object! {}), {})]); } #[test] - fn parse_decos() { - d!("*Technische UniversitΓ€t Berlin* [n]\n [n]" - => [(0:33, 0:34, ValidFuncName), (1:33, 1:34, ValidFuncName)]); + fn parse_one_keyword_argument() { + // Correct + p!("[val: x=true]" => [func!("val", (), { "x" => Bool(true) })]); + d!("[val: x=true]" => [(0:6, 0:7, ArgumentKey), (0:1, 0:4, ValidFuncName)]); + + // Spacing around keyword arguments + p!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => [S, func!("val", (), { "hi" => Str("s\n") })]); + d!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => [(2:1, 2:3, ArgumentKey), (1:2, 1:5, ValidFuncName)]); + e!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" => []); + + // Missing value + p!("[val: x=]" => [func!("val")]); + e!("[val: x=]" => [(0:8, 0:8, "expected value")]); + d!("[val: x=]" => [(0:6, 0:7, ArgumentKey), (0:1, 0:4, ValidFuncName)]); + } + + #[test] + fn parse_multiple_mixed_arguments() { + p!("[val: a,]" => [func!("val", (Id("a")), {})]); + e!("[val: a,]" => []); + p!("[val: 12pt, key=value]" => [func!("val", (Pt(12.0)), { "key" => Id("value") })]); + d!("[val: 12pt, key=value]" => [(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")), {})]); + e!("[val: a , \"b\" , c]" => []); + } + + #[test] + fn parse_invalid_values() { + e!("[val: )]" => [(0:6, 0:7, "expected value, found closing paren")]); + e!("[val: }]" => [(0:6, 0:7, "expected value, found closing brace")]); + e!("[val: :]" => [(0:6, 0:7, "expected value, found colon")]); + e!("[val: ,]" => [(0:6, 0:7, "expected value, found comma")]); + e!("[val: =]" => [(0:6, 0:7, "expected value, found equals sign")]); + e!("[val: 🌎]" => [(0:6, 0:7, "expected value, found invalid token")]); + e!("[val: 12ept]" => [(0:6, 0:11, "expected value, found invalid token")]); + e!("[val: [hi]]" => [(0:6, 0:10, "expected value, found function")]); + d!("[val: [hi]]" => [(0:1, 0:4, ValidFuncName)]); + } + + #[test] + fn parse_invalid_key_value_pairs() { + // Invalid keys + p!("[val: true=you]" => [func!("val", (Bool(true), Id("you")), {})]); + e!("[val: true=you]" => [ + (0:10, 0:10, "expected comma"), + (0:10, 0:11, "expected value, found equals sign"), + ]); + d!("[val: true=you]" => [(0:1, 0:4, ValidFuncName)]); + + p!("[box: z=y=4]" => [func!("box", (Num(4.0)), { "z" => Id("y") })]); + e!("[box: z=y=4]" => [ + (0:9, 0:9, "expected comma"), + (0:9, 0:10, "expected value, found equals sign"), + ]); + + // Invalid colon after keyable positional argument + p!("[val: key:12]" => [func!("val", (Id("key"), Num(12.0)), {})]); + e!("[val: key:12]" => [ + (0:9, 0:9, "expected comma"), + (0:9, 0:10, "expected value, found colon"), + ]); + d!("[val: key:12]" => [(0:1, 0:4, ValidFuncName)]); + + // Invalid colon after non-keyable positional argument + p!("[val: true:12]" => [func!("val", (Bool(true), Num(12.0)), {})]); + e!("[val: true:12]" => [ + (0:10, 0:10, "expected comma"), + (0:10, 0:11, "expected value, found colon"), + ]); + d!("[val: true:12]" => [(0:1, 0:4, ValidFuncName)]); + } + + #[test] + fn parse_invalid_commas() { + // Missing commas + p!("[val: 1pt 1]" => [func!("val", (Pt(1.0), Num(1.0)), {})]); + e!("[val: 1pt 1]" => [(0:9, 0:9, "expected comma")]); + p!(r#"[val: _"s"]"# => [func!("val", (Id("_"), Str("s")), {})]); + e!(r#"[val: _"s"]"# => [(0:7, 0:7, "expected comma")]); + + // Unexpected commas + p!("[val:,]" => [func!("val")]); + e!("[val:,]" => [(0:5, 0:6, "expected value, found comma")]); + p!("[val:, true]" => [func!("val", (Bool(true)), {})]); + e!("[val:, true]" => [(0:5, 0:6, "expected value, found comma")]); + p!("[val: key=,]" => [func!("val")]); + e!("[val: key=,]" => [(0:10, 0:11, "expected value, found comma")]); + } + + #[test] + fn parse_bodies() { + p!("[val][Hi]" => [func!("val"; [T("Hi")])]); + + // Body nodes in bodies. + p!("[val:*][*Hi*]" => [func!("val"; [Bold, T("Hi"), Bold])]); + e!("[val:*][*Hi*]" => [(0:5, 0:6, "expected value, found invalid token")]); + + // Errors in bodies. + p!(" [val][ */ ]" => [S, func!("val"; [S, S])]); + e!(" [val][ */ ]" => [(0:8, 0:10, "unexpected end of block comment")]); + } + + #[test] + fn parse_spanned_functions() { + // Space before function + p!(" [val]" => [(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))]); + d!(" [val]" => [(0:2, 0:5, ValidFuncName)]); + + // Newline before function + p!(" \n\r\n[val]" => [(0:0, 2:0, N), (2:0, 2:5, func!((0:1, 0:4, "val")))]); + d!(" \n\r\n[val]" => [(2:1, 2:4, ValidFuncName)]); + + // Content before function + p!("hello [val][world] 🌎" => [ + (0:0, 0:5, T("hello")), + (0:5, 0:6, S), + (0:6, 0:18, func!((0:1, 0:4, "val"); [(0:6, 0:11, T("world"))])), + (0:18, 0:19, S), + (0:19, 0:20, T("🌎")), + ]); + d!("hello [val][world] 🌎" => [(0:7, 0:10, ValidFuncName)]); + e!("hello [val][world] 🌎" => []); + + // Nested function + p!(" [val][\nbody[ box]\n ]" => [ + (0:0, 0:1, S), + (0:1, 2:2, func!((0:1, 0:4, "val"); [ + (0:6, 1:0, S), + (1:0, 1:4, T("body")), + (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), + (1:6, 1:9, ValidFuncName) + ]); } } diff --git a/src/syntax/test.rs b/src/syntax/test.rs index 6c89b4f52..22ce45e50 100644 --- a/src/syntax/test.rs +++ b/src/syntax/test.rs @@ -43,6 +43,10 @@ macro_rules! spanned { } }); + (item $v:expr) => { + $crate::syntax::test::zspan($v) + }; + (vec $(($sl:tt:$sc:tt, $el:tt:$ec:tt, $v:expr)),* $(,)?) => { (vec![$(spanned![item ($sl:$sc, $el:$ec, $v)]),*], true) }; diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index 41acb94fe..210e5a343 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -34,9 +34,8 @@ pub enum Token<'s> { /// ```typst /// [header][hello *world*] /// ^^^^^^^^^^^^^ + /// ^-- The span is relative to right before this bracket /// ``` - /// - /// The span includes the brackets while the string does not. body: Option>, /// Whether the last closing bracket was present. /// - `[func]` or `[func][body]` => terminated @@ -288,15 +287,15 @@ impl<'s> Tokens<'s> { return Function { header, body: None, terminated }; } + self.eat(); + let body_start = self.pos() - start; - self.eat(); - let (body, terminated) = self.read_function_part(); - self.eat(); - - let body_end = self.pos(); + let body_end = self.pos() - start; let span = Span::new(body_start, body_end); + self.eat(); + Function { header, body: Some(Spanned { v: body, span }), terminated } } @@ -476,7 +475,10 @@ mod tests { LineComment as LC, BlockComment as BC, LeftParen as LP, RightParen as RP, LeftBrace as LB, RightBrace as RB, - ExprIdent as Id, ExprNumber as Num, ExprBool as Bool, + ExprIdent as Id, + ExprNumber as Num, + ExprSize as Sz, + ExprBool as Bool, Text as T, }; @@ -563,10 +565,10 @@ mod tests { t!(Header, "__main__" => [Id("__main__")]); t!(Header, ".func.box" => [Id(".func.box")]); t!(Header, "--arg, _b, _1" => [Id("--arg"), Comma, S(0), Id("_b"), Comma, S(0), Id("_1")]); - t!(Header, "12_pt, 12pt" => [Invalid("12_pt"), Comma, S(0), ExprSize(Size::pt(12.0))]); - t!(Header, "1e5in" => [ExprSize(Size::inches(100000.0))]); - t!(Header, "2.3cm" => [ExprSize(Size::cm(2.3))]); - t!(Header, "02.4mm" => [ExprSize(Size::mm(2.4))]); + t!(Header, "12_pt, 12pt" => [Invalid("12_pt"), Comma, S(0), Sz(Size::pt(12.0))]); + t!(Header, "1e5in" => [Sz(Size::inches(100000.0))]); + t!(Header, "2.3cm" => [Sz(Size::cm(2.3))]); + t!(Header, "02.4mm" => [Sz(Size::mm(2.4))]); t!(Header, "2.4.cm" => [Invalid("2.4.cm")]); t!(Header, "πŸŒ“, 🌍," => [Invalid("πŸŒ“"), Comma, S(0), Invalid("🌍"), Comma]); } @@ -586,10 +588,14 @@ mod tests { #[test] fn tokenize_functions() { + t!(Body, "a[f]" => [T("a"), func!("f", None, true)]); + t!(Body, "[f]a" => [func!("f", None, true), T("a")]); + t!(Body, "\n\n[f][ ]" => [S(2), func!("f", Some((0:4, 0:5, " ")), true)]); + t!(Body, "abc [f][ ]a" => [T("abc"), S(0), func!("f", Some((0:4, 0:5, " ")), true), T("a")]); t!(Body, "[f: [=][*]]" => [func!("f: [=][*]", None, true)]); - t!(Body, "[_][[,],]," => [func!("_", Some((0:3, 0:9, "[,],")), true), T(",")]); - t!(Body, "[=][=][=]" => [func!("=", Some((0:3, 0:6, "=")), true), func!("=", None, true)]); - t!(Body, "[=][[=][=][=]]" => [func!("=", Some((0:3, 0:14, "[=][=][=]")), true)]); + t!(Body, "[_][[,],]," => [func!("_", Some((0:4, 0:8, "[,],")), true), T(",")]); + t!(Body, "[=][=][=]" => [func!("=", Some((0:4, 0:5, "=")), true), func!("=", None, true)]); + t!(Body, "[=][[=][=][=]]" => [func!("=", Some((0:4, 0:13, "[=][=][=]")), true)]); t!(Header, "[" => [func!("", None, false)]); t!(Header, "]" => [Invalid("]")]); }