From cbd481161f7342c8238e2147f45e6048ad32ff4b Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sun, 15 Feb 2026 22:02:48 -0800 Subject: [PATCH] chore: don't be stupid regarding marko tags --- examples/marko-run/src/md/example.mdo | 1 - examples/marko-run/src/tags/photo-grid.marko | 2 +- src/lib.rs | 128 +-- src/marko.rs | 963 +++++++------------ src/marko_ast.rs | 230 +++++ src/plugin/mod.rs | 2 +- src/plugin/statement.rs | 13 +- src/plugin/tags.rs | 109 +-- src/plugin/toc.rs | 6 +- src/typescript.rs | 136 ++- tests/fixtures/23-outline-extracting.marko | 3 + 11 files changed, 735 insertions(+), 858 deletions(-) create mode 100644 src/marko_ast.rs diff --git a/examples/marko-run/src/md/example.mdo b/examples/marko-run/src/md/example.mdo index d7b658e89cfa8848ab3f7fbebf4937e80b8b97c7..52c8c2af67656b0afc78d7114d784a8ac81cfa96 100644 --- a/examples/marko-run/src/md/example.mdo +++ b/examples/marko-run/src/md/example.mdo @@ -28,7 +28,6 @@ a lot less brace hell for non-string attributes, and more treats! <@img src="IMG_4838.jpeg" w=2 /> <@img src="IMG_4839.jpeg" w=2 align="top" /> <@img src="IMG_4833.jpeg" h=2 /> - <@img src="IMG_4832.jpeg" w=2 /> ## in conclusion diff --git a/examples/marko-run/src/tags/photo-grid.marko b/examples/marko-run/src/tags/photo-grid.marko index 7d92d6c4cc66896ce88ec8db7d93eef068b6136c..fdbab98b76e27e3e2770e7db4dd8c276b98049df 100644 --- a/examples/marko-run/src/tags/photo-grid.marko +++ b/examples/marko-run/src/tags/photo-grid.marko @@ -46,7 +46,7 @@ export interface Input { "grid-template-rows": input.rows.map(row => `${row}px`).join(" "), }> - +
diff --git a/src/lib.rs b/src/lib.rs index 0a8838413757c36cdf212fc1742b34630ec4bfdc..b9f2e4d4d3da77da648eccbbeec4ac0328543e2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ pub mod marko; +pub mod marko_ast; pub mod plugin; pub mod typescript; - pub mod wasm; use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; +use oxc_span::Span; use plugin::tags::{MarkoClose, MarkoOpen}; use serde::Serialize; use serde_json; @@ -94,7 +95,7 @@ pub fn transform( }; // Extract statements before rendering - they need to be hoisted above Layout - let extracted_statements = extract_statements(&mut ast); + let extracted_statements = hoist_statements(&mut ast); let mut text = ast.render(); @@ -177,9 +178,9 @@ pub fn adjust_err(mut err: OxcDiagnostic, offset: isize) -> OxcDiagnostic { err } -/// Extract all StatementBlock content from the AST and clear them. -/// Returns the collected statement content as a single string. -fn extract_statements(node: &mut markdown_it::Node) -> String { +/// Extract `StatementBlock` contents from the AST, replacing them with an empty +/// string so they render as nothing. Returns the statements hoisted. +fn hoist_statements(node: &mut markdown_it::Node) -> String { let mut statements = String::new(); fn walk(node: &mut markdown_it::Node, statements: &mut String) { @@ -198,33 +199,15 @@ fn extract_statements(node: &mut markdown_it::Node) -> String { /// Check if the AST contains any Marko-specific features fn has_marko_features(node: &markdown_it::Node) -> bool { - // Check if this node is a RawBlock or StatementBlock - if node.cast::().is_some() { - return true; - } - if node.cast::().is_some() { - return true; - } - - // Check for Marko tags - if node.cast::().is_some() + node.cast::().is_some() + || node.cast::().is_some() + || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() || node.cast::().is_some() - { - return true; - } - - // Recursively check children - for child in &node.children { - if has_marko_features(child) { - return true; - } - } - - false + || node.children.iter().any(|child| has_marko_features(child)) } /// Validate that Marko open/close tags are properly matched @@ -233,68 +216,57 @@ fn validate_marko_tags( errors: &mut Vec, preamble_offset: usize, ) { - #[derive(Debug)] - struct OpenTag { - name: String, - /// Byte offset of the tag name in the source (after '<') - name_start: usize, - /// Length of the tag name - name_len: usize, - } - - let mut stack: Vec = vec![]; + // Stack of (tag_name, absolute_span_of_name) + let mut stack: Vec<(String, Span)> = vec![]; fn walk( node: &markdown_it::Node, - stack: &mut Vec, + stack: &mut Vec<(String, Span)>, errors: &mut Vec, - offset: usize, + offset: u32, ) { if let Some(open) = node.cast::() { let (start, _) = node.srcmap.unwrap().get_byte_offsets(); - stack.push(OpenTag { - name: open.tag_name.clone(), - name_start: start + offset + 1, // +1 to skip '<' - name_len: open.tag_name_len, - }); + let rel_span = open.open.as_ref().tag_name_span(); + let abs_span = Span::new( + start as u32 + offset + rel_span.start, + start as u32 + offset + rel_span.end, + ); + stack.push((open.open.as_ref().tag_name().to_owned(), abs_span)); } else if let Some(open) = node.cast::() { let (start, _) = node.srcmap.unwrap().get_byte_offsets(); - stack.push(OpenTag { - name: open.tag_name.clone(), - name_start: start + offset + 1, // +1 to skip '<' - name_len: open.tag_name_len, - }); + let rel_span = open.open.as_ref().tag_name_span(); + let abs_span = Span::new( + start as u32 + offset + rel_span.start, + start as u32 + offset + rel_span.end, + ); + stack.push((open.open.as_ref().tag_name().to_owned(), abs_span)); } else if let Some(close) = node.cast::() { let (close_start, _) = node.srcmap.unwrap().get_byte_offsets(); + let close_start = close_start as u32 + offset; // Close tag name starts after ' { + // valid + } (None, Some(_)) => { // closes anything - valid } - (Some(name), Some(top)) if name == &top.name => { - // Exact match - valid - } - (Some(name), Some(top)) => { - // Mismatched: expected , got + (Some(name), Some((top_name, top_span))) => { + // Mismatched: expected , got errors.push( OxcDiagnostic::error(format!( "Mismatched closing tag: expected , found ", - top.name, name + top_name, name )) .with_labels(vec![ - LabeledSpan::new( - Some("opened here".into()), - top.name_start, - top.name_len, - ), - LabeledSpan::new( - Some("closed here".into()), - close_name_start, - close_name_len, - ), + top_span.label("opened here"), + close_span.unwrap().label("closed here"), ]), ); } @@ -304,18 +276,14 @@ fn validate_marko_tags( OxcDiagnostic::error(format!( "Closing tag without matching open" )) - .with_label(LabeledSpan::new( - None, - close_name_start, - close_name_len, - )), + .with_label(close_span.unwrap()), ); } (None, None) => { // without any open tag - highlight the whole errors.push( OxcDiagnostic::error("Closing tag without matching open") - .with_label(LabeledSpan::new(None, close_start + offset, 3)), + .with_label(Span::new(close_start, close_start + 3)), ); } } @@ -327,18 +295,14 @@ fn validate_marko_tags( } } - walk(node, &mut stack, errors, preamble_offset); + walk(node, &mut stack, errors, preamble_offset as u32); // Check for unclosed tags - for unclosed in stack { - errors.push( - OxcDiagnostic::error(format!("Unclosed tag <{}>", unclosed.name)).with_label( - LabeledSpan::new(None, unclosed.name_start, unclosed.name_len), - ), - ); + for (name, span) in stack { + errors.push(OxcDiagnostic::error(format!("Unclosed tag <{}>", name)).with_label(span)); } } -pub fn err>>(str: T, offset: usize, length: usize) -> OxcDiagnostic { - OxcDiagnostic::error(str).and_label(LabeledSpan::new(None, offset, length)) +pub fn err>>(str: T, offset: u32, length: usize) -> OxcDiagnostic { + OxcDiagnostic::error(str).and_label(LabeledSpan::new(None, offset as usize, length)) } diff --git a/src/marko.rs b/src/marko.rs index 02e0dc3bc92ab37bd2cee78f657faf938898e7e7..c6cbfce2a2df80a84b0910d1d15c43295b6e2d16 100644 --- a/src/marko.rs +++ b/src/marko.rs @@ -1,74 +1,16 @@ -use std::{borrow::Cow, iter::Map}; - use oxc_diagnostics::OxcDiagnostic; use oxc_span::Span; +use serde_yml::libyml::tag; use crate::{ err, + marko_ast::*, typescript::{ parse_call_arguments, parse_expr, parse_expr_without_gt, parse_fn_params_and_body, parse_var_binding, }, }; -#[derive(Debug, Clone, PartialEq)] -pub struct Open<'a> { - pub tag_name: &'a str, - pub content: &'a str, - pub self_closing: bool, -} - -impl<'a> Open<'a> { - /// Returns the span of the tag name relative to the start of the tag. - pub fn tag_name_span(&self) -> Span { - // fast case, when a literal tag name is passed - if !self.tag_name.is_empty() { - Span::new(1, 1 + (self.tag_name.len() as u32)) - } else { - // slow path, re-parse the js expression. not that deep because you - // only do this in the error case. TODO: actually store `Open` in - // the md tree, the lifetimes are complicated rn - let mut l = LexState::new(self.content); - l.advance(3); - parse_expr(&mut l).expect("validated text should pass"); - Span::new(3, l.offset() as u32) - } - } - - pub fn id_attr(&self) -> Option<&'a str> { - todo!(); - } - - pub fn set_id_attr(&mut self, str: &'a str) { - todo!(); - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Close<'a> { - pub tag_name: Option<&'a str>, - pub length: usize, -} - -impl Close<'_> { - /// Returns the span of the tag name relative to the start of the tag. - /// For `
`, returns Span(2, 5) pointing to "div". - /// For ``, returns None. - pub fn tag_name_span(&self) -> Option { - self.tag_name.map(|name| { - // Close tag starts with ' { - Open(Open<'a>), - Close(Close<'a>), -} - pub fn parse_tag(src: &str) -> Result { assert_eq!(src.as_bytes()[0], b'<'); if src.starts_with(" Result { } } -pub struct LexState<'a> { - pub src: &'a str, - pub offset: usize, -} - -impl<'a> LexState<'a> { - pub fn new(src: &'a str) -> LexState<'a> { - LexState { src, offset: 0 } - } - - pub fn offset(&self) -> usize { - self.offset - } - - pub fn advance(&mut self, n: usize) { - self.offset += n; - } - - pub fn restore(&mut self, n: usize) { - self.offset = n; - } - - pub fn peek_byte(&self) -> Option { - self.src.as_bytes().get(self.offset).copied() - } - - pub fn peek_rest(&self) -> &'a str { - &self.src[self.offset..] - } - - pub fn expect_byte(&mut self) -> Result { - if let Some(b) = self.peek_byte() { - self.offset += 1; - return Ok(b); - } - Err(err("Unexpected end of file", self.offset, 1)) - } - - pub fn expect(&mut self, expected: &str) -> Result<(), OxcDiagnostic> { - if self.src[self.offset..].starts_with(expected) { - self.offset += expected.len(); - Ok(()) - } else { - Err(err(format!("Expected {expected}"), self.offset, 1)) - } - } - - pub fn skip_whitespace(&mut self) { - loop { - match self.peek_byte() { - Some(byte) => match byte { - b' ' | b'\n' | b'\t' | b'\r' => self.offset += 1, - _ => break, - }, - None => break, - } - } - } -} - pub fn parse_open(src: &str) -> Result { assert!(src.starts_with("<")); let mut l = LexState { src, offset: 1 }; @@ -145,21 +27,33 @@ pub fn parse_open(src: &str) -> Result { let is_attribute_tag = tag_name.starts_with('@'); // class and id shorthand - let mut has_id = false; + let mut id_attr = AttributeValue::None; while let Some(byte @ b'#' | byte @ b'.') = l.peek_byte() { - let offset = l.offset; + let id_start = l.offset; l.advance(1); let name = parse_class_name_shorthand(&mut l, byte == b'#')?; - _ = name; + let id_end = l.offset; if byte == b'#' { - if has_id { - return Err(err("Cannot specify two ID shorthands", offset, name.len())); + if !matches!(id_attr, AttributeValue::None) { + return Err(err("Cannot specify two IDs", id_start, name.len())); + } + + // Check if the shorthand contains ${} (dynamic) + if name.contains("${") { + id_attr = AttributeValue::Dynamic; + } else { + id_attr = AttributeValue::Static { + span: Span::new(id_start + 1, id_end), + is_quoted: false, + }; } - has_id = true; } } + // Track where shorthands end (for inserting new #id if needed) + let shorthand_end = l.offset; + // in any order: parameters, arguments, variable let mut has_js_arguments = false; let mut has_js_params = false; @@ -239,14 +133,14 @@ pub fn parse_open(src: &str) -> Result { return Err(err( "Attribute tags do not support variables", offset, - l.offset - offset, + (l.offset - offset) as usize, )); } if is_attribute_tag && byte == b'(' { return Err(err( "Attribute tags do not support arguments", offset, - l.offset - offset, + (l.offset - offset) as usize, )); } } @@ -274,8 +168,36 @@ pub fn parse_open(src: &str) -> Result { self_closing = true; break; } - parse_attribute(&mut l)?; - l.skip_whitespace(); + // Check for id= attribute (only if we don't already have an id) + if l.peek_rest().starts_with("id=") { + if !matches!(id_attr, AttributeValue::None) { + return Err(err( + "Cannot combine the 'id' attribute with ID shorthand", + l.offset(), + 3, + )); + } + + l.advance(3); // skip "id=" + l.skip_whitespace(); + let value_start = l.offset; + let first_byte = l.peek_byte(); + parse_expr_without_gt(&mut l)?; + let value_end = l.offset; + l.skip_whitespace(); + + if first_byte == Some(b'"') || first_byte == Some(b'\'') { + id_attr = AttributeValue::Static { + span: Span::new(value_start, value_end), + is_quoted: true, + }; + } else { + id_attr = AttributeValue::Dynamic; + } + } else { + parse_attribute(&mut l)?; + l.skip_whitespace(); + } } } } @@ -287,11 +209,13 @@ pub fn parse_open(src: &str) -> Result { } } - Ok(Open { - tag_name, - content: &src[0..l.offset], + Ok(Open::new( + &src[0..l.offset as usize], + Span::new(1, (tag_name.len() + 1) as u32), self_closing, - }) + shorthand_end, + id_attr, + )) } pub fn parse_close(src: &str) -> Result { @@ -329,7 +253,7 @@ fn parse_tag_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> { _ => break, } } - let tag_name = &l.src[start..l.offset]; + let tag_name = &l.src[start as usize..l.offset as usize]; if !tag_name.is_empty() { Ok(tag_name) } else { @@ -345,7 +269,7 @@ fn parse_attr_name<'a>(l: &mut LexState<'a>) -> Result<&'a str, OxcDiagnostic> { _ => break, } } - let tag_name = &l.src[start..l.offset]; + let tag_name = &l.src[start as usize..l.offset as usize]; if !tag_name.is_empty() { Ok(tag_name) } else { @@ -408,7 +332,7 @@ fn parse_class_name_shorthand<'a>( _ => l.offset += 1, } } - let tag_name = &l.src[start..l.offset]; + let tag_name = &l.src[start as usize..l.offset as usize]; if !tag_name.is_empty() { Ok(tag_name) } else { @@ -428,133 +352,73 @@ fn parse_class_name_shorthand<'a>( mod tests { use super::*; + /// Helper to check basic Open properties without worrying about id_info/shorthand_end + fn check_open(src: &str, tag_name: &str, content: &str, self_closing: bool) { + let open = parse_open(src).unwrap(); + assert_eq!(open.tag_name(), tag_name, "tag_name mismatch for {src}"); + assert_eq!(open.src, content, "content mismatch for {src}"); + assert_eq!( + open.self_closing, self_closing, + "self_closing mismatch for {src}" + ); + } + #[test] fn test_open_basic() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) + check_open("
", "div", "
", false); + check_open( + " etfdas", + "footer", + "", + true, ); - assert_eq!( - parse_open(" etfdas"), - Ok(Open { - tag_name: "footer", - content: "", - self_closing: true, - }) - ); - assert_eq!( - parse_open(" 4 /> etfdas"), - Ok(Open { - tag_name: "movie", - content: "", - self_closing: false, - }) + check_open( + " 4 /> etfdas", + "movie", + "", + false, ); } #[test] fn test_open_id() { - assert_eq!( - parse_open("A Technical Review"), - Ok(Open { - tag_name: "h2", - content: "", - self_closing: false, - }) + check_open( + "A Technical Review", + "h2", + "", + false, ); - assert_eq!( - parse_open(" etfdas"), - Ok(Open { - tag_name: "footer", - content: "", - self_closing: true, - }) + check_open( + " etfdas", + "footer", + "", + true, ); - assert_eq!( - parse_open(" 4 /> etfdas"), - Ok(Open { - tag_name: "movie", - content: "", - self_closing: false, - }) + check_open( + " 4 /> etfdas", + "movie", + "", + false, ); } #[test] fn test_open_self_closing_with_space() { - // Self-closing with space before /> works - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "input", - content: "", - self_closing: true, - }) - ); + check_open("", "input", "", true); } #[test] fn test_open_self_closing_no_space() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "br", - content: "
", - self_closing: true, - }) - ); - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-component", - content: "", - self_closing: true, - }) - ); + check_open("
", "br", "
", true); + check_open("", "my-component", "", true); } #[test] fn test_open_tag_names() { - // kebab-case - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-custom-tag", - content: "", - self_closing: false, - }) - ); - // PascalCase (for custom tags referencing imports) - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "MyComponent", - content: "", - self_closing: false, - }) - ); - // with underscore - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my_tag", - content: "", - self_closing: false, - }) - ); - // with $ (valid in Marko) - assert_eq!( - parse_open("<$tag>"), - Ok(Open { - tag_name: "$tag", - content: "<$tag>", - self_closing: false, - }) - ); + check_open("", "my-custom-tag", "", false); + check_open("", "MyComponent", "", false); + check_open("", "my_tag", "", false); + check_open("<$tag>", "$tag", "<$tag>", false); } #[test] @@ -602,53 +466,32 @@ mod tests { #[test] fn test_attribute_tag_basic_open() { - // @attr tag without self-closing works - assert_eq!( - parse_open("<@header>"), - Ok(Open { - tag_name: "@header", - content: "<@header>", - self_closing: false, - }) - ); + check_open("<@header>", "@header", "<@header>", false); } #[test] fn test_attribute_tag_self_closing() { - assert_eq!( - parse_open("<@item/>"), - Ok(Open { - tag_name: "@item", - content: "<@item/>", - self_closing: true, - }) - ); + check_open("<@item/>", "@item", "<@item/>", true); } #[test] fn test_attribute_tag_with_string_attr() { - assert_eq!( - parse_open("<@option value=\"foo\">"), - Ok(Open { - tag_name: "@option", - content: "<@option value=\"foo\">", - self_closing: false, - }) + check_open( + "<@option value=\"foo\">", + "@option", + "<@option value=\"foo\">", + false, ); } #[test] fn test_attribute_tag_no_variable() { - // Attribute tags do not support tag variables - correctly errors - let result = parse_open("<@header/myVar>"); - assert!(result.is_err()); + assert!(parse_open("<@header/myVar>").is_err()); } #[test] fn test_attribute_tag_no_arguments() { - // Attribute tags do not support tag arguments - correctly errors - let result = parse_open("<@header(arg)>"); - assert!(result.is_err()); + assert!(parse_open("<@header(arg)>").is_err()); } // =========================================== @@ -657,97 +500,47 @@ mod tests { #[test] fn test_shorthand_id() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "div", - content: "", - self_closing: false, - }) - ); + check_open("", "div", "", false); } #[test] fn test_shorthand_class() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "div", - content: "", - self_closing: false, - }) - ); + check_open("", "div", "", false); } #[test] fn test_shorthand_multiple_classes() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "div", - content: "", - self_closing: false, - }) - ); + check_open("", "div", "", false); } #[test] fn test_shorthand_id_and_classes() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "div", - content: "", - self_closing: false, - }) - ); + check_open("", "div", "", false); } #[test] fn test_shorthand_classes_then_id() { - // Order shouldn't matter - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "div", - content: "", - self_closing: false, - }) - ); + check_open("", "div", "", false); } #[test] fn test_shorthand_duplicate_id_error() { - // Cannot have two ID shorthands - correctly errors - let result = parse_open(""); - assert!(result.is_err()); + assert!(parse_open("").is_err()); } #[test] fn test_shorthand_with_hyphen() { - // Classes/IDs can contain hyphens - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "div", - content: "", - self_closing: false, - }) - ); + check_open("", "div", "", false); } #[test] fn test_shorthand_empty_class_error() { - // Empty class correctly errors - let result = parse_open(""); - assert!(result.is_err()); + assert!(parse_open("").is_err()); } #[test] fn test_shorthand_empty_id_error() { - // Empty id correctly errors - let result = parse_open(""); - assert!(result.is_err()); + assert!(parse_open("").is_err()); } // =========================================== @@ -756,475 +549,309 @@ mod tests { #[test] fn test_tag_variable_simple() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } - // BUG: Variable binding followed by /> fails #[test] fn test_tag_variable_self_closing() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "input", - content: "", - self_closing: true, - }) - ); + check_open("", "input", "", true); } #[test] fn test_tag_variable_destructure_object() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } #[test] fn test_tag_variable_destructure_array() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } #[test] fn test_tag_variable_with_type() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "const", - content: "", - self_closing: false, - }) + check_open( + "", + "const", + "", + false, ); } #[test] fn test_tag_variable_with_attrs() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) + check_open( + "", + "my-tag", + "", + false, ); } // =========================================== // Tag arguments (args) - // BUG: Tag arguments without whitespace before ( fail - // The parser only enters argument parsing after skip_whitespace, - // but has no whitespace before ( // =========================================== #[test] fn test_tag_arguments_empty() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } #[test] fn test_tag_arguments_simple() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } #[test] fn test_tag_arguments_expressions() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) + check_open( + "", + "my-tag", + "", + false, ); } #[test] fn test_tag_arguments_spread() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } #[test] fn test_tag_arguments_object() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) + check_open( + "", + "my-tag", + "", + false, ); } - // Test that tag arguments WITH whitespace work #[test] fn test_tag_arguments_with_space() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "my-tag", - content: "", - self_closing: false, - }) - ); + check_open("", "my-tag", "", false); } // =========================================== // Attributes // =========================================== - // BUG: String literals fail - expression parser doesn't terminate at > after string #[test] fn test_attr_simple_string() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) - ); + check_open("
", "div", "
", false); } #[test] fn test_attr_expression() { - // Identifier expressions work - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) - ); + check_open("
", "div", "
", false); } - // BUG: Template literals fail #[test] fn test_attr_template_literal() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) + check_open( + "
", + "div", + "
", + false, ); } #[test] fn test_attr_spread() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) - ); + check_open("
", "div", "
", false); } - // BUG: Object literals with > in them fail #[test] fn test_attr_spread_object() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) + check_open( + "
", + "div", + "
", + false, ); } - // BUG: Multiple attributes with string values fail #[test] fn test_attr_multiple_spreads() { - assert_eq!( - parse_open("
"), - Ok(Open { - tag_name: "div", - content: "
", - self_closing: false, - }) + check_open( + "
", + "div", + "
", + false, ); } #[test] fn test_attr_two_way_binding() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "counter", - content: "", - self_closing: false, - }) + check_open( + "", + "counter", + "", + false, ); } #[test] fn test_attr_two_way_binding_property() { - assert_eq!( - parse_open(""), - Ok(Open { - tag_name: "counter", - content: "", - self_closing: false, - }) + check_open( + "", + "counter", + "", + false, ); } #[test] fn test_attr_method_shorthand() { - assert_eq!( - parse_open("