diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 01527689..245f48b0 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -84,11 +84,20 @@ impl Default for Engine { } impl Engine { - async fn handle(&self, input: String) -> Result { - mrml::async_parse_with_options(input, self.parser.clone()) + async fn handle(&self, input: String) -> Result { + let item = mrml::async_parse_with_options(input, self.parser.clone()) .await - .map_err(EngineError::Parse) - .and_then(|mjml| mjml.render(&self.render).map_err(EngineError::Render)) + .map_err(EngineError::Parse)?; + + let content = item + .element + .render(&self.render) + .map_err(EngineError::Render)?; + + Ok(Response { + content, + warnings: item.warnings.into_iter().map(Warning::from).collect(), + }) } } @@ -97,12 +106,35 @@ struct Payload { template: String, } +#[derive(Debug, serde::Serialize)] +struct Response { + content: String, + warnings: Vec, +} + +#[derive(Debug, serde::Serialize)] +struct Warning { + code: &'static str, + start: usize, + end: usize, +} + +impl From for Warning { + fn from(value: mrml::prelude::parser::Warning) -> Self { + Warning { + code: value.kind.as_str(), + start: value.span.start, + end: value.span.end, + } + } +} + #[axum::debug_handler] async fn handler( State(engine): State, Json(payload): Json, -) -> Result { - engine.handle(payload.template).await +) -> Result, EngineError> { + engine.handle(payload.template).await.map(Json) } fn create_app() -> axum::Router { diff --git a/packages/mrml-cli/Cargo.toml b/packages/mrml-cli/Cargo.toml index 2ae241cb..fc15e8e0 100644 --- a/packages/mrml-cli/Cargo.toml +++ b/packages/mrml-cli/Cargo.toml @@ -16,8 +16,8 @@ name = "mrml" [dependencies] mrml = { version = "4.0.1", path = "../mrml-core", features = [ - "http-loader-ureq", - "local-loader", + "http-loader-ureq", + "local-loader", ] } clap = { version = "4.5", features = ["derive"] } env_logger = "0.11" diff --git a/packages/mrml-cli/src/main.rs b/packages/mrml-cli/src/main.rs index 5a3f4769..ee9f7609 100644 --- a/packages/mrml-cli/src/main.rs +++ b/packages/mrml-cli/src/main.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::collections::HashSet; +use std::error::Error; use std::fs::File; use std::io::prelude::*; use std::iter::FromIterator; @@ -12,54 +13,16 @@ use mrml::prelude::parser::loader::IncludeLoader; use mrml::prelude::parser::local_loader::LocalIncludeLoader; use mrml::prelude::parser::multi_loader::MultiIncludeLoader; use mrml::prelude::parser::noop_loader::NoopIncludeLoader; -use mrml::prelude::parser::{Error as ParserError, ParserOptions}; +use mrml::prelude::parser::{Error as ParserError, ParseOutput, ParserOptions}; use mrml::prelude::print::Printable; use mrml::prelude::render::RenderOptions; fn format_parser_error(error: ParserError) -> String { - let msg = match error { - ParserError::EndOfStream => String::from("invalid format"), - ParserError::UnexpectedAttribute(token) => { - format!( - "unexpected attribute at position {}:{}", - token.start, token.end - ) - } - ParserError::UnexpectedElement(token) => { - format!( - "unexpected element at position {}:{}", - token.start, token.end - ) - } - ParserError::UnexpectedToken(token) => { - format!("unexpected token at position {}:{}", token.start, token.end) - } - ParserError::InvalidAttribute(token) => { - format!( - "invalid attribute at position {}:{}", - token.start, token.end - ) - } - ParserError::InvalidFormat(token) => { - format!("invalid format at position {}:{}", token.start, token.end) - } - ParserError::IncludeLoaderError { position, source } => { - format!( - "something when wront when loading include at position {}:{}: {source:?}", - position.start, position.end - ) - } - ParserError::MissingAttribute(name, span) => format!( - "missing attribute {name:?} at position {}:{}", - span.start, span.end - ), - ParserError::SizeLimit => String::from("reached the max size limit"), - ParserError::NoRootNode => { - String::from("couldn't parse document: couldn't find mjml element") - } - ParserError::ParserError(inner) => format!("something went wront while parsing {inner}"), - }; - format!("couldn't parse document: {msg}") + if let Some(src) = error.source() { + format!("{error}: {src}") + } else { + format!("{error}") + } } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] @@ -151,7 +114,7 @@ impl Options { }) } - fn parse_mjml(&self, input: &str) -> Result { + fn parse_mjml(&self, input: &str) -> Result, String> { log::debug!("parsing mjml input"); let options = ParserOptions { include_loader: self.include_loader()?, @@ -159,17 +122,25 @@ impl Options { Mjml::parse_with_options(input, &options).map_err(format_parser_error) } - fn parse_input(&self, input: &str) -> Result { + fn parse_input(&self, input: String) -> Result, String> { if let Some(ref filename) = self.input { if filename.ends_with(".json") { - self.parse_json(input) + self.parse_json(&input).map(|element| ParseOutput { + element, + warnings: Vec::new(), + }) } else if filename.ends_with(".mjml") { - self.parse_mjml(input) + self.parse_mjml(&input) } else { Err(format!("unable to detect file type for {filename:?}")) } } else { - self.parse_mjml(input).or_else(|_| self.parse_json(input)) + self.parse_mjml(&input).or_else(|_| { + self.parse_json(&input).map(|element| ParseOutput { + element, + warnings: Vec::new(), + }) + }) } } @@ -183,8 +154,9 @@ impl Options { pub fn execute(self) -> Result<(), String> { let root = self.read_input()?; - let root = self.parse_input(&root)?; - self.subcmd.execute(&root) + let root = self.parse_input(root)?; + + self.subcmd.execute(root) } } @@ -201,23 +173,23 @@ enum SubCommand { } impl SubCommand { - pub fn execute(self, root: &Mjml) -> Result<(), String> { + pub fn execute(self, root: ParseOutput) -> Result<(), String> { match self { Self::FormatJSON(opts) => { log::debug!("format to json"); let output = if opts.pretty { - serde_json::to_string_pretty(root).expect("couldn't format to JSON") + serde_json::to_string_pretty(&root.element).expect("couldn't format to JSON") } else { - serde_json::to_string(root).expect("couldn't format to JSON") + serde_json::to_string(&root.element).expect("couldn't format to JSON") }; println!("{}", output); } Self::FormatMjml(opts) => { log::debug!("format to mjml"); let output = if opts.pretty { - root.print_pretty() + root.element.print_pretty() } else { - root.print_dense() + root.element.print_dense() } .expect("couldn't format mjml"); println!("{}", output); @@ -225,10 +197,18 @@ impl SubCommand { Self::Render(render) => { log::debug!("render"); let render_opts = RenderOptions::from(render); - let output = root.render(&render_opts).expect("couldn't render template"); + let output = root + .element + .render(&render_opts) + .expect("couldn't render template"); println!("{}", output); } - Self::Validate => log::debug!("validate"), + Self::Validate => { + log::debug!("validate"); + for warning in root.warnings { + log::warn!("{warning}"); + } + } }; Ok(()) } @@ -273,16 +253,193 @@ fn main() { mod tests { use clap::Parser; + use crate::format_parser_error; + use super::Options; + use mrml::prelude::parser::{loader::IncludeLoaderError, Error as ParserError, Origin, Span}; + + fn origin_include() -> Origin { + Origin::Include { + path: String::from("foo.mjml"), + } + } + + const fn any_span() -> Span { + Span { start: 10, end: 20 } + } + + #[test] + fn format_parser_error_end_of_stream_in_root() { + assert_eq!( + format_parser_error(ParserError::EndOfStream { + origin: Origin::Root + }), + "unexpected end of stream in root template" + ); + } + + #[test] + fn format_parser_error_end_of_stream_in_include() { + assert_eq!( + format_parser_error(ParserError::EndOfStream { + origin: origin_include() + }), + "unexpected end of stream in template from \"foo.mjml\"" + ); + } + + #[test] + fn format_parser_error_unexpected_element_in_root() { + assert_eq!( + format_parser_error(ParserError::UnexpectedElement { + origin: Origin::Root, + position: any_span() + }), + "unexpected element in root template at position 10..20" + ); + } + + #[test] + fn format_parser_error_unexpected_element_in_include() { + assert_eq!( + format_parser_error(ParserError::UnexpectedElement { + origin: origin_include(), + position: any_span() + }), + "unexpected element in template from \"foo.mjml\" at position 10..20" + ); + } + + #[test] + fn format_parser_error_invalid_attribute_in_root() { + assert_eq!( + format_parser_error(ParserError::InvalidAttribute { + origin: Origin::Root, + position: any_span() + }), + "invalid attribute in root template at position 10..20" + ); + } + + #[test] + fn format_parser_error_invalid_attribute_in_include() { + assert_eq!( + format_parser_error(ParserError::InvalidAttribute { + origin: origin_include(), + position: any_span() + }), + "invalid attribute in template from \"foo.mjml\" at position 10..20" + ); + } + + #[test] + fn format_parser_error_invalid_format_in_root() { + assert_eq!( + format_parser_error(ParserError::InvalidFormat { + origin: Origin::Root, + position: any_span() + }), + "invalid format in root template at position 10..20" + ); + } + + #[test] + fn format_parser_error_invalid_format_in_include() { + assert_eq!( + format_parser_error(ParserError::InvalidFormat { + origin: origin_include(), + position: any_span() + }), + "invalid format in template from \"foo.mjml\" at position 10..20" + ); + } + + #[test] + fn format_parser_error_include_loader_error_in_root() { + assert_eq!( + format_parser_error(ParserError::IncludeLoaderError { + origin: Origin::Root, + position: any_span(), + source: IncludeLoaderError { + path: String::from("foo.mjml"), + reason: std::io::ErrorKind::NotFound, + message: None, + cause: None, + } + }), + "unable to load included template in root template at position 10..20: foo.mjml entity not found" + ); + } + + #[test] + fn format_parser_error_include_loader_error_in_include() { + assert_eq!( + format_parser_error(ParserError::IncludeLoaderError { + origin: Origin::Root, + position: any_span(), + source: IncludeLoaderError { + path: String::from("foo.mjml"), + reason: std::io::ErrorKind::NotFound, + message: None, + cause: None, + } + }), + "unable to load included template in root template at position 10..20: foo.mjml entity not found" + ); + } + + #[test] + fn format_parser_error_missing_attribute_in_root() { + assert_eq!( + format_parser_error(ParserError::MissingAttribute { + name: "name", + origin: Origin::Root, + position: any_span() + }), + "missing attribute \"name\" in element in root template at position 10..20" + ); + } + + #[test] + fn format_parser_error_missing_attribute_in_include() { + assert_eq!( + format_parser_error(ParserError::MissingAttribute { + name: "name", + origin: origin_include(), + position: any_span() + }), + "missing attribute \"name\" in element in template from \"foo.mjml\" at position 10..20" + ); + } + + #[test] + fn format_parser_error_size_limit_in_root() { + assert_eq!( + format_parser_error(ParserError::SizeLimit { + origin: Origin::Root + }), + "size limit reached in root template" + ); + } + + #[test] + fn format_parser_error_size_limit_in_include() { + assert_eq!( + format_parser_error(ParserError::SizeLimit { + origin: origin_include() + }), + "size limit reached in template from \"foo.mjml\"" + ); + } fn execute(args: [&str; N]) { Options::parse_from(args).execute().unwrap() } - fn execute_stdin(args: [&str; N], input: &str) { + fn execute_stdin>(args: [&str; N], input: I) { let opts = Options::parse_from(args); - let root = opts.parse_input(input).unwrap(); - opts.subcmd.execute(&root).unwrap() + let root = opts.parse_input(input.into()).unwrap(); + opts.subcmd.execute(root).unwrap() } #[test] diff --git a/packages/mrml-core/benches/basic.rs b/packages/mrml-core/benches/basic.rs index fc792067..480e6767 100644 --- a/packages/mrml-core/benches/basic.rs +++ b/packages/mrml-core/benches/basic.rs @@ -4,7 +4,7 @@ use mrml::prelude::render::RenderOptions; fn render(input: &str) { let opts = RenderOptions::default(); let root = mrml::mjml::Mjml::parse(input).unwrap(); - root.render(&opts).unwrap(); + root.element.render(&opts).unwrap(); } fn criterion_benchmark(c: &mut Criterion) { diff --git a/packages/mrml-core/benches/template.rs b/packages/mrml-core/benches/template.rs index 4b31dea6..f18c40e2 100644 --- a/packages/mrml-core/benches/template.rs +++ b/packages/mrml-core/benches/template.rs @@ -4,7 +4,7 @@ use mrml::prelude::render::RenderOptions; fn render(input: &str) { let opts = RenderOptions::default(); let root = mrml::mjml::Mjml::parse(input).unwrap(); - root.render(&opts).unwrap(); + root.element.render(&opts).unwrap(); } fn criterion_benchmark(c: &mut Criterion) { diff --git a/packages/mrml-core/src/comment/render.rs b/packages/mrml-core/src/comment/render.rs index ba1759cc..e0d25698 100644 --- a/packages/mrml-core/src/comment/render.rs +++ b/packages/mrml-core/src/comment/render.rs @@ -38,7 +38,7 @@ mod tests { fn render_enabled() { let opts = RenderOptions::default(); let root = Mjml::parse(r#""#).unwrap(); - let result = root.render(&opts).unwrap(); + let result = root.element.render(&opts).unwrap(); assert!(result.contains("Hello World!")); } @@ -49,7 +49,7 @@ mod tests { ..Default::default() }; let root = Mjml::parse(r#""#).unwrap(); - let result = root.render(&opts).unwrap(); + let result = root.element.render(&opts).unwrap(); assert!(!result.contains("Hello World!")); } @@ -57,7 +57,7 @@ mod tests { fn render_with_is_raw() { let opts = RenderOptions::default(); let root = Mjml::parse(r#""#).unwrap(); - let result = root.render(&opts).unwrap(); + let result = root.element.render(&opts).unwrap(); assert!(result.contains("Hello World!")); } } diff --git a/packages/mrml-core/src/helper/mod.rs b/packages/mrml-core/src/helper/mod.rs index 5ef6481e..ca42a62f 100644 --- a/packages/mrml-core/src/helper/mod.rs +++ b/packages/mrml-core/src/helper/mod.rs @@ -1,6 +1,6 @@ #[cfg(feature = "render")] pub mod size; -#[cfg(any(feature = "render", feature = "print"))] +#[cfg(feature = "render")] pub mod sort; #[cfg(feature = "render")] pub mod spacing; diff --git a/packages/mrml-core/src/lib.rs b/packages/mrml-core/src/lib.rs index cd5611c5..21e4affa 100644 --- a/packages/mrml-core/src/lib.rs +++ b/packages/mrml-core/src/lib.rs @@ -19,7 +19,7 @@ //! # { //! let root = mrml::parse("").expect("parse template"); //! let opts = mrml::prelude::render::Options::default(); -//! match root.render(&opts) { +//! match root.element.render(&opts) { //! Ok(content) => println!("{}", content), //! Err(_) => println!("couldn't render mjml template"), //! }; @@ -165,6 +165,7 @@ pub mod prelude; pub mod text; // Only used to ignore the comments at the root level +#[cfg(feature = "parse")] mod root; mod helper; @@ -194,9 +195,15 @@ mod helper; pub fn parse_with_options>( input: T, opts: &crate::prelude::parser::ParserOptions, -) -> Result { +) -> Result, prelude::parser::Error> { let root = crate::root::Root::parse_with_options(input, opts)?; - root.into_mjml().ok_or(prelude::parser::Error::NoRootNode) + Ok(crate::prelude::parser::ParseOutput { + element: root + .element + .into_mjml() + .ok_or(prelude::parser::Error::NoRootNode)?, + warnings: root.warnings, + }) } #[cfg(all(feature = "parse", feature = "async"))] @@ -227,9 +234,15 @@ pub fn parse_with_options>( pub async fn async_parse_with_options>( input: T, opts: std::sync::Arc, -) -> Result { +) -> Result, prelude::parser::Error> { let root = crate::root::Root::async_parse_with_options(input, opts).await?; - root.into_mjml().ok_or(prelude::parser::Error::NoRootNode) + Ok(crate::prelude::parser::ParseOutput { + element: root + .element + .into_mjml() + .ok_or(prelude::parser::Error::NoRootNode)?, + warnings: root.warnings, + }) } #[cfg(feature = "parse")] @@ -242,7 +255,9 @@ pub async fn async_parse_with_options>( /// Err(err) => eprintln!("Something went wrong: {err:?}"), /// } /// ``` -pub fn parse>(input: T) -> Result { +pub fn parse>( + input: T, +) -> Result, prelude::parser::Error> { let opts = crate::prelude::parser::ParserOptions::default(); parse_with_options(input, &opts) } @@ -259,7 +274,9 @@ pub fn parse>(input: T) -> Result>(input: T) -> Result { +pub async fn async_parse>( + input: T, +) -> Result, prelude::parser::Error> { let opts = std::sync::Arc::new(crate::prelude::parser::AsyncParserOptions::default()); async_parse_with_options(input, opts).await } diff --git a/packages/mrml-core/src/mj_accordion/json.rs b/packages/mrml-core/src/mj_accordion/json.rs index 4e991d7a..c5edfa8e 100644 --- a/packages/mrml-core/src/mj_accordion/json.rs +++ b/packages/mrml-core/src/mj_accordion/json.rs @@ -1,13 +1,15 @@ #[cfg(test)] mod tests { - use crate::mj_accordion::MjAccordion; + use crate::mj_accordion::{MjAccordion, MjAccordionChild}; use crate::mj_accordion_element::MjAccordionElement; #[test] fn serialize() { let mut elt = MjAccordion::default(); elt.attributes.insert("margin".into(), "42px".into()); - elt.children.push(MjAccordionElement::default().into()); + elt.children.push(MjAccordionChild::MjAccordionElement( + MjAccordionElement::default(), + )); assert_eq!( serde_json::to_string(&elt).unwrap(), r#"{"type":"mj-accordion","attributes":{"margin":"42px"},"children":[{"type":"mj-accordion-element"}]}"# diff --git a/packages/mrml-core/src/mj_accordion/mod.rs b/packages/mrml-core/src/mj_accordion/mod.rs index c9314b7f..071dc8e5 100644 --- a/packages/mrml-core/src/mj_accordion/mod.rs +++ b/packages/mrml-core/src/mj_accordion/mod.rs @@ -4,7 +4,7 @@ //! let template = include_str!("../../resources/compare/success/mj-accordion.mjml"); //! let root = mrml::parse(template).expect("parse template"); //! let opts = mrml::prelude::render::Options::default(); -//! match root.render(&opts) { +//! match root.element.render(&opts) { //! Ok(content) => println!("{content}"), //! Err(_) => println!("couldn't render mjml template"), //! }; @@ -79,24 +79,25 @@ mod tests { #[cfg(feature = "json")] #[test] fn chaining_json_parse() { - use crate::mj_accordion::MjAccordion; + use crate::mj_accordion::{MjAccordion, MjAccordionChild}; use crate::mj_accordion_element::{MjAccordionElement, MjAccordionElementChildren}; use crate::mj_accordion_title::MjAccordionTitle; use crate::text::Text; let element = MjAccordion::new( Default::default(), - vec![MjAccordionElement::new( - Default::default(), - MjAccordionElementChildren { - title: Some(MjAccordionTitle::new( - Default::default(), - vec![Text::from("Hello World!".to_string())], - )), - text: None, - }, - ) - .into()], + vec![MjAccordionChild::MjAccordionElement( + MjAccordionElement::new( + Default::default(), + MjAccordionElementChildren { + title: Some(MjAccordionTitle::new( + Default::default(), + vec![Text::from("Hello World!".to_string())], + )), + text: None, + }, + ), + )], ); let initial_json = serde_json::to_string(&element).unwrap(); let result: MjAccordion = serde_json::from_str(initial_json.as_str()).unwrap(); diff --git a/packages/mrml-core/src/mj_accordion/parse.rs b/packages/mrml-core/src/mj_accordion/parse.rs index 928f3d10..ac7714c7 100644 --- a/packages/mrml-core/src/mj_accordion/parse.rs +++ b/packages/mrml-core/src/mj_accordion/parse.rs @@ -24,14 +24,22 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { self.parse(cursor, inner.local)?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -60,14 +68,22 @@ impl AsyncParseChildren> for AsyncMrmlParser { self.async_parse(cursor, inner.local).await?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -100,13 +116,13 @@ mod tests { should_error_with_text, MjAccordion, "Hello", - "UnexpectedToken(Span { start: 14, end: 19 })" + "UnexpectedToken { origin: Root, position: Span { start: 14, end: 19 } }" ); crate::should_not_sync_parse!( should_error_with_unknown_element, MjAccordion, "", - "UnexpectedElement(Span { start: 14, end: 19 })" + "UnexpectedElement { origin: Root, position: Span { start: 14, end: 19 } }" ); } diff --git a/packages/mrml-core/src/mj_accordion_element/parse.rs b/packages/mrml-core/src/mj_accordion_element/parse.rs index d85120a1..c4160049 100644 --- a/packages/mrml-core/src/mj_accordion_element/parse.rs +++ b/packages/mrml-core/src/mj_accordion_element/parse.rs @@ -25,7 +25,10 @@ impl<'opts> ParseChildren for MrmlParser<'opts> { result.title = Some(self.parse(cursor, inner.local)?); } _ => { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } }, MrmlToken::ElementClose(inner) => { @@ -33,7 +36,10 @@ impl<'opts> ParseChildren for MrmlParser<'opts> { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -61,7 +67,10 @@ impl AsyncParseChildren for AsyncMrmlParser { result.title = Some(self.async_parse(cursor, inner.local).await?); } _ => { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } }, MrmlToken::ElementClose(inner) => { @@ -69,7 +78,10 @@ impl AsyncParseChildren for AsyncMrmlParser { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -90,13 +102,13 @@ mod tests { should_error_with_unknown_child, MjAccordionElement, "", - "UnexpectedElement(Span { start: 22, end: 27 })" + "UnexpectedElement { origin: Root, position: Span { start: 22, end: 27 } }" ); crate::should_not_sync_parse!( should_error_with_comment, MjAccordionElement, "", - "UnexpectedToken(Span { start: 22, end: 38 }" + "UnexpectedToken { origin: Root, position: Span { start: 22, end: 38 } }" ); } diff --git a/packages/mrml-core/src/mj_accordion_element/render.rs b/packages/mrml-core/src/mj_accordion_element/render.rs index 2b58d20b..7d52782f 100644 --- a/packages/mrml-core/src/mj_accordion_element/render.rs +++ b/packages/mrml-core/src/mj_accordion_element/render.rs @@ -136,6 +136,7 @@ mod tests { use crate::mj_accordion_element::{MjAccordionElement, MjAccordionElementChildren}; use crate::mj_accordion_text::MjAccordionText; use crate::mj_accordion_title::MjAccordionTitle; + use crate::mj_raw::MjRawChild; use crate::prelude::render::*; use crate::text::Text; @@ -154,7 +155,7 @@ mod tests { )), text: Some(MjAccordionText::new( Default::default(), - vec![Text::from("Lorem Ipsum".to_string()).into()], + vec![MjRawChild::Text(Text::from("Lorem Ipsum".to_string()))], )), }, ); diff --git a/packages/mrml-core/src/mj_accordion_text/json.rs b/packages/mrml-core/src/mj_accordion_text/json.rs index 5d140472..f2f64db7 100644 --- a/packages/mrml-core/src/mj_accordion_text/json.rs +++ b/packages/mrml-core/src/mj_accordion_text/json.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use crate::mj_accordion_text::MjAccordionText; + use crate::mj_accordion_text::{MjAccordionText, MjRawChild}; use crate::text::Text; #[test] @@ -8,8 +8,8 @@ mod tests { let mut elt = MjAccordionText::default(); elt.attributes .insert("margin".to_string(), "12px".to_string()); - elt.children.push(Text::from("Hello").into()); - elt.children.push(Text::from("World").into()); + elt.children.push(MjRawChild::Text(Text::from("Hello"))); + elt.children.push(MjRawChild::Text(Text::from("World"))); assert_eq!( serde_json::to_string(&elt).unwrap(), r#"{"type":"mj-accordion-text","attributes":{"margin":"12px"},"children":["Hello","World"]}"# diff --git a/packages/mrml-core/src/mj_attributes/json.rs b/packages/mrml-core/src/mj_attributes/json.rs index 75891ace..63a5c203 100644 --- a/packages/mrml-core/src/mj_attributes/json.rs +++ b/packages/mrml-core/src/mj_attributes/json.rs @@ -1,23 +1,24 @@ #[cfg(test)] mod tests { - use crate::mj_attributes::MjAttributes; + use crate::mj_attributes::{MjAttributes, MjAttributesChild}; use crate::mj_attributes_all::MjAttributesAll; use crate::mj_attributes_class::{MjAttributesClass, MjAttributesClassAttributes}; #[test] fn serialize() { let mut elt = MjAttributes::default(); - elt.children.push(MjAttributesAll::default().into()); - elt.children.push( + elt.children.push(MjAttributesChild::MjAttributesAll( + MjAttributesAll::default(), + )); + elt.children.push(MjAttributesChild::MjAttributesClass( MjAttributesClass::new( MjAttributesClassAttributes { name: "name".into(), others: Default::default(), }, (), - ) - .into(), - ); + ), + )); assert_eq!( serde_json::to_string(&elt).unwrap(), r#"{"type":"mj-attributes","children":[{"type":"mj-all"},{"type":"mj-class","attributes":{"name":"name"}}]}"# diff --git a/packages/mrml-core/src/mj_attributes/parse.rs b/packages/mrml-core/src/mj_attributes/parse.rs index 6747f490..73bc6381 100644 --- a/packages/mrml-core/src/mj_attributes/parse.rs +++ b/packages/mrml-core/src/mj_attributes/parse.rs @@ -53,7 +53,12 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -78,7 +83,12 @@ impl AsyncParseChildren> for AsyncMrmlParser { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } diff --git a/packages/mrml-core/src/mj_attributes_class/mod.rs b/packages/mrml-core/src/mj_attributes_class/mod.rs index 91d88e2e..4dab99fb 100644 --- a/packages/mrml-core/src/mj_attributes_class/mod.rs +++ b/packages/mrml-core/src/mj_attributes_class/mod.rs @@ -31,7 +31,7 @@ pub struct MjAttributesClassAttributes { pub type MjAttributesClass = Component, MjAttributesClassAttributes, ()>; -#[cfg(test)] +#[cfg(all(test, feature = "json"))] impl MjAttributesClassAttributes { #[inline] fn new(name: String) -> Self { diff --git a/packages/mrml-core/src/mj_attributes_class/parse.rs b/packages/mrml-core/src/mj_attributes_class/parse.rs index e057abca..fa622c57 100644 --- a/packages/mrml-core/src/mj_attributes_class/parse.rs +++ b/packages/mrml-core/src/mj_attributes_class/parse.rs @@ -6,12 +6,16 @@ use crate::prelude::parser::{parse_attributes_map, Error, MrmlCursor, MrmlParser #[cfg(feature = "async")] use crate::prelude::parser::{AsyncMrmlParser, AsyncParseElement}; -#[inline] +#[inline(always)] fn parse<'a>(cursor: &mut MrmlCursor<'a>, tag: StrSpan<'a>) -> Result { let mut others: Map = parse_attributes_map(cursor)?; let name: String = others .remove("name") - .ok_or_else(|| Error::MissingAttribute("name", tag.into()))?; + .ok_or_else(|| Error::MissingAttribute { + name: "name", + origin: cursor.origin(), + position: tag.into(), + })?; let attributes = MjAttributesClassAttributes { name, others }; let ending = cursor.assert_element_end()?; @@ -58,12 +62,12 @@ mod tests { should_have_name, MjAttributesClass, r#""#, - "MissingAttribute(\"name\", Span { start: 1, end: 9 })" + "MissingAttribute { name: \"name\", origin: Root, position: Span { start: 1, end: 9 } }" ); crate::should_not_sync_parse!( should_close, MjAttributesClass, r#""#, - "UnexpectedToken(Span { start: 33, end: 42 })" + "UnexpectedToken { origin: Root, position: Span { start: 33, end: 42 } }" ); } diff --git a/packages/mrml-core/src/mj_body/json.rs b/packages/mrml-core/src/mj_body/json.rs index f79443b5..6e661c01 100644 --- a/packages/mrml-core/src/mj_body/json.rs +++ b/packages/mrml-core/src/mj_body/json.rs @@ -1,13 +1,14 @@ #[cfg(test)] mod tests { - use crate::mj_body::MjBody; + use crate::mj_body::{MjBody, MjBodyChild}; use crate::text::Text; #[test] fn serialize() { let mut elt = MjBody::default(); elt.attributes.insert("margin".into(), "42px".into()); - elt.children.push(Text::from("Hello World!").into()); + elt.children + .push(MjBodyChild::Text(Text::from("Hello World!"))); assert_eq!( serde_json::to_string(&elt).unwrap(), r#"{"type":"mj-body","attributes":{"margin":"42px"},"children":["Hello World!"]}"# diff --git a/packages/mrml-core/src/mj_body/parse.rs b/packages/mrml-core/src/mj_body/parse.rs index bd8274f3..cf124762 100644 --- a/packages/mrml-core/src/mj_body/parse.rs +++ b/packages/mrml-core/src/mj_body/parse.rs @@ -171,7 +171,10 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -204,7 +207,10 @@ impl AsyncParseChildren> for AsyncMrmlParser { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } diff --git a/packages/mrml-core/src/mj_breakpoint/parse.rs b/packages/mrml-core/src/mj_breakpoint/parse.rs index 01c468ba..7a95a260 100644 --- a/packages/mrml-core/src/mj_breakpoint/parse.rs +++ b/packages/mrml-core/src/mj_breakpoint/parse.rs @@ -1,7 +1,9 @@ +use xmlparser::StrSpan; + use super::MjBreakpointAttributes; #[cfg(feature = "async")] use crate::prelude::parser::AsyncMrmlParser; -use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes}; +use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes, WarningKind}; #[inline] fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { @@ -10,7 +12,7 @@ fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result ParseAttributes for MrmlParser<'opts> { fn parse_attributes( &self, cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, ) -> Result { parse_attributes(cursor) } @@ -30,6 +33,7 @@ impl ParseAttributes for AsyncMrmlParser { fn parse_attributes( &self, cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, ) -> Result { parse_attributes(cursor) } @@ -39,10 +43,16 @@ impl ParseAttributes for AsyncMrmlParser { mod tests { use crate::mj_breakpoint::MjBreakpoint; - crate::should_sync_parse!(success, MjBreakpoint, r#""#); - crate::should_not_sync_parse!( + crate::should_sync_parse!( + success, + MjBreakpoint, + r#""#, + 0 + ); + crate::should_sync_parse!( unexpected_attributes, MjBreakpoint, - r#""# + r#""#, + 1 ); } diff --git a/packages/mrml-core/src/mj_button/json.rs b/packages/mrml-core/src/mj_button/json.rs index 50f6f7d0..b0787e1e 100644 --- a/packages/mrml-core/src/mj_button/json.rs +++ b/packages/mrml-core/src/mj_button/json.rs @@ -1,13 +1,14 @@ #[cfg(test)] mod tests { - use crate::mj_button::MjButton; + use crate::mj_button::{MjBodyChild, MjButton}; use crate::text::Text; #[test] fn serialize() { let mut elt = MjButton::default(); elt.attributes.insert("margin".into(), "42px".into()); - elt.children.push(Text::from("Hello World!").into()); + elt.children + .push(MjBodyChild::Text(Text::from("Hello World!"))); assert_eq!( serde_json::to_string(&elt).unwrap(), r#"{"type":"mj-button","attributes":{"margin":"42px"},"children":["Hello World!"]}"# diff --git a/packages/mrml-core/src/mj_carousel/children.rs b/packages/mrml-core/src/mj_carousel/children.rs index fa4c50e9..472e2ead 100644 --- a/packages/mrml-core/src/mj_carousel/children.rs +++ b/packages/mrml-core/src/mj_carousel/children.rs @@ -9,12 +9,3 @@ pub enum MjCarouselChild { Comment(Comment), MjCarouselImage(MjCarouselImage), } - -impl MjCarouselChild { - pub(crate) fn as_mj_carousel_image(&self) -> Option<&MjCarouselImage> { - match self { - Self::MjCarouselImage(inner) => Some(inner), - _ => None, - } - } -} diff --git a/packages/mrml-core/src/mj_carousel/parse.rs b/packages/mrml-core/src/mj_carousel/parse.rs index 70df2d83..f7a29a31 100644 --- a/packages/mrml-core/src/mj_carousel/parse.rs +++ b/packages/mrml-core/src/mj_carousel/parse.rs @@ -22,14 +22,22 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { self.parse(cursor, inner.local)?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -56,14 +64,22 @@ impl AsyncParseChildren> for AsyncMrmlParser { self.async_parse(cursor, inner.local).await?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } diff --git a/packages/mrml-core/src/mj_carousel/render.rs b/packages/mrml-core/src/mj_carousel/render.rs index 3bfedc13..a3583044 100644 --- a/packages/mrml-core/src/mj_carousel/render.rs +++ b/packages/mrml-core/src/mj_carousel/render.rs @@ -3,6 +3,15 @@ use crate::helper::size::{Pixel, Size}; use crate::helper::style::Style; use crate::prelude::render::*; +impl MjCarouselChild { + fn as_mj_carousel_image(&self) -> Option<&crate::mj_carousel_image::MjCarouselImage> { + match self { + Self::MjCarouselImage(inner) => Some(inner), + _ => None, + } + } +} + impl<'render, 'root: 'render> Renderable<'render, 'root> for MjCarouselChild { fn renderer( &'root self, diff --git a/packages/mrml-core/src/mj_column/mod.rs b/packages/mrml-core/src/mj_column/mod.rs index b15fd3d6..f1ec3272 100644 --- a/packages/mrml-core/src/mj_column/mod.rs +++ b/packages/mrml-core/src/mj_column/mod.rs @@ -6,8 +6,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_column/parse.rs b/packages/mrml-core/src/mj_column/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_column/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_divider/mod.rs b/packages/mrml-core/src/mj_divider/mod.rs index 75db48c4..bf7a7234 100644 --- a/packages/mrml-core/src/mj_divider/mod.rs +++ b/packages/mrml-core/src/mj_divider/mod.rs @@ -5,8 +5,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_divider/parse.rs b/packages/mrml-core/src/mj_divider/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_divider/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_font/mod.rs b/packages/mrml-core/src/mj_font/mod.rs index 36c35535..d7235532 100644 --- a/packages/mrml-core/src/mj_font/mod.rs +++ b/packages/mrml-core/src/mj_font/mod.rs @@ -28,7 +28,7 @@ impl StaticTag for MjFontTag { pub type MjFont = Component, MjFontAttributes, ()>; -#[cfg(all(test, feature = "render"))] +#[cfg(all(test, any(feature = "render", feature = "print")))] impl MjFont { pub(crate) fn build, H: Into>(name: N, href: H) -> Self { Self::new( diff --git a/packages/mrml-core/src/mj_font/parse.rs b/packages/mrml-core/src/mj_font/parse.rs index af6fada4..e4eb20fb 100644 --- a/packages/mrml-core/src/mj_font/parse.rs +++ b/packages/mrml-core/src/mj_font/parse.rs @@ -1,9 +1,11 @@ +use xmlparser::StrSpan; + use super::MjFontAttributes; #[cfg(feature = "async")] use crate::prelude::parser::AsyncMrmlParser; -use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes}; +use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes, WarningKind}; -#[inline] +#[inline(always)] fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { let mut result = MjFontAttributes::default(); @@ -11,7 +13,7 @@ fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result result.name = attrs.value.to_string(), "href" => result.href = attrs.value.to_string(), - _ => return Err(Error::UnexpectedAttribute(attrs.span.into())), + _ => cursor.add_warning(WarningKind::UnexpectedAttribute, attrs.span), } } @@ -19,14 +21,22 @@ fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result ParseAttributes for MrmlParser<'opts> { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result { parse_attributes(cursor) } } #[cfg(feature = "async")] impl ParseAttributes for AsyncMrmlParser { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result { parse_attributes(cursor) } } @@ -41,9 +51,10 @@ mod tests { r#""# ); - crate::should_not_sync_parse!( + crate::should_sync_parse!( unexpected_attribute, MjFont, - r#""# + r#""#, + 1 ); } diff --git a/packages/mrml-core/src/mj_group/mod.rs b/packages/mrml-core/src/mj_group/mod.rs index c53f6232..54e314dc 100644 --- a/packages/mrml-core/src/mj_group/mod.rs +++ b/packages/mrml-core/src/mj_group/mod.rs @@ -6,8 +6,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_group/parse.rs b/packages/mrml-core/src/mj_group/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_group/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_head/parse.rs b/packages/mrml-core/src/mj_head/parse.rs index a45a3d16..d67c470d 100644 --- a/packages/mrml-core/src/mj_head/parse.rs +++ b/packages/mrml-core/src/mj_head/parse.rs @@ -32,7 +32,10 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -63,7 +66,10 @@ impl AsyncParseChildren> for AsyncMrmlParser { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -85,7 +91,10 @@ impl<'opts> ParseElement for MrmlParser<'opts> { MJ_RAW => self.parse(cursor, tag).map(MjHeadChild::MjRaw), MJ_STYLE => self.parse(cursor, tag).map(MjHeadChild::MjStyle), MJ_TITLE => self.parse(cursor, tag).map(MjHeadChild::MjTitle), - _ => Err(Error::UnexpectedElement(tag.into())), + _ => Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: tag.into(), + }), } } } @@ -126,7 +135,10 @@ impl AsyncParseElement for AsyncMrmlParser { .async_parse(cursor, tag) .await .map(MjHeadChild::MjTitle), - _ => Err(Error::UnexpectedElement(tag.into())), + _ => Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: tag.into(), + }), } } } diff --git a/packages/mrml-core/src/mj_head/print.rs b/packages/mrml-core/src/mj_head/print.rs index a832013a..42dbc0ec 100644 --- a/packages/mrml-core/src/mj_head/print.rs +++ b/packages/mrml-core/src/mj_head/print.rs @@ -24,12 +24,7 @@ mod tests { "#; let root = crate::mjml::Mjml::parse(origin).unwrap(); - similar_asserts::assert_eq!(origin, root.print_pretty().unwrap()); - let head = root.head().unwrap(); - assert_eq!(head.breakpoint().unwrap().value(), "12px"); - assert_eq!(head.preview().unwrap().content(), "Hello World with all!"); - assert_eq!(head.title().unwrap().content(), "Hello World!"); - assert_eq!(head.children().len(), 5); + similar_asserts::assert_eq!(origin, root.element.print_pretty().unwrap()); } #[test] diff --git a/packages/mrml-core/src/mj_hero/mod.rs b/packages/mrml-core/src/mj_hero/mod.rs index 52d64951..ebb499a0 100644 --- a/packages/mrml-core/src/mj_hero/mod.rs +++ b/packages/mrml-core/src/mj_hero/mod.rs @@ -6,8 +6,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_hero/parse.rs b/packages/mrml-core/src/mj_hero/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_hero/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_image/mod.rs b/packages/mrml-core/src/mj_image/mod.rs index 47939b12..2d480e65 100644 --- a/packages/mrml-core/src/mj_image/mod.rs +++ b/packages/mrml-core/src/mj_image/mod.rs @@ -5,8 +5,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_image/parse.rs b/packages/mrml-core/src/mj_image/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_image/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_include/body/mod.rs b/packages/mrml-core/src/mj_include/body/mod.rs index 00e91a79..71168598 100644 --- a/packages/mrml-core/src/mj_include/body/mod.rs +++ b/packages/mrml-core/src/mj_include/body/mod.rs @@ -9,8 +9,6 @@ mod render; use std::marker::PhantomData; -#[cfg(any(feature = "print", feature = "json"))] -use super::NAME; use crate::prelude::{Component, StaticTag}; #[derive(Clone, Debug)] diff --git a/packages/mrml-core/src/mj_include/body/parse.rs b/packages/mrml-core/src/mj_include/body/parse.rs index a5cbbe56..0eb5ed3c 100644 --- a/packages/mrml-core/src/mj_include/body/parse.rs +++ b/packages/mrml-core/src/mj_include/body/parse.rs @@ -1,5 +1,3 @@ -use std::convert::TryFrom; - use xmlparser::StrSpan; use super::{MjIncludeBody, MjIncludeBodyAttributes, MjIncludeBodyChild, MjIncludeBodyKind}; @@ -25,6 +23,7 @@ use crate::mj_wrapper::{MjWrapper, NAME as MJ_WRAPPER}; use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement}; use crate::prelude::parser::{ Error, MrmlCursor, MrmlParser, MrmlToken, ParseAttributes, ParseChildren, ParseElement, + WarningKind, }; use crate::text::Text; @@ -51,7 +50,10 @@ impl<'opts> ParseElement for MrmlParser<'opts> { MJ_TABLE => Ok(MjIncludeBodyChild::MjTable(self.parse(cursor, tag)?)), MJ_TEXT => Ok(MjIncludeBodyChild::MjText(self.parse(cursor, tag)?)), MJ_WRAPPER => Ok(MjIncludeBodyChild::MjWrapper(self.parse(cursor, tag)?)), - _ => Err(Error::UnexpectedElement(tag.into())), + _ => Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: tag.into(), + }), } } } @@ -114,25 +116,36 @@ impl AsyncParseElement for AsyncMrmlParser { MJ_WRAPPER => Ok(MjIncludeBodyChild::MjWrapper( self.async_parse(cursor, tag).await?, )), - _ => Err(Error::UnexpectedElement(tag.into())), + _ => Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: tag.into(), + }), } } } -impl<'a> TryFrom> for MjIncludeBodyKind { - type Error = Error; - - fn try_from(s: StrSpan<'a>) -> Result { - match s.as_str() { - "html" => Ok(Self::Html), - "mjml" => Ok(Self::Mjml), - _ => Err(Error::InvalidAttribute(s.into())), +impl MjIncludeBodyKind { + fn maybe_parse(span: &StrSpan<'_>) -> Option { + match span.as_str() { + "html" => Some(Self::Html), + "mjml" => Some(Self::Mjml), + _ => None, } } + + fn parse(cursor: &mut MrmlCursor<'_>, span: StrSpan<'_>) -> Result { + Self::maybe_parse(&span).ok_or_else(|| Error::InvalidAttribute { + origin: cursor.origin(), + position: span.into(), + }) + } } #[inline] -fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { +fn parse_attributes( + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, +) -> Result { let mut path = None; let mut kind: Option = None; while let Some(attr) = cursor.next_attribute()? { @@ -141,15 +154,19 @@ fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { - kind = Some(MjIncludeBodyKind::try_from(attr.value)?); + kind = Some(MjIncludeBodyKind::parse(cursor, attr.value)?); } _ => { - return Err(Error::UnexpectedAttribute(attr.span.into())); + cursor.add_warning(WarningKind::UnexpectedAttribute, attr.span); } } } Ok(MjIncludeBodyAttributes { - path: path.ok_or_else(|| Error::MissingAttribute("path", Default::default()))?, + path: path.ok_or_else(|| Error::MissingAttribute { + name: "path", + origin: cursor.origin(), + position: tag.into(), + })?, kind: kind.unwrap_or_default(), }) } @@ -158,8 +175,9 @@ impl<'opts> ParseAttributes for MrmlParser<'opts> { fn parse_attributes( &self, cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, ) -> Result { - parse_attributes(cursor) + parse_attributes(cursor, tag) } } @@ -168,8 +186,9 @@ impl ParseAttributes for AsyncMrmlParser { fn parse_attributes( &self, cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, ) -> Result { - parse_attributes(cursor) + parse_attributes(cursor, tag) } } @@ -197,7 +216,12 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { MrmlToken::Text(inner) => { result.push(MjIncludeBodyChild::Text(Text::from(inner.text.as_str()))); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } @@ -232,7 +256,12 @@ impl AsyncParseChildren> for AsyncMrmlParser { MrmlToken::Text(inner) => { result.push(MjIncludeBodyChild::Text(Text::from(inner.text.as_str()))); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } @@ -247,7 +276,7 @@ impl<'opts> ParseElement for MrmlParser<'opts> { tag: StrSpan<'a>, ) -> Result { let (attributes, children): (MjIncludeBodyAttributes, Vec) = - self.parse_attributes_and_children(cursor)?; + self.parse_attributes_and_children(cursor, &tag)?; // if a mj-include has some content, we don't load it let children: Vec = if children.is_empty() { @@ -256,21 +285,25 @@ impl<'opts> ParseElement for MrmlParser<'opts> { .include_loader .resolve(&attributes.path) .map_err(|source| Error::IncludeLoaderError { + origin: cursor.origin(), position: tag.into(), source, })?; match attributes.kind { MjIncludeBodyKind::Html => { - let mut sub = cursor.new_child(child.as_str()); + let mut sub = cursor.new_child(&attributes.path, child.as_str()); let children: Vec = self.parse_children(&mut sub)?; + cursor.with_warnings(sub.warnings()); vec![MjIncludeBodyChild::MjWrapper(MjWrapper::new( Default::default(), children, ))] } MjIncludeBodyKind::Mjml => { - let mut sub = cursor.new_child(child.as_str()); - self.parse_children(&mut sub)? + let mut sub = cursor.new_child(&attributes.path, child.as_str()); + let children = self.parse_children(&mut sub)?; + cursor.with_warnings(sub.warnings()); + children } } } else { @@ -293,7 +326,7 @@ impl crate::prelude::parser::AsyncParseElement for AsyncMrmlParse use crate::prelude::parser::AsyncParseChildren; let (attributes, children): (MjIncludeBodyAttributes, Vec) = - self.parse_attributes_and_children(cursor).await?; + self.parse_attributes_and_children(cursor, &tag).await?; // if a mj-include has some content, we don't load it let children: Vec = if children.is_empty() { @@ -303,12 +336,13 @@ impl crate::prelude::parser::AsyncParseElement for AsyncMrmlParse .async_resolve(&attributes.path) .await .map_err(|source| Error::IncludeLoaderError { + origin: cursor.origin(), position: tag.into(), source, })?; match attributes.kind { MjIncludeBodyKind::Html => { - let mut sub = cursor.new_child(child.as_str()); + let mut sub = cursor.new_child(&attributes.path, child.as_str()); let children: Vec = self.async_parse_children(&mut sub).await?; vec![MjIncludeBodyChild::MjWrapper(MjWrapper::new( Default::default(), @@ -316,8 +350,10 @@ impl crate::prelude::parser::AsyncParseElement for AsyncMrmlParse ))] } MjIncludeBodyKind::Mjml => { - let mut sub = cursor.new_child(child.as_str()); - self.async_parse_children(&mut sub).await? + let mut sub = cursor.new_child(&attributes.path, child.as_str()); + let children = self.async_parse_children(&mut sub).await?; + cursor.with_warnings(sub.warnings()); + children } } } else { @@ -330,39 +366,37 @@ impl crate::prelude::parser::AsyncParseElement for AsyncMrmlParse #[cfg(test)] mod tests { - use std::convert::TryFrom; - use xmlparser::StrSpan; use crate::mj_include::body::{MjIncludeBody, MjIncludeBodyKind}; use crate::prelude::parser::memory_loader::MemoryIncludeLoader; - use crate::prelude::parser::{MrmlCursor, MrmlParser, ParserOptions}; + use crate::prelude::parser::{MrmlCursor, MrmlParser, ParserOptions, WarningKind}; #[test] fn kind_parser() { assert_eq!( - MjIncludeBodyKind::try_from(StrSpan::from("html")).unwrap(), + MjIncludeBodyKind::maybe_parse(&StrSpan::from("html")).unwrap(), MjIncludeBodyKind::Html ); assert_eq!( - MjIncludeBodyKind::try_from(StrSpan::from("mjml")).unwrap(), + MjIncludeBodyKind::maybe_parse(&StrSpan::from("mjml")).unwrap(), MjIncludeBodyKind::Mjml ); - assert!(MjIncludeBodyKind::try_from(StrSpan::from("foo")).is_err()); + assert!(MjIncludeBodyKind::maybe_parse(&StrSpan::from("foo")).is_none()); } crate::should_not_parse!( invalid_kind, MjIncludeBody, r#""#, - "InvalidAttribute(Span { start: 18, end: 21 })" + "InvalidAttribute { origin: Root, position: Span { start: 18, end: 21 } }" ); crate::should_not_parse!( not_found, MjIncludeBody, r#""#, - "IncludeLoaderError { position: Span { start: 1, end: 11 }, source: IncludeLoaderError { path: \"basic.mjml\", reason: NotFound, message: None, cause: None } }" + "IncludeLoaderError { origin: Root, position: Span { start: 1, end: 11 }, source: IncludeLoaderError { path: \"basic.mjml\", reason: NotFound, message: None, cause: None } }" ); crate::should_parse!( @@ -468,20 +502,43 @@ mod tests { r#" "#, - "UnexpectedElement(Span { start: 38, end: 41 })" + "UnexpectedElement { origin: Root, position: Span { start: 38, end: 41 } }" ); - crate::should_not_parse!( + crate::should_parse!( invalid_attribute, MjIncludeBody, r#""#, - "UnexpectedAttribute(Span { start: 12, end: 31 })" + 1 ); crate::should_not_parse!( missing_path, MjIncludeBody, r#""#, - "MissingAttribute(\"path\", Span { start: 0, end: 0 })" + "MissingAttribute { name: \"path\", origin: Root, position: Span { start: 1, end: 11 } }" ); + + #[test] + fn warnings_from_include_child() { + let resolver = MemoryIncludeLoader::from(vec![( + "partial.html", + "

Hello World!

", + )]); + let opts = ParserOptions { + include_loader: Box::new(resolver), + }; + let raw = r#""#; + let mut cursor = MrmlCursor::new(raw); + let include: MjIncludeBody = MrmlParser::new(&opts).parse_root(&mut cursor).unwrap(); + assert_eq!(include.0.attributes.kind, MjIncludeBodyKind::Html); + let warnings = cursor.warnings(); + assert_eq!(warnings.len(), 1); + let warning = warnings.first().unwrap(); + assert_eq!(warning.kind, WarningKind::UnexpectedAttribute); + assert_eq!( + warning.to_string(), + "unexpected attribute in template from \"partial.html\" at position 8..17" + ); + } } diff --git a/packages/mrml-core/src/mj_include/body/print.rs b/packages/mrml-core/src/mj_include/body/print.rs index 9d985172..0d417336 100644 --- a/packages/mrml-core/src/mj_include/body/print.rs +++ b/packages/mrml-core/src/mj_include/body/print.rs @@ -15,7 +15,7 @@ impl PrintableElement for super::MjIncludeBody { type Children = (); fn tag(&self) -> &str { - super::NAME + crate::mj_include::NAME } fn attributes(&self) -> &Self::Attrs { diff --git a/packages/mrml-core/src/mj_include/head/mod.rs b/packages/mrml-core/src/mj_include/head/mod.rs index 39e91190..886459fd 100644 --- a/packages/mrml-core/src/mj_include/head/mod.rs +++ b/packages/mrml-core/src/mj_include/head/mod.rs @@ -9,8 +9,6 @@ mod render; use std::marker::PhantomData; -#[cfg(any(feature = "print", feature = "json"))] -use super::NAME; use crate::prelude::{Component, StaticTag}; #[derive(Clone, Debug)] diff --git a/packages/mrml-core/src/mj_include/head/parse.rs b/packages/mrml-core/src/mj_include/head/parse.rs index 0540a02f..daa03ccc 100644 --- a/packages/mrml-core/src/mj_include/head/parse.rs +++ b/packages/mrml-core/src/mj_include/head/parse.rs @@ -1,5 +1,3 @@ -use std::convert::TryFrom; - use xmlparser::StrSpan; use super::{MjIncludeHead, MjIncludeHeadAttributes, MjIncludeHeadChild, MjIncludeHeadKind}; @@ -15,6 +13,7 @@ use crate::mj_title::NAME as MJ_TITLE; use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement}; use crate::prelude::parser::{ Error, MrmlCursor, MrmlParser, MrmlToken, ParseAttributes, ParseChildren, ParseElement, + WarningKind, }; use crate::text::Text; @@ -36,7 +35,10 @@ impl<'opts> ParseElement for MrmlParser<'opts> { MJ_RAW => self.parse(cursor, tag).map(MjIncludeHeadChild::MjRaw), MJ_STYLE => self.parse(cursor, tag).map(MjIncludeHeadChild::MjStyle), MJ_TITLE => self.parse(cursor, tag).map(MjIncludeHeadChild::MjTitle), - _ => Err(Error::UnexpectedElement(tag.into())), + _ => Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: tag.into(), + }), } } } @@ -79,13 +81,19 @@ impl AsyncParseElement for AsyncMrmlParser { .async_parse(cursor, tag) .await .map(MjIncludeHeadChild::MjTitle), - _ => Err(Error::UnexpectedElement(tag.into())), + _ => Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: tag.into(), + }), } } } #[inline] -fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { +fn parse_attributes( + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, +) -> Result { let mut path = None; let mut kind = None; while let Some(attr) = cursor.next_attribute()? { @@ -94,15 +102,19 @@ fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { - kind = Some(MjIncludeHeadKind::try_from(attr.value)?); + kind = Some(MjIncludeHeadKind::parse(cursor, attr.value)?); } _ => { - return Err(Error::UnexpectedAttribute(attr.span.into())); + cursor.add_warning(WarningKind::UnexpectedAttribute, attr.span); } } } Ok(MjIncludeHeadAttributes { - path: path.ok_or_else(|| Error::MissingAttribute("path", Default::default()))?, + path: path.ok_or_else(|| Error::MissingAttribute { + name: "path", + origin: cursor.origin(), + position: tag.into(), + })?, kind: kind.unwrap_or_default(), }) } @@ -111,8 +123,9 @@ impl<'opts> ParseAttributes for MrmlParser<'opts> { fn parse_attributes( &self, cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, ) -> Result { - parse_attributes(cursor) + parse_attributes(cursor, tag) } } @@ -121,8 +134,9 @@ impl ParseAttributes for AsyncMrmlParser { fn parse_attributes( &self, cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, ) -> Result { - parse_attributes(cursor) + parse_attributes(cursor, tag) } } @@ -150,7 +164,10 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -185,7 +202,10 @@ impl AsyncParseChildren> for AsyncMrmlParser { return Ok(result); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -200,7 +220,7 @@ impl<'opts> ParseElement for MrmlParser<'opts> { tag: StrSpan<'a>, ) -> Result { let (attributes, children): (MjIncludeHeadAttributes, Vec) = - self.parse_attributes_and_children(cursor)?; + self.parse_attributes_and_children(cursor, &tag)?; // if a mj-include has some content, we don't load it let children: Vec = if children.is_empty() { @@ -209,6 +229,7 @@ impl<'opts> ParseElement for MrmlParser<'opts> { .include_loader .resolve(&attributes.path) .map_err(|source| Error::IncludeLoaderError { + origin: cursor.origin(), position: tag.into(), source, })?; @@ -221,8 +242,10 @@ impl<'opts> ParseElement for MrmlParser<'opts> { } MjIncludeHeadKind::Css { inline: true } => unimplemented!(), MjIncludeHeadKind::Mjml => { - let mut sub = cursor.new_child(child.as_str()); - self.parse_children(&mut sub)? + let mut sub = cursor.new_child(&attributes.path, child.as_str()); + let children = self.parse_children(&mut sub)?; + cursor.with_warnings(sub.warnings()); + children } MjIncludeHeadKind::Html => todo!(), } @@ -244,7 +267,7 @@ impl AsyncParseElement for AsyncMrmlParser { tag: StrSpan<'a>, ) -> Result { let (attributes, children): (MjIncludeHeadAttributes, Vec) = - self.parse_attributes_and_children(cursor).await?; + self.parse_attributes_and_children(cursor, &tag).await?; // if a mj-include has some content, we don't load it let children: Vec = if children.is_empty() { @@ -254,6 +277,7 @@ impl AsyncParseElement for AsyncMrmlParser { .async_resolve(&attributes.path) .await .map_err(|source| Error::IncludeLoaderError { + origin: cursor.origin(), position: tag.into(), source, })?; @@ -266,8 +290,10 @@ impl AsyncParseElement for AsyncMrmlParser { } MjIncludeHeadKind::Css { inline: true } => unimplemented!(), MjIncludeHeadKind::Mjml => { - let mut sub = cursor.new_child(child.as_str()); - self.async_parse_children(&mut sub).await? + let mut sub = cursor.new_child(&attributes.path, child.as_str()); + let children = self.async_parse_children(&mut sub).await?; + cursor.with_warnings(sub.warnings()); + children } MjIncludeHeadKind::Html => unimplemented!(), } @@ -279,23 +305,26 @@ impl AsyncParseElement for AsyncMrmlParser { } } -impl<'a> TryFrom> for MjIncludeHeadKind { - type Error = Error; - - fn try_from(s: StrSpan<'a>) -> Result { - match s.as_str() { - "html" => Ok(Self::Html), - "mjml" => Ok(Self::Mjml), - "css" => Ok(Self::Css { inline: false }), - _ => Err(Error::InvalidAttribute(s.into())), +impl MjIncludeHeadKind { + fn maybe_parse(span: &StrSpan<'_>) -> Option { + match span.as_str() { + "html" => Some(Self::Html), + "mjml" => Some(Self::Mjml), + "css" => Some(Self::Css { inline: false }), + _ => None, } } + + fn parse(cursor: &mut MrmlCursor<'_>, span: StrSpan<'_>) -> Result { + Self::maybe_parse(&span).ok_or_else(|| Error::InvalidAttribute { + origin: cursor.origin(), + position: span.into(), + }) + } } #[cfg(test)] mod tests { - use std::convert::TryFrom; - use xmlparser::StrSpan; use crate::mj_include::head::{MjIncludeHead, MjIncludeHeadKind}; @@ -305,32 +334,32 @@ mod tests { #[test] fn should_parse_every_kind() { assert_eq!( - MjIncludeHeadKind::try_from(StrSpan::from("html")).unwrap(), + MjIncludeHeadKind::maybe_parse(&StrSpan::from("html")).unwrap(), MjIncludeHeadKind::Html ); assert_eq!( - MjIncludeHeadKind::try_from(StrSpan::from("mjml")).unwrap(), + MjIncludeHeadKind::maybe_parse(&StrSpan::from("mjml")).unwrap(), MjIncludeHeadKind::Mjml ); assert_eq!( - MjIncludeHeadKind::try_from(StrSpan::from("css")).unwrap(), + MjIncludeHeadKind::maybe_parse(&StrSpan::from("css")).unwrap(), MjIncludeHeadKind::Css { inline: false } ); - assert!(MjIncludeHeadKind::try_from(StrSpan::from("other")).is_err()); + assert!(MjIncludeHeadKind::maybe_parse(&StrSpan::from("other")).is_none()); } crate::should_not_parse!( should_error_when_no_path, MjIncludeHead, "", - "MissingAttribute(\"path\", Span { start: 0, end: 0 })" + "MissingAttribute { name: \"path\", origin: Root, position: Span { start: 1, end: 11 } }" ); crate::should_not_parse!( should_error_when_unknown_attribute, MjIncludeHead, r#""#, - "UnexpectedAttribute(Span { start: 12, end: 25 })" + "MissingAttribute { name: \"path\", origin: Root, position: Span { start: 1, end: 11 } }" ); crate::should_parse!( @@ -352,14 +381,14 @@ mod tests { should_error_unknown_children, MjIncludeHead, r#"
"#, - "UnexpectedElement(Span { start: 29, end: 32 })" + "UnexpectedElement { origin: Root, position: Span { start: 29, end: 32 } }" ); crate::should_not_parse!( basic_in_noop_resolver, MjIncludeHead, r#""#, - "IncludeLoaderError { position: Span { start: 1, end: 11 }, source: IncludeLoaderError { path: \"basic.mjml\", reason: NotFound, message: None, cause: None } }" + "IncludeLoaderError { origin: Root, position: Span { start: 1, end: 11 }, source: IncludeLoaderError { path: \"basic.mjml\", reason: NotFound, message: None, cause: None } }" ); #[test] diff --git a/packages/mrml-core/src/mj_include/head/print.rs b/packages/mrml-core/src/mj_include/head/print.rs index 3f90b6f0..883d70b4 100644 --- a/packages/mrml-core/src/mj_include/head/print.rs +++ b/packages/mrml-core/src/mj_include/head/print.rs @@ -6,7 +6,7 @@ impl PrintableElement for super::MjIncludeHead { type Children = (); fn tag(&self) -> &str { - super::NAME + crate::mj_include::NAME } fn attributes(&self) -> &Self::Attrs { diff --git a/packages/mrml-core/src/mj_include/head/render.rs b/packages/mrml-core/src/mj_include/head/render.rs index f68c31e4..707eab4e 100644 --- a/packages/mrml-core/src/mj_include/head/render.rs +++ b/packages/mrml-core/src/mj_include/head/render.rs @@ -73,7 +73,7 @@ impl super::MjIncludeHeadKind { mod tests { use crate::mj_body::MjBody; use crate::mj_breakpoint::MjBreakpoint; - use crate::mj_head::MjHead; + use crate::mj_head::{MjHead, MjHeadChild}; use crate::mj_include::head::{ MjIncludeHead, MjIncludeHeadAttributes, MjIncludeHeadChild, MjIncludeHeadKind, }; @@ -91,8 +91,10 @@ mod tests { mj_breakpoint.attributes.width = "500px".into(); let mj_title = MjTitle::from("Hello Old World!".to_string()); let mut mj_head = MjHead::default(); - mj_head.children.push(mj_breakpoint.into()); - mj_head.children.push(mj_title.into()); + mj_head + .children + .push(MjHeadChild::MjBreakpoint(mj_breakpoint)); + mj_head.children.push(MjHeadChild::MjTitle(mj_title)); let mut root = Mjml::default(); root.children.head = Some(mj_head); root.children.body = Some(MjBody::default()); @@ -109,9 +111,11 @@ mod tests { vec![MjIncludeHeadChild::MjTitle(mj_include_title)], ); let mut mj_head = MjHead::default(); - mj_head.children.push(mj_include.into()); - mj_head.children.push(mj_breakpoint.into()); - mj_head.children.push(mj_title.into()); + mj_head.children.push(MjHeadChild::MjInclude(mj_include)); + mj_head + .children + .push(MjHeadChild::MjBreakpoint(mj_breakpoint)); + mj_head.children.push(MjHeadChild::MjTitle(mj_title)); let mut root = Mjml::default(); root.children.head = Some(mj_head); root.children.body = Some(MjBody::default()); @@ -128,8 +132,10 @@ mod tests { mj_breakpoint.attributes.width = "500px".into(); let mj_title = MjTitle::from("Hello New World!".to_string()); let mut mj_head = MjHead::default(); - mj_head.children.push(mj_breakpoint.into()); - mj_head.children.push(mj_title.into()); + mj_head + .children + .push(MjHeadChild::MjBreakpoint(mj_breakpoint)); + mj_head.children.push(MjHeadChild::MjTitle(mj_title)); let mut root = Mjml::default(); root.children.head = Some(mj_head); root.children.body = Some(MjBody::default()); @@ -146,9 +152,11 @@ mod tests { vec![MjIncludeHeadChild::MjTitle(mj_include_title)], ); let mut mj_head = MjHead::default(); - mj_head.children.push(mj_breakpoint.into()); - mj_head.children.push(mj_title.into()); - mj_head.children.push(mj_include.into()); + mj_head + .children + .push(MjHeadChild::MjBreakpoint(mj_breakpoint)); + mj_head.children.push(MjHeadChild::MjTitle(mj_title)); + mj_head.children.push(MjHeadChild::MjInclude(mj_include)); let mut root = Mjml::default(); root.children.head = Some(mj_head); root.children.body = Some(MjBody::default()); @@ -162,12 +170,12 @@ mod tests { let expected = { let opts = RenderOptions::default(); let mut mj_head = MjHead::default(); - mj_head - .children - .push(MjTitle::from("Hello World!".to_string()).into()); - mj_head - .children - .push(MjStyle::from("* { background-color: red; }".to_string()).into()); + mj_head.children.push(MjHeadChild::MjTitle(MjTitle::from( + "Hello World!".to_string(), + ))); + mj_head.children.push(MjHeadChild::MjStyle(MjStyle::from( + "* { background-color: red; }".to_string(), + ))); let mut root = Mjml::default(); root.children.head = Some(mj_head); root.children.body = Some(MjBody::default()); @@ -185,8 +193,8 @@ mod tests { let mj_head = MjHead::new( (), vec![ - MjTitle::from("Hello World!".to_string()).into(), - mj_include.into(), + MjHeadChild::MjTitle(MjTitle::from("Hello World!".to_string())), + MjHeadChild::MjInclude(mj_include), ], ); let root = Mjml::new( diff --git a/packages/mrml-core/src/mj_include/mod.rs b/packages/mrml-core/src/mj_include/mod.rs index 6ec31857..b6bea76a 100644 --- a/packages/mrml-core/src/mj_include/mod.rs +++ b/packages/mrml-core/src/mj_include/mod.rs @@ -29,7 +29,6 @@ mod tests { }, ) .unwrap(); - println!("with include: {with_include:?}"); let basic = Mjml::parse( r#" @@ -42,8 +41,11 @@ mod tests { ) .unwrap(); - let basic = basic.render(&RenderOptions::default()).unwrap(); - let with_include = with_include.render(&RenderOptions::default()).unwrap(); + let basic = basic.element.render(&RenderOptions::default()).unwrap(); + let with_include = with_include + .element + .render(&RenderOptions::default()) + .unwrap(); similar_asserts::assert_eq!(basic, with_include); } @@ -68,7 +70,6 @@ mod tests { }, ) .unwrap(); - println!("with include: {with_include:?}"); let basic = Mjml::parse( r#" @@ -81,8 +82,11 @@ mod tests { ) .unwrap(); - let basic = basic.render(&RenderOptions::default()).unwrap(); - let with_include = with_include.render(&RenderOptions::default()).unwrap(); + let basic = basic.element.render(&RenderOptions::default()).unwrap(); + let with_include = with_include + .element + .render(&RenderOptions::default()) + .unwrap(); similar_asserts::assert_eq!(basic, with_include); } } diff --git a/packages/mrml-core/src/mj_navbar/parse.rs b/packages/mrml-core/src/mj_navbar/parse.rs index 1261ff84..00718f7f 100644 --- a/packages/mrml-core/src/mj_navbar/parse.rs +++ b/packages/mrml-core/src/mj_navbar/parse.rs @@ -22,14 +22,22 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { self.parse(cursor, inner.local)?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -56,14 +64,22 @@ impl AsyncParseChildren> for AsyncMrmlParser { self.async_parse(cursor, inner.local).await?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -95,12 +111,12 @@ mod tests { assert_fail!( should_error_with_text, "Hello", - "UnexpectedToken(Span { start: 11, end: 16 })" + "UnexpectedToken { origin: Root, position: Span { start: 11, end: 16 } }" ); assert_fail!( should_error_with_other_element, "", - "UnexpectedElement(Span { start: 11, end: 16 })" + "UnexpectedElement { origin: Root, position: Span { start: 11, end: 16 } }" ); } diff --git a/packages/mrml-core/src/mj_raw/json.rs b/packages/mrml-core/src/mj_raw/json.rs index e3a4082e..2947a027 100644 --- a/packages/mrml-core/src/mj_raw/json.rs +++ b/packages/mrml-core/src/mj_raw/json.rs @@ -1,13 +1,13 @@ #[cfg(test)] mod tests { - use crate::mj_raw::MjRaw; + use crate::mj_raw::{MjRaw, MjRawChild}; use crate::text::Text; #[test] fn serialize() { let mut elt = MjRaw::default(); - elt.children.push(Text::from("Hello").into()); - elt.children.push(Text::from("World").into()); + elt.children.push(MjRawChild::Text(Text::from("Hello"))); + elt.children.push(MjRawChild::Text(Text::from("World"))); assert_eq!( serde_json::to_string(&elt).unwrap(), r#"{"type":"mj-raw","children":["Hello","World"]}"# diff --git a/packages/mrml-core/src/mj_raw/parse.rs b/packages/mrml-core/src/mj_raw/parse.rs index e844a373..256a9ff5 100644 --- a/packages/mrml-core/src/mj_raw/parse.rs +++ b/packages/mrml-core/src/mj_raw/parse.rs @@ -17,7 +17,7 @@ impl<'opts> ParseElement> for MrmlParser<'opts> { cursor: &mut MrmlCursor<'a>, tag: StrSpan<'a>, ) -> Result, Error> { - let attributes = self.parse_attributes(cursor)?; + let attributes = self.parse_attributes(cursor, &tag)?; let ending = cursor.assert_element_end()?; if ending.empty || is_void_element(tag.as_str()) { return Ok(Node { @@ -47,7 +47,7 @@ impl AsyncParseElement> for AsyncMrmlParser { cursor: &mut MrmlCursor<'a>, tag: StrSpan<'a>, ) -> Result, Error> { - let attributes = self.parse_attributes(cursor)?; + let attributes = self.parse_attributes(cursor, &tag)?; let ending = cursor.assert_element_end()?; if ending.empty || is_void_element(tag.as_str()) { return Ok(Node { @@ -87,7 +87,12 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { cursor.rewind(MrmlToken::ElementClose(close)); return Ok(children); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -118,7 +123,12 @@ impl AsyncParseChildren> for AsyncMrmlParser { cursor.rewind(MrmlToken::ElementClose(close)); return Ok(children); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } diff --git a/packages/mrml-core/src/mj_section/mod.rs b/packages/mrml-core/src/mj_section/mod.rs index 2ed6601c..3fe6bb20 100644 --- a/packages/mrml-core/src/mj_section/mod.rs +++ b/packages/mrml-core/src/mj_section/mod.rs @@ -6,8 +6,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_section/parse.rs b/packages/mrml-core/src/mj_section/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_section/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_social/parse.rs b/packages/mrml-core/src/mj_social/parse.rs index f2646718..76eeb28e 100644 --- a/packages/mrml-core/src/mj_social/parse.rs +++ b/packages/mrml-core/src/mj_social/parse.rs @@ -22,14 +22,22 @@ impl<'opts> ParseChildren> for MrmlParser<'opts> { self.parse(cursor, inner.local)?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -56,14 +64,22 @@ impl AsyncParseChildren> for AsyncMrmlParser { self.async_parse(cursor, inner.local).await?, )); } else { - return Err(Error::UnexpectedElement(inner.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); } } MrmlToken::ElementClose(inner) => { cursor.rewind(MrmlToken::ElementClose(inner)); return Ok(result); } - other => return Err(Error::UnexpectedToken(other.span())), + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } } } } @@ -95,12 +111,12 @@ mod tests { assert_fail!( should_error_with_text, "Hello", - "UnexpectedToken(Span { start: 11, end: 16 })" + "UnexpectedToken { origin: Root, position: Span { start: 11, end: 16 } }" ); assert_fail!( should_error_with_other_element, "", - "UnexpectedElement(Span { start: 11, end: 16 })" + "UnexpectedElement { origin: Root, position: Span { start: 11, end: 16 } }" ); } diff --git a/packages/mrml-core/src/mj_spacer/mod.rs b/packages/mrml-core/src/mj_spacer/mod.rs index 74d4a093..d970fc83 100644 --- a/packages/mrml-core/src/mj_spacer/mod.rs +++ b/packages/mrml-core/src/mj_spacer/mod.rs @@ -5,8 +5,6 @@ use crate::prelude::{Component, StaticTag}; #[cfg(feature = "json")] mod json; -#[cfg(feature = "parse")] -mod parse; #[cfg(feature = "print")] mod print; #[cfg(feature = "render")] diff --git a/packages/mrml-core/src/mj_spacer/parse.rs b/packages/mrml-core/src/mj_spacer/parse.rs deleted file mode 100644 index 8b137891..00000000 --- a/packages/mrml-core/src/mj_spacer/parse.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/mrml-core/src/mj_style/parse.rs b/packages/mrml-core/src/mj_style/parse.rs index 9b770bcc..65f38bfc 100644 --- a/packages/mrml-core/src/mj_style/parse.rs +++ b/packages/mrml-core/src/mj_style/parse.rs @@ -1,30 +1,40 @@ +use xmlparser::StrSpan; + use super::MjStyleAttributes; #[cfg(feature = "async")] use crate::prelude::parser::AsyncMrmlParser; -use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes}; +use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes, WarningKind}; -#[inline] +#[inline(always)] fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { let mut result = MjStyleAttributes::default(); while let Some(attr) = cursor.next_attribute()? { if attr.local.as_str() == "inline" { result.inline = Some(attr.value.to_string()); } else { - return Err(Error::UnexpectedAttribute(attr.span.into())); + cursor.add_warning(WarningKind::UnexpectedAttribute, attr.span); } } Ok(result) } impl<'opts> ParseAttributes for MrmlParser<'opts> { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result { parse_attributes(cursor) } } #[cfg(feature = "async")] impl ParseAttributes for AsyncMrmlParser { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result { parse_attributes(cursor) } } @@ -47,12 +57,12 @@ mod tests { ); #[test] - #[should_panic(expected = "UnexpectedAttribute(Span { start: 10, end: 21 })")] - fn should_error_with_unknown_attribute() { + fn should_warn_with_unknown_attribute() { let template = r#".whatever {background-color: red};"#; let opts = ParserOptions::default(); let parser = MrmlParser::new(&opts); let mut cursor = MrmlCursor::new(template); let _: MjStyle = parser.parse_root(&mut cursor).unwrap(); + assert_eq!(cursor.warnings().len(), 1); } } diff --git a/packages/mrml-core/src/mjml/parse.rs b/packages/mrml-core/src/mjml/parse.rs index 9ae498ff..8779b056 100644 --- a/packages/mrml-core/src/mjml/parse.rs +++ b/packages/mrml-core/src/mjml/parse.rs @@ -1,3 +1,5 @@ +use xmlparser::StrSpan; + use super::{Mjml, MjmlAttributes, MjmlChildren}; use crate::mj_body::NAME as MJ_BODY; use crate::mj_head::NAME as MJ_HEAD; @@ -5,10 +7,10 @@ use crate::mj_head::NAME as MJ_HEAD; use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement}; use crate::prelude::parser::{ Error, MrmlCursor, MrmlParser, MrmlToken, ParseAttributes, ParseChildren, ParseElement, - ParserOptions, + ParseOutput, ParserOptions, WarningKind, }; -#[inline] +#[inline(always)] fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result { let mut attrs = MjmlAttributes::default(); while let Some(token) = cursor.next_attribute()? { @@ -16,14 +18,18 @@ fn parse_attributes(cursor: &mut MrmlCursor<'_>) -> Result attrs.owa = Some(token.value.to_string()), "lang" => attrs.lang = Some(token.value.to_string()), "dir" => attrs.dir = Some(token.value.to_string()), - _ => return Err(Error::UnexpectedAttribute(token.span.into())), + _ => cursor.add_warning(WarningKind::UnexpectedAttribute, token.span), } } Ok(attrs) } impl<'opts> ParseAttributes for MrmlParser<'opts> { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result { parse_attributes(cursor) } } @@ -46,11 +52,17 @@ impl<'opts> ParseChildren for MrmlParser<'opts> { children.body = Some(self.parse(cursor, start.local)?); } _ => { - return Err(Error::UnexpectedElement(start.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: start.span.into(), + }); } }, other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -59,7 +71,11 @@ impl<'opts> ParseChildren for MrmlParser<'opts> { #[cfg(feature = "async")] impl ParseAttributes for AsyncMrmlParser { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result { parse_attributes(cursor) } } @@ -88,11 +104,17 @@ impl AsyncParseChildren for AsyncMrmlParser { children.body = Some(self.async_parse(cursor, start.local).await?); } _ => { - return Err(Error::UnexpectedElement(start.span.into())); + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: start.span.into(), + }); } }, other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -125,38 +147,54 @@ impl Mjml { pub fn parse_with_options>( value: T, opts: &ParserOptions, - ) -> Result { + ) -> Result, Error> { let parser = MrmlParser::new(opts); let mut cursor = MrmlCursor::new(value.as_ref()); - parser.parse_root(&mut cursor) + let element = parser.parse_root(&mut cursor)?; + Ok(ParseOutput { + element, + warnings: cursor.warnings(), + }) } #[cfg(feature = "async")] pub async fn async_parse_with_options>( value: T, opts: std::sync::Arc, - ) -> Result { + ) -> Result, Error> { let parser = AsyncMrmlParser::new(opts); let mut cursor = MrmlCursor::new(value.as_ref()); - parser.parse_root(&mut cursor).await + let element = parser.parse_root(&mut cursor).await?; + Ok(ParseOutput { + element, + warnings: cursor.warnings(), + }) } /// Function to parse a raw mjml template using the default parsing /// [options](crate::prelude::parser::ParserOptions). - pub fn parse>(value: T) -> Result { + pub fn parse>(value: T) -> Result, Error> { let opts = ParserOptions::default(); let parser = MrmlParser::new(&opts); let mut cursor = MrmlCursor::new(value.as_ref()); - parser.parse_root(&mut cursor) + let element = parser.parse_root(&mut cursor)?; + Ok(ParseOutput { + element, + warnings: cursor.warnings(), + }) } #[cfg(feature = "async")] /// Function to parse a raw mjml template using the default parsing /// [options](crate::prelude::parser::ParserOptions). - pub async fn async_parse>(value: T) -> Result { + pub async fn async_parse>(value: T) -> Result, Error> { let parser = AsyncMrmlParser::default(); let mut cursor = MrmlCursor::new(value.as_ref()); - parser.parse_root(&mut cursor).await + let element = parser.parse_root(&mut cursor).await?; + Ok(ParseOutput { + element, + warnings: cursor.warnings(), + }) } } @@ -167,102 +205,104 @@ mod tests { #[test] fn should_parse_with_options_sync() { let template = ""; - let elt = Mjml::parse_with_options(template, &Default::default()).unwrap(); - assert!(elt.children.body.is_none()); - assert!(elt.children.head.is_none()); + let output = Mjml::parse_with_options(template, &Default::default()).unwrap(); + assert!(output.element.children.body.is_none()); + assert!(output.element.children.head.is_none()); } #[cfg(feature = "async")] #[tokio::test] async fn should_parse_with_options_async() { let template = ""; - let elt = Mjml::async_parse_with_options(template, Default::default()) + let output = Mjml::async_parse_with_options(template, Default::default()) .await .unwrap(); - assert!(elt.children.body.is_none()); - assert!(elt.children.head.is_none()); + assert!(output.element.children.body.is_none()); + assert!(output.element.children.head.is_none()); } #[test] fn should_parse_sync() { let template = ""; - let elt = Mjml::parse(template).unwrap(); - assert!(elt.children.body.is_none()); - assert!(elt.children.head.is_none()); + let output = Mjml::parse(template).unwrap(); + assert!(output.element.children.body.is_none()); + assert!(output.element.children.head.is_none()); } #[cfg(feature = "async")] #[tokio::test] async fn should_parse_async() { let template = ""; - let elt = Mjml::async_parse(template).await.unwrap(); - assert!(elt.children.body.is_none()); - assert!(elt.children.head.is_none()); + let output = Mjml::async_parse(template).await.unwrap(); + assert!(output.element.children.body.is_none()); + assert!(output.element.children.head.is_none()); } #[test] fn should_parse_without_children_sync() { let template = ""; - let elt: Mjml = Mjml::parse(template).unwrap(); - assert!(elt.children.body.is_none()); - assert!(elt.children.head.is_none()); + let output: ParseOutput = Mjml::parse(template).unwrap(); + assert!(output.element.children.body.is_none()); + assert!(output.element.children.head.is_none()); } #[cfg(feature = "async")] #[tokio::test] async fn should_parse_without_children_async() { let template = ""; - let elt: Mjml = Mjml::async_parse(template).await.unwrap(); - assert!(elt.children.body.is_none()); - assert!(elt.children.head.is_none()); + let output: ParseOutput = Mjml::async_parse(template).await.unwrap(); + assert!(output.element.children.body.is_none()); + assert!(output.element.children.head.is_none()); } #[test] fn should_parse_with_lang_sync() { let template = ""; - let elt = Mjml::parse(template).unwrap(); - assert_eq!(elt.attributes.lang.unwrap(), "fr"); + let output = Mjml::parse(template).unwrap(); + assert_eq!(output.element.attributes.lang.unwrap(), "fr"); } #[cfg(feature = "async")] #[tokio::test] async fn should_parse_with_lang_async() { let template = ""; - let elt = Mjml::async_parse(template).await.unwrap(); - assert_eq!(elt.attributes.lang.unwrap(), "fr"); + let output = Mjml::async_parse(template).await.unwrap(); + assert_eq!(output.element.attributes.lang.unwrap(), "fr"); } #[test] fn should_parse_with_owa() { let template = ""; - let elt = Mjml::parse(template).unwrap(); - assert_eq!(elt.attributes.owa.unwrap(), "desktop"); + let output = Mjml::parse(template).unwrap(); + assert_eq!(output.element.attributes.owa.unwrap(), "desktop"); } #[test] fn should_parse_with_dir() { let template = ""; - let elt = Mjml::parse(template).unwrap(); - assert_eq!(elt.attributes.dir.unwrap(), "rtl"); + let output = Mjml::parse(template).unwrap(); + assert_eq!(output.element.attributes.dir.unwrap(), "rtl"); } #[test] - #[should_panic(expected = "UnexpectedAttribute(Span { start: 6, end: 20 })")] - fn should_fail_with_unknown_param() { + fn should_not_fail_with_unknown_param() { let template = ""; - let elt = Mjml::parse(template).unwrap(); - assert_eq!(elt.attributes.dir.unwrap(), "rtl"); + let _output = Mjml::parse(template).unwrap(); } #[test] - #[should_panic(expected = "UnexpectedToken(Span { start: 6, end: 11 })")] + #[should_panic( + expected = "UnexpectedToken { origin: Root, position: Span { start: 6, end: 11 } }" + )] fn should_fail_with_text_as_child() { let template = "Hello"; let _ = Mjml::parse(template).unwrap(); } #[test] - #[should_panic(expected = "UnexpectedElement(Span { start: 6, end: 10 })")] + #[should_panic( + expected = "UnexpectedElement { origin: Root, position: Span { start: 6, end: 10 } }" + )] fn should_fail_with_other_child() { let template = "
"; let _ = Mjml::parse(template).unwrap(); diff --git a/packages/mrml-core/src/mjml/render.rs b/packages/mrml-core/src/mjml/render.rs index 59e59a9d..f6cbf6bd 100644 --- a/packages/mrml-core/src/mjml/render.rs +++ b/packages/mrml-core/src/mjml/render.rs @@ -84,7 +84,7 @@ mod tests { let opts = RenderOptions::default(); let template = include_str!("../../resources/template/amario.mjml"); let root = Mjml::parse(template).unwrap(); - assert!(root.render(&opts).is_ok()); + assert!(root.element.render(&opts).is_ok()); } #[test] @@ -93,7 +93,7 @@ mod tests { let template = include_str!("../../resources/template/air-astana.mjml"); let expected = include_str!("../../resources/template/air-astana.html"); let root = Mjml::parse(template).unwrap(); - html_compare::assert_similar(expected, root.render(&opts).unwrap().as_str()); + html_compare::assert_similar(expected, root.element.render(&opts).unwrap().as_str()); } #[test] @@ -104,8 +104,8 @@ mod tests { let root_1 = Mjml::parse(source).unwrap(); let root_2 = Mjml::parse(source).unwrap(); - let output_1 = root_1.render(&options).unwrap(); - let output_2 = root_2.render(&options).unwrap(); + let output_1 = root_1.element.render(&options).unwrap(); + let output_2 = root_2.element.render(&options).unwrap(); assert_eq!(output_1, output_2); } diff --git a/packages/mrml-core/src/node/render.rs b/packages/mrml-core/src/node/render.rs index f78112ef..fbf648bc 100644 --- a/packages/mrml-core/src/node/render.rs +++ b/packages/mrml-core/src/node/render.rs @@ -73,7 +73,7 @@ mod tests { "#; let root = Mjml::parse(template).unwrap(); - let result = root.render(&opts).unwrap(); + let result = root.element.render(&opts).unwrap(); assert!(result.contains("")); } } diff --git a/packages/mrml-core/src/prelude/parser/loader.rs b/packages/mrml-core/src/prelude/parser/loader.rs index 055730da..78861a15 100644 --- a/packages/mrml-core/src/prelude/parser/loader.rs +++ b/packages/mrml-core/src/prelude/parser/loader.rs @@ -44,13 +44,9 @@ impl IncludeLoaderError { impl std::fmt::Display for IncludeLoaderError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(msg) = self.message { - write!( - f, - "Unable to load template {}: {} ({})", - self.path, msg, self.reason - ) + write!(f, "{} {} ({msg})", self.path, self.reason) } else { - write!(f, "Unable to load template {}: {}", self.path, self.reason) + write!(f, "{} {}", self.path, self.reason) } } } @@ -95,7 +91,7 @@ mod tests { fn should_display_basic() { assert_eq!( IncludeLoaderError::new("foo.mjml", ErrorKind::NotFound).to_string(), - "Unable to load template foo.mjml: entity not found", + "foo.mjml entity not found", ); } @@ -105,7 +101,7 @@ mod tests { IncludeLoaderError::new("foo.mjml", ErrorKind::NotFound) .with_message("oops") .to_string(), - "Unable to load template foo.mjml: oops (entity not found)", + "foo.mjml entity not found (oops)", ); } @@ -118,7 +114,7 @@ mod tests { ErrorKind::InvalidInput ))) .to_string(), - "Unable to load template foo.mjml: entity not found", + "foo.mjml entity not found", ); } } diff --git a/packages/mrml-core/src/prelude/parser/local_loader.rs b/packages/mrml-core/src/prelude/parser/local_loader.rs index 157d0d8b..6b620a72 100644 --- a/packages/mrml-core/src/prelude/parser/local_loader.rs +++ b/packages/mrml-core/src/prelude/parser/local_loader.rs @@ -121,7 +121,7 @@ mod tests { .unwrap_err(); assert_eq!(err.reason, ErrorKind::InvalidInput); - assert_eq!(err.to_string(), "Unable to load template /resources/compare/success/mj-body.mjml: the path should start with file:/// (invalid input parameter)"); + assert_eq!(err.to_string(), "/resources/compare/success/mj-body.mjml invalid input parameter (the path should start with file:///)"); } #[test] @@ -159,7 +159,7 @@ mod tests { let err = loader.build_path("file:///../partial.mjml").unwrap_err(); assert_eq!(err.reason, ErrorKind::NotFound); - assert_eq!(err.to_string(), "Unable to load template file:///../partial.mjml: the path should stay in the context of the loader (entity not found)"); + assert_eq!(err.to_string(), "file:///../partial.mjml entity not found (the path should stay in the context of the loader)"); } #[test] diff --git a/packages/mrml-core/src/prelude/parser/mod.rs b/packages/mrml-core/src/prelude/parser/mod.rs index 48e0fafa..8e1dd2a0 100644 --- a/packages/mrml-core/src/prelude/parser/mod.rs +++ b/packages/mrml-core/src/prelude/parser/mod.rs @@ -1,8 +1,6 @@ -use std::convert::TryFrom; -use std::fmt::Display; use std::marker::PhantomData; -use xmlparser::{StrSpan, Token, Tokenizer}; +use xmlparser::{StrSpan, Tokenizer}; use self::loader::IncludeLoaderError; use super::hash::Map; @@ -16,73 +14,61 @@ pub mod memory_loader; pub mod multi_loader; pub mod noop_loader; -#[derive(Clone, Debug, Default)] -pub struct Span { - pub start: usize, - pub end: usize, -} +mod output; +mod token; -impl Display for Span { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}..{}", self.start, self.end) - } -} +pub use output::*; +pub use token::*; -impl<'a> From> for Span { - fn from(value: StrSpan<'a>) -> Self { - Self { - start: value.start(), - end: value.end(), - } - } +#[derive(Clone, Debug)] +pub enum Origin { + Root, + Include { path: String }, } -impl<'a> From> for Span { - fn from(value: Token<'a>) -> Self { - match value { - Token::Attribute { span, .. } - | Token::Cdata { span, .. } - | Token::Comment { span, .. } - | Token::Declaration { span, .. } - | Token::DtdEnd { span } - | Token::DtdStart { span, .. } - | Token::ElementEnd { span, .. } - | Token::ElementStart { span, .. } - | Token::EmptyDtd { span, .. } - | Token::EntityDeclaration { span, .. } - | Token::ProcessingInstruction { span, .. } => span.into(), - Token::Text { text } => text.into(), +impl std::fmt::Display for Origin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Root => write!(f, "root template"), + Self::Include { path } => write!(f, "template from {path:?}"), } } } #[derive(Clone, Debug, thiserror::Error)] pub enum Error { - #[error("unexpected attribute at position {0}")] - UnexpectedAttribute(Span), - #[error("unexpected element at position {0}")] - UnexpectedElement(Span), - #[error("unexpected token at position {0}")] - UnexpectedToken(Span), - #[error("missing attribute {0} in element at position {1}")] - MissingAttribute(&'static str, Span), - #[error("invalid attribute at position {0}")] - InvalidAttribute(Span), - #[error("invalid format at position {0}")] - InvalidFormat(Span), - #[error("unexpected end of stream")] - EndOfStream, + #[error("unexpected element in {origin} at position {position}")] + UnexpectedElement { origin: Origin, position: Span }, + #[error("unexpected token in {origin} at position {position}")] + UnexpectedToken { origin: Origin, position: Span }, + #[error("missing attribute {name:?} in element in {origin} at position {position}")] + MissingAttribute { + name: &'static str, + origin: Origin, + position: Span, + }, + #[error("invalid attribute in {origin} at position {position}")] + InvalidAttribute { origin: Origin, position: Span }, + #[error("invalid format in {origin} at position {position}")] + InvalidFormat { origin: Origin, position: Span }, + #[error("unexpected end of stream in {origin}")] + EndOfStream { origin: Origin }, /// The input string should be smaller than 4GiB. - #[error("size limit reached")] - SizeLimit, + #[error("size limit reached in {origin}")] + SizeLimit { origin: Origin }, /// Errors detected by the `xmlparser` crate. - #[error("unable to load included template")] - ParserError(#[from] xmlparser::Error), + #[error("unable to parse next template in {origin}")] + ParserError { + origin: Origin, + #[source] + source: xmlparser::Error, + }, /// The Mjml document must have at least one element. - #[error("no root node found")] + #[error("unable to find mjml element")] NoRootNode, - #[error("unable to load included template")] + #[error("unable to load included template in {origin} at position {position}")] IncludeLoaderError { + origin: Origin, position: Span, #[source] source: IncludeLoaderError, @@ -119,120 +105,6 @@ impl Default for AsyncParserOptions { } } -#[derive(Debug)] -pub(crate) enum MrmlToken<'a> { - Attribute(Attribute<'a>), - Comment(Comment<'a>), - ElementClose(ElementClose<'a>), - ElementEnd(ElementEnd<'a>), - ElementStart(ElementStart<'a>), - Text(Text<'a>), -} - -impl<'a> TryFrom> for MrmlToken<'a> { - type Error = Error; - - fn try_from(value: Token<'a>) -> Result { - match value { - Token::Attribute { - prefix, - local, - value, - span, - } => Ok(MrmlToken::Attribute(Attribute { - prefix, - local, - value, - span, - })), - Token::Comment { text, span } => Ok(MrmlToken::Comment(Comment { span, text })), - Token::ElementEnd { - end: xmlparser::ElementEnd::Close(prefix, local), - span, - } => Ok(MrmlToken::ElementClose(ElementClose { - span, - prefix, - local, - })), - Token::ElementEnd { - end: xmlparser::ElementEnd::Empty, - span, - } => Ok(MrmlToken::ElementEnd(ElementEnd { span, empty: true })), - Token::ElementEnd { - end: xmlparser::ElementEnd::Open, - span, - } => Ok(MrmlToken::ElementEnd(ElementEnd { span, empty: false })), - Token::ElementStart { - prefix, - local, - span, - } => Ok(MrmlToken::ElementStart(ElementStart { - prefix, - local, - span, - })), - Token::Text { text } => Ok(MrmlToken::Text(Text { text })), - other => Err(Error::UnexpectedToken(other.into())), - } - } -} - -impl<'a> MrmlToken<'a> { - pub fn span(&self) -> Span { - match self { - Self::Attribute(item) => item.span, - Self::Comment(item) => item.span, - Self::ElementClose(item) => item.span, - Self::ElementEnd(item) => item.span, - Self::ElementStart(item) => item.span, - Self::Text(item) => item.text, - } - .into() - } -} - -#[derive(Debug)] -pub(crate) struct Attribute<'a> { - #[allow(unused)] - pub prefix: StrSpan<'a>, - pub local: StrSpan<'a>, - pub value: StrSpan<'a>, - pub span: StrSpan<'a>, -} - -#[derive(Debug)] -pub(crate) struct Comment<'a> { - pub span: StrSpan<'a>, - pub text: StrSpan<'a>, -} - -#[derive(Debug)] -pub(crate) struct ElementClose<'a> { - #[allow(unused)] - pub prefix: StrSpan<'a>, - pub local: StrSpan<'a>, - pub span: StrSpan<'a>, -} - -#[derive(Debug)] -pub(crate) struct ElementStart<'a> { - #[allow(unused)] - pub prefix: StrSpan<'a>, - pub local: StrSpan<'a>, - pub span: StrSpan<'a>, -} - -#[derive(Debug)] -pub(crate) struct ElementEnd<'a> { - pub span: StrSpan<'a>, - pub empty: bool, -} - -#[derive(Debug)] -pub(crate) struct Text<'a> { - pub text: StrSpan<'a>, -} - pub(crate) trait ParseElement { fn parse<'a>(&self, cursor: &mut MrmlCursor<'a>, tag: StrSpan<'a>) -> Result; } @@ -249,7 +121,7 @@ pub(crate) trait AsyncParseElement { } pub(crate) trait ParseAttributes { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result; + fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>, tag: &StrSpan<'_>) -> Result; } pub(crate) trait ParseChildren { @@ -266,6 +138,8 @@ pub(crate) trait AsyncParseChildren { pub struct MrmlCursor<'a> { tokenizer: Tokenizer<'a>, buffer: Vec>, + origin: Origin, + warnings: Vec, } impl<'a> MrmlCursor<'a> { @@ -273,98 +147,28 @@ impl<'a> MrmlCursor<'a> { Self { tokenizer: Tokenizer::from(source), buffer: Default::default(), + origin: Origin::Root, + warnings: Default::default(), } } - pub(crate) fn new_child<'b>(&self, source: &'b str) -> MrmlCursor<'b> { + pub(crate) fn new_child<'b, O: Into>( + &self, + origin: O, + source: &'b str, + ) -> MrmlCursor<'b> { MrmlCursor { tokenizer: Tokenizer::from(source), buffer: Default::default(), + origin: Origin::Include { + path: origin.into(), + }, + warnings: Default::default(), } } - fn read_next_token(&mut self) -> Option, Error>> { - self.tokenizer - .next() - .map(|res| res.map_err(Error::from).and_then(MrmlToken::try_from)) - .and_then(|token| match token { - Ok(MrmlToken::Text(inner)) - if inner.text.starts_with('\n') && inner.text.trim().is_empty() => - { - self.read_next_token() - } - other => Some(other), - }) - } - - pub(crate) fn next_token(&mut self) -> Option, Error>> { - if let Some(item) = self.buffer.pop() { - Some(Ok(item)) - } else { - self.read_next_token() - } - } - - pub(crate) fn rewind(&mut self, token: MrmlToken<'a>) { - self.buffer.push(token); - } - - pub(crate) fn assert_next(&mut self) -> Result, Error> { - self.next_token().unwrap_or_else(|| Err(Error::EndOfStream)) - } - - pub(crate) fn next_attribute(&mut self) -> Result>, Error> { - match self.next_token() { - Some(Ok(MrmlToken::Attribute(inner))) => Ok(Some(inner)), - Some(Ok(other)) => { - self.rewind(other); - Ok(None) - } - Some(Err(inner)) => Err(inner), - None => Err(Error::EndOfStream), - } - } - - pub(crate) fn assert_element_start(&mut self) -> Result, Error> { - match self.next_token() { - Some(Ok(MrmlToken::ElementStart(inner))) => Ok(inner), - Some(Ok(other)) => Err(Error::UnexpectedToken(other.span())), - Some(Err(inner)) => Err(inner), - None => Err(Error::EndOfStream), - } - } - - pub(crate) fn assert_element_end(&mut self) -> Result, Error> { - match self.next_token() { - Some(Ok(MrmlToken::ElementEnd(inner))) => Ok(inner), - Some(Ok(other)) => Err(Error::UnexpectedToken(other.span())), - Some(Err(inner)) => Err(inner), - None => Err(Error::EndOfStream), - } - } - - pub(crate) fn assert_element_close(&mut self) -> Result, Error> { - match self.next_token() { - Some(Ok(MrmlToken::ElementClose(inner))) => Ok(inner), - Some(Ok(MrmlToken::Text(inner))) if inner.text.trim().is_empty() => { - self.assert_element_close() - } - Some(Ok(other)) => Err(Error::UnexpectedToken(other.span())), - Some(Err(inner)) => Err(inner), - None => Err(Error::EndOfStream), - } - } - - pub(crate) fn next_text(&mut self) -> Result>, Error> { - match self.next_token() { - Some(Ok(MrmlToken::Text(inner))) => Ok(Some(inner)), - Some(Ok(other)) => { - self.rewind(other); - Ok(None) - } - Some(Err(inner)) => Err(inner), - None => Err(Error::EndOfStream), - } + pub(crate) fn origin(&self) -> Origin { + self.origin.clone() } } @@ -390,13 +194,14 @@ impl<'opts> MrmlParser<'opts> { pub(crate) fn parse_attributes_and_children( &self, cursor: &mut MrmlCursor, + tag: &StrSpan<'_>, ) -> Result<(A, C), Error> where MrmlParser<'opts>: ParseAttributes, MrmlParser<'opts>: ParseChildren, C: Default, { - let attributes: A = self.parse_attributes(cursor)?; + let attributes: A = self.parse_attributes(cursor, tag)?; let ending = cursor.assert_element_end()?; if ending.empty { return Ok((attributes, Default::default())); @@ -411,13 +216,21 @@ impl<'opts> MrmlParser<'opts> { } impl<'opts> ParseAttributes> for MrmlParser<'opts> { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result, Error> { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result, Error> { parse_attributes_map(cursor) } } impl<'opts> ParseAttributes<()> for MrmlParser<'opts> { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result<(), Error> { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result<(), Error> { parse_attributes_empty(cursor) } } @@ -457,13 +270,14 @@ impl AsyncMrmlParser { pub(crate) async fn parse_attributes_and_children( &self, cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, ) -> Result<(A, C), Error> where AsyncMrmlParser: ParseAttributes, AsyncMrmlParser: AsyncParseChildren, C: Default, { - let attributes: A = self.parse_attributes(cursor)?; + let attributes: A = self.parse_attributes(cursor, tag)?; let ending = cursor.assert_element_end()?; if ending.empty { return Ok((attributes, Default::default())); @@ -479,14 +293,22 @@ impl AsyncMrmlParser { #[cfg(feature = "async")] impl ParseAttributes> for AsyncMrmlParser { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result, Error> { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result, Error> { parse_attributes_map(cursor) } } #[cfg(feature = "async")] impl ParseAttributes<()> for AsyncMrmlParser { - fn parse_attributes(&self, cursor: &mut MrmlCursor<'_>) -> Result<(), Error> { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + _tag: &StrSpan<'_>, + ) -> Result<(), Error> { parse_attributes_empty(cursor) } } @@ -515,7 +337,7 @@ pub(crate) fn parse_attributes_map( pub(crate) fn parse_attributes_empty(cursor: &mut MrmlCursor<'_>) -> Result<(), Error> { if let Some(attr) = cursor.next_attribute()? { - return Err(Error::UnexpectedAttribute(Span::from(attr.span))); + cursor.add_warning(WarningKind::UnexpectedAttribute, attr.span); } Ok(()) } @@ -529,9 +351,9 @@ where fn parse<'a>( &self, cursor: &mut MrmlCursor<'a>, - _tag: StrSpan<'a>, + tag: StrSpan<'a>, ) -> Result, A, C>, Error> { - let (attributes, children) = self.parse_attributes_and_children(cursor)?; + let (attributes, children) = self.parse_attributes_and_children(cursor, &tag)?; Ok(super::Component { tag: PhantomData::, @@ -549,9 +371,9 @@ where fn parse<'a>( &self, cursor: &mut MrmlCursor<'a>, - _tag: StrSpan<'a>, + tag: StrSpan<'a>, ) -> Result, A, ()>, Error> { - let attributes = self.parse_attributes(cursor)?; + let attributes = self.parse_attributes(cursor, &tag)?; let ending = cursor.assert_element_end()?; if !ending.empty { cursor.assert_element_close()?; @@ -578,9 +400,9 @@ where async fn async_parse<'a>( &self, cursor: &mut MrmlCursor<'a>, - _tag: StrSpan<'a>, + tag: StrSpan<'a>, ) -> Result, A, C>, Error> { - let (attributes, children) = self.parse_attributes_and_children(cursor).await?; + let (attributes, children) = self.parse_attributes_and_children(cursor, &tag).await?; Ok(super::Component { tag: PhantomData::, @@ -602,9 +424,9 @@ where async fn async_parse<'a>( &self, cursor: &mut MrmlCursor<'a>, - _tag: StrSpan<'a>, + tag: StrSpan<'a>, ) -> Result, A, ()>, Error> { - let attributes = self.parse_attributes(cursor)?; + let attributes = self.parse_attributes(cursor, &tag)?; let ending = cursor.assert_element_end()?; if !ending.empty { cursor.assert_element_close()?; @@ -625,12 +447,19 @@ macro_rules! should_parse { $crate::should_sync_parse!($name, $target, $template); $crate::should_async_parse!($name, $target, $template); }; + ($name: ident, $target: ty, $template: literal, $warnings: literal) => { + $crate::should_sync_parse!($name, $target, $template, $warnings); + $crate::should_async_parse!($name, $target, $template, $warnings); + }; } #[cfg(test)] #[macro_export] macro_rules! should_sync_parse { ($name: ident, $target: ty, $template: literal) => { + $crate::should_sync_parse!($name, $target, $template, 0); + }; + ($name: ident, $target: ty, $template: literal, $warnings: literal) => { concat_idents::concat_idents!(fn_name = $name, _, sync { #[test] fn fn_name() { @@ -638,6 +467,7 @@ macro_rules! should_sync_parse { let parser = $crate::prelude::parser::MrmlParser::new(&opts); let mut cursor = $crate::prelude::parser::MrmlCursor::new($template); let _: $target = parser.parse_root(&mut cursor).unwrap(); + assert_eq!(cursor.warnings().len(), $warnings); } }); }; @@ -647,6 +477,9 @@ macro_rules! should_sync_parse { #[macro_export] macro_rules! should_async_parse { ($name: ident, $target: ty, $template: literal) => { + $crate::should_async_parse!($name, $target, $template, 0); + }; + ($name: ident, $target: ty, $template: literal, $warnings: literal) => { concat_idents::concat_idents!(fn_name = $name, _, "async" { #[cfg(feature = "async")] #[tokio::test] @@ -654,6 +487,7 @@ macro_rules! should_async_parse { let parser = $crate::prelude::parser::AsyncMrmlParser::default(); let mut cursor = $crate::prelude::parser::MrmlCursor::new($template); let _: $target = parser.parse_root(&mut cursor).await.unwrap(); + assert_eq!(cursor.warnings().len(), $warnings); } }); }; diff --git a/packages/mrml-core/src/prelude/parser/output.rs b/packages/mrml-core/src/prelude/parser/output.rs new file mode 100644 index 00000000..245e1f7f --- /dev/null +++ b/packages/mrml-core/src/prelude/parser/output.rs @@ -0,0 +1,58 @@ +pub struct ParseOutput { + pub element: E, + pub warnings: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WarningKind { + UnexpectedAttribute, +} + +impl WarningKind { + pub const fn as_str(&self) -> &'static str { + "unexpected-attribute" + } +} + +impl std::fmt::Display for WarningKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnexpectedAttribute => f.write_str("unexpected attribute"), + } + } +} + +#[derive(Clone, Debug)] +pub struct Warning { + pub kind: WarningKind, + pub origin: super::Origin, + pub span: super::Span, +} + +impl<'a> super::MrmlCursor<'a> { + pub(crate) fn add_warning>(&mut self, kind: WarningKind, span: S) { + self.warnings.push(Warning { + kind, + origin: self.origin.clone(), + span: span.into(), + }); + } + + pub(crate) fn warnings(self) -> Vec { + self.warnings + } + + pub(crate) fn with_warnings(&mut self, others: Vec) { + self.warnings.extend(others); + } +} + +impl std::fmt::Display for Warning { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} in {} at position {}", + self.kind, self.origin, self.span + ) + } +} diff --git a/packages/mrml-core/src/prelude/parser/token.rs b/packages/mrml-core/src/prelude/parser/token.rs new file mode 100644 index 00000000..2601b6f3 --- /dev/null +++ b/packages/mrml-core/src/prelude/parser/token.rs @@ -0,0 +1,286 @@ +use std::fmt::Display; +use xmlparser::{StrSpan, Token}; + +use super::MrmlCursor; + +#[derive(Clone, Copy, Debug)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl Display for Span { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}..{}", self.start, self.end) + } +} + +impl<'a> From<&StrSpan<'a>> for Span { + fn from(value: &StrSpan<'a>) -> Self { + Self { + start: value.start(), + end: value.end(), + } + } +} + +impl<'a> From> for Span { + fn from(value: StrSpan<'a>) -> Self { + Self { + start: value.start(), + end: value.end(), + } + } +} + +impl<'a> From> for Span { + fn from(value: Token<'a>) -> Self { + match value { + Token::Attribute { span, .. } + | Token::Cdata { span, .. } + | Token::Comment { span, .. } + | Token::Declaration { span, .. } + | Token::DtdEnd { span } + | Token::DtdStart { span, .. } + | Token::ElementEnd { span, .. } + | Token::ElementStart { span, .. } + | Token::EmptyDtd { span, .. } + | Token::EntityDeclaration { span, .. } + | Token::ProcessingInstruction { span, .. } => span.into(), + Token::Text { text } => text.into(), + } + } +} + +#[derive(Debug)] +pub(crate) enum MrmlToken<'a> { + Attribute(Attribute<'a>), + Comment(Comment<'a>), + ElementClose(ElementClose<'a>), + ElementEnd(ElementEnd<'a>), + ElementStart(ElementStart<'a>), + Text(Text<'a>), +} + +impl<'a> MrmlToken<'a> { + pub(crate) fn parse( + cursor: &mut MrmlCursor<'a>, + value: Token<'a>, + ) -> Result { + match value { + Token::Attribute { + prefix, + local, + value, + span, + } => Ok(MrmlToken::Attribute(Attribute { + prefix, + local, + value, + span, + })), + Token::Comment { text, span } => Ok(MrmlToken::Comment(Comment { span, text })), + Token::ElementEnd { + end: xmlparser::ElementEnd::Close(prefix, local), + span, + } => Ok(MrmlToken::ElementClose(ElementClose { + span, + prefix, + local, + })), + Token::ElementEnd { + end: xmlparser::ElementEnd::Empty, + span, + } => Ok(MrmlToken::ElementEnd(ElementEnd { span, empty: true })), + Token::ElementEnd { + end: xmlparser::ElementEnd::Open, + span, + } => Ok(MrmlToken::ElementEnd(ElementEnd { span, empty: false })), + Token::ElementStart { + prefix, + local, + span, + } => Ok(MrmlToken::ElementStart(ElementStart { + prefix, + local, + span, + })), + Token::Text { text } => Ok(MrmlToken::Text(Text { text })), + other => Err(super::Error::UnexpectedToken { + origin: cursor.origin(), + position: other.into(), + }), + } + } +} + +impl<'a> MrmlToken<'a> { + pub fn span(&self) -> Span { + match self { + Self::Attribute(item) => item.span, + Self::Comment(item) => item.span, + Self::ElementClose(item) => item.span, + Self::ElementEnd(item) => item.span, + Self::ElementStart(item) => item.span, + Self::Text(item) => item.text, + } + .into() + } +} + +#[derive(Debug)] +pub(crate) struct Attribute<'a> { + #[allow(unused)] + pub prefix: StrSpan<'a>, + pub local: StrSpan<'a>, + pub value: StrSpan<'a>, + pub span: StrSpan<'a>, +} + +#[derive(Debug)] +pub(crate) struct Comment<'a> { + pub span: StrSpan<'a>, + pub text: StrSpan<'a>, +} + +#[derive(Debug)] +pub(crate) struct ElementClose<'a> { + #[allow(unused)] + pub prefix: StrSpan<'a>, + pub local: StrSpan<'a>, + pub span: StrSpan<'a>, +} + +#[derive(Debug)] +pub(crate) struct ElementStart<'a> { + #[allow(unused)] + pub prefix: StrSpan<'a>, + pub local: StrSpan<'a>, + pub span: StrSpan<'a>, +} + +#[derive(Debug)] +pub(crate) struct ElementEnd<'a> { + pub span: StrSpan<'a>, + pub empty: bool, +} + +#[derive(Debug)] +pub(crate) struct Text<'a> { + pub text: StrSpan<'a>, +} + +impl<'a> super::MrmlCursor<'a> { + fn read_next_token(&mut self) -> Option, super::Error>> { + self.tokenizer + .next() + .map(|res| { + res.map_err(|source| super::Error::ParserError { + origin: self.origin(), + source, + }) + .and_then(|token| MrmlToken::parse(self, token)) + }) + .and_then(|token| match token { + Ok(MrmlToken::Text(inner)) + if inner.text.starts_with('\n') && inner.text.trim().is_empty() => + { + self.read_next_token() + } + other => Some(other), + }) + } + + pub(crate) fn next_token(&mut self) -> Option, super::Error>> { + if let Some(item) = self.buffer.pop() { + Some(Ok(item)) + } else { + self.read_next_token() + } + } + + pub(crate) fn rewind(&mut self, token: MrmlToken<'a>) { + self.buffer.push(token); + } + + pub(crate) fn assert_next(&mut self) -> Result, super::Error> { + self.next_token().unwrap_or_else(|| { + Err(super::Error::EndOfStream { + origin: self.origin(), + }) + }) + } + + pub(crate) fn next_attribute(&mut self) -> Result>, super::Error> { + match self.next_token() { + Some(Ok(MrmlToken::Attribute(inner))) => Ok(Some(inner)), + Some(Ok(other)) => { + self.rewind(other); + Ok(None) + } + Some(Err(inner)) => Err(inner), + None => Err(super::Error::EndOfStream { + origin: self.origin(), + }), + } + } + + pub(crate) fn assert_element_start(&mut self) -> Result, super::Error> { + match self.next_token() { + Some(Ok(MrmlToken::ElementStart(inner))) => Ok(inner), + Some(Ok(other)) => Err(super::Error::UnexpectedToken { + origin: self.origin(), + position: other.span(), + }), + Some(Err(inner)) => Err(inner), + None => Err(super::Error::EndOfStream { + origin: self.origin(), + }), + } + } + + pub(crate) fn assert_element_end(&mut self) -> Result, super::Error> { + match self.next_token() { + Some(Ok(MrmlToken::ElementEnd(inner))) => Ok(inner), + Some(Ok(other)) => Err(super::Error::UnexpectedToken { + origin: self.origin(), + position: other.span(), + }), + Some(Err(inner)) => Err(inner), + None => Err(super::Error::EndOfStream { + origin: self.origin(), + }), + } + } + + pub(crate) fn assert_element_close(&mut self) -> Result, super::Error> { + match self.next_token() { + Some(Ok(MrmlToken::ElementClose(inner))) => Ok(inner), + Some(Ok(MrmlToken::Text(inner))) if inner.text.trim().is_empty() => { + self.assert_element_close() + } + Some(Ok(other)) => Err(super::Error::UnexpectedToken { + origin: self.origin(), + position: other.span(), + }), + Some(Err(inner)) => Err(inner), + None => Err(super::Error::EndOfStream { + origin: self.origin(), + }), + } + } + + pub(crate) fn next_text(&mut self) -> Result>, super::Error> { + match self.next_token() { + Some(Ok(MrmlToken::Text(inner))) => Ok(Some(inner)), + Some(Ok(other)) => { + self.rewind(other); + Ok(None) + } + Some(Err(inner)) => Err(inner), + None => Err(super::Error::EndOfStream { + origin: self.origin(), + }), + } + } +} diff --git a/packages/mrml-core/src/prelude/render/mod.rs b/packages/mrml-core/src/prelude/render/mod.rs index 3e1f8a65..c003d9ef 100644 --- a/packages/mrml-core/src/prelude/render/mod.rs +++ b/packages/mrml-core/src/prelude/render/mod.rs @@ -289,7 +289,7 @@ macro_rules! should_render { let template = include_str!(concat!("../../resources/compare/success/", $template, ".mjml")); let expected = include_str!(concat!("../../resources/compare/success/", $template, ".html")); let root = $crate::parse(template).unwrap(); - html_compare::assert_similar(expected, root.render(&opts).unwrap().as_str()); + html_compare::assert_similar(expected, root.element.render(&opts).unwrap().as_str()); } }); concat_idents::concat_idents!(fn_name = $name, _, "async" { @@ -300,7 +300,7 @@ macro_rules! should_render { let template = include_str!(concat!("../../resources/compare/success/", $template, ".mjml")); let expected = include_str!(concat!("../../resources/compare/success/", $template, ".html")); let root = $crate::async_parse(template).await.unwrap(); - html_compare::assert_similar(expected, root.render(&opts).unwrap().as_str()); + html_compare::assert_similar(expected, root.element.render(&opts).unwrap().as_str()); } }); }; diff --git a/packages/mrml-core/src/root/mod.rs b/packages/mrml-core/src/root/mod.rs index c2b60797..f9f2653f 100644 --- a/packages/mrml-core/src/root/mod.rs +++ b/packages/mrml-core/src/root/mod.rs @@ -9,6 +9,7 @@ mod render; #[derive(Debug)] enum RootChild { Mjml(Mjml), + #[allow(dead_code)] Comment(Comment), } diff --git a/packages/mrml-core/src/root/parse.rs b/packages/mrml-core/src/root/parse.rs index 669c4b3c..c3906717 100644 --- a/packages/mrml-core/src/root/parse.rs +++ b/packages/mrml-core/src/root/parse.rs @@ -1,7 +1,7 @@ use super::RootChild; use crate::comment::Comment; use crate::prelude::parser::{ - Error, MrmlCursor, MrmlParser, MrmlToken, ParseChildren, ParserOptions, + Error, MrmlCursor, MrmlParser, MrmlToken, ParseChildren, ParseOutput, ParserOptions, }; impl<'opts> crate::prelude::parser::ParseChildren> for MrmlParser<'opts> { @@ -18,7 +18,10 @@ impl<'opts> crate::prelude::parser::ParseChildren> for MrmlParser result.push(RootChild::Mjml(self.parse(cursor, inner.local)?)); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -49,7 +52,10 @@ impl crate::prelude::parser::AsyncParseChildren> result.push(RootChild::Mjml(element)); } other => { - return Err(Error::UnexpectedToken(other.span())); + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); } } } @@ -63,21 +69,29 @@ impl super::Root { pub(crate) fn parse_with_options>( value: T, opts: &ParserOptions, - ) -> Result { + ) -> Result, Error> { let parser = MrmlParser::new(opts); let mut cursor = MrmlCursor::new(value.as_ref()); - Ok(Self(parser.parse_children(&mut cursor)?)) + let element = Self(parser.parse_children(&mut cursor)?); + Ok(ParseOutput { + element, + warnings: cursor.warnings(), + }) } #[cfg(feature = "async")] pub(crate) async fn async_parse_with_options>( value: T, opts: std::sync::Arc, - ) -> Result { + ) -> Result, Error> { use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren}; let parser = AsyncMrmlParser::new(opts); let mut cursor = MrmlCursor::new(value.as_ref()); - Ok(Self(parser.async_parse_children(&mut cursor).await?)) + let element = Self(parser.async_parse_children(&mut cursor).await?); + Ok(ParseOutput { + element, + warnings: cursor.warnings(), + }) } } diff --git a/packages/mrml-core/tests/issue-439.rs b/packages/mrml-core/tests/issue-439.rs index 7465597e..06bea5df 100644 --- a/packages/mrml-core/tests/issue-439.rs +++ b/packages/mrml-core/tests/issue-439.rs @@ -14,6 +14,7 @@ fn should_preserve_whitespaces() { let root = mrml::parse(template).unwrap(); let html = root + .element .render(&mrml::prelude::render::RenderOptions::default()) .unwrap(); assert!( diff --git a/packages/mrml-core/tests/local_loading.rs b/packages/mrml-core/tests/local_loading.rs index 7e6bd13b..dfbc1389 100644 --- a/packages/mrml-core/tests/local_loading.rs +++ b/packages/mrml-core/tests/local_loading.rs @@ -21,7 +21,7 @@ fn loading_include() { include_loader: Box::new(resolver), }; let parsed = mrml::parse_with_options(template, &options).unwrap(); - let output = parsed.render(&RenderOptions::default()).unwrap(); + let output = parsed.element.render(&RenderOptions::default()).unwrap(); assert!(output.contains("Hello World")); } diff --git a/packages/mrml-core/tests/mj_head_include.rs b/packages/mrml-core/tests/mj_head_include.rs index 98d7989f..6e3d6752 100644 --- a/packages/mrml-core/tests/mj_head_include.rs +++ b/packages/mrml-core/tests/mj_head_include.rs @@ -20,5 +20,8 @@ fn should_apply_head_includes() { let template = include_str!("resources/mj-head-include.mjml"); let expected = include_str!("resources/mj-head-include.html"); let root = parse_with_options(template, &parser_opts).unwrap(); - html_compare::assert_similar(expected, root.render(&render_opts).unwrap().as_str()); + html_compare::assert_similar( + expected, + root.element.render(&render_opts).unwrap().as_str(), + ); } diff --git a/packages/mrml-core/tests/multiple-mj-font.rs b/packages/mrml-core/tests/multiple-mj-font.rs index be732cee..ba60147d 100644 --- a/packages/mrml-core/tests/multiple-mj-font.rs +++ b/packages/mrml-core/tests/multiple-mj-font.rs @@ -18,6 +18,6 @@ fn should_have_a_single_roboto_font_imported() { "#).unwrap(); - let rendered = parsed.render(&RenderOptions::default()).unwrap(); + let rendered = parsed.element.render(&RenderOptions::default()).unwrap(); assert!(!rendered.contains("Roboto:300,400,500,700")); } diff --git a/packages/mrml-python/mrml.pyi b/packages/mrml-python/mrml.pyi index 0a332201..e68440f9 100644 --- a/packages/mrml-python/mrml.pyi +++ b/packages/mrml-python/mrml.pyi @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Set, Union +from typing import Any, Dict, Optional, Set, Union, List class NoopIncludeLoaderOptions: """No-operation loader options class, which requires no specific configuration.""" @@ -71,10 +71,27 @@ class RenderOptions: fonts: Optional[Dict[str, str]] = None, ) -> None: ... +class Warning: + @property + def origin(self) -> Optional[str]: ... + @property + def kind(self) -> str: ... + @property + def start(self) -> int: ... + @property + def end(self) -> int: ... + +class Output: + """to_html result, containing content field and warnings""" + @property + def content(self) -> str: ... + @property + def warnings(self) -> List[Warning] + def to_html( input: str, parser_options: Optional[ParserOptions] = None, render_options: Optional[RenderOptions] = None, -) -> str: +) -> Output: """Function to convert input a MJML string to HTML using optional parser and render configurations.""" ... diff --git a/packages/mrml-python/src/lib.rs b/packages/mrml-python/src/lib.rs index b3bd6d16..a281e402 100644 --- a/packages/mrml-python/src/lib.rs +++ b/packages/mrml-python/src/lib.rs @@ -193,23 +193,69 @@ impl From for mrml::prelude::render::RenderOptions { } } +#[pyclass] +#[derive(Clone, Debug, Default)] +pub struct Warning { + #[pyo3(get, set)] + pub origin: Option, + #[pyo3(get)] + pub kind: &'static str, + #[pyo3(get)] + pub start: usize, + #[pyo3(get)] + pub end: usize, +} + +impl Warning { + fn from_vec(input: Vec) -> Vec { + input.into_iter().map(Self::from).collect() + } +} + +impl From for Warning { + fn from(value: mrml::prelude::parser::Warning) -> Self { + Self { + origin: match value.origin { + mrml::prelude::parser::Origin::Root => None, + mrml::prelude::parser::Origin::Include { path } => Some(path), + }, + kind: value.kind.as_str(), + start: value.span.start, + end: value.span.end, + } + } +} + +#[pyclass] +#[derive(Clone, Debug, Default)] +pub struct Output { + #[pyo3(get)] + pub content: String, + #[pyo3(get)] + pub warnings: Vec, +} + #[pyfunction] #[pyo3(name = "to_html", signature = (input, parser_options=None, render_options=None))] fn to_html( input: String, parser_options: Option, render_options: Option, -) -> PyResult { +) -> PyResult { let parser_options = parser_options.unwrap_or_default().into(); let parsed = mrml::parse_with_options(input, &parser_options) .map_err(|err| PyOSError::new_err(err.to_string()))?; let render_options = render_options.unwrap_or_default().into(); - let output = parsed + let content = parsed + .element .render(&render_options) .map_err(|err| PyOSError::new_err(err.to_string()))?; - Ok(output) + Ok(Output { + content, + warnings: Warning::from_vec(parsed.warnings), + }) } #[pymodule] @@ -222,6 +268,8 @@ fn register(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(to_html, m)?)?; m.add_function(wrap_pyfunction!(noop_loader, m)?)?; m.add_function(wrap_pyfunction!(local_loader, m)?)?; diff --git a/packages/mrml-python/tests/test_loader.py b/packages/mrml-python/tests/test_loader.py index 82386c2e..2e748f66 100644 --- a/packages/mrml-python/tests/test_loader.py +++ b/packages/mrml-python/tests/test_loader.py @@ -13,7 +13,8 @@ def test_memory_loader(): '', parser_options=parser_options, ) - assert result.startswith("") + assert result.content.startswith("") + assert len(result.warnings) == 0 def test_local_loader_success(): @@ -24,7 +25,8 @@ def test_local_loader_success(): '', parser_options=parser_options, ) - assert result.startswith("") + assert result.content.startswith("") + assert len(result.warnings) == 0 def test_local_loader_missing(): @@ -58,7 +60,8 @@ def test_http_loader_success(): """, parser_options=parser_options, ) - assert result.startswith("") + assert result.content.startswith("") + assert len(result.warnings) == 0 def test_http_loader_failed_not_in_allow_list(): diff --git a/packages/mrml-python/tests/test_main.py b/packages/mrml-python/tests/test_main.py index af2ee064..76eae337 100644 --- a/packages/mrml-python/tests/test_main.py +++ b/packages/mrml-python/tests/test_main.py @@ -2,11 +2,20 @@ def test_simple_template(): result = mrml.to_html("") - assert result.startswith("") + assert result.content.startswith("") + assert len(result.warnings) == 0 + +def test_with_warnings(): + result = mrml.to_html("") + assert result.content.startswith("") + assert len(result.warnings) == 1 + assert result.warnings[0].kind == "unexpected-attribute" + assert result.warnings[0].start == 6 + assert result.warnings[0].end == 16 def test_template_with_options(): parser_options = mrml.ParserOptions(include_loader = mrml.memory_loader({ 'hello-world.mjml': 'Hello World!', })) result = mrml.to_html("", parser_options = parser_options) - assert result.startswith("") + assert result.content.startswith("") diff --git a/packages/mrml-wasm/examples/node/main.test.js b/packages/mrml-wasm/examples/node/main.test.js index 635fe0a8..24811003 100644 --- a/packages/mrml-wasm/examples/node/main.test.js +++ b/packages/mrml-wasm/examples/node/main.test.js @@ -2,14 +2,16 @@ const assert = require("assert"); const { Engine } = require("mrml-wasm/nodejs/mrml_wasm"); const { describe, it } = require("node:test"); -describe('mrml-wasm in node', function () { - it('should render to html', function () { +describe("mrml-wasm in node", function () { + it("should render to html", function () { const engine = new Engine(); - const result = engine.toHtml("Hello world"); - assert.equal(result.type, 'success'); + const result = engine.toHtml( + "Hello world", + ); + assert.equal(result.type, "success"); }); - it('should disable the comments', function () { + it("should disable the comments", function () { const engine = new Engine(); engine.setRenderOptions({ disableComments: true, @@ -22,11 +24,12 @@ describe('mrml-wasm in node', function () { `); - assert.equal(result.type, 'success'); + assert.equal(result.type, "success"); assert.doesNotMatch(result.content, /Goodbye/); + assert.equal(result.warnings.length, 0); }); - it('should use noop include loader by default', function () { + it("should use noop include loader by default", function () { const engine = new Engine(); engine.setRenderOptions({ disableComments: true, @@ -37,16 +40,16 @@ describe('mrml-wasm in node', function () { `); - assert.equal(result.type, 'error'); + assert.equal(result.type, "error"); }); - it('should use memory include loader', function () { + it("should use memory include loader", function () { const engine = new Engine(); engine.setParserOptions({ includeLoader: { - type: 'memory', + type: "memory", content: { - './header.mjml': 'Hello World', + "./header.mjml": "Hello World", }, }, }); @@ -59,15 +62,16 @@ describe('mrml-wasm in node', function () { `); - assert.equal(result.type, 'success'); + assert.equal(result.type, "success"); assert.match(result.content, /Hello/); + assert.equal(result.warnings.length, 0); }); - it('should use network include loader', async function () { + it("should use network include loader", async function () { const engine = new Engine(); engine.setAsyncParserOptions({ includeLoader: { - type: 'reqwest', + type: "reqwest", headers: {}, }, }); @@ -80,7 +84,8 @@ describe('mrml-wasm in node', function () { `); - assert.equal(result.type, 'success'); + assert.equal(result.type, "success"); assert.match(result.content, /Hello World/); + assert.equal(result.warnings.length, 0); }); }); diff --git a/packages/mrml-wasm/src/lib.rs b/packages/mrml-wasm/src/lib.rs index 4bbce3f6..01034837 100644 --- a/packages/mrml-wasm/src/lib.rs +++ b/packages/mrml-wasm/src/lib.rs @@ -21,10 +21,10 @@ fn to_html( input: &str, parser_options: &mrml::prelude::parser::ParserOptions, render_options: &mrml::prelude::render::RenderOptions, -) -> Result { +) -> Result<(String, Vec), ToHtmlError> { let element = mrml::parse_with_options(input, parser_options)?; - let html = element.render(render_options)?; - Ok(html) + let html = element.element.render(render_options)?; + Ok((html, Warning::from_vec(element.warnings))) } #[cfg(feature = "async")] @@ -33,10 +33,10 @@ async fn to_html_async( input: &str, parser_options: std::sync::Arc, render_options: &mrml::prelude::render::RenderOptions, -) -> Result { +) -> Result<(String, Vec), ToHtmlError> { let element = mrml::async_parse_with_options(input, parser_options).await?; - let html = element.render(render_options)?; - Ok(html) + let html = element.element.render(render_options)?; + Ok((html, Warning::from_vec(element.warnings))) } #[derive(Debug, Default)] @@ -80,7 +80,7 @@ impl Engine { #[wasm_bindgen(js_name = "toHtml")] pub fn to_html(&self, input: &str) -> ToHtmlResult { match to_html(input, &self.parser, &self.render) { - Ok(content) => ToHtmlResult::Success { content }, + Ok((content, warnings)) => ToHtmlResult::Success { content, warnings }, Err(error) => ToHtmlResult::Error(error), } } @@ -90,12 +90,85 @@ impl Engine { #[wasm_bindgen(js_name = "toHtmlAsync")] pub async fn to_html_async(&self, input: &str) -> ToHtmlResult { match to_html_async(input, self.async_parser.clone(), &self.render).await { - Ok(content) => ToHtmlResult::Success { content }, + Ok((content, warnings)) => ToHtmlResult::Success { content, warnings }, Err(error) => ToHtmlResult::Error(error), } } } +#[derive(Debug, serde::Deserialize, serde::Serialize, tsify::Tsify)] +#[serde(rename_all = "kebab-case", tag = "type")] +#[tsify(into_wasm_abi)] +pub enum Origin { + Root, + Include { path: String }, +} + +impl From for Origin { + fn from(value: mrml::prelude::parser::Origin) -> Self { + match value { + mrml::prelude::parser::Origin::Root => Self::Root, + mrml::prelude::parser::Origin::Include { path } => Self::Include { path }, + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, tsify::Tsify)] +#[tsify(into_wasm_abi)] +pub struct Span { + start: usize, + end: usize, +} + +impl From for Span { + fn from(value: mrml::prelude::parser::Span) -> Self { + Self { + start: value.start, + end: value.end, + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, tsify::Tsify)] +#[serde(rename_all = "kebab-case")] +#[tsify(into_wasm_abi)] +pub enum WarningKind { + UnexpectedAttributes, +} + +impl From for WarningKind { + fn from(value: mrml::prelude::parser::WarningKind) -> Self { + match value { + mrml::prelude::parser::WarningKind::UnexpectedAttribute => Self::UnexpectedAttributes, + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, tsify::Tsify)] +#[tsify(into_wasm_abi)] +pub struct Warning { + kind: WarningKind, + origin: Origin, + span: Span, +} + +impl Warning { + #[inline] + fn from_vec(list: Vec) -> Vec { + list.into_iter().map(Warning::from).collect() + } +} + +impl From for Warning { + fn from(value: mrml::prelude::parser::Warning) -> Self { + Self { + origin: value.origin.into(), + kind: value.kind.into(), + span: value.span.into(), + } + } +} + #[derive(Debug, serde::Deserialize, serde::Serialize, tsify::Tsify)] #[serde(rename_all = "camelCase", tag = "origin")] #[tsify(into_wasm_abi)] @@ -124,14 +197,17 @@ impl From for ToHtmlError { #[serde(rename_all = "camelCase", tag = "type")] #[tsify(into_wasm_abi)] pub enum ToHtmlResult { - Success { content: String }, + Success { + content: String, + warnings: Vec, + }, Error(ToHtmlError), } impl ToHtmlResult { pub fn into_success(self) -> String { match self { - Self::Success { content } => content, + Self::Success { content, .. } => content, Self::Error(inner) => panic!("unexpected error {:?}", inner), } } diff --git a/packages/mrml-wasm/tests/node.rs b/packages/mrml-wasm/tests/node.rs index 2b0366fb..45918b35 100644 --- a/packages/mrml-wasm/tests/node.rs +++ b/packages/mrml-wasm/tests/node.rs @@ -30,7 +30,7 @@ fn it_should_fail_when_render_template() { match result { mrml_wasm::ToHtmlResult::Error(inner) => match inner { ToHtmlError::Parser { message } => { - assert_eq!(message, "unable to load included template") + assert_eq!(message, "unable to parse next template in root template") } other => panic!("unexpected error {:?}", other), }, @@ -47,7 +47,7 @@ async fn it_should_fail_when_render_template_async() { match result { mrml_wasm::ToHtmlResult::Error(inner) => match inner { ToHtmlError::Parser { message } => { - assert_eq!(message, "unable to load included template") + assert_eq!(message, "unable to parse next template in root template") } other => panic!("unexpected error {:?}", other), }, @@ -65,8 +65,9 @@ fn it_should_disable_comments() { }); let result = engine.to_html(template); match result { - mrml_wasm::ToHtmlResult::Success { content } => { + mrml_wasm::ToHtmlResult::Success { content, warnings } => { assert_eq!(content.matches("Goodbye").count(), 0); + assert!(warnings.is_empty()) } err => panic!("shouldn't fail {:?}", err), } @@ -83,8 +84,9 @@ async fn it_should_disable_comments_async() { }); let result = engine.to_html_async(template).await; match result { - mrml_wasm::ToHtmlResult::Success { content } => { + mrml_wasm::ToHtmlResult::Success { content, warnings } => { assert_eq!(content.matches("Goodbye").count(), 0); + assert!(warnings.is_empty()); } err => panic!("shouldn't fail {:?}", err), } @@ -102,7 +104,10 @@ fn it_should_use_noop_include_loader_by_default() { match result { mrml_wasm::ToHtmlResult::Error(inner) => match inner { ToHtmlError::Parser { message } => { - assert_eq!(message, "unable to load included template") + assert_eq!( + message, + "unable to load included template in root template at position 46..56" + ) } other => panic!("unexpected error {:?}", other), }, @@ -123,7 +128,10 @@ async fn it_should_use_noop_include_loader_by_default_async() { match result { mrml_wasm::ToHtmlResult::Error(inner) => match inner { ToHtmlError::Parser { message } => { - assert_eq!(message, "unable to load included template") + assert_eq!( + message, + "unable to load included template in root template at position 46..56" + ) } other => panic!("unexpected error {:?}", other), },