diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e5ce17a9f..435438f8f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -103,6 +103,20 @@ jobs: cargo test --package askama_warp --all-targets cargo clippy --package askama_warp --all-targets -- -D warnings + Tide: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + - run: | + cargo test --package askama_tide --all-targets + cargo clippy --package askama_tide --all-targets -- -D warnings + Lint: runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 55a3dc850..2f0bf6c11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "askama_iron", "askama_rocket", "askama_shared", + "askama_tide", "askama_warp", "testing", ] diff --git a/README.md b/README.md index 4007ffd43..b6d40cae6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ in a for-profit context, please consider supporting my open source work on * Construct templates using a familiar, easy-to-use syntax * Template code is compiled into your crate for [optimal performance][benchmarks] * Benefit from the safety provided by Rust's type system -* Optional built-in support for Actix, Gotham, Iron, Rocket and warp web frameworks +* Optional built-in support for Actix, Gotham, Iron, Rocket, tide, and warp web frameworks * Debugging features to assist you in template development * Templates must be valid UTF-8 and produce UTF-8 when rendered * Works on stable Rust diff --git a/askama/Cargo.toml b/askama/Cargo.toml index 21ad6e253..aa21dcb4f 100644 --- a/askama/Cargo.toml +++ b/askama/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "askama" -version = "0.10.0" +version = "0.10.2" authors = ["Dirkjan Ochtman "] description = "Type-safe, compiled Jinja-like templates for Rust" documentation = "https://docs.rs/askama" @@ -24,16 +24,17 @@ urlencode = ["askama_shared/percent-encoding"] serde-json = ["askama_shared/json"] serde-yaml = ["askama_shared/yaml"] num-traits = ["askama_shared/num-traits"] -with-iron = ["askama_derive/iron"] -with-rocket = ["askama_derive/rocket"] with-actix-web = ["askama_derive/actix-web"] with-gotham = ["askama_derive/gotham"] +with-iron = ["askama_derive/iron"] +with-rocket = ["askama_derive/rocket"] +with-tide = ["askama_derive/tide"] with-warp = ["askama_derive/warp"] [dependencies] -askama_derive = { version = "0.10", path = "../askama_derive" } +askama_derive = { version = "0.10.2", path = "../askama_derive" } askama_escape = { version = "0.10", path = "../askama_escape" } -askama_shared = { version = "0.10", path = "../askama_shared", default-features = false } +askama_shared = { version = "0.10.3", path = "../askama_shared", default-features = false } mime = { version = "0.3", optional = true } mime_guess = { version = "2.0.0-alpha", optional = true } diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml index 4ed4e7b3f..8e97647f4 100644 --- a/askama_derive/Cargo.toml +++ b/askama_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "askama_derive" -version = "0.10.0" +version = "0.10.2" authors = ["Dirkjan Ochtman "] description = "Procedural macro package for Askama" homepage = "https://github.com/djc/askama" @@ -14,14 +14,15 @@ edition = "2018" proc-macro = true [features] -iron = [] -rocket = [] actix-web = [] gotham = [] +iron = [] +rocket = [] +tide = [] warp = [] [dependencies] -askama_shared = { version = "0.10", path = "../askama_shared" } +askama_shared = { version = "0.10.3", path = "../askama_shared", default-features = false } proc-macro2 = "1" quote = "1" syn = "1" diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs index fcd857d2e..227794276 100644 --- a/askama_derive/src/lib.rs +++ b/askama_derive/src/lib.rs @@ -89,5 +89,6 @@ const INTEGRATIONS: Integrations = Integrations { gotham: cfg!(feature = "gotham"), iron: cfg!(feature = "iron"), rocket: cfg!(feature = "rocket"), + tide: cfg!(feature = "tide"), warp: cfg!(feature = "warp"), }; diff --git a/askama_shared/Cargo.toml b/askama_shared/Cargo.toml index d902b4113..40d25f037 100644 --- a/askama_shared/Cargo.toml +++ b/askama_shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "askama_shared" -version = "0.10.1" +version = "0.10.3" authors = ["Dirkjan Ochtman "] description = "Shared code for Askama" homepage = "https://github.com/djc/askama" diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs index 180a3568e..82f68ff61 100644 --- a/askama_shared/src/generator.rs +++ b/askama_shared/src/generator.rs @@ -94,18 +94,22 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { self.impl_template(ctx, &mut buf); self.impl_display(&mut buf); - if self.integrations.iron { - self.impl_modifier_response(&mut buf); - } - if self.integrations.rocket { - self.impl_rocket_responder(&mut buf); - } + if self.integrations.actix { self.impl_actix_web_responder(&mut buf); } if self.integrations.gotham { self.impl_gotham_into_response(&mut buf); } + if self.integrations.iron { + self.impl_iron_modifier_response(&mut buf); + } + if self.integrations.rocket { + self.impl_rocket_responder(&mut buf); + } + if self.integrations.tide { + self.impl_tide_integrations(&mut buf); + } if self.integrations.warp { self.impl_warp_reply(&mut buf); } @@ -199,8 +203,41 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { buf.writeln("}"); } + // Implement Actix-web's `Responder`. + fn impl_actix_web_responder(&mut self, buf: &mut Buffer) { + self.write_header(buf, "::actix_web::Responder", None); + buf.writeln("type Future = ::askama_actix::futures::Ready<::std::result::Result<::actix_web::HttpResponse, Self::Error>>;"); + buf.writeln("type Error = ::actix_web::Error;"); + buf.writeln( + "fn respond_to(self, _req: &::actix_web::HttpRequest) \ + -> Self::Future {", + ); + + buf.writeln("use ::askama_actix::TemplateIntoResponse;"); + buf.writeln("::askama_actix::futures::ready(self.into_response())"); + + buf.writeln("}"); + buf.writeln("}"); + } + + // Implement gotham's `IntoResponse`. + fn impl_gotham_into_response(&mut self, buf: &mut Buffer) { + self.write_header(buf, "::askama_gotham::IntoResponse", None); + buf.writeln( + "fn into_response(self, _state: &::askama_gotham::State)\ + -> ::askama_gotham::Response<::askama_gotham::Body> {", + ); + let ext = match self.input.path.extension() { + Some(s) => s.to_str().unwrap(), + None => "txt", + }; + buf.writeln(&format!("::askama_gotham::respond(&self, {:?})", ext)); + buf.writeln("}"); + buf.writeln("}"); + } + // Implement iron's Modifier if enabled - fn impl_modifier_response(&mut self, buf: &mut Buffer) { + fn impl_iron_modifier_response(&mut self, buf: &mut Buffer) { self.write_header( buf, "::askama_iron::Modifier<::askama_iron::Response>", @@ -251,37 +288,30 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> { buf.writeln("}"); } - // Implement Actix-web's `Responder`. - fn impl_actix_web_responder(&mut self, buf: &mut Buffer) { - self.write_header(buf, "::actix_web::Responder", None); - buf.writeln("type Future = ::askama_actix::futures::Ready<::std::result::Result<::actix_web::HttpResponse, Self::Error>>;"); - buf.writeln("type Error = ::actix_web::Error;"); - buf.writeln( - "fn respond_to(self, _req: &::actix_web::HttpRequest) \ - -> Self::Future {", - ); - - buf.writeln("use ::askama_actix::TemplateIntoResponse;"); - buf.writeln("::askama_actix::futures::ready(self.into_response())"); - - buf.writeln("}"); - buf.writeln("}"); - } + fn impl_tide_integrations(&mut self, buf: &mut Buffer) { + let ext = self + .input + .path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("txt"); - // Implement gotham's `IntoResponse`. - fn impl_gotham_into_response(&mut self, buf: &mut Buffer) { - self.write_header(buf, "::askama_gotham::IntoResponse", None); + self.write_header( + buf, + "std::convert::TryInto<::askama_tide::tide::Body>", + None, + ); buf.writeln( - "fn into_response(self, _state: &::askama_gotham::State)\ - -> ::askama_gotham::Response<::askama_gotham::Body> {", + "type Error = ::askama_tide::askama::Error;\n\ + fn try_into(self) -> ::askama_tide::askama::Result<::askama_tide::tide::Body> {", ); - let ext = match self.input.path.extension() { - Some(s) => s.to_str().unwrap(), - None => "txt", - }; - buf.writeln(&format!("::askama_gotham::respond(&self, {:?})", ext)); + buf.writeln(&format!("::askama_tide::try_into_body(&self, {:?})", &ext)); buf.writeln("}"); buf.writeln("}"); + self.write_header(buf, "Into<::askama_tide::tide::Response>", None); + buf.writeln("fn into(self) -> ::askama_tide::tide::Response {"); + buf.writeln(&format!("::askama_tide::into_response(&self, {:?})", ext)); + buf.writeln("}\n}"); } fn impl_warp_reply(&mut self, buf: &mut Buffer) { diff --git a/askama_shared/src/lib.rs b/askama_shared/src/lib.rs index e40543944..2e3d50227 100644 --- a/askama_shared/src/lib.rs +++ b/askama_shared/src/lib.rs @@ -266,6 +266,7 @@ pub struct Integrations { pub gotham: bool, pub iron: bool, pub rocket: bool, + pub tide: bool, pub warp: bool, } diff --git a/askama_shared/src/parser.rs b/askama_shared/src/parser.rs index 3a3157a4c..1f3be075a 100644 --- a/askama_shared/src/parser.rs +++ b/askama_shared/src/parser.rs @@ -307,13 +307,13 @@ fn expr_var(i: &[u8]) -> IResult<&[u8], Expr> { } fn expr_var_call(i: &[u8]) -> IResult<&[u8], Expr> { - let (i, (s, args)) = tuple((identifier, arguments))(i)?; + let (i, (s, args)) = tuple((ws(identifier), arguments))(i)?; Ok((i, Expr::VarCall(s, args))) } fn path(i: &[u8]) -> IResult<&[u8], Vec<&str>> { - let tail = separated_nonempty_list(tag("::"), identifier); - let (i, (start, _, rest)) = tuple((identifier, tag("::"), tail))(i)?; + let tail = separated_nonempty_list(ws(tag("::")), identifier); + let (i, (start, _, rest)) = tuple((identifier, ws(tag("::")), tail))(i)?; let mut path = vec![start]; path.extend(rest); @@ -326,12 +326,12 @@ fn expr_path(i: &[u8]) -> IResult<&[u8], Expr> { } fn expr_path_call(i: &[u8]) -> IResult<&[u8], Expr> { - let (i, (path, args)) = tuple((path, arguments))(i)?; + let (i, (path, args)) = tuple((ws(path), arguments))(i)?; Ok((i, Expr::PathCall(path, args))) } fn variant_path(i: &[u8]) -> IResult<&[u8], MatchVariant> { - map(separated_nonempty_list(tag("::"), identifier), |path| { + map(separated_nonempty_list(ws(tag("::")), identifier), |path| { MatchVariant::Path(path) })(i) } @@ -358,7 +358,11 @@ fn param_name(i: &[u8]) -> IResult<&[u8], MatchParameter> { } fn arguments(i: &[u8]) -> IResult<&[u8], Vec> { - delimited(tag("("), separated_list(tag(","), ws(expr_any)), tag(")"))(i) + delimited( + ws(tag("(")), + separated_list(tag(","), ws(expr_any)), + ws(tag(")")), + )(i) } fn macro_arguments(i: &[u8]) -> IResult<&[u8], &str> { @@ -414,7 +418,11 @@ fn nested_parenthesis(i: &[u8]) -> ParserError<&str> { } fn parameters(i: &[u8]) -> IResult<&[u8], Vec<&str>> { - delimited(tag("("), separated_list(tag(","), ws(identifier)), tag(")"))(i) + delimited( + ws(tag("(")), + separated_list(tag(","), ws(identifier)), + ws(tag(")")), + )(i) } fn with_parameters(i: &[u8]) -> IResult<&[u8], MatchParameters> { @@ -446,7 +454,7 @@ fn match_named_parameters(i: &[u8]) -> IResult<&[u8], MatchParameters> { } fn expr_group(i: &[u8]) -> IResult<&[u8], Expr> { - map(delimited(char('('), expr_any, char(')')), |s| { + map(delimited(ws(char('(')), expr_any, ws(char(')'))), |s| { Expr::Group(Box::new(s)) })(i) } @@ -488,7 +496,8 @@ fn match_named_parameter(i: &[u8]) -> IResult<&[u8], (&str, Option IResult<&[u8], (&str, Option>)> { - let (i, (_, attr, args)) = tuple((tag("."), alt((num_lit, identifier)), opt(arguments)))(i)?; + let (i, (_, attr, args)) = + tuple((ws(tag(".")), alt((num_lit, identifier)), ws(opt(arguments))))(i)?; Ok((i, (attr, args))) } @@ -522,7 +531,7 @@ fn expr_index(i: &[u8]) -> IResult<&[u8], Expr> { } fn filter(i: &[u8]) -> IResult<&[u8], (&str, Option>)> { - let (i, (_, fname, args)) = tuple((tag("|"), identifier, opt(arguments)))(i)?; + let (i, (_, fname, args)) = tuple((tag("|"), ws(identifier), opt(arguments)))(i)?; Ok((i, (fname, args))) } @@ -545,7 +554,7 @@ fn expr_filtered(i: &[u8]) -> IResult<&[u8], Expr> { } fn expr_unary(i: &[u8]) -> IResult<&[u8], Expr> { - let (i, (op, expr)) = tuple((opt(alt((tag("!"), tag("-")))), expr_filtered))(i)?; + let (i, (op, expr)) = tuple((opt(alt((ws(tag("!")), ws(tag("-"))))), expr_filtered))(i)?; Ok(( i, match op { diff --git a/askama_tide/Cargo.toml b/askama_tide/Cargo.toml new file mode 100644 index 000000000..8492611a2 --- /dev/null +++ b/askama_tide/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "askama_tide" +version = "0.1.0" +authors = ["Jacob Rothstein "] +edition = "2018" +description = "Tide integration for Askama templates" +keywords = ["markup", "template", "jinja2", "html", "tide", "http-types"] +homepage = "https://github.com/djc/askama" +repository = "https://github.com/djc/askama" +documentation = "https://docs.rs/askama" +license = "MIT OR Apache-2.0" +workspace = ".." +readme = "README.md" + +[dependencies] +askama = { version = "0.10.2", path = "../askama", features = ["with-tide"] } +tide = "0.11" +async-std = { version = "1.6.0", features = ["unstable", "attributes"] } diff --git a/askama_tide/LICENSE-APACHE b/askama_tide/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/askama_tide/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/askama_tide/LICENSE-MIT b/askama_tide/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/askama_tide/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/askama_tide/README.md b/askama_tide/README.md new file mode 100644 index 000000000..05aa275f2 --- /dev/null +++ b/askama_tide/README.md @@ -0,0 +1,9 @@ +# askama_tide: Askama integration with tide + +[![Documentation](https://docs.rs/askama_tide/badge.svg)](https://docs.rs/askama_tide/) +[![Latest version](https://img.shields.io/crates/v/askama_tide.svg)](https://crates.io/crates/askama_tide) +[![Build Status](https://github.com/djc/askama/workflows/CI/badge.svg)](https://github.com/djc/askama/actions?query=workflow%3ACI) +[![Chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/djc/askama) + +Integration of the [Askama](https://github.com/djc/askama) templating engine in +code building on the tide web framework. diff --git a/askama_tide/src/lib.rs b/askama_tide/src/lib.rs new file mode 100644 index 000000000..0458348e6 --- /dev/null +++ b/askama_tide/src/lib.rs @@ -0,0 +1,32 @@ +pub use askama; +pub use tide; + +use askama::*; +use tide::{http::Mime, Body, Response}; + +pub fn try_into_body(t: &T, ext: &str) -> Result { + let string = t.render()?; + let mut body = Body::from_string(string); + + if let Some(mime) = Mime::from_extension(ext) { + body.set_mime(mime); + } + + Ok(body) +} + +pub fn into_response(t: &T, ext: &str) -> Response { + match try_into_body(t, ext) { + Ok(body) => { + let mut response = Response::new(200); + response.set_body(body); + response + } + + Err(e) => { + let mut response = Response::new(500); + response.set_body(e.to_string()); + response + } + } +} diff --git a/askama_tide/templates/hello.html b/askama_tide/templates/hello.html new file mode 100644 index 000000000..8149be7a6 --- /dev/null +++ b/askama_tide/templates/hello.html @@ -0,0 +1 @@ +Hello, {{ name }}! diff --git a/askama_tide/tests/tide.rs b/askama_tide/tests/tide.rs new file mode 100644 index 000000000..6fa1418f6 --- /dev/null +++ b/askama_tide/tests/tide.rs @@ -0,0 +1,29 @@ +use askama::Template; +use async_std::prelude::*; +use std::convert::TryInto; +use tide::{http::mime::HTML, Body, Response}; + +#[derive(Template)] +#[template(path = "hello.html")] +struct HelloTemplate<'a> { + name: &'a str, +} + +#[async_std::test] +async fn template_to_response() { + let mut res: Response = HelloTemplate { name: "world" }.into(); + assert_eq!(res.status(), 200); + assert_eq!(res.content_type(), Some(HTML)); + + let res: &mut tide::http::Response = res.as_mut(); + assert_eq!(res.body_string().await.unwrap(), "Hello, world!"); +} + +#[async_std::test] +async fn template_to_body() { + let mut body: Body = HelloTemplate { name: "world" }.try_into().unwrap(); + assert_eq!(body.mime(), &HTML); + let mut body_string = String::new(); + body.read_to_string(&mut body_string).await.unwrap(); + assert_eq!(body_string, "Hello, world!"); +} diff --git a/book/src/integrations.md b/book/src/integrations.md index 25bcc18b0..2d016c729 100644 --- a/book/src/integrations.md +++ b/book/src/integrations.md @@ -50,3 +50,12 @@ Enabling the `with-warp` feature appends an implementation of Warp's `Reply` trait for each template type. This makes it simple to return a template from a Warp filter. See [the example](https://github.com/djc/askama/blob/master/askama_warp/tests/warp.rs) from the Askama test suite for more on how to integrate. + +## Tide integration + +Enabling the `with-tide` feature appends `Into` and +`TryInto` implementations for each template type. This +provides the ability for tide apps to build a response directly from +a template, or to append a templated body to an existing +`Response`. See [the example](https://github.com/djc/askama/blob/master/askama_tide/tests/tide.rs) +from the Askama test suite for more on how to integrate. diff --git a/testing/templates/allow-whitespaces.html b/testing/templates/allow-whitespaces.html new file mode 100644 index 000000000..80f7ecd18 --- /dev/null +++ b/testing/templates/allow-whitespaces.html @@ -0,0 +1,63 @@ + +{{ tuple.0 }} +{{ tuple .1 }} +{{ tuple. 2 }} +{{ tuple . 3 }} +{% let ( t0 , t1 , t2 , t3 , ) = tuple %} + +{{ string }} +{{ string.len( ) }} +{{ string . len () }} +{{ string . len () }} +{{ string. len () }} +{{ string . len ( ) }} + +{{ nested_1 . nested_2 . array [0] }} +{{ nested_1 .nested_2. array [ 1 ] }} +{{ nested_1 .nested_2. hash [ "key" ] }} + +{% let array = nested_1.nested_2.array %} +{# +{% let array = nested_1.nested_2.array %} +{% let array = nested_1 . nested_2 . array %} +#} +{# +{% let hash = &nested_1.nested_2.hash %} +#} + +{{ array| json }} +{{ array[..]| json }}{{ array [ .. ]| json }} +{{ array[1..2]| json }}{{ array [ 1 .. 2 ]| json }} +{{ array[1..=2]| json }}{{ array [ 1 ..= 2 ]| json }} +{{ array[(0+1)..(3-2)]| json }}{{ array [ ( 0 + 1 ) .. ( 3 - 2 ) ]| json }} + +{{-1}}{{ -1 }}{{ - 1 }} +{{1+2}}{{ 1+2 }}{{ 1 +2 }}{{ 1+ 2 }} {{ 1 + 2 }} +{{1*2}}{{ 1*2 }}{{ 1 *2 }}{{ 1* 2 }} {{ 1 * 2 }} +{{1&2}}{{ 1&2 }}{{ 1 &2 }}{{ 1& 2 }} {{ 1 & 2 }} +{{1|2}}{{ 1|2 }}{{ 1 |2 }}{{ 1| 2 }} {{ 1 | 2 }} + +{{true}}{{false}} +{{!true}}{{ !true }}{{ ! true }} +{# +{{true&&false}}{{ true&&false }}{{ true &&false }}{{ true&& false }} {{ true && false }} +{{true||false}}{{ true||false }}{{ true ||false }}{{ true|| false }} {{ true || false }} +#} + +{{ self.f0() }}{{ self.f0 () }}{{ self.f0 ( ) }} +{{ self.f1("1") }}{{ self.f1 ( "1" ) }}{{ self.f1 ( "1" ) }} +{{ self.f2("1","2") }}{{ self.f2 ( "1" ,"2" ) }}{{ self.f2 ( "1" , "2" ) }} + +{% for s in 0..5 %}{% endfor %} +{% for s in 0 .. 5 %}{% endfor %} + +{% match option %} +{% when Option :: Some with ( option ) %} +{% when std :: option :: Option :: None %} +{% endmatch %} + +{{ std::string::String::new () }} +{# +{{ ::std::string::String::new () }} +#} + diff --git a/testing/tests/whitespace.rs b/testing/tests/whitespace.rs new file mode 100644 index 000000000..befc7d137 --- /dev/null +++ b/testing/tests/whitespace.rs @@ -0,0 +1,41 @@ +use askama::Template; + +#[derive(askama::Template, Default)] +#[template(path = "allow-whitespaces.html")] +struct AllowWhitespaces { + tuple: (u64, u64, u64, u64), + string: &'static str, + option: Option, + nested_1: AllowWhitespacesNested1, +} + +#[derive(Default)] +struct AllowWhitespacesNested1 { + nested_2: AllowWhitespacesNested2, +} + +#[derive(Default)] +struct AllowWhitespacesNested2 { + array: &'static [&'static str], + hash: std::collections::HashMap<&'static str, &'static str>, +} + +impl AllowWhitespaces { + fn f0(&self) -> &str { + "" + } + fn f1(&self, _a: &str) -> &str { + "" + } + fn f2(&self, _a: &str, _b: &str) -> &str { + "" + } +} + +#[test] +fn test_extra_whitespace() { + let mut template = AllowWhitespaces::default(); + template.nested_1.nested_2.array = &["a0", "a1", "a2", "a3"]; + template.nested_1.nested_2.hash.insert("key", "value"); + assert_eq!(template.render().unwrap(), "\n0\n0\n0\n0\n\n\n\n0\n0\n0\n0\n0\n\na0\na1\nvalue\n\n\n\n\n\n[\n \"a0\",\n \"a1\",\n \"a2\",\n \"a3\"\n]\n[\n \"a0\",\n \"a1\",\n \"a2\",\n \"a3\"\n][\n \"a0\",\n \"a1\",\n \"a2\",\n \"a3\"\n]\n[\n \"a1\"\n][\n \"a1\"\n]\n[\n \"a1\",\n \"a2\"\n][\n \"a1\",\n \"a2\"\n]\n[][]1-1-1\n3333 3\n2222 2\n0000 0\n3333 3\n\ntruefalse\nfalsefalsefalse\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); +}