diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04a94f8..f53e824 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: ci -on: [push, pull_request] +on: + pull_request: + branches: + - main + push: + branches: + - main jobs: ci: @@ -20,19 +26,19 @@ jobs: - name: build run: cargo build - - name: test - run: cargo test -p rstml - - name: clippy run: cargo clippy --workspace + - name: test on Stable + run: cargo test --workspace + + - name: Tests with rawtext hack + run: cargo test -p rstml --features "rawtext-stable-hack-module" + + - name: Test extendable feature in rstml-control-flow + run: cargo test -p rstml-control-flow --features "extendable" + - uses: dtolnay/rust-toolchain@nightly - name: test on Nightly - run: cargo test --workspace - - - name: coverage - run: | - cargo install cargo-tarpaulin - cargo tarpaulin --out xml - bash <(curl -s https://codecov.io/bash) + run: cargo test --workspace diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..307cdd0 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,33 @@ +name: coverage + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + test: + name: coverage + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@nightly + + - name: coverage nightly + run: | + cargo install cargo-tarpaulin + cargo tarpaulin --out xml --output-dir ./nightly --workspace + - uses: dtolnay/rust-toolchain@stable + + - name: coverage nightly + run: | + cargo tarpaulin --out xml --output-dir ./stable --workspace + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./stable/cobertura.xml,./nightly/cobertura.xml + name: RSTML \ No newline at end of file diff --git a/.tarpaulin.toml b/.tarpaulin.toml new file mode 100644 index 0000000..08486a4 --- /dev/null +++ b/.tarpaulin.toml @@ -0,0 +1,8 @@ +[unquoted_text_on_stable] +features = "rstml/rawtext-stable-hack" + +[custom_node_extendable] +features = "rstml-control-flow/extendable" + +[report] +out = ["Xml", "Html"] diff --git a/Cargo.toml b/Cargo.toml index bf100e1..0dd25c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "rstml" description = "Rust templating for XML-based formats (HTML, SVG, MathML) implemented on top of proc-macro::TokenStreams" version = "0.11.2" -authors = ["vldm ","stoically "] +authors = ["vldm ", "stoically "] keywords = ["syn", "jsx", "rsx", "html", "macro"] edition = "2018" repository = "https://github.com/rs-tml/rstml" @@ -15,17 +15,22 @@ include = ["/src", "LICENSE"] bench = false [dependencies] -proc-macro2 = "1.0.47" +proc-macro2 = { version = "1.0.47", features = ["span-locations"] } quote = "1.0.21" -syn = { version = "2.0.15", features = ["full", "parsing", "extra-traits"] } +syn = { version = "2.0.15", features = [ + "visit-mut", + "full", + "parsing", + "extra-traits", +] } thiserror = "1.0.37" syn_derive = "0.1.6" proc-macro2-diagnostics = { version = "0.10", default-features = false } derive-where = "1.2.5" [dev-dependencies] -proc-macro2 = {version = "1.0.47", features = ["span-locations"]} -criterion = "0.4.0" +proc-macro2 = { version = "1.0.47", features = ["span-locations"] } +criterion = "0.5.1" eyre = "0.6.8" [[bench]] @@ -34,10 +39,14 @@ harness = false path = "benches/bench.rs" [workspace] -members = [ - "examples/html-to-string-macro" -] +members = ["examples/html-to-string-macro", "rstml-control-flow"] [features] default = ["colors"] +# Hack that parse input two times, using `proc-macro2::fallback` to recover spaces, and persist original spans. +# It has no penalty in nightly, but in stable it parses input two times. +# In order to use this feature, one should also set `ParserConfig::macro_call_pattern`. +rawtext-stable-hack = ["rawtext-stable-hack-module"] +# Export inters of rawtext_stable_hack. It is usefull if you need support of `UnquotedText` on stable but your macro is called from other one. +rawtext-stable-hack-module = [] colors = ["proc-macro2-diagnostics/colors"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..386474d --- /dev/null +++ b/build.rs @@ -0,0 +1,17 @@ +use std::{env, process::Command, str::from_utf8}; + +fn main() { + if is_rustc_nightly() { + println!("cargo:rustc-cfg=rstml_signal_nightly"); + } +} + +fn is_rustc_nightly() -> bool { + || -> Option { + let rustc = env::var_os("RUSTC")?; + let output = Command::new(rustc).arg("--version").output().ok()?; + let version = from_utf8(&output.stdout).ok()?; + Some(version.contains("nightly")) + }() + .unwrap_or_default() +} diff --git a/examples/html-to-string-macro/Cargo.toml b/examples/html-to-string-macro/Cargo.toml index d633790..7904171 100644 --- a/examples/html-to-string-macro/Cargo.toml +++ b/examples/html-to-string-macro/Cargo.toml @@ -2,7 +2,7 @@ name = "rstml-to-string-macro" description = "simple html to string macro powered by rstml" version = "0.1.0" -authors = ["vldm ","stoically "] +authors = ["vldm ", "stoically "] keywords = ["html-to-string", "html", "macro"] edition = "2021" repository = "https://github.com/rs-tml/rstml/tree/main/examples/html-to-string-macro" @@ -16,8 +16,13 @@ proc-macro = true proc-macro2 = "1.0.47" quote = "1.0.21" syn = "2.0.15" -rstml = { version = "0.11.0", path = "../../" } +syn_derive = "0.1" +rstml = { version = "0.11.0", path = "../../", features = [ + "rawtext-stable-hack", +] } proc-macro2-diagnostics = "0.10" +derive-where = "1.2.5" +rstml-control-flow = { version = "0.1.0", path = "../../rstml-control-flow" } [dev-dependencies] trybuild = "1.0" diff --git a/examples/html-to-string-macro/src/lib.rs b/examples/html-to-string-macro/src/lib.rs index 55f9600..c805cdf 100644 --- a/examples/html-to-string-macro/src/lib.rs +++ b/examples/html-to-string-macro/src/lib.rs @@ -1,16 +1,16 @@ use std::collections::HashSet; use proc_macro::TokenStream; -use proc_macro2::{Literal, TokenTree}; use quote::{quote, quote_spanned, ToTokens}; use rstml::{ node::{Node, NodeAttribute, NodeName}, + visitor::{visit_attributes, visit_nodes, Visitor}, Parser, ParserConfig, }; use syn::spanned::Spanned; - +// mod escape; #[derive(Default)] -struct WalkNodesOutput<'a> { +struct WalkNodesOutput { static_format: String, // Use proc_macro2::TokenStream instead of syn::Expr // to provide more errors to the end user. @@ -21,101 +21,144 @@ struct WalkNodesOutput<'a> { // No differences between open tag and closed tag. // Also multiple tags with same name can be present, // because we need to mark each of them. - collected_elements: Vec<&'a NodeName>, + collected_elements: Vec, +} +struct WalkNodes<'a> { + empty_elements: &'a HashSet<&'a str>, + output: WalkNodesOutput, +} +impl<'a> WalkNodes<'a> { + fn child_output(&self) -> Self { + Self { + empty_elements: self.empty_elements, + output: WalkNodesOutput::default(), + } + } } -impl<'a> WalkNodesOutput<'a> { - fn extend(&mut self, other: WalkNodesOutput<'a>) { + +impl WalkNodesOutput { + fn extend(&mut self, other: WalkNodesOutput) { self.static_format.push_str(&other.static_format); self.values.extend(other.values); self.diagnostics.extend(other.diagnostics); self.collected_elements.extend(other.collected_elements); } } +impl<'a> syn::visit_mut::VisitMut for WalkNodes<'a> {} -fn walk_nodes<'a>(empty_elements: &HashSet<&str>, nodes: &'a Vec) -> WalkNodesOutput<'a> { - let mut out = WalkNodesOutput::default(); +impl<'a, C> Visitor for WalkNodes<'a> +where + C: rstml::node::CustomNode + 'static, +{ + fn visit_doctype(&mut self, doctype: &mut rstml::node::NodeDoctype) -> bool { + let value = &doctype.value.to_token_stream_string(); + self.output + .static_format + .push_str(&format!("", value)); + false + } + fn visit_text_node(&mut self, node: &mut rstml::node::NodeText) -> bool { + self.output.static_format.push_str(&node.value_string()); + false + } + fn visit_raw_node( + &mut self, + node: &mut rstml::node::RawText, + ) -> bool { + self.output.static_format.push_str(&node.to_string_best()); + false + } + fn visit_fragment(&mut self, fragment: &mut rstml::node::NodeFragment) -> bool { + let visitor = self.child_output(); + let child_output = visit_nodes(&mut fragment.children, visitor); + self.output.extend(child_output.output); + false + } - for node in nodes { - match node { - Node::Doctype(doctype) => { - let value = &doctype.value.to_token_stream_string(); - out.static_format.push_str(&format!("", value)); - } - Node::Element(element) => { - let name = element.name().to_string(); - out.static_format.push_str(&format!("<{}", name)); - out.collected_elements.push(&element.open_tag.name); - if let Some(e) = &element.close_tag { - out.collected_elements.push(&e.name) - } + fn visit_comment(&mut self, comment: &mut rstml::node::NodeComment) -> bool { + self.output + .static_format + .push_str(&format!("", comment.value.value())); + false + } + fn visit_block(&mut self, block: &mut rstml::node::NodeBlock) -> bool { + self.output.static_format.push_str("{}"); + self.output.values.push(block.to_token_stream()); + false + } + fn visit_element(&mut self, element: &mut rstml::node::NodeElement) -> bool { + let name = element.name().to_string(); + self.output.static_format.push_str(&format!("<{}", name)); + self.output + .collected_elements + .push(element.open_tag.name.clone()); + if let Some(e) = &element.close_tag { + self.output.collected_elements.push(e.name.clone()) + } - // attributes - for attribute in element.attributes() { - match attribute { - NodeAttribute::Block(block) => { - // If the nodes parent is an attribute we prefix with whitespace - out.static_format.push(' '); - out.static_format.push_str("{}"); - out.values.push(block.to_token_stream()); - } - NodeAttribute::Attribute(attribute) => { - out.static_format.push_str(&format!(" {}", attribute.key)); - if let Some(value) = attribute.value() { - out.static_format.push_str(r#"="{}""#); - out.values.push(value.to_token_stream()); - } - } - } - } - // Ignore childs of special Empty elements - if empty_elements.contains(element.open_tag.name.to_string().as_str()) { - out.static_format - .push_str(&format!("/", element.open_tag.name)); - if !element.children.is_empty() { - let warning = proc_macro2_diagnostics::Diagnostic::spanned( - element.open_tag.name.span(), - proc_macro2_diagnostics::Level::Warning, - "Element is processed as empty, and cannot have any child", - ); - out.diagnostics.push(warning.emit_as_expr_tokens()) - } - - continue; - } - out.static_format.push('>'); + let visitor = self.child_output(); + let attribute_visitor = visit_attributes(element.attributes_mut(), visitor); + self.output.extend(attribute_visitor.output); - // children - let other_output = walk_nodes(empty_elements, &element.children); - out.extend(other_output); - out.static_format.push_str(&format!("", name)); - } - Node::Text(text) => { - out.static_format.push_str(&text.value_string()); - } - Node::RawText(text) => { - out.static_format.push_str("{}"); - let tokens = text.to_string_best(); - let literal = Literal::string(&tokens); + self.output.static_format.push('>'); - out.values.push(TokenTree::from(literal).into()); - } - Node::Fragment(fragment) => { - let other_output = walk_nodes(empty_elements, &fragment.children); - out.extend(other_output) + // Ignore childs of special Empty elements + if self + .empty_elements + .contains(element.open_tag.name.to_string().as_str()) + { + self.output + .static_format + .push_str(&format!("/", element.open_tag.name)); + if !element.children.is_empty() { + let warning = proc_macro2_diagnostics::Diagnostic::spanned( + element.open_tag.name.span(), + proc_macro2_diagnostics::Level::Warning, + "Element is processed as empty, and cannot have any child", + ); + self.output.diagnostics.push(warning.emit_as_expr_tokens()) } - Node::Comment(comment) => { - out.static_format.push_str(""); - out.values.push(comment.value.to_token_stream()); + + return false; + } + // children + + let visitor = self.child_output(); + let child_output = visit_nodes(&mut element.children, visitor); + self.output.extend(child_output.output); + self.output.static_format.push_str(&format!("", name)); + false + } + fn visit_attribute(&mut self, attribute: &mut NodeAttribute) -> bool { + // attributes + match attribute { + NodeAttribute::Block(block) => { + // If the nodes parent is an attribute we prefix with whitespace + self.output.static_format.push(' '); + self.output.static_format.push_str("{}"); + self.output.values.push(block.to_token_stream()); } - Node::Block(block) => { - out.static_format.push_str("{}"); - out.values.push(block.to_token_stream()); + NodeAttribute::Attribute(attribute) => { + self.output + .static_format + .push_str(&format!(" {}", attribute.key)); + if let Some(value) = attribute.value() { + self.output.static_format.push_str(r#"="{}""#); + self.output.values.push(value.to_token_stream()); + } } - Node::Custom(custom) => match *custom {}, } + false } - - out +} +fn walk_nodes<'a>(empty_elements: &'a HashSet<&'a str>, nodes: &'a mut [Node]) -> WalkNodesOutput { + let visitor = WalkNodes { + empty_elements, + output: WalkNodesOutput::default(), + }; + let mut nodes = nodes.to_vec(); + let output = visit_nodes(&mut nodes, visitor); + output.output } /// Converts HTML to `String`. @@ -164,19 +207,20 @@ fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream { let config = ParserConfig::new() .recover_block(true) .always_self_closed_elements(empty_elements.clone()) - .raw_text_elements(["script", "style"].into_iter().collect()); + .raw_text_elements(["script", "style"].into_iter().collect()) + .macro_call_pattern(quote!(html! {%%})); let parser = Parser::new(config); - let (nodes, errors) = parser.parse_recoverable(tokens).split_vec(); + let (mut nodes, errors) = parser.parse_recoverable(tokens).split_vec(); let WalkNodesOutput { static_format: html_string, values, collected_elements: elements, diagnostics, - } = walk_nodes(&empty_elements, &nodes); + } = walk_nodes(&empty_elements, &mut nodes); let docs = if ide_helper { - generate_tags_docs(elements) + generate_tags_docs(&elements) } else { vec![] }; @@ -196,7 +240,7 @@ fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream { .into() } -fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec { +fn generate_tags_docs(elements: &[NodeName]) -> Vec { // Mark some of elements as type, // and other as elements as fn in crate::docs, // to give an example how to link tag with docs. diff --git a/examples/html-to-string-macro/tests/compiletests.rs b/examples/html-to-string-macro/tests/compiletests.rs index 870c2f9..0b99dac 100644 --- a/examples/html-to-string-macro/tests/compiletests.rs +++ b/examples/html-to-string-macro/tests/compiletests.rs @@ -1,5 +1,7 @@ #[test] fn ui() { let t = trybuild::TestCases::new(); - t.compile_fail("tests/ui/*.rs"); + if cfg!(rstml_signal_nightly) { + t.compile_fail("tests/ui/*.rs") + }; } diff --git a/examples/html-to-string-macro/tests/tests.rs b/examples/html-to-string-macro/tests/tests.rs index 0ecd413..5274c0a 100644 --- a/examples/html-to-string-macro/tests/tests.rs +++ b/examples/html-to-string-macro/tests/tests.rs @@ -1,3 +1,4 @@ +use rstml::node::RawText; use rstml_to_string_macro::html_ide; // Using this parser, one can write docs and link html tags to them. @@ -9,20 +10,18 @@ pub mod docs { } #[test] fn test() { - let nightly_unqoted = " Hello world with spaces "; - let stable_unqoted = "Hello world with spaces"; - let unquoted_text = if cfg!(rstml_signal_nightly) { - nightly_unqoted - } else { - stable_unqoted - }; + let unquoted_text = " Hello world with spaces "; + assert_eq!( + cfg!(rstml_signal_nightly), + RawText::is_source_text_available() + ); let world = "planet"; assert_eq!( html_ide! { - "Example" + "Example" diff --git a/rstml-control-flow/Cargo.toml b/rstml-control-flow/Cargo.toml new file mode 100644 index 0000000..f1d7bad --- /dev/null +++ b/rstml-control-flow/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rstml-control-flow" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = "1.0.47" +quote = "1.0.21" +syn = "2.0.15" +syn_derive = "0.1" +rstml = { version = "0.11.0", path = "../", features = ["rawtext-stable-hack"] } +proc-macro2-diagnostics = "0.10" +derive-where = "1.2.5" + +[features] +default = [] +# If feature activated Node should be parsed through `ExtendableCustomNode::parse2_with_config` +extendable = [] diff --git a/rstml-control-flow/README.md b/rstml-control-flow/README.md new file mode 100644 index 0000000..0458a3b --- /dev/null +++ b/rstml-control-flow/README.md @@ -0,0 +1,89 @@ +# Control flow implementation for `rstml` + +The collection of `rstml` `CustomNode`s for the control flow. + +## Motivation + +This crate aims to provide an example of how to extend `rstml` with custom nodes. +Using custom nodes instead of using inner macro calls decreases the complexity of written templates, and allows `rstml` to parse the whole template at once. + + +## Custom nodes +Custom nodes in `rstml` are allowing external code to extend the `Node` enum. This is useful for supporting +custom syntax that is not common for HTML/XML documents. It is only allowed to be used in the context of `Node`, not in element attributes or node names. + +# Control flow implementation + +The common use case for custom nodes is implementing if/else operators and loops. This crate provides two different ways of implementing if/else control flow. + +## Control flow using tags + +The first way is to use custom tags. This is the most native way of implementing control flow in HTML templates since control flow looks like a regular HTML element. The downside of this approach is that it is not possible to properly parse `Rust` expressions inside HTML element attributes. +For example, for ` bar> ` it is hard to determine where the tag with attributes ends and where the content starts. + +In this crate, we force the user to use a special delimiter at the end of the tag. +so instead of ` bar> ` we have to write ` bar !> `, where `!>` is a delimiter. This special syntax is used inside `` and `` tags as well. + +Example: +```rust +use rstml::{parse2_with_config, node::*}; +use rstml_controll_flow::tags::*; +use quote::quote; + + +let template = quote!{ + +

foo is true

+ +

bar is true

+ + +

foo and bar are false

+ +
+} + +let nodes = parse2_with_config(template, Default::default().with_custom_nodes::()) +.unwrap(); +``` +Note: that `else if` and `else` tags are optional and their content is moved to the fields of the `IfNode`. Other nodes inside the `if` tag are all collected into the `IfNode::body` field, even if they were between `` and `` tags in the example above. + + +## Controll flow using escape symbol in unquoted text. + +The second way is to use the escape symbol in unquoted text. +This approach is more native for `Rust` since it is declared in the same way as in `Rust` code. +The only difference is that the block inside `{}` is not `Rust` code, but `rstml` template. + +Example: +```rust +use rstml::{parse2_with_config, node::*}; +use rstml_controll_flow::escape::*; +use quote::quote; + + +let template = quote!{ +

+ @if foo { +

foo is true

+ } else if bar { +

bar is true

+ } else { +

foo and bar are false

+ } +

+}; + +let nodes = parse2_with_config(template, Default::default().with_custom_nodes::()) +``` + +`EscapedCode` escape character is configurable, and by default uses the "@" symbol. + + +## Using multiple `CustomNode`s at once +It is also possible to use more than one `CustomNode` at once. +For example, if you want to use both `Conditions` and `EscapedCode` custom nodes. +`rstml-control-flow` crate provides an `ExtendableCustomNode` struct that can be used to combine multiple `CustomNode`s into one. Check out `extendable.rs` docs and tests in `lib.rs` for more details. + + +```rust \ No newline at end of file diff --git a/rstml-control-flow/src/escape.rs b/rstml-control-flow/src/escape.rs new file mode 100644 index 0000000..c852b6d --- /dev/null +++ b/rstml-control-flow/src/escape.rs @@ -0,0 +1,814 @@ +//! +//! This module contain implementation of flow-controll based on rstml custom +//! nodes. +//! +//! This is usefull when one need to define optional or repetetive html(*ml) +//! elements. It contain implementation of If, Match and For constructions that +//! is very simmilar to rust. The main difference in implementation is that body +//! of construction (part inside curly brackets) is expected to be an html(*ml), +//! but condition is expected to be in rust syntax. + +use quote::{ToTokens, TokenStreamExt}; +use rstml::{ + node::{CustomNode, Node as RNode}, + recoverable::{ParseRecoverable, RecoverableContext}, + visitor::Visitor, +}; +use syn::{ + braced, + parse::{Parse, ParseStream}, + token::Brace, + Expr, Pat, Token, +}; + +use crate::{Either, TryIntoOrCloneRef}; + +#[cfg(not(feature = "extendable"))] +type CustomNodeType = EscapeCode; + +#[cfg(feature = "extendable")] +type CustomNodeType = crate::ExtendableCustomNode; + +type Node = RNode; + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct Block { + #[syn(braced)] + pub brace_token: Brace, + #[syn(in = brace_token)] + #[to_tokens(|tokens, val| tokens.append_all(val))] + pub body: Vec, +} + +impl ParseRecoverable for Block { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + // we use this closure, because `braced!` + // has private api and force it's usage inside methods that return Result + let inner_parser = |parser: &mut RecoverableContext, input: ParseStream| { + let content; + let brace_token = braced!(content in input); + let mut body = vec![]; + while !content.is_empty() { + let Some(val) = parser.parse_recoverable(&content) else { + return Ok(None); + }; + body.push(val); + } + Ok(Some(Block { brace_token, body })) + }; + parser.parse_mixed_fn(input, inner_parser)? + } +} + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct ElseIf { + pub else_token: Token![else], + pub if_token: Token![if], + pub condition: Expr, + pub then_branch: Block, +} + +impl ParseRecoverable for ElseIf { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + Some(ElseIf { + else_token: parser.parse_simple(input)?, + if_token: parser.parse_simple(input)?, + condition: parser.parse_mixed_fn(input, |_, input| { + input.call(Expr::parse_without_eager_brace) + })?, + then_branch: parser.parse_recoverable(input)?, + }) + } +} + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct Else { + pub else_token: Token![else], + pub then_branch: Block, +} +impl ParseRecoverable for Else { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + Some(Else { + else_token: parser.parse_simple(input)?, + then_branch: parser.parse_recoverable(input)?, + }) + } +} + +/// If construction: +/// Condition is any valid rust syntax, while body is expected yo be *ml +/// element. +/// +/// Example: +/// `@if x > 2 {
}` +/// `@if let Some(x) = foo {
}` +/// +/// As in rust can contain arbitrary amount of `else if .. {..}` constructs and +/// one `else {..}`. +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct IfExpr { + pub keyword: Token![if], + pub condition: Expr, + pub then_branch: Block, + #[to_tokens(TokenStreamExt::append_all)] + pub else_ifs: Vec, + pub else_branch: Option, +} + +impl ParseRecoverable for IfExpr { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + let keyword = parser.parse_simple(input)?; + + let condition = parser.parse_mixed_fn(input, |_, input| { + input.call(Expr::parse_without_eager_brace) + })?; + + let then_branch = parser.parse_recoverable(input)?; + let mut else_ifs = vec![]; + + while input.peek(Token![else]) && input.peek2(Token![if]) { + else_ifs.push(parser.parse_recoverable(input)?) + } + let mut else_branch = None; + if input.peek(Token![else]) { + else_branch = Some(parser.parse_recoverable(input)?) + } + Some(IfExpr { + keyword, + condition, + then_branch, + else_ifs, + else_branch, + }) + } +} + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct ForExpr { + pub keyword: Token![for], + pub pat: Pat, + pub token_in: Token![in], + pub expr: Expr, + pub block: Block, +} + +impl ParseRecoverable for ForExpr { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + let keyword = parser.parse_simple(input)?; + let pat = parser.parse_mixed_fn(input, |_parse, input| { + Pat::parse_multi_with_leading_vert(input) + })?; + let token_in = parser.parse_simple(input)?; + let expr = parser.parse_mixed_fn(input, |_, input| { + input.call(Expr::parse_without_eager_brace) + })?; + let block = parser.parse_recoverable(input)?; + Some(ForExpr { + keyword, + pat, + token_in, + expr, + block, + }) + } +} + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct Arm { + pub pat: Pat, + // pub guard: Option<(If, Box)>, + pub fat_arrow_token: Token![=>], + pub body: Block, + pub comma: Option, +} + +impl ParseRecoverable for Arm { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + Some(Arm { + pat: parser + .parse_mixed_fn(input, |_, input| Pat::parse_multi_with_leading_vert(input))?, + fat_arrow_token: parser.parse_simple(input)?, + body: parser.parse_recoverable(input)?, + comma: parser.parse_simple(input)?, + }) + } +} + +// match foo { +// 1|3 => {}, +// x if x > 10 => {}, +// | x => {} +// } + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct MatchExpr { + pub keyword: Token![match], + pub expr: Expr, + #[syn(braced)] + pub brace_token: Brace, + #[syn(in = brace_token)] + #[to_tokens(TokenStreamExt::append_all)] + pub arms: Vec, +} + +impl ParseRecoverable for MatchExpr { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + // we use this closure, because `braced!` + // has private api and force it's usage inside methods that return Result + let inner_parser = |parser: &mut RecoverableContext, input: ParseStream| { + let Some(keyword) = parser.parse_simple(input) else { + return Ok(None); + }; + let content; + let expr = Expr::parse_without_eager_brace(input)?; + let brace_token = braced!(content in input); + let mut arms = Vec::new(); + while !content.is_empty() { + let Some(val) = parser.parse_recoverable(&content) else { + return Ok(None); + }; + arms.push(val); + } + Ok(Some(MatchExpr { + keyword, + expr, + brace_token, + arms, + })) + }; + parser.parse_mixed_fn(input, inner_parser)? + } +} + +// Minimal version of syn::Expr, that uses custom `Block` with `Node` array +// instead of `syn::Block` that contain valid rust code. + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub enum EscapedExpr { + If(IfExpr), + For(ForExpr), + Match(MatchExpr), +} + +impl ParseRecoverable for EscapedExpr { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + let res = if input.peek(Token![if]) { + EscapedExpr::If(parser.parse_recoverable(input)?) + } else if input.peek(Token![for]) { + EscapedExpr::For(parser.parse_recoverable(input)?) + } else if input.peek(Token![match]) { + EscapedExpr::Match(parser.parse_recoverable(input)?) + } else { + return None; + }; + + Some(res) + } +} +#[cfg(feature = "extendable")] +impl TryIntoOrCloneRef for crate::ExtendableCustomNode { + fn try_into_or_clone_ref(self) -> Either { + if let Some(val) = self.try_downcast_ref::() { + Either::A(val.clone()) + } else { + Either::B(self.clone()) + } + } + fn new_from_value(value: EscapeCode) -> Self { + Self::from_value(value) + } +} + +#[derive(Clone, Debug, syn_derive::ToTokens)] +pub struct EscapeCode { + pub escape_token: T, + pub expression: EscapedExpr, +} + +impl ParseRecoverable for EscapeCode { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + let escape_token = parser.parse_simple(input)?; + let expression = parser.parse_recoverable(input)?; + + Some(Self { + escape_token, + expression, + }) + } +} + +impl CustomNode for EscapeCode +where + T: Parse + ToTokens, +{ + fn peek_element(input: syn::parse::ParseStream) -> bool { + if input.parse::().is_err() { + return false; + } + input.peek(Token![if]) || input.peek(Token![for]) || input.peek(Token![match]) + } +} + +pub mod visitor_impl { + use rstml::visitor::{CustomNodeWalker, RustCode}; + + use super::*; + + pub struct EscapeCodeWalker; + impl CustomNodeWalker for EscapeCodeWalker { + type Custom = CustomNodeType; + fn walk_custom_node_fields( + visitor: &mut VisitorImpl, + node: &mut Self::Custom, + ) -> bool + where + VisitorImpl: Visitor, + { + EscapeCode::visit_custom_children(visitor, node) + } + } + + impl Block { + pub fn visit_custom_children>( + visitor: &mut V, + block: &mut Self, + ) -> bool { + block.body.iter_mut().all(|val| visitor.visit_node(val)) + } + } + + impl ElseIf { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + visitor.visit_rust_code(RustCode::Expr(&mut expr.condition)); + Block::visit_custom_children(visitor, &mut expr.then_branch) + } + } + + impl Else { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + Block::visit_custom_children(visitor, &mut expr.then_branch) + } + } + + impl IfExpr { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + visitor.visit_rust_code(RustCode::Expr(&mut expr.condition)); + Block::visit_custom_children(visitor, &mut expr.then_branch) + || expr + .else_ifs + .iter_mut() + .all(|val| ElseIf::visit_custom_children(visitor, val)) + || expr + .else_branch + .as_mut() + .map(|val| Else::visit_custom_children(visitor, val)) + .unwrap_or(true) + } + } + impl ForExpr { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + visitor.visit_rust_code(RustCode::Pat(&mut expr.pat)); + visitor.visit_rust_code(RustCode::Expr(&mut expr.expr)); + Block::visit_custom_children(visitor, &mut expr.block) + } + } + impl Arm { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + visitor.visit_rust_code(RustCode::Pat(&mut expr.pat)); + Block::visit_custom_children(visitor, &mut expr.body) + } + } + impl MatchExpr { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + visitor.visit_rust_code(RustCode::Expr(&mut expr.expr)); + + expr.arms + .iter_mut() + .all(|val| Arm::visit_custom_children(visitor, val)) + } + } + impl EscapedExpr { + pub fn visit_custom_children>( + visitor: &mut V, + expr: &mut Self, + ) -> bool { + match expr { + EscapedExpr::If(expr) => IfExpr::visit_custom_children(visitor, expr), + EscapedExpr::For(expr) => ForExpr::visit_custom_children(visitor, expr), + EscapedExpr::Match(expr) => MatchExpr::visit_custom_children(visitor, expr), + } + } + } + impl EscapeCode { + pub fn visit_custom_children>( + visitor: &mut V, + node: &mut CustomNodeType, + ) -> bool { + let Either::A(mut this): Either = node.clone().try_into_or_clone_ref() + else { + return true; + }; + let result = EscapedExpr::visit_custom_children(visitor, &mut this.expression); + *node = TryIntoOrCloneRef::new_from_value(this); + result + } + } +} + +#[cfg(test)] +#[cfg(not(feature = "extendable"))] +mod test_typed { + use quote::quote; + use rstml::{node::Node, recoverable::Recoverable, Parser, ParserConfig}; + use syn::{parse_quote, Token}; + + use super::{EscapeCode, EscapedExpr}; + + type MyCustomNode = EscapeCode; + type MyNode = Node; + #[test] + fn if_complex_expression() { + let actual: Recoverable = parse_quote! { + @if just && an || expression { +
+ } + }; + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::If(expr) = actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(just && an || expression)); + } + + #[test] + fn if_let_expr() { + let actual: Recoverable = parse_quote! { + @if let (foo) = Bar { +
+ } + }; + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::If(expr) = actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(let (foo) = Bar)); + } + + #[test] + fn if_simple() { + let actual: Recoverable = parse_quote! { + @if foo > bar { + +
+
+ } + }; + + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::If(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(foo > bar)); + } + + #[test] + fn if_else_if() { + let actual: Recoverable = parse_quote! { + @if foo > bar { + + } else if foo == 2 { + + } else if foo == 3 { + + } else { + + } + }; + + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::If(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(foo > bar)); + assert_eq!(expr.else_ifs[0].condition, parse_quote!(foo == 2)); + assert_eq!(expr.else_ifs[1].condition, parse_quote!(foo == 3)); + assert!(expr.else_branch.is_some()); + } + + #[test] + fn for_simple() { + let actual: Recoverable = parse_quote! { + @for x in foo { +
+ {x} +
+ } + }; + + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::For(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.pat, parse_quote!(x)); + assert_eq!(expr.expr, parse_quote!(foo)); + } + + #[test] + fn for_binding() { + let actual: Recoverable = parse_quote! { + @for (ref x, f) in foo { +
+ {x} +
+ } + }; + + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::For(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.pat, parse_quote!((ref x, f))); + assert_eq!(expr.expr, parse_quote!(foo)); + } + + #[test] + fn match_simple() { + let actual: Recoverable = parse_quote! { + @match foo { + x => {} + _ => {} + } + }; + + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::Match(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.expr, parse_quote!(foo)); + assert_eq!(expr.arms[0].pat, parse_quote!(x)); + assert_eq!(expr.arms[1].pat, parse_quote!(_)); + } + + #[test] + fn match_complex_exprs() { + let actual: Recoverable = parse_quote! { + @match foo > 2 * 2 { + Some(x) => {} + y|z => {} + } + }; + + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::Match(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.expr, parse_quote!(foo > 2 * 2)); + assert_eq!(expr.arms[0].pat, parse_quote!(Some(x))); + assert_eq!(expr.arms[1].pat, parse_quote!(y | z)); + } + + #[test] + fn check_if_inside_if() { + let actual: Recoverable = parse_quote! { + @if just && an || expression { + @if foo > bar { +
+ } + } + }; + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::If(expr) = actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(just && an || expression)); + let node = expr.then_branch.body.iter().next().unwrap(); + let Node::Custom(actual) = node else { panic!() }; + let EscapedExpr::If(expr) = &actual.expression else { + panic!() + }; + assert_eq!(expr.condition, parse_quote!(foo > bar)); + } + + #[test] + fn for_inside_if() { + let actual: Recoverable = parse_quote! { + @if just && an || expression { + @for x in foo { +
+ } + } + }; + let Node::Custom(actual) = actual.inner() else { + panic!() + }; + + let EscapedExpr::If(expr) = actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(just && an || expression)); + let node = expr.then_branch.body.iter().next().unwrap(); + let Node::Custom(actual) = node else { panic!() }; + let EscapedExpr::For(expr) = &actual.expression else { + panic!() + }; + assert_eq!(expr.pat, parse_quote!(x)); + assert_eq!(expr.expr, parse_quote!(foo)); + } + + #[test] + fn custom_node_using_config() { + let actual = Parser::new( + ParserConfig::new() + .element_close_use_default_wildcard_ident(false) + .custom_node::(), + ) + .parse_simple(quote! { + @if just && an || expression { + +
+
+ } + }) + .unwrap(); + let Node::Custom(actual) = &actual[0] else { + panic!() + }; + + let EscapedExpr::If(expr) = &actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(just && an || expression)); + } +} + +#[cfg(test)] +mod test_universal { + use proc_macro2::TokenStream; + use quote::quote; + use rstml::{ + recoverable::Recoverable, visitor::visit_nodes_with_custom, ParserConfig, ParsingResult, + }; + use syn::{parse_quote, visit_mut::VisitMut}; + + use super::*; + use crate::escape::visitor_impl::EscapeCodeWalker; + + // #[cfg(feature="extendable")] + #[cfg(not(feature = "extendable"))] + fn parse_universal(input: TokenStream) -> ParsingResult> { + use rstml::Parser; + + let actual = + Parser::new(ParserConfig::new().custom_node::()).parse_recoverable(input); + + return actual; + } + #[cfg(feature = "extendable")] + fn parse_universal(input: TokenStream) -> ParsingResult> { + use crate::ExtendableCustomNode; + + let result = + ExtendableCustomNode::parse2_with_config::<(EscapeCode,)>(ParserConfig::new(), input); + return result; + } + + // For extendable custom node it is safe to use only after parsing. + fn reparse_concrete(val: A) -> EscapeCode { + let recoverable: Recoverable<_> = parse_quote!(#val); + recoverable.inner() + } + + #[test] + fn if_node_reparsable() { + let tokens = quote! { + @if just && an || expression { +
+ } + }; + + let actual = &parse_universal(tokens).into_result().unwrap()[0]; + + let Node::Custom(actual) = actual else { + panic!() + }; + let actual = reparse_concrete(actual); + + let EscapedExpr::If(expr) = actual.expression else { + panic!() + }; + + assert_eq!(expr.condition, parse_quote!(just && an || expression)); + } + #[test] + fn if_node_visitor_rstml() { + let tokens = quote! { + @if just && an || expression { +
+ } + }; + + let mut actual = parse_universal(tokens).into_result().unwrap(); + + struct ElementVisitor { + elements: Vec, + } + impl Visitor for ElementVisitor { + fn visit_element(&mut self, node: &mut rstml::node::NodeElement) -> bool { + self.elements.push(node.open_tag.name.to_string()); + true + } + } + impl VisitMut for ElementVisitor {} + + let visitor = ElementVisitor { + elements: Vec::new(), + }; + let elements = + visit_nodes_with_custom::<_, _, EscapeCodeWalker>(&mut actual, visitor).elements; + + assert_eq!(&elements, &["div"]); + } + + #[test] + fn if_node_visitor_syn_expr() { + let tokens = quote! { + @if just && an || expression { +
+ } + }; + + let mut actual = parse_universal(tokens).into_result().unwrap(); + + struct ElementVisitor { + exprs: Vec, + } + impl Visitor for ElementVisitor {} + impl VisitMut for ElementVisitor { + fn visit_expr_mut(&mut self, exprs: &mut syn::Expr) { + self.exprs.push(exprs.to_token_stream().to_string()); + } + } + + let visitor = ElementVisitor { exprs: Vec::new() }; + let elements = + visit_nodes_with_custom::<_, _, EscapeCodeWalker>(&mut actual, visitor).exprs; + + assert_eq!(&elements, &["just && an || expression"]); + } +} diff --git a/rstml-control-flow/src/extendable.rs b/rstml-control-flow/src/extendable.rs new file mode 100644 index 0000000..2212c0c --- /dev/null +++ b/rstml-control-flow/src/extendable.rs @@ -0,0 +1,193 @@ +//! +//! Extendable `CustomNode`. +//! Provide a way to mix different `CustomNode` implementation and parse them in +//! mixed context. Uses `thread_local` to save parsing routines, therefore it +//! should be initialized before usage. +//! +//! If implementors of custom node, want to support mixed context, and allow +//! using their code in mixed with other custom nodes, they should follow next +//! steps: +//! +//! 1. type Node +//! Type alias that change it's meaning depending on feature flag `extendable`. +//! Example: +//! ```no_compile +//! use rstml::node::Node as RNode; +//! +//! #[cfg(not(feature = "extendable"))] +//! type Node = RNode; +//! #[cfg(feature = "extendable")] +//! type Node = RNode; +//! ``` +//! This allows custom node implementation to be used in both contexts. +//! +//! 2. (optional) Trait that allows working with both contexts (can be used one +//! from) +//! ```no_compile +//! pub trait TryIntoOrCloneRef: Sized { +//! fn try_into_or_clone_ref(self) -> Either; +//! fn new_from_value(value: T) -> Self; +//! } +//! ``` +//! And implementation of TryIntoOrCloneRef for both: +//! 2.1. `impl TryIntoOrCloneRef for T` in order to work with custom nodes +//! without extending. +//! 2.2. `impl TryIntoOrCloneRef for +//! crate::ExtendableCustomNode` in order to work with custom nodes with +//! extending. +//! +//! 3. (optional) Implement trait `CustomNode` for `MyCustomNode` that will use +//! trait defined in 2. + +use std::{any::Any, cell::RefCell, rc::Rc}; + +use proc_macro2_diagnostics::Diagnostic; +use quote::ToTokens; +use rstml::{ + node::{CustomNode, Node}, + recoverable::{ParseRecoverable, RecoverableContext}, + Parser, ParserConfig, ParsingResult, +}; +use syn::parse::ParseStream; + +type ToTokensHandler = Box; +type ParseRecoverableHandler = + Box Option>; +type PeekHandler = Box bool>; + +thread_local! { + static TO_TOKENS: RefCell > = RefCell::new(None); + static PARSE_RECOVERABLE: RefCell > = RefCell::new(None); + static PEEK: RefCell > = RefCell::new(None); +} + +#[derive(Clone, Debug)] +pub struct ExtendableCustomNode { + value: Rc, +} + +impl ToTokens for ExtendableCustomNode { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + TO_TOKENS.with_borrow(|f| f.as_ref().unwrap()(self, tokens)) + } +} + +impl ParseRecoverable for ExtendableCustomNode { + fn parse_recoverable(ctx: &mut RecoverableContext, input: ParseStream) -> Option { + PARSE_RECOVERABLE.with_borrow(|f| f.as_ref().unwrap()(ctx, input)) + } +} + +impl CustomNode for ExtendableCustomNode { + fn peek_element(input: ParseStream) -> bool { + PEEK.with_borrow(|f| f.as_ref().unwrap()(input)) + } +} +trait Sealed {} + +#[allow(private_bounds)] +pub trait Tuple: Sealed { + fn to_tokens(this: &ExtendableCustomNode, tokens: &mut proc_macro2::TokenStream); + fn parse_recoverable( + ctx: &mut RecoverableContext, + input: ParseStream, + ) -> Option; + fn peek(input: ParseStream) -> bool; +} + +macro_rules! impl_tuple { + ($($name:ident),*) => { + impl<$($name: CustomNode + 'static),*> Sealed for ($($name,)*) {} + impl<$($name: CustomNode + 'static+ std::fmt::Debug),*> Tuple for ($($name,)*) { + fn to_tokens(this: &ExtendableCustomNode, tokens: &mut proc_macro2::TokenStream) { + $(if let Some(v) = this.try_downcast_ref::<$name>() { + v.to_tokens(tokens); + })* + } + fn parse_recoverable(ctx: &mut RecoverableContext, input: ParseStream) -> Option { + + $(if $name::peek_element(&input.fork()) { + $name::parse_recoverable(ctx, input).map(ExtendableCustomNode::from_value) + })else* + else { + ctx.push_diagnostic(Diagnostic::new(proc_macro2_diagnostics::Level::Error, "Parsing invalid custom node")); + None + } + } + fn peek(input: ParseStream) -> bool { + $($name::peek_element(&input.fork()))||* + } + } + }; +} + +impl_tuple!(A); +impl_tuple!(A, B); +impl_tuple!(A, B, C); +impl_tuple!(A, B, C, D); +impl_tuple!(A, B, C, D, E); +impl_tuple!(A, B, C, D, E, F); +impl_tuple!(A, B, C, D, E, F, G); +impl_tuple!(A, B, C, D, E, F, G, H); + +fn init_extendable_node() { + assert_context_empty(); + TO_TOKENS.with_borrow_mut(|f| *f = Some(Box::new(E::to_tokens))); + PARSE_RECOVERABLE.with_borrow_mut(|f| *f = Some(Box::new(E::parse_recoverable))); + PEEK.with_borrow_mut(|f| *f = Some(Box::new(E::peek))); +} + +fn assert_context_empty() { + TO_TOKENS.with_borrow(|f| { + assert!( + f.is_none(), + "Cannot init ExtendableCustomNode context multiple times" + ) + }); + PARSE_RECOVERABLE.with_borrow(|f| { + assert!( + f.is_none(), + "Cannot init ExtendableCustomNode context multiple times" + ) + }); + PEEK.with_borrow(|f| { + assert!( + f.is_none(), + "Cannot init ExtendableCustomNode context multiple times" + ) + }); +} + +pub fn clear_context() { + TO_TOKENS.with_borrow_mut(|f| *f = None); + PARSE_RECOVERABLE.with_borrow_mut(|f| *f = None); + PEEK.with_borrow_mut(|f| *f = None); +} + +impl ExtendableCustomNode { + pub fn from_value(value: T) -> Self { + Self { + value: Rc::new(value), + } + } + pub fn try_downcast_ref(&self) -> Option<&T> { + self.value.downcast_ref::() + } + + /// Parses token stream into `Vec>` + /// + /// Note: This function are using context from thread local storage, + /// after call it lefts context initiliazed, so to_tokens implementation can + /// be used. But second call to parse2_with_config will fail, because + /// context is already initialized. + /// You can use `clear_context` to clear context manually. + pub fn parse2_with_config( + config: ParserConfig, + tokens: proc_macro2::TokenStream, + ) -> ParsingResult>> { + init_extendable_node::(); + let result = + Parser::new(config.custom_node::()).parse_recoverable(tokens); + result + } +} diff --git a/rstml-control-flow/src/lib.rs b/rstml-control-flow/src/lib.rs new file mode 100644 index 0000000..65e108b --- /dev/null +++ b/rstml-control-flow/src/lib.rs @@ -0,0 +1,191 @@ +//! Example of controll flow implementations: +//! 1. One variant is based on tags `` `` `` `` +//! 2. another variand is based on escape character inside unquoted texts `@if +//! {}` `@for foo in array {}` +use std::marker::PhantomData; + +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; + +pub mod escape; +#[cfg(feature = "extendable")] +pub mod extendable; +pub mod tags; + +#[cfg(feature = "extendable")] +pub use extendable::ExtendableCustomNode; + +// Either variant, with Parse/ToTokens implementation +#[derive(Copy, Clone, Debug)] +pub enum Either { + A(A), + B(B), +} +impl Parse for Either { + fn parse(input: ParseStream) -> syn::Result { + if Self::peek_a(input) { + input.parse().map(Self::A) + } else { + input.parse().map(Self::B) + } + } +} +impl ToTokens for Either { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::A(a) => a.to_tokens(tokens), + Self::B(b) => b.to_tokens(tokens), + } + } +} + +#[allow(dead_code)] +impl Either { + pub fn peek_a(stream: ParseStream) -> bool + where + A: Parse, + B: Parse, + { + stream.fork().parse::().is_ok() + } + pub fn to_b(self) -> Option { + match self { + Self::A(_) => None, + Self::B(b) => Some(b), + } + } + pub fn to_a(self) -> Option { + match self { + Self::A(a) => Some(a), + Self::B(_) => None, + } + } + pub fn is_b(self) -> bool { + match self { + Self::A(_) => false, + Self::B(_) => true, + } + } + pub fn is_a(self) -> bool { + match self { + Self::A(_) => true, + Self::B(_) => false, + } + } +} + +pub struct EitherA(pub A, pub PhantomData); +pub struct EitherB(pub PhantomData, pub B); + +impl TryFrom> for EitherA { + type Error = Either; + fn try_from(value: Either) -> Result { + match value { + Either::A(a) => Ok(EitherA(a, PhantomData)), + rest => Err(rest), + } + } +} + +impl TryFrom> for EitherB { + type Error = Either; + fn try_from(value: Either) -> Result { + match value { + Either::B(b) => Ok(EitherB(PhantomData, b)), + rest => Err(rest), + } + } +} + +impl From> for Either { + fn from(value: EitherA) -> Self { + Self::A(value.0) + } +} + +impl From> for Either { + fn from(value: EitherB) -> Self { + Self::B(value.1) + } +} + +pub trait TryIntoOrCloneRef: Sized { + fn try_into_or_clone_ref(self) -> Either; + fn new_from_value(value: T) -> Self; +} + +impl TryIntoOrCloneRef for T { + fn try_into_or_clone_ref(self) -> Either { + Either::A(self) + } + fn new_from_value(value: T) -> Self { + value + } +} + +#[cfg(test)] +mod tests { + + #[test] + #[cfg(feature = "extendable")] + fn test_mixed_tags_and_escape() { + use quote::ToTokens; + use rstml::node::Node; + + use crate::{escape, tags, ExtendableCustomNode}; + + let tokens = quote::quote! { + @if true { +

True

+ +

Foo

+
+ } + else { +

False

+ } + @for foo in array { + +

Foo

+
+

Foo

+ } + }; + + let result = ExtendableCustomNode::parse2_with_config::<( + tags::Conditions, + escape::EscapeCode, + )>(Default::default(), tokens); + let ok = result.into_result().unwrap(); + assert_eq!(ok.len(), 2); + + let Node::Custom(c) = &ok[0] else { + unreachable!() + }; + let escape_if = c.try_downcast_ref::().unwrap(); + let escape::EscapedExpr::If(if_) = &escape_if.expression else { + unreachable!() + }; + assert_eq!(if_.condition.to_token_stream().to_string(), "true"); + let for_tag = &if_.then_branch.body[1]; + let Node::Custom(c) = &for_tag else { + unreachable!() + }; + let for_tag = c.try_downcast_ref::().unwrap(); + let tags::Conditions::For(for_) = for_tag else { + unreachable!() + }; + + assert_eq!(for_.pat.to_token_stream().to_string(), "foo"); + + let Node::Custom(c) = &ok[1] else { + unreachable!() + }; + + let escape_for = c.try_downcast_ref::().unwrap(); + let escape::EscapedExpr::For(for_) = &escape_for.expression else { + unreachable!() + }; + assert_eq!(for_.pat.to_token_stream().to_string(), "foo"); + } +} diff --git a/rstml-control-flow/src/tags.rs b/rstml-control-flow/src/tags.rs new file mode 100644 index 0000000..19cd8a8 --- /dev/null +++ b/rstml-control-flow/src/tags.rs @@ -0,0 +1,568 @@ +use std::fmt::Debug; + +use derive_where::derive_where; +use proc_macro2_diagnostics::Diagnostic; +use quote::{ToTokens, TokenStreamExt}; +use rstml::{ + atoms::{self, CloseTagStart, OpenTag, OpenTagEnd}, + node::{CustomNode, Node as RNode, NodeElement}, + recoverable::{ParseRecoverable, RecoverableContext}, +}; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned, + Expr, Pat, Token, +}; + +#[cfg(not(feature = "extendable"))] +type Node = RNode; + +#[cfg(feature = "extendable")] +type Node = RNode; + +use super::Either; +#[cfg(feature = "extendable")] +use crate::ExtendableCustomNode; +use crate::TryIntoOrCloneRef; +/// End part of control flow tag ending +/// `/>` (self closed) or `!>` (regular, token `!` can be changed) +/// +/// Rust expressions can contain `>` (compare operator), which makes harder +/// for parser to distinguish whenewer it part of expression or tag end.To +/// solve this issue, we enforce for controll flow tags to have `!>` at the end. +/// We also allow `/>` ending, to have self closed controll flow tags. +#[derive_where(Clone; EndToken: Clone)] +#[derive_where(Debug; EndToken: std::fmt::Debug)] +pub struct ControlFlowTagEnd { + pub self_close_marker: Either, + pub token_gt: Token![>], +} + +impl Parse for ControlFlowTagEnd +where + EndToken: Parse, +{ + fn parse(input: ParseStream) -> syn::Result { + let self_close_marker = if input.peek(Token![/]) { + Either::B(input.parse()?) + } else { + Either::A(input.parse()?) + }; + let token_gt = input.parse()?; + Ok(Self { + self_close_marker, + token_gt, + }) + } +} + +impl ToTokens for ControlFlowTagEnd +where + EndToken: ToTokens, +{ + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.self_close_marker.to_tokens(tokens); + self.token_gt.to_tokens(tokens); + } +} + +impl ControlFlowTagEnd { + pub fn is_start(&self) -> bool { + matches!(self.self_close_marker, Either::A(_)) + } +} + +/// If construction: +/// instead of attributes receive rust expression that can evaluate to bool. +/// As any controll flow tag - should contain marker `!` at the end of tag, or +/// be self closed If expression returns true - process child nodes. +/// example: +/// ` 2 !>` +/// +/// As in rust can contain arbitrary amount of `` constructs and +/// one `` at the end close tag is expected. +#[derive(syn_derive::ToTokens)] +// #[derive_where(Clone, Debug; EndToken: Clone + std::fmt::Debug, C: Clone + std::fmt::Debug)] +#[derive(Clone, Debug)] +pub struct IfNode { + pub token_lt: Token![<], + pub token_if: Token![if], + pub condition: Expr, + pub open_tag_end: ControlFlowTagEnd, + #[to_tokens(TokenStreamExt::append_all)] + pub body: Vec, + #[to_tokens(TokenStreamExt::append_all)] + pub else_ifs: Vec, + pub else_child: Option, + pub close_tag: Option, +} + +#[derive(syn_derive::ToTokens, Clone, Debug)] +pub struct ElseIfNode { + pub token_lt: Token![<], + pub token_else_if: ElseIfToken, + pub condition: Expr, + pub open_tag_end: ControlFlowTagEnd, + #[to_tokens(TokenStreamExt::append_all)] + pub body: Vec, + pub close_tag: Option, +} + +/// Close tag for element, `` +#[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] +pub struct ElseIfCloseTag { + pub start_tag: CloseTagStart, + pub token_else_if: ElseIfToken, + pub token_gt: Token![>], +} + +impl ElseIfCloseTag { + pub fn parse_with_start_tag( + parser: &mut RecoverableContext, + input: syn::parse::ParseStream, + start_tag: Option, + ) -> Option { + Some(Self { + start_tag: start_tag?, + token_else_if: parser.parse_simple(input)?, + token_gt: parser.parse_simple(input)?, + }) + } +} + +#[derive(syn_derive::ToTokens, Clone, Debug)] +pub struct ElseNode { + pub token_lt: Token![<], + pub token_else: Token![else], + // Use same type as in if + pub open_tag_end: OpenTagEnd, + #[to_tokens(TokenStreamExt::append_all)] + pub body: Vec, + pub close_tag: Option, +} + +#[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] +pub struct ElseIfToken { + pub token_else: Token![else], + pub token_if: Token![if], +} + +#[derive(syn_derive::ToTokens, Clone, Debug)] +pub struct ForNode { + pub token_lt: Token![<], + pub token_for: Token![for], + pub pat: Pat, + pub token_in: Token![in], + pub expr: Expr, + pub open_tag_end: ControlFlowTagEnd, + #[to_tokens(TokenStreamExt::append_all)] + pub body: Vec, + pub close_tag: Option, +} + +impl ParseRecoverable for ForNode { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + let token_lt = OpenTag::parse_start_tag(parser, input)?; + let token_for = parser.parse_simple(input)?; + let pat = parser.parse_mixed_fn(input, |_parse, input| { + Pat::parse_multi_with_leading_vert(input) + })?; + let token_in = parser.parse_simple(input)?; + let (expr, open_tag_end): (_, ControlFlowTagEnd) = parser.parse_simple_until(input)?; + + let (body, close_tag) = if open_tag_end.is_start() { + // If node is not raw use any closing tag as separator, to early report about + // invalid closing tags. + // Also parse only (input, CloseTagStart::parse); + + let close_tag = atoms::CloseTag::parse_with_start_tag(parser, input, close_tag); + (children, close_tag) + } else { + (Vec::new(), None) + }; + Some(Self { + token_lt, + token_for, + pat, + token_in, + expr, + open_tag_end, + body, + close_tag, + }) + } +} + +impl ParseRecoverable for ElseIfNode { + fn parse_recoverable( + parser: &mut rstml::recoverable::RecoverableContext, + input: ParseStream, + ) -> Option { + let token_lt = OpenTag::parse_start_tag(parser, input)?; + let token_else_if = parser.parse_simple(input)?; + let (condition, open_tag_end): (_, ControlFlowTagEnd) = parser.parse_simple_until(input)?; + + let (body, close_tag) = if open_tag_end.is_start() { + // If node is not raw use any closing tag as separator, to early report about + // invalid closing tags. + // Also parse only (input, CloseTagStart::parse); + + let close_tag = ElseIfCloseTag::parse_with_start_tag(parser, input, close_tag); + (children, close_tag) + } else { + (Vec::new(), None) + }; + Some(Self { + token_lt, + token_else_if, + condition, + open_tag_end, + body, + close_tag, + }) + } +} + +impl ParseRecoverable for ElseNode { + fn parse_recoverable( + parser: &mut rstml::recoverable::RecoverableContext, + input: ParseStream, + ) -> Option { + let token_lt = OpenTag::parse_start_tag(parser, input)?; + let token_else = parser.parse_simple(input)?; + let open_tag_end: OpenTagEnd = parser.parse_simple(input)?; + + let (body, close_tag) = if open_tag_end.token_solidus.is_none() { + let open_tag_end = OpenTagEnd { + token_gt: open_tag_end.token_gt, + token_solidus: None, + }; + // Passed to allow parsing of close_tag + NodeElement::parse_children( + parser, + input, + false, + &OpenTag { + token_lt, + name: parse_quote!(#token_else), + generics: Default::default(), + attributes: Default::default(), + end_tag: open_tag_end, + }, + )? + } else { + (Vec::new(), None) + }; + Some(Self { + token_lt, + token_else, + open_tag_end, + body, + close_tag, + }) + } +} + +#[cfg(feature = "extendable")] +impl TryIntoOrCloneRef for ExtendableCustomNode { + fn try_into_or_clone_ref(self) -> Either { + let Some(ref_val) = self.try_downcast_ref::() else { + return Either::B(self); + }; + Either::A(ref_val.clone()) + } + fn new_from_value(value: Conditions) -> Self { + ExtendableCustomNode::from_value(value) + } +} + +// A lot of bounds o§n type definition require a lot of bounds in type +// declaration. +impl ParseRecoverable for IfNode { + // Parse as regular element, + // then ensure that else if and else are in correct order + // in the way move all else ifs and elses to separate fields. + fn parse_recoverable( + parser: &mut rstml::recoverable::RecoverableContext, + input: ParseStream, + ) -> Option { + let token_lt = OpenTag::parse_start_tag(parser, input)?; + let token_if = parser.parse_simple(input)?; + let (condition, open_tag_end): (_, ControlFlowTagEnd) = parser.parse_simple_until(input)?; + + let open_tag_end_ = OpenTagEnd { + token_gt: open_tag_end.token_gt, + token_solidus: None, + }; + let (body, close_tag) = NodeElement::parse_children( + parser, + input, + false, + &OpenTag { + token_lt, + name: parse_quote!(#token_if), + generics: Default::default(), + attributes: Default::default(), + end_tag: open_tag_end_, + }, + )?; + let mut else_ifs = Vec::new(); + let mut else_child = None; + let mut modified_body = Vec::new(); + for el in body { + let rest = match el { + Node::Custom(c) => match c.try_into_or_clone_ref() { + Either::A(Conditions::ElseIf(else_if)) => { + if else_child.is_some() { + parser.push_diagnostic(Diagnostic::spanned( + else_if.span(), + proc_macro2_diagnostics::Level::Error, + "else if after else", + )); + return None; + } + else_ifs.push(else_if); + continue; + } + Either::A(Conditions::Else(else_)) => { + else_child = Some(else_); + continue; + } + Either::A(condition) => { + Node::Custom(TryIntoOrCloneRef::new_from_value(condition)) + } + Either::B(rest) => Node::Custom(rest), + }, + _ => el, + }; + modified_body.push(rest); + } + + Some(Self { + token_lt, + token_if, + condition, + open_tag_end, + body: modified_body, + else_ifs, + else_child, + close_tag, + }) + } +} + +/// Conditions can be either if, else if, else, match or for +/// Make sure to check is_highlevel before using, to avoid toplevel else/else if +/// nodes. +#[derive(Clone, Debug)] +pub enum Conditions { + ElseIf(ElseIfNode), + Else(ElseNode), + If(IfNode), + For(ForNode), + // Match(Match), +} + +impl Conditions { + pub fn is_highlevel(&self) -> bool { + match self { + Self::If(_) | Self::For(_) => true, + _ => false, + } + } +} + +impl ToTokens for Conditions { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::ElseIf(else_if) => else_if.to_tokens(tokens), + Self::Else(else_) => else_.to_tokens(tokens), + Self::If(if_) => if_.to_tokens(tokens), + Self::For(for_) => for_.to_tokens(tokens), + } + } +} + +impl ParseRecoverable for Conditions { + fn parse_recoverable( + parser: &mut rstml::recoverable::RecoverableContext, + input: ParseStream, + ) -> Option { + let variants = if input.peek2(Token![if]) { + let if_ = IfNode::parse_recoverable(parser, input)?; + Self::If(if_) + } else if input.peek2(Token![else]) { + if input.peek3(Token![if]) { + let else_if = ElseIfNode::parse_recoverable(parser, input)?; + Self::ElseIf(else_if) + } else { + let else_ = ElseNode::parse_recoverable(parser, input)?; + Self::Else(else_) + } + } else if input.peek2(Token![for]) { + let for_ = ForNode::parse_recoverable(parser, input)?; + Self::For(for_) + } else { + return None; + }; + Some(variants) + } +} + +impl CustomNode for Conditions { + fn peek_element(input: syn::parse::ParseStream) -> bool { + input.peek(Token![<]) + && (input.peek2(Token![else]) || input.peek2(Token![if]) || input.peek2(Token![for])) + } +} + +#[cfg(not(feature = "extendable"))] +#[cfg(test)] +mod test { + use quote::quote; + use rstml::{node::Node, recoverable::Recoverable, Parser, ParserConfig}; + use syn::parse_quote; + + use super::ControlFlowTagEnd; + use crate::tags::Conditions; + + #[test] + fn control_flow_tag_end_parsable() { + let _tokens: ControlFlowTagEnd = parse_quote! { + !> + }; + + let _tokens: ControlFlowTagEnd = parse_quote! { + /> + }; + } + #[test] + fn custom_node_origin() { + let actual: Recoverable> = parse_quote! { + +
+
+
+ + }; + let Node::Custom(Conditions::If(actual)) = actual.inner() else { + panic!() + }; + + assert_eq!(actual.condition, parse_quote!(just && an || expression)); + } + + #[test] + fn custom_node_origin_cmp() { + let actual: Recoverable> = parse_quote! { + bar !> +
+
+
+ + }; + + let Node::Custom(Conditions::If(actual)) = actual.inner() else { + panic!() + }; + + assert_eq!(actual.condition, parse_quote!(foo > bar)); + } + + #[test] + fn custom_node_using_config() { + let actual = Parser::new( + ParserConfig::new() + .element_close_use_default_wildcard_ident(false) + .custom_node::(), + ) + .parse_simple(quote! { + bar !> +
+
+
+ + }) + .unwrap(); + let Node::Custom(Conditions::If(actual)) = &actual[0] else { + panic!() + }; + + assert_eq!(actual.condition, parse_quote!(foo > bar)); + } + + #[test] + fn custom_node_if_else() { + let actual: Recoverable> = parse_quote! { + bar !> +
+
+
+ +
+
+
+ + + }; + let Node::Custom(Conditions::If(actual)) = actual.inner() else { + panic!() + }; + + assert_eq!(actual.condition, parse_quote!(foo > bar)); + } + #[test] + fn custom_node_else_ifs() { + let actual: Recoverable> = parse_quote! { + + "foo < bar" + + }; + let Node::Custom(Conditions::ElseIf(actual)) = actual.inner() else { + panic!() + }; + + assert_eq!(actual.condition, parse_quote!(foo < bar)); + } + + #[test] + fn custom_node_if_else_ifs() { + let actual: Recoverable> = parse_quote! { + bar !> + "foo > bar" + + "foo < bar" + + + "foo == bar" + + + }; + let Node::Custom(Conditions::If(actual)) = actual.inner() else { + panic!() + }; + + assert_eq!(actual.condition, parse_quote!(foo > bar)); + } + + #[test] + fn constom_node_for() { + let actual: Recoverable> = parse_quote! { + +
+ "value is:" {x} + + }; + + let Node::Custom(Conditions::For(actual)) = actual.inner() else { + panic!() + }; + + assert_eq!(actual.pat, parse_quote!(x)); + } +} diff --git a/src/config.rs b/src/config.rs index cface34..6e92e10 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,19 +1,20 @@ -use std::{collections::HashSet, convert::Infallible, fmt::Debug, marker::PhantomData, rc::Rc}; +use std::{collections::HashSet, fmt::Debug, marker::PhantomData, rc::Rc}; -use derive_where::derive_where; use proc_macro2::TokenStream; use syn::{parse::ParseStream, Result}; +#[cfg(feature = "rawtext-stable-hack")] +use crate::rawtext_stable_hack::MacroPattern; use crate::{ atoms::{CloseTag, OpenTag}, node::{CustomNode, NodeType}, + Infallible, }; pub type TransformBlockFn = dyn Fn(ParseStream) -> Result>; pub type ElementWildcardFn = dyn Fn(&OpenTag, &CloseTag) -> bool; /// Configures the `Parser` behavior -#[derive_where(Clone)] pub struct ParserConfig { pub(crate) flat_tree: bool, pub(crate) number_of_top_level_nodes: Option, @@ -23,9 +24,29 @@ pub struct ParserConfig { pub(crate) always_self_closed_elements: HashSet<&'static str>, pub(crate) raw_text_elements: HashSet<&'static str>, pub(crate) element_close_wildcard: Option>, + #[cfg(feature = "rawtext-stable-hack")] + pub(crate) macro_pattern: MacroPattern, custom_node: PhantomData, } +impl Clone for ParserConfig { + fn clone(&self) -> Self { + Self { + flat_tree: self.flat_tree.clone(), + number_of_top_level_nodes: self.number_of_top_level_nodes, + type_of_top_level_nodes: self.type_of_top_level_nodes.clone(), + transform_block: self.transform_block.clone(), + recover_block: self.recover_block.clone(), + always_self_closed_elements: self.always_self_closed_elements.clone(), + raw_text_elements: self.raw_text_elements.clone(), + element_close_wildcard: self.element_close_wildcard.clone(), + #[cfg(feature = "rawtext-stable-hack")] + macro_pattern: self.macro_pattern.clone(), + custom_node: self.custom_node.clone(), + } + } +} + impl Default for ParserConfig { fn default() -> Self { Self { @@ -37,6 +58,8 @@ impl Default for ParserConfig { always_self_closed_elements: Default::default(), raw_text_elements: Default::default(), element_close_wildcard: Default::default(), + #[cfg(feature = "rawtext-stable-hack")] + macro_pattern: Default::default(), custom_node: Default::default(), } } @@ -44,8 +67,9 @@ impl Default for ParserConfig { impl Debug for ParserConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ParserConfig") - .field("flat_tree", &self.flat_tree) + let mut s = f.debug_struct("ParserConfig"); + + s.field("flat_tree", &self.flat_tree) .field("number_of_top_level_nodes", &self.number_of_top_level_nodes) .field("type_of_top_level_nodes", &self.type_of_top_level_nodes) .field("recover_block", &self.recover_block) @@ -57,8 +81,10 @@ impl Debug for ParserConfig { .field( "element_close_wildcard", &self.element_close_wildcard.is_some(), - ) - .finish() + ); + #[cfg(feature = "rawtext-stable-hack")] + s.field("macro_pattern", &self.macro_pattern); + s.finish() } } @@ -209,6 +235,50 @@ impl ParserConfig { }) } + /// + /// Provide pattern of macro call. + /// + /// It is used with feature = "rawtext-stable-hack" to retrive + /// space information in `RawText`. + /// Checkout https://github.com/rs-tml/rstml/issues/5 for details. + /// + /// + /// This method receive macro pattern as `TokenStream`, uses tokens `%%` + /// as marker for input `rstml::parse`. + /// Support only one marker in pattern. + /// Currently, don't check other tokens for equality with pattern. + /// + /// Example: + /// + /// Imagine one have macro, that used like this: + /// ```no_compile + /// html! {some_context, provided, [ can use groups, etc], {
}, [other context]}; + /// ``` + /// + /// Then one can set `macro_call_pattern` with following arguments: + /// ```no_compile + /// config.macro_call_pattern(quote!(html! // macro name currently is not checked + /// {ident, ident, // can use real idents, or any other + /// [/* can ignore context of auxilary groups */], + /// {%%}, // important part + /// [] + /// })) + /// ``` + /// + /// And rstml will do the rest for you. + /// + /// Panics if no `%%` token was found. + /// + /// If macro_call_patern is set rstml will parse input two times in order to + /// recover spaces in `RawText`. Rstml will panic if macro source text + /// is not possible to recover. + #[cfg(feature = "rawtext-stable-hack")] + pub fn macro_call_pattern(mut self, pattern: TokenStream) -> Self { + self.macro_pattern = + MacroPattern::from_token_stream(pattern).expect("No %% token found in pattern."); + self + } + /// Enables parsing for [`Node::Custom`] using a type implementing /// [`CustomNode`]. pub fn custom_node(self) -> ParserConfig { @@ -221,6 +291,8 @@ impl ParserConfig { always_self_closed_elements: self.always_self_closed_elements, raw_text_elements: self.raw_text_elements, element_close_wildcard: self.element_close_wildcard, + #[cfg(feature = "rawtext-stable-hack")] + macro_pattern: self.macro_pattern, custom_node: Default::default(), } } diff --git a/src/lib.rs b/src/lib.rs index 37eca6b..a748dd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -231,9 +231,13 @@ mod config; mod error; pub mod node; mod parser; +#[doc(hidden)] // Currently its api is not planned to be stable. +#[cfg(feature = "rawtext-stable-hack-module")] +pub mod rawtext_stable_hack; +pub mod visitor; pub use config::ParserConfig; pub use error::Error; -pub use node::atoms; +pub use node::{atoms, Infallible}; use node::{CustomNode, Node}; pub use parser::{recoverable, recoverable::ParsingResult, Parser}; @@ -252,7 +256,7 @@ pub fn parse(tokens: proc_macro::TokenStream) -> Result> { /// [`Node`]: struct.Node.html /// [`ParserConfig`]: struct.ParserConfig.html #[deprecated(since = "0.10.2", note = "use rstml::Parser::parse_simple instead")] -pub fn parse_with_config( +pub fn parse_with_config( tokens: proc_macro::TokenStream, config: ParserConfig, ) -> Result>> { @@ -273,7 +277,7 @@ pub fn parse2(tokens: proc_macro2::TokenStream) -> Result> { /// [`Node`]: struct.Node.html /// [`ParserConfig`]: struct.ParserConfig.html #[deprecated(since = "0.10.2", note = "use rstml::Parser::parse_simple instead")] -pub fn parse2_with_config( +pub fn parse2_with_config( tokens: proc_macro2::TokenStream, config: ParserConfig, ) -> Result>> { diff --git a/src/node/atoms.rs b/src/node/atoms.rs index 9b385e9..a70c445 100644 --- a/src/node/atoms.rs +++ b/src/node/atoms.rs @@ -43,7 +43,7 @@ pub(crate) mod tokens { // // /// Start part of doctype tag /// `` - #[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] + #[derive(Eq, PartialEq, Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] pub struct ComEnd { #[parse(parse::parse_array_of2_tokens)] #[to_tokens(parse::to_tokens_array)] @@ -72,7 +72,7 @@ pub(crate) mod tokens { /// End part of element's open tag /// `/>` or `>` - #[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] + #[derive(Eq, PartialEq, Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] pub struct OpenTagEnd { pub token_solidus: Option, pub token_gt: Token![>], @@ -81,7 +81,7 @@ pub(crate) mod tokens { /// Start part of element's close tag. /// Its commonly used as separator /// `` -#[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] +#[derive(Eq, PartialEq, Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] pub struct FragmentOpen { pub token_lt: Token![<], pub token_gt: Token![>], @@ -100,7 +100,7 @@ pub struct FragmentOpen { /// Fragment close part /// `` -#[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] +#[derive(Eq, PartialEq, Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)] pub struct FragmentClose { pub start_tag: tokens::CloseTagStart, pub token_gt: Token![>], diff --git a/src/node/mod.rs b/src/node/mod.rs index ef57214..f589302 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1,9 +1,10 @@ //! Tree of nodes. -use std::{convert::Infallible, fmt}; +use std::{convert, fmt}; use atoms::{tokens, FragmentClose, FragmentOpen}; use proc_macro2::{Ident, TokenStream}; +use quote::ToTokens; use syn::{parse::ParseStream, ExprPath, LitStr, Token}; pub mod atoms; @@ -18,10 +19,10 @@ pub use attribute::{ AttributeValueExpr, FnBinding, KeyedAttribute, KeyedAttributeValue, NodeAttribute, }; pub use node_name::{NodeName, NodeNameFragment}; -pub use node_value::NodeBlock; +pub use node_value::{InvalidBlock, NodeBlock}; pub use self::raw_text::RawText; -use crate::recoverable::RecoverableContext; +use crate::recoverable::{ParseRecoverable, RecoverableContext}; /// Node types. #[derive(Debug, Clone, PartialEq, Eq)] @@ -56,17 +57,32 @@ impl fmt::Display for NodeType { } /// Node in the tree. -#[derive(Clone, Debug, syn_derive::ToTokens)] -pub enum Node { +#[derive(Clone, Debug)] +pub enum Node { Comment(NodeComment), Doctype(NodeDoctype), Fragment(NodeFragment), Element(NodeElement), Block(NodeBlock), Text(NodeText), - RawText(RawText), + RawText(RawText), Custom(C), } +// Manual implementation, because derive macro doesn't support generics. +impl ToTokens for Node { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Comment(comment) => comment.to_tokens(tokens), + Self::Doctype(doctype) => doctype.to_tokens(tokens), + Self::Fragment(fragment) => fragment.to_tokens(tokens), + Self::Element(element) => element.to_tokens(tokens), + Self::Block(block) => block.to_tokens(tokens), + Self::Text(text) => text.to_tokens(tokens), + Self::RawText(raw_text) => raw_text.to_tokens(tokens), + Self::Custom(custom) => custom.to_tokens(tokens), + } + } +} impl Node { pub fn flatten(mut self) -> Vec { @@ -118,13 +134,24 @@ impl Node { /// /// A HTMLElement tag, with optional children and attributes. /// Potentially selfclosing. Any tag name is valid. -#[derive(Clone, Debug, syn_derive::ToTokens)] -pub struct NodeElement { +#[derive(Clone, Debug)] +pub struct NodeElement { pub open_tag: atoms::OpenTag, - #[to_tokens(parse::to_tokens_array)] pub children: Vec>, pub close_tag: Option, } +// Manual implementation, because derive macro doesn't support generics. +impl ToTokens for NodeElement { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.open_tag.to_tokens(tokens); + for child in &self.children { + child.to_tokens(tokens); + } + if let Some(close_tag) = &self.close_tag { + close_tag.to_tokens(tokens); + } + } +} impl NodeElement { pub fn name(&self) -> &NodeName { @@ -133,6 +160,15 @@ impl NodeElement { pub fn attributes(&self) -> &[NodeAttribute] { &self.open_tag.attributes } + pub fn attributes_mut(&mut self) -> &mut Vec { + &mut self.open_tag.attributes + } + pub fn chidlren(&self) -> &[Node] { + &self.children + } + pub fn children_mut(&mut self) -> &mut Vec> { + &mut self.children + } } /// Text node. @@ -182,16 +218,36 @@ pub struct NodeDoctype { /// Fragement node. /// /// Fragment: `<>` -#[derive(Clone, Debug, syn_derive::ToTokens)] -pub struct NodeFragment { +#[derive(Clone, Debug)] +pub struct NodeFragment { /// Open fragment token pub tag_open: FragmentOpen, /// Children of the fragment node. - #[to_tokens(parse::to_tokens_array)] pub children: Vec>, /// Close fragment token pub tag_close: Option, } +// Manual implementation, because derive macro doesn't support generics. +impl ToTokens for NodeFragment { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.tag_open.to_tokens(tokens); + for child in &self.children { + child.to_tokens(tokens); + } + if let Some(close_tag) = &self.tag_close { + close_tag.to_tokens(tokens); + } + } +} + +impl NodeFragment { + pub fn children(&self) -> &[Node] { + &self.children + } + pub fn children_mut(&mut self) -> &mut Vec> { + &mut self.children + } +} fn path_to_string(expr: &ExprPath) -> String { expr.path @@ -202,34 +258,37 @@ fn path_to_string(expr: &ExprPath) -> String { .join("::") } -pub trait CustomNode: Sized { - /// Should correspond to [`ToTokens::to_tokens`]. - /// - /// [`ToTokens::to_tokens`]: quote::ToTokens::to_tokens - fn to_tokens(&self, tokens: &mut TokenStream); +pub trait CustomNode: ParseRecoverable + ToTokens { /// Peeks the token stream to decide whether this node should be parsed. /// /// Recieves a [`ParseStream::fork`]. /// /// [`ParseStream::fork`]: syn::parse::ParseBuffer::fork fn peek_element(input: ParseStream) -> bool; - /// Parses the custom node, only called when [`peek_element`] returns - /// `true`. - /// - /// [`peek_element`]: Self::peek_element - fn parse_element(parser: &mut RecoverableContext, input: ParseStream) -> Option; } -impl CustomNode for Infallible { - fn to_tokens(&self, _tokens: &mut TokenStream) { - match *self {} - } +/// Newtype for `std::convert::Infallible` used to implement +/// `ToTokens` for `Infallible`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Infallible(convert::Infallible); - fn peek_element(_input: ParseStream) -> bool { - false +impl From for Infallible { + fn from(s: convert::Infallible) -> Self { + match s {} } - - fn parse_element(_parser: &mut RecoverableContext, _input: ParseStream) -> Option { +} +impl ToTokens for Infallible { + fn to_tokens(&self, _tokens: &mut TokenStream) { + match self.0 {} + } +} +impl ParseRecoverable for Infallible { + fn parse_recoverable(_: &mut RecoverableContext, _: ParseStream) -> Option { unreachable!("Infallible::peek_element returns false") } } +impl CustomNode for Infallible { + fn peek_element(_: ParseStream) -> bool { + false + } +} diff --git a/src/node/node_value.rs b/src/node/node_value.rs index 8671415..aefd6db 100644 --- a/src/node/node_value.rs +++ b/src/node/node_value.rs @@ -3,21 +3,25 @@ use std::convert::TryFrom; use proc_macro2::TokenStream; -use quote::ToTokens; use syn::{token::Brace, Block}; +#[derive(Clone, Debug, syn_derive::ToTokens, syn_derive::Parse)] +pub struct InvalidBlock { + #[syn(braced)] + brace: Brace, + #[syn(in = brace)] + body: TokenStream, +} + /// Block node. /// /// Arbitrary rust code in braced `{}` blocks. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, syn_derive::ToTokens)] pub enum NodeBlock { /// The block value.. ValidBlock(Block), - Invalid { - brace: Brace, - body: TokenStream, - }, + Invalid(InvalidBlock), } impl NodeBlock { @@ -40,7 +44,7 @@ impl NodeBlock { pub fn try_block(&self) -> Option<&Block> { match self { Self::ValidBlock(b) => Some(b), - Self::Invalid { .. } => None, + Self::Invalid(_) => None, } } } @@ -50,21 +54,10 @@ impl TryFrom for Block { fn try_from(v: NodeBlock) -> Result { match v { NodeBlock::ValidBlock(v) => Ok(v), - NodeBlock::Invalid { .. } => Err(syn::Error::new_spanned( + NodeBlock::Invalid(_) => Err(syn::Error::new_spanned( v, "Cant parse expression as block.", )), } } } - -impl ToTokens for NodeBlock { - fn to_tokens(&self, tokens: &mut TokenStream) { - match self { - Self::Invalid { brace, body } => { - brace.surround(tokens, |tokens| body.to_tokens(tokens)) - } - Self::ValidBlock(b) => b.to_tokens(tokens), - } - } -} diff --git a/src/node/parse.rs b/src/node/parse.rs index 860158e..7a23bb8 100644 --- a/src/node/parse.rs +++ b/src/node/parse.rs @@ -1,7 +1,7 @@ //! //! Implementation of ToTokens and Spanned for node related structs -use proc_macro2::{extra::DelimSpan, Delimiter, TokenStream, TokenTree}; +use proc_macro2::{extra::DelimSpan, Delimiter, TokenStream}; use proc_macro2_diagnostics::{Diagnostic, Level}; use quote::ToTokens; use syn::{ @@ -38,14 +38,7 @@ impl ParseRecoverable for NodeBlock { } Err(e) if parser.config().recover_block => { parser.push_diagnostic(e); - let try_block = || { - let content; - Ok(NodeBlock::Invalid { - brace: braced!(content in input), - body: content.parse()?, - }) - }; - parser.save_diagnostics(try_block())? + NodeBlock::Invalid(parser.parse_simple(input)?) } Err(e) => { parser.push_diagnostic(e); @@ -69,7 +62,7 @@ impl ParseRecoverable for NodeFragment { (vec![Node::::RawText(child)], closed_tag) } else { let (child, close_tag_start) = - parser.parse_tokens_until::, _, _>(input, CloseTagStart::parse); + parser.parse_tokens_until_call::, _, _>(input, CloseTagStart::parse); ( child, FragmentClose::parse_with_start_tag(parser, input, close_tag_start), @@ -149,7 +142,10 @@ impl ParseRecoverable for OpenTag { let generics = parser.parse_simple(input)?; let (attributes, end_tag) = parser - .parse_tokens_with_ending::(input, tokens::OpenTagEnd::parse); + .parse_tokens_with_conflicted_ending::( + input, + tokens::OpenTagEnd::parse, + ); if end_tag.is_none() { parser.push_diagnostic(Diagnostic::new(Level::Error, "expected end of tag '>'")); @@ -193,7 +189,7 @@ impl NodeElement { // invalid closing tags. // Also parse only , _, _>(input, CloseTagStart::parse); + parser.parse_tokens_until_call::, _, _>(input, CloseTagStart::parse); let close_tag = CloseTag::parse_with_start_tag(parser, input, close_tag); @@ -288,7 +284,7 @@ impl ParseRecoverable for NodeElement { impl ParseRecoverable for Node { fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { let node = if C::peek_element(&input.fork()) { - Node::Custom(C::parse_element(parser, input)?) + Node::Custom(C::parse_recoverable(parser, input)?) } else if input.peek(Token![<]) { if input.peek2(Token![!]) { if input.peek3(Ident) { @@ -307,7 +303,7 @@ impl ParseRecoverable for Node { Node::Text(parser.parse_simple(input)?) } else if !input.is_empty() { // Parse any input except of any other Node starting - Node::RawText(parser.parse_simple(input)?) + Node::RawText(parser.parse_recoverable(input)?) } else { return None; }; @@ -315,148 +311,6 @@ impl ParseRecoverable for Node { } } -impl RecoverableContext { - /// Parse array of toknes that is seperated by spaces(tabs, or new lines). - /// Stop parsing array when other branch could parse anything. - /// - /// Example: - /// ```ignore - /// # use syn::{parse::{Parser, ParseStream}, Ident, Result, parse_macro_input, Token}; - /// # use rstml::{parse_tokens_until}; - /// # fn main() -> syn::Result<()>{ - /// let tokens:proc_macro2::TokenStream = quote::quote!(few idents seperated by spaces and then minus sign - that will stop parsing).into(); - /// let concat_idents_without_minus = |input: ParseStream| -> Result { - /// let (idents, _minus) = parser.parse_tokens_until::(input, |i| - /// i.parse::() - /// )?; - /// let mut new_str = String::new(); - /// for ident in idents { - /// new_str.push_str(&ident.to_string()) - /// } - /// // .. skip rest idents in input - /// # while !input.is_empty() { - /// # input.parse::()?; - /// # } - /// Ok(new_str) - /// }; - /// let concated = concat_idents_without_minus.parse2(tokens)?; - /// assert_eq!(concated, "fewidentsseperatedbyspacesandthenminussign"); - /// # Ok(()) - /// # } - /// ``` - pub(crate) fn parse_tokens_until( - &mut self, - input: ParseStream, - stop: F, - ) -> (Vec, Option) - where - T: ParseRecoverable + Spanned, - F: Fn(ParseStream) -> syn::Result, - { - let mut collection = vec![]; - let res = loop { - let old_cursor = input.cursor(); - let fork = input.fork(); - if let Ok(res) = stop(&fork) { - input.advance_to(&fork); - break Some(res); - } - if let Some(o) = self.parse_recoverable(input) { - collection.push(o) - } - - if old_cursor == input.cursor() { - break None; - } - }; - (collection, res) - } - /// Two-phase parsing, firstly find separator, and then parse array of - /// tokens before separator. For simple inputs method work like - /// `parse_tokens_until`, but it creates intermediate TokenStream and - /// copy of all tokens until separator token is found. It is usefull - /// when separator (or it's part) can be treated as part of token T. - /// - /// - /// Example: - /// ```ignore - /// let tokens = quote!(some_expr_seperated + with - lt_gt * tokens <> other part); - /// ``` - /// In this example "<" can can be parsed as part of expression, but we want - /// to split tokens after "<>" was found. So instead of parsing all - /// input as expression, firstly we need to seperate it into two chunks. - pub(crate) fn parse_tokens_with_ending( - &mut self, - input: ParseStream, - separator: F, - ) -> (Vec, Option) - where - T: ParseRecoverable, - F: Fn(ParseStream) -> syn::Result, - { - let parser = |parser: &mut Self, tokens: TokenStream| { - let parse = |input: ParseStream| { - let mut collection = vec![]; - - while !input.is_empty() { - let old_cursor = input.cursor(); - if let Some(o) = parser.parse_recoverable(input) { - collection.push(o) - } - if old_cursor == input.cursor() { - break; - } - } - let eated_tokens = input.parse::()?; - Ok((collection, eated_tokens)) - }; - let (collection, eaten_tokens) = parse.parse2(tokens).expect("No errors allowed"); - if !eaten_tokens.is_empty() { - parser.push_diagnostic(Diagnostic::spanned( - eaten_tokens.span(), - Level::Error, - "tokens was ignored during parsing", - )) - } - collection - }; - self.parse_with_ending(input, parser, separator) - } - - pub(crate) fn parse_with_ending( - &mut self, - input: ParseStream, - parser: CNV, - ending: F, - ) -> (V, Option) - where - F: Fn(ParseStream) -> syn::Result, - CNV: Fn(&mut Self, TokenStream) -> V, - { - let mut tokens = TokenStream::new(); - let res = loop { - // Use fork, because we can't limit separator to be only Peekable for custom - // tokens but we also need to parse complex expressions like - // "foo=x/y" or "/>" - let fork = input.fork(); - if let Ok(end) = ending(&fork) { - input.advance_to(&fork); - break Some(end); - } - - if input.is_empty() { - break None; - } - - let next: TokenTree = self - .parse_simple(input) - .expect("TokenTree should always be parsable"); - tokens.extend([next]); - }; - (parser(self, tokens), res) - } -} - // This method couldn't be const generic until https://github.com/rust-lang/rust/issues/63569 /// Parse array of tokens with pub(super) fn parse_array_of2_tokens(input: ParseStream) -> syn::Result<[T; 2]> { diff --git a/src/node/parser_ext.rs b/src/node/parser_ext.rs index c3f3e31..eaec44c 100644 --- a/src/node/parser_ext.rs +++ b/src/node/parser_ext.rs @@ -1,7 +1,11 @@ use proc_macro2::{TokenStream, TokenTree}; -use syn::parse::{discouraged::Speculative, Parse, ParseStream}; +use proc_macro2_diagnostics::{Diagnostic, Level}; +use syn::{ + parse::{discouraged::Speculative, Parse, ParseStream, Parser}, + spanned::Spanned, +}; -use crate::recoverable::RecoverableContext; +use crate::recoverable::{ParseRecoverable, RecoverableContext}; impl RecoverableContext { /// Like [`parse_simple`], but splits the tokenstream at `E` first only @@ -9,17 +13,14 @@ impl RecoverableContext { /// /// **Note:** This is an internal function exported to make parsing of /// custom nodes easier. It has some quirks, e.g., - /// `parse_simple_with_ending]>`, would not support any + /// `parse_simple_until]>`, would not support any /// [`Expr`] containing a `>`. /// /// It is not considered stable. /// /// [`parse_simple`]: #method.parse_simple /// [`Expr`]: https://docs.rs/syn/latest/syn/enum.Expr.html - pub fn parse_simple_with_ending( - &mut self, - input: ParseStream, - ) -> Option<(T, E)> { + pub fn parse_simple_until(&mut self, input: ParseStream) -> Option<(T, E)> { let mut tokens = TokenStream::new(); let res = loop { // Use fork, because we can't limit separator to be only Peekable for custom @@ -45,4 +46,154 @@ impl RecoverableContext { .map(|val| (val, res)) }) } + + /// Parse array of toknes using recoverable parser. + /// Stops parsing when other branch could parse anything. + /// + /// **Note:** This is an internal function exported to make parsing of + /// custom nodes easier. + /// It is not considered stable. + /// + /// Example: + /// ```ignore + /// # use syn::{parse::{Parser, ParseStream}, Ident, Result, parse_macro_input, Token}; + /// # use rstml::{parse_tokens_until}; + /// # fn main() -> syn::Result<()>{ + /// let tokens:proc_macro2::TokenStream = quote::quote!(few idents seperated by spaces and then minus sign - that will stop parsing).into(); + /// let concat_idents_without_minus = |input: ParseStream| -> Result { + /// let (idents, _minus) = parser.parse_tokens_until::(input, |i| + /// i.parse::() + /// )?; + /// let mut new_str = String::new(); + /// for ident in idents { + /// new_str.push_str(&ident.to_string()) + /// } + /// // .. skip rest idents in input + /// # while !input.is_empty() { + /// # input.parse::()?; + /// # } + /// Ok(new_str) + /// }; + /// let concated = concat_idents_without_minus.parse2(tokens)?; + /// assert_eq!(concated, "fewidentsseperatedbyspacesandthenminussign"); + /// # Ok(()) + /// # } + /// ``` + pub fn parse_tokens_until_call( + &mut self, + input: ParseStream, + stop_fn: F, + ) -> (Vec, Option) + where + T: ParseRecoverable + Spanned, + F: Fn(ParseStream) -> syn::Result, + { + let mut collection = vec![]; + let res = loop { + let old_cursor = input.cursor(); + let fork = input.fork(); + if let Ok(res) = stop_fn(&fork) { + input.advance_to(&fork); + break Some(res); + } + if let Some(o) = self.parse_recoverable(input) { + collection.push(o) + } + + if old_cursor == input.cursor() { + break None; + } + }; + (collection, res) + } + /// Two-phase parsing, firstly find separator, and then parses array of + /// tokens before separator. + /// For simple input this method will work like + /// `parse_tokens_until`. + /// Internally it creates intermediate `TokenStream`` and + /// copy of all tokens until separator token is found. It is usefull + /// when separator (or it's part) can be treated as part of token T. + /// + /// + /// **Note:** This is an internal function exported to make parsing of + /// custom nodes easier. + /// It is not considered stable. + /// + /// Example: + /// ```ignore + /// let tokens = quote!(some_expr_seperated + with - lt_gt * tokens <> other part); + /// ``` + /// In this example "<" can can be parsed as part of expression, but we want + /// to split tokens after "<>" was found. So instead of parsing all + /// input as expression, firstly we need to seperate it into two chunks. + pub fn parse_tokens_with_conflicted_ending( + &mut self, + input: ParseStream, + separator: F, + ) -> (Vec, Option) + where + T: ParseRecoverable, + F: Fn(ParseStream) -> syn::Result, + { + let parser = |parser: &mut Self, tokens: TokenStream| { + let parse = |input: ParseStream| { + let mut collection = vec![]; + + while !input.is_empty() { + let old_cursor = input.cursor(); + if let Some(o) = parser.parse_recoverable(input) { + collection.push(o) + } + if old_cursor == input.cursor() { + break; + } + } + let eated_tokens = input.parse::()?; + Ok((collection, eated_tokens)) + }; + let (collection, eaten_tokens) = parse.parse2(tokens).expect("No errors allowed"); + if !eaten_tokens.is_empty() { + parser.push_diagnostic(Diagnostic::spanned( + eaten_tokens.span(), + Level::Error, + "tokens was ignored during parsing", + )) + } + collection + }; + self.parse_with_ending(input, parser, separator) + } + + pub(crate) fn parse_with_ending( + &mut self, + input: ParseStream, + parser: CNV, + ending: F, + ) -> (V, Option) + where + F: Fn(ParseStream) -> syn::Result, + CNV: Fn(&mut Self, TokenStream) -> V, + { + let mut tokens = TokenStream::new(); + let res = loop { + // Use fork, because we can't limit separator to be only Peekable for custom + // tokens but we also need to parse complex expressions like + // "foo=x/y" or "/>" + let fork = input.fork(); + if let Ok(end) = ending(&fork) { + input.advance_to(&fork); + break Some(end); + } + + if input.is_empty() { + break None; + } + + let next: TokenTree = self + .parse_simple(input) + .expect("TokenTree should always be parsable"); + tokens.extend([next]); + }; + (parser(self, tokens), res) + } } diff --git a/src/node/raw_text.rs b/src/node/raw_text.rs index 4566a79..b72a6d3 100644 --- a/src/node/raw_text.rs +++ b/src/node/raw_text.rs @@ -1,13 +1,12 @@ +use std::marker::PhantomData; + +use derive_where::derive_where; use proc_macro2::{Span, TokenStream, TokenTree}; use quote::ToTokens; -use syn::{ - parse::{Parse, ParseStream}, - spanned::Spanned, - token::Brace, - LitStr, Token, -}; +use syn::{parse::ParseStream, spanned::Spanned, token::Brace, LitStr, Token}; -use super::{CustomNode, Node}; +use super::{CustomNode, Infallible, Node}; +use crate::recoverable::ParseRecoverable; /// Raw unquoted text /// @@ -25,13 +24,41 @@ use super::{CustomNode, Node}; /// source_text method is not available in `quote!` context, or in context where /// input is generated by another macro. In still can return default formatting /// for TokenStream. -#[derive(Clone, Debug, Default)] -pub struct RawText { +#[derive_where(Clone, Debug)] +pub struct RawText { token_stream: TokenStream, // Span that started before previous token, and after next. context_span: Option<(Span, Span)>, + #[cfg(feature = "rawtext-stable-hack-module")] + recovered_text: Option, + // Use type parameter to make it possible to find custom nodes in the raw_node. + _c: PhantomData, } -impl RawText { + +impl Default for RawText { + fn default() -> Self { + Self { + token_stream: Default::default(), + context_span: Default::default(), + #[cfg(feature = "rawtext-stable-hack-module")] + recovered_text: Default::default(), + _c: PhantomData, + } + } +} + +impl RawText { + /// Custom node type parameter is used only for parsing, so it can be + /// changed during usage. + pub fn convert_custom(self) -> RawText { + RawText { + token_stream: self.token_stream, + context_span: self.context_span, + #[cfg(feature = "rawtext-stable-hack-module")] + recovered_text: self.recovered_text, + _c: PhantomData, + } + } pub(crate) fn set_tag_spans(&mut self, before: impl Spanned, after: impl Spanned) { // todo: use span.after/before when it will be available in proc_macro2 // for now just join full span an remove tokens from it. @@ -84,11 +111,14 @@ impl RawText { self.token_stream.is_empty() } - pub(crate) fn vec_set_context( + pub(crate) fn vec_set_context( open_tag_end: Span, close_tag_start: Option, mut children: Vec>, - ) -> Vec> { + ) -> Vec> + where + C: CustomNode, + { let spans: Vec = Some(open_tag_end) .into_iter() .chain(children.iter().map(|n| n.span())) @@ -104,44 +134,96 @@ impl RawText { } /// Trying to return best string representation available: - /// 1. calls `to_source_text(true)` - /// 2. calls `to_source_text(false)` - /// 3. as fallback calls `to_token_stream_string()` + /// 1. calls `to_source_text_hack()`. + /// 2. calls `to_source_text(true)` + /// 3. calls `to_source_text(false)` + /// 4. as fallback calls `to_token_stream_string()` pub fn to_string_best(&self) -> String { + #[cfg(feature = "rawtext-stable-hack-module")] + if let Some(recovered) = &self.recovered_text { + return recovered.clone(); + } self.to_source_text(true) .or_else(|| self.to_source_text(false)) .unwrap_or_else(|| self.to_token_stream_string()) } + + // Returns text recovered using recover_space_hack. + // If feature "rawtext-stable-hack-module" wasn't activated returns None. + // + // Recovered text, tries to save whitespaces if possible. + pub fn to_source_text_hack(&self) -> Option { + #[cfg(feature = "rawtext-stable-hack-module")] + { + return self.recovered_text.clone(); + } + #[cfg(not(feature = "rawtext-stable-hack-module"))] + None + } + + #[cfg(feature = "rawtext-stable-hack-module")] + pub(crate) fn recover_space(&mut self, other: &Self) { + self.recovered_text = Some( + other + .to_source_text(self.context_span.is_some()) + .expect("Cannot recover space in this context"), + ) + } + #[cfg(feature = "rawtext-stable-hack-module")] + pub(crate) fn init_recover_space(&mut self, init: String) { + self.recovered_text = Some(init) + } +} + +impl RawText { + /// Returns true if we on nightly rust and join_spans available + pub fn is_source_text_available() -> bool { + // TODO: Add feature join_spans check. + cfg!(rstml_signal_nightly) + } } -impl Parse for RawText { - fn parse(input: ParseStream) -> syn::Result { +impl ParseRecoverable for RawText { + fn parse_recoverable( + parser: &mut crate::recoverable::RecoverableContext, + input: ParseStream, + ) -> Option { let mut token_stream = TokenStream::new(); - let any_node = - |input: ParseStream| input.peek(Token![<]) || input.peek(Brace) || input.peek(LitStr); + let any_node = |input: ParseStream| { + input.peek(Token![<]) + || input.peek(Brace) + || input.peek(LitStr) + || C::peek_element(&input.fork()) + }; // Parse any input until catching any node. // Fail only on eof. while !any_node(input) && !input.is_empty() { - token_stream.extend([input.parse::()?]) + token_stream.extend([parser.save_diagnostics(input.parse::())?]) } - Ok(Self { + Some(Self { token_stream, context_span: None, + #[cfg(feature = "rawtext-stable-hack-module")] + recovered_text: None, + _c: PhantomData, }) } } -impl ToTokens for RawText { +impl ToTokens for RawText { fn to_tokens(&self, tokens: &mut TokenStream) { self.token_stream.to_tokens(tokens) } } -impl From for RawText { +impl From for RawText { fn from(token_stream: TokenStream) -> Self { Self { token_stream, context_span: None, + #[cfg(feature = "rawtext-stable-hack-module")] + recovered_text: None, + _c: PhantomData, } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9db4f7c..66d082c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8,9 +8,13 @@ use syn::{parse::ParseStream, spanned::Spanned, Result}; pub mod recoverable; +#[cfg(feature = "rawtext-stable-hack")] +use {proc_macro2::Span, std::str::FromStr}; + use self::recoverable::{ParseRecoverable, ParsingResult, RecoverableContext}; +#[cfg(feature = "rawtext-stable-hack")] +use crate::rawtext_stable_hack; use crate::{node::*, ParserConfig}; - /// /// Primary library interface to RSX Parser /// @@ -21,7 +25,7 @@ pub struct Parser { config: ParserConfig, } -impl Parser { +impl Parser { /// Create a new parser with the given [`ParserConfig`]. pub fn new(config: ParserConfig) -> Self { Parser { config } @@ -41,9 +45,67 @@ impl Parser { /// of partial parsing. pub fn parse_recoverable(&self, v: impl Into) -> ParsingResult>> { use syn::parse::Parser as _; + let parser = move |input: ParseStream| Ok(self.parse_syn_stream(input)); - let res = parser.parse2(v.into()); - res.expect("No errors from parser") + let source = parser.parse2(v.into()).expect("No errors from parser"); + + #[cfg(feature = "rawtext-stable-hack")] + // re-parse using proc_macro2::fallback, only if output without error + let source = Self::reparse_raw_text(&self, parser, source); + source + } + #[cfg(feature = "rawtext-stable-hack")] + fn reparse_raw_text( + &self, + parser: Parser, + mut source: ParsingResult>>, + ) -> ParsingResult>> + where + Parser: FnOnce(ParseStream) -> syn::Result>>>, + { + use syn::parse::Parser as _; + // in case we already have valid raw_text, we can skip re-parsing + if rawtext_stable_hack::is_join_span_available() { + return source; + } + // Source is err, so we need to use fallback. + if !source.is_ok() { + let (mut source, errors) = source.split_vec(); + rawtext_stable_hack::inject_raw_text_default(&mut source); + return ParsingResult::from_parts_vec(source, errors); + } + // return error, if macro source_text is not available. + if !rawtext_stable_hack::is_macro_args_recoverable() { + source.push_diagnostic(Diagnostic::new( + proc_macro2_diagnostics::Level::Warning, + "Failed to retrive source text of macro call, maybe macro was called from other macro?", + )); + return source; + } + // Feature is additive, this mean that top-level crate can activate + // "rawtext-stable-hack", but other crates will not use macro_pattern + if self.config.macro_pattern.is_empty() { + return source; + } + let text = Span::call_site() + .source_text() + .expect("Source text should be available"); + + proc_macro2::fallback::force(); + let stream = TokenStream::from_str(&text).unwrap(); + let stream = self + .config + .macro_pattern + .match_content(stream) + .expect("Cannot find macro pattern inside Span::call_site"); + let hacked = parser.parse2(stream).expect("No errors from parser"); + + let mut source = source.into_result().expect("was checked"); + let hacked = hacked.into_result().expect("was checked"); + proc_macro2::fallback::unforce(); + rawtext_stable_hack::inject_raw_text(&mut source, &hacked); + + return ParsingResult::Ok(source); } /// Parse a given [`ParseStream`]. diff --git a/src/parser/recoverable.rs b/src/parser/recoverable.rs index 3c2afe2..dd7d64b 100644 --- a/src/parser/recoverable.rs +++ b/src/parser/recoverable.rs @@ -45,6 +45,7 @@ use syn::parse::{Parse, ParseStream}; use crate::{ config::{ElementWildcardFn, TransformBlockFn}, + node::CustomNode, ParserConfig, }; @@ -52,7 +53,7 @@ use crate::{ /// Used to extend parsing functionality by user needs. /// /// Can't be created directly, instead use [`From::from`]. -#[derive(Default)] +#[derive(Default, Clone)] pub struct RecoveryConfig { /// /// Try to parse invalid syn::Block as something. @@ -67,6 +68,16 @@ pub struct RecoveryConfig { /// Allows wildcard closing tag matching for blocks pub(crate) element_close_wildcard: Option>, } +impl PartialEq for RecoveryConfig { + fn eq(&self, other: &Self) -> bool { + self.recover_block == other.recover_block + && self.always_self_closed_elements == other.always_self_closed_elements + && self.raw_text_elements == other.raw_text_elements + && self.transform_block.is_some() == other.transform_block.is_some() + && self.element_close_wildcard.is_some() == other.element_close_wildcard.is_some() + } +} +impl Eq for RecoveryConfig {} impl Debug for RecoveryConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -89,11 +100,26 @@ impl Debug for RecoveryConfig { /// Used to save [`Diagnostic`] messages or [`syn::Result`]. /// /// Also can be extended with user needs through [`RecoveryConfig`]. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct RecoverableContext { pub(super) diagnostics: Vec, config: RecoveryConfig, } + +impl PartialEq for RecoverableContext { + fn eq(&self, other: &Self) -> bool { + if self.diagnostics.len() != other.diagnostics.len() || self.config != other.config { + return false; + } + + self.diagnostics + .iter() + .zip(other.diagnostics.iter()) + .all(|(a, b)| format!("{:?}", a) == format!("{:?}", b)) + } +} +impl Eq for RecoverableContext {} + impl RecoverableContext { pub fn new(config: RecoveryConfig) -> Self { Self { @@ -113,7 +139,20 @@ impl RecoverableContext { match input.parse() { Ok(v) => Some(v), Err(e) => { - self.diagnostics.push(e.into()); + self.push_diagnostic(e); + None + } + } + } + /// Parse token using closure + pub fn parse_mixed_fn(&mut self, input: ParseStream, mut parser: F) -> Option + where + F: FnMut(&mut Self, ParseStream) -> Result, + { + match parser(self, input) { + Ok(v) => Some(v), + Err(e) => { + self.push_diagnostic(e); None } } @@ -131,7 +170,7 @@ impl RecoverableContext { match val { Ok(v) => Some(v), Err(e) => { - self.diagnostics.push(e.into()); + self.push_diagnostic(e); None } } @@ -140,7 +179,13 @@ impl RecoverableContext { /// Push custom message of [`syn::Error`] or /// [`proc_macro2_diagnostics::Diagnostic`] pub fn push_diagnostic(&mut self, diagnostic: impl Into) { - self.diagnostics.push(diagnostic.into()); + let diag = diagnostic.into(); + // println!( + // "Push diagnostic: {:?}, backtrace={}", + // diag, + // std::backtrace::Backtrace::capture() + // ); + self.diagnostics.push(diag); } } @@ -169,12 +214,13 @@ impl ParsingResult { /// /// Convert into [`syn::Result`], with fail on first diagnostic message, + /// Ignores any diagnostic non error message when result is available. /// Returns Error on [`ParsingResult::Failed`], and /// [`ParsingResult::Partial`]. pub fn into_result(self) -> syn::Result { match self { ParsingResult::Ok(r) => Ok(r), - ParsingResult::Failed(errors) | ParsingResult::Partial(_, errors) => Err(errors + ParsingResult::Failed(errors) => Err(errors .into_iter() .next() .unwrap_or_else(|| { @@ -184,6 +230,17 @@ impl ParsingResult { ) }) .into()), + ParsingResult::Partial(ok, errors) => { + if let Some(err) = errors + .into_iter() + .filter(|p| p.level() == Level::Error) + .next() + { + Err(err.into()) + } else { + Ok(ok) + } + } } } @@ -194,6 +251,22 @@ impl ParsingResult { Self::Partial(r, errors) => (Some(r), errors), } } + + pub fn push_diagnostic(&mut self, diagnostic: Diagnostic) { + *self = match std::mem::replace(self, ParsingResult::Failed(vec![])) { + Self::Ok(r) => Self::Partial(r, vec![diagnostic]), + Self::Failed(errors) => { + Self::Failed(errors.into_iter().chain(Some(diagnostic)).collect()) + } + Self::Partial(r, errors) => { + Self::Partial(r, errors.into_iter().chain(Some(diagnostic)).collect()) + } + }; + } + + pub fn is_ok(&self) -> bool { + matches!(self, Self::Ok(_)) + } } impl ParsingResult> { @@ -201,6 +274,13 @@ impl ParsingResult> { let (r, e) = self.split(); (r.unwrap_or_default(), e) } + pub fn from_parts_vec(value: Vec, errors: Vec) -> Self { + match (value, errors) { + (v, err) if err.is_empty() => Self::Ok(v), + (v, err) if !v.is_empty() => Self::Partial(v, err), + (_, err) => Self::Failed(err), + } + } } impl From> for ParsingResult { @@ -215,7 +295,7 @@ impl From> for ParsingResult { } } -impl From> for RecoveryConfig { +impl From> for RecoveryConfig { fn from(config: ParserConfig) -> Self { RecoveryConfig { recover_block: config.recover_block, diff --git a/src/rawtext_stable_hack.rs b/src/rawtext_stable_hack.rs new file mode 100644 index 0000000..0586c74 --- /dev/null +++ b/src/rawtext_stable_hack.rs @@ -0,0 +1,344 @@ +//! Recover space from token stream, using hack described in https://github.com/rs-tml/rstml/issues/5 +//! This hack can be activated using feature = "rawtext-stable-hack", it has no +//! penalty in nightly, or rust-analyzer but on stable it parses input two +//! times. + +use proc_macro2::{Span, TokenStream, TokenTree}; + +use crate::node::{CustomNode, Node}; + +/// Returns true if join span is available. +pub fn is_join_span_available() -> bool { + let join_call_span = Span::call_site().join(Span::call_site()).is_some(); + + // Hack: Rust analyzer will return first span on join, but source_text is + // missing in this case. + let source_text_available = Span::call_site().source_text().is_some(); + join_call_span && source_text_available +} + +/// Returns true if current macro call is available inside file. +/// Returns false if it is from other macro call. +pub fn is_macro_args_recoverable() -> bool { + Span::call_site().source_text().is_some() +} + +// Inject default text to every raw node, just to avoid panics. +pub fn inject_raw_text_default(source: &mut [Node]) { + for source in source.into_iter() { + replace_node_default(source) + } +} + +// Inject raw text to every raw node, recovered using proc-macro2 second +// parsing; +pub fn inject_raw_text( + source: &mut [Node], + hacked: &[Node], +) { + assert_eq!( + source.len(), + hacked.len(), + "Second parsing return different result in recover_space_hack" + ); + for (source, hacked) in source.into_iter().zip(hacked) { + replace_node(source, hacked) + } +} + +pub fn replace_node_default(source: &mut Node) { + match source { + Node::RawText(source )=> source.init_recover_space(String::from("")), + Node::Fragment(source) => { + inject_raw_text_default(&mut source.children) + } + Node::Element(source) => { + inject_raw_text_default(&mut source.children) + } + Node::Doctype(_) | // => source.value.recover_space(&hacked.value), + Node::Block(_) + | Node::Comment(_) + | Node::Custom(_) + | Node::Text(_) => {} + } +} + +pub fn replace_node(source: &mut Node, hacked: &Node) { + match (source, hacked) { + (Node::RawText(source), Node::RawText(hacked)) => source.recover_space(&hacked), + (Node::Fragment(source), Node::Fragment(hacked)) => { + inject_raw_text(&mut source.children, &hacked.children) + } + (Node::Element(source), Node::Element(hacked)) => { + inject_raw_text(&mut source.children, &hacked.children) + } + (Node::Doctype(_), Node::Doctype(_)) | // => source.value.recover_space(&hacked.value), + (Node::Block(_), Node::Block(_)) + | (Node::Comment(_), Node::Comment(_)) + | (Node::Custom(_), Node::Custom(_)) + | (Node::Text(_), Node::Text(_)) => {} + (source, hacked) => { + panic!( + "Mismatched node type in recover_space_hack {:?}, {:?}", + source, hacked + ) + } + } +} + +// TODO: Add possibility to check macro name. +#[derive(Debug, Clone)] +pub enum TokenStreamOperations { + SkipToken(usize), + SkipUntil(TokenStream), + UnwrapGroup, +} + +#[derive(Clone, Debug, Default)] +pub struct MacroPattern { + macro_operations: Vec, +} + +impl MacroPattern { + pub fn new() -> Self { + Self::default() + } + pub fn skip_tokens(mut self, num_tokens: usize) -> Self { + assert!(num_tokens > 0, "num_tokens should be > 0"); + if let Some(TokenStreamOperations::SkipToken(ref mut already_skipped)) = + self.macro_operations.last_mut() + { + *already_skipped += num_tokens; + } else { + self.macro_operations + .push(TokenStreamOperations::SkipToken(num_tokens)); + } + self + } + + pub fn unwrap_group(mut self) -> Self { + self.macro_operations + .push(TokenStreamOperations::UnwrapGroup); + self + } + + pub fn skip_until(mut self, expected_stream: TokenStream) -> Self { + self.macro_operations + .push(TokenStreamOperations::SkipUntil(expected_stream)); + self + } + /// Try to create `RecoverSpacePattern` from token_stream example. + /// Find first occurence of '%%' tokens, and use its position in tokens as + /// marker. Example: + /// original macro: + /// ```no_compile + /// html! {some_context, provided, [ can use guards, etc], {
}, [other context]}; + /// RecoverSpacePattern::from_token_stream(quote!( + /// html! {ident, ident, // can use real idents, or any other + /// [/* can ignore context of auxilary groups */], + /// {%%}, // important part + /// [] + /// } + /// )) + /// .unwrap() + /// ``` + pub fn from_token_stream(stream: TokenStream) -> Option { + let mut pattern = MacroPattern::new(); + let mut stream = stream.into_iter(); + + // skip any token before first '!' + // to allow using macro with differnet paths. + while let Some(t) = stream.next() { + if let TokenTree::Punct(p) = t { + if p.as_char() == '!' { + break; + } + } + } + pattern = pattern.skip_until(quote::quote! {!}); + + if let Some(post_res) = Self::from_token_stream_inner(stream) { + pattern + .macro_operations + .extend_from_slice(&post_res.macro_operations); + return Some(pattern); + } + + None + } + fn from_token_stream_inner(stream: impl IntoIterator) -> Option { + let mut pattern = MacroPattern::new(); + + let mut last_token_percent = false; + + for tt in stream { + match tt { + TokenTree::Group(group) => { + if let Some(post_res) = Self::from_token_stream_inner(group.stream()) { + pattern = pattern.unwrap_group(); + pattern + .macro_operations + .extend_from_slice(&post_res.macro_operations); + return Some(pattern); + } + } + TokenTree::Punct(p) => match (p.as_char(), last_token_percent) { + ('%', true) => return Some(pattern), + ('%', false) => { + last_token_percent = true; + // ignore skip + continue; + } + (_, true) => { + last_token_percent = false; + // skip previous token + pattern = pattern.skip_tokens(1); + } + (_, false) => {} + }, + _ => {} + } + pattern = pattern.skip_tokens(1) + } + None + } + + /// Try to capture macro input from outer `TokenStream` using initialized + /// `MacroPattern`. Returns None if needed TokenTree::Group wasn't + /// found. + /// + /// "html!{/*content*/}" ==> "/*content*/" + /// If pattern `is_empty` returns source stream. + pub fn match_content(&self, source: TokenStream) -> Option { + let mut stream = source.into_iter(); + for op in &self.macro_operations { + match op { + TokenStreamOperations::SkipToken(num) => { + let _ = stream.nth(num - 1); + } + TokenStreamOperations::UnwrapGroup => match stream.next() { + Some(TokenTree::Group(g)) => stream = g.stream().into_iter(), + _ => return None, + }, + // Skip tokens until expected [`TokenTree`] is found. + // Compare `TokenTree` using `to_string()` function. + TokenStreamOperations::SkipUntil(expected) => 'compare: loop { + let needed = expected.clone().into_iter(); + for expected in needed { + let Some(t) = stream.next() else { return None }; + + if t.to_string() != expected.to_string() { + continue 'compare; + } + } + break; + }, + } + } + Some(stream.collect()) + } + + pub fn is_empty(&self) -> bool { + self.macro_operations.is_empty() + } +} + +#[cfg(test)] +mod test { + use quote::quote; + + use super::MacroPattern; + + #[test] + fn macro_content_matcher() { + let pattern = MacroPattern::from_token_stream(quote! {html!{ctx, other_arg, %%}}).unwrap(); + let content = pattern + .match_content(quote! {html!{ctx, div,
}}) + .unwrap(); + + assert_eq!( + content.to_string(), + quote! {
}.to_string() + ); + } + + #[test] + fn macro_content_matcher_ignore_single_percent() { + let pattern = MacroPattern::from_token_stream(quote! {html!{ctx, %other_arg, %%}}).unwrap(); + // also check that ignore percent inside html input + let content = pattern + .match_content(quote! {html!{ctx, %div,
%%
}}) + .unwrap(); + + assert_eq!( + content.to_string(), + quote! {
%%
}.to_string() + ); + } + + #[test] + fn macro_content_matcher_group() { + // group type does not count + let pattern = + MacroPattern::from_token_stream(quote! {html!{ctx, other_arg, {%%}}}).unwrap(); + let content = pattern + .match_content(quote! {html!{ctx, div, [
]}}) + .unwrap(); + + assert_eq!( + content.to_string(), + quote! {
}.to_string() + ); + } + + #[test] + fn macro_content_matcher_with_postfix_group() { + // group type does not count + let pattern = MacroPattern::from_token_stream( + quote! {html!{ctx, other_arg, {%%}, any other context}}, + ) + .unwrap(); + let content = pattern + .match_content(quote! {html!{ctx, div, [
], foo}}) + .unwrap(); + + assert_eq!( + content.to_string(), + quote! {
}.to_string() + ); + } + + #[test] + fn extend_macro_matcher_using_until_token() { + // group type does not count + let pattern = MacroPattern::from_token_stream( + quote! {html!{ctx, other_arg, {%%}, any other context}}, + ) + .unwrap() + .skip_until(quote! {}); + + let content = pattern + .match_content(quote! {html!{ctx, div, [any content before Baz tag is ignored
{foo:red}
], foo}}) + .unwrap(); + + assert_eq!( + content.to_string(), + quote! {
}.to_string() + ); + } + + #[test] + fn check_macro_patch_differ() { + let pattern = MacroPattern::from_token_stream(quote! {html!{ctx, other_arg, %%}}).unwrap(); + let content = pattern + .match_content( + quote! {some_other::path::to_macro::html!{ctx, div,
}}, + ) + .unwrap(); + + assert_eq!( + content.to_string(), + quote! {
}.to_string() + ); + } +} diff --git a/src/visitor.rs b/src/visitor.rs new file mode 100644 index 0000000..0204522 --- /dev/null +++ b/src/visitor.rs @@ -0,0 +1,690 @@ +use std::marker::PhantomData; + +use super::node::*; +use crate::{ + atoms::{CloseTag, OpenTag}, + Infallible, +}; + +/// Enum that represents the different types with valid Rust code that can be +/// visited using `syn::Visitor`. Usually `syn::Block` or `syn::Expr`. +pub enum RustCode<'a> { + Block(&'a mut syn::Block), + Expr(&'a mut syn::Expr), + LitStr(&'a mut syn::LitStr), + Pat(&'a mut syn::Pat), +} +/// Visitor api provide a way to traverse the node tree and modify its +/// components. The api allows modification of all types of nodes, and some +/// atoms like InvalidBlock or NodeName. +/// +/// Each method returns a bool that indicates if the visitor should continue to +/// traverse the tree. If the method returns false, the visitor will stop +/// traversing the tree. +/// +/// By default Visitor are abstract over CustomNode, but it is possible to +/// implement a Visitor for concrete CustomNode. +pub trait Visitor { + // Visit node types + fn visit_node(&mut self, _node: &mut Node) -> bool { + true + } + fn visit_block(&mut self, _node: &mut NodeBlock) -> bool { + true + } + fn visit_comment(&mut self, _node: &mut NodeComment) -> bool { + true + } + fn visit_doctype(&mut self, _node: &mut NodeDoctype) -> bool { + true + } + fn visit_raw_node(&mut self, _node: &mut RawText) -> bool { + true + } + fn visit_custom(&mut self, _node: &mut Custom) -> bool { + true + } + fn visit_text_node(&mut self, _node: &mut NodeText) -> bool { + true + } + fn visit_element(&mut self, _node: &mut NodeElement) -> bool { + true + } + fn visit_fragment(&mut self, _node: &mut NodeFragment) -> bool { + true + } + + // Visit atoms + fn visit_rust_code(&mut self, _code: RustCode) -> bool { + true + } + fn visit_invalid_block(&mut self, _block: &mut InvalidBlock) -> bool { + true + } + fn visit_node_name(&mut self, _name: &mut NodeName) -> bool { + true + } + + fn visit_open_tag(&mut self, _open_tag: &mut OpenTag) -> bool { + true + } + fn visit_close_tag(&mut self, _closed_tag: &mut CloseTag) -> bool { + true + } + // Visit Attributes + fn visit_attribute(&mut self, _attribute: &mut NodeAttribute) -> bool { + true + } + fn visit_keyed_attribute(&mut self, _attribute: &mut KeyedAttribute) -> bool { + true + } + fn visit_attribute_flag(&mut self, _key: &mut NodeName) -> bool { + true + } + fn visit_attribute_binding(&mut self, _key: &mut NodeName, _value: &mut FnBinding) -> bool { + true + } + fn visit_attribute_value( + &mut self, + _key: &mut NodeName, + _value: &mut AttributeValueExpr, + ) -> bool { + true + } +} + +#[derive(Debug, Default, Clone, PartialEq, PartialOrd, Ord, Copy, Eq)] +pub struct AnyWalker(PhantomData); + +/// Define walker for `CustomNode`. +pub trait CustomNodeWalker { + type Custom: CustomNode; + fn walk_custom_node_fields>( + visitor: &mut VisitorImpl, + node: &mut Self::Custom, + ) -> bool; +} + +impl CustomNodeWalker for AnyWalker +where + C: CustomNode, +{ + type Custom = C; + fn walk_custom_node_fields>( + _visitor: &mut VisitorImpl, + _node: &mut C, + ) -> bool { + true + } +} + +macro_rules! visit_inner { + ($self:ident.$visitor:ident.$method:ident($($tokens:tt)*)) => { + if !$self.$visitor.$method($($tokens)*) { + return false; + } + }; +} + +macro_rules! try_visit { + ($self:ident.$method:ident($($tokens:tt)*)) => { + if !$self.$method($($tokens)*) { + return false; + } + }; +} + +/// Wrapper for visitor that calls inner visitors. +/// Inner visitor should implement `Visitor` trait and +/// `syn::visit_mut::VisitMut`. +/// +/// For regular usecases it is recommended to use `visit_nodes`, +/// `visit_nodes_with_custom` or `visit_attributes` functions. +/// +/// But if you need it can be used by calling `visit_*` methods directly. +/// +/// Example: +/// ```rust +/// use quote::quote; +/// use rstml::{ +/// node::{Node, NodeText}, +/// visitor::{Visitor, Walker}, +/// Infallible, +/// }; +/// use syn::parse_quote; +/// +/// struct TestVisitor; +/// impl Visitor for TestVisitor { +/// fn visit_text_node(&mut self, node: &mut NodeText) -> bool { +/// *node = parse_quote!("modified"); +/// true +/// } +/// } +/// impl syn::visit_mut::VisitMut for TestVisitor {} +/// +/// let mut visitor = Walker::new(TestVisitor); +/// +/// let tokens = quote! { +///
+/// "Some raw text" +/// "And text after span" +///
+/// }; +/// let mut nodes = rstml::parse2(tokens).unwrap(); +/// for node in &mut nodes { +/// visitor.visit_node(node); +/// } +/// let result = quote! { +/// #(#nodes)* +/// }; +/// assert_eq!( +/// result.to_string(), +/// quote! { +///
+/// "modified" +/// "modified" +///
+/// } +/// .to_string() +/// ); +/// ``` +pub struct Walker> +where + C: CustomNode, + V: Visitor + syn::visit_mut::VisitMut, + CW: CustomNodeWalker, +{ + visitor: V, + // we use callbakc instead of marker for `CustomNodeWalker` + // because it will fail to resolve with infinite recursion + walker: PhantomData, + _pd: PhantomData, +} + +impl Walker +where + C: CustomNode, + V: Visitor + syn::visit_mut::VisitMut, +{ + pub fn new(visitor: V) -> Self { + Self { + visitor, + walker: PhantomData, + _pd: PhantomData, + } + } + pub fn with_custom_handler(visitor: V) -> Walker + where + OtherCW: CustomNodeWalker, + { + Walker { + visitor, + walker: PhantomData, + _pd: PhantomData, + } + } +} +impl Walker +where + C: CustomNode, + V: Visitor + syn::visit_mut::VisitMut, + CW: CustomNodeWalker, +{ + pub fn destruct(self) -> V { + self.visitor + } +} + +impl Visitor for Walker +where + C: CustomNode, + V: Visitor + syn::visit_mut::VisitMut, + CW: CustomNodeWalker, +{ + fn visit_node(&mut self, node: &mut Node) -> bool { + visit_inner!(self.visitor.visit_node(node)); + + match node { + Node::Block(b) => self.visit_block(b), + Node::Comment(c) => self.visit_comment(c), + Node::Doctype(d) => self.visit_doctype(d), + Node::Element(e) => self.visit_element(e), + Node::Fragment(f) => self.visit_fragment(f), + Node::Text(t) => self.visit_text_node(t), + Node::RawText(r) => self.visit_raw_node(r), + Node::Custom(c) => self.visit_custom(c), + } + } + fn visit_block(&mut self, node: &mut NodeBlock) -> bool { + visit_inner!(self.visitor.visit_block(node)); + + match node { + NodeBlock::Invalid(b) => self.visit_invalid_block(b), + NodeBlock::ValidBlock(b) => self.visit_rust_code(RustCode::Block(b)), + } + } + fn visit_comment(&mut self, node: &mut NodeComment) -> bool { + visit_inner!(self.visitor.visit_comment(node)); + + self.visit_rust_code(RustCode::LitStr(&mut node.value)) + } + fn visit_doctype(&mut self, node: &mut NodeDoctype) -> bool { + visit_inner!(self.visitor.visit_doctype(node)); + + self.visit_raw_node(&mut node.value) + } + fn visit_raw_node(&mut self, node: &mut RawText) -> bool { + visit_inner!(self.visitor.visit_raw_node(node)); + + true + } + fn visit_custom(&mut self, node: &mut C) -> bool { + visit_inner!(self.visitor.visit_custom(node)); + + CW::walk_custom_node_fields(self, node) + } + fn visit_text_node(&mut self, node: &mut NodeText) -> bool { + visit_inner!(self.visitor.visit_text_node(node)); + + self.visit_rust_code(RustCode::LitStr(&mut node.value)) + } + fn visit_element(&mut self, node: &mut NodeElement) -> bool { + visit_inner!(self.visitor.visit_element(node)); + + try_visit!(self.visit_open_tag(&mut node.open_tag)); + + for attribute in node.attributes_mut() { + try_visit!(self.visit_attribute(attribute)) + } + for child in node.children_mut() { + try_visit!(self.visit_node(child)) + } + + if let Some(close_tag) = &mut node.close_tag { + try_visit!(self.visit_close_tag(close_tag)); + } + true + } + fn visit_fragment(&mut self, node: &mut NodeFragment) -> bool { + visit_inner!(self.visitor.visit_fragment(node)); + + for child in node.children_mut() { + try_visit!(self.visit_node(child)) + } + true + } + + fn visit_open_tag(&mut self, open_tag: &mut OpenTag) -> bool { + visit_inner!(self.visitor.visit_open_tag(open_tag)); + + try_visit!(self.visit_node_name(&mut open_tag.name)); + + true + } + fn visit_close_tag(&mut self, closed_tag: &mut CloseTag) -> bool { + visit_inner!(self.visitor.visit_close_tag(closed_tag)); + + try_visit!(self.visit_node_name(&mut closed_tag.name)); + + true + } + + fn visit_attribute(&mut self, attribute: &mut NodeAttribute) -> bool { + visit_inner!(self.visitor.visit_attribute(attribute)); + + match attribute { + NodeAttribute::Attribute(a) => self.visit_keyed_attribute(a), + NodeAttribute::Block(b) => self.visit_block(b), + } + } + fn visit_keyed_attribute(&mut self, attribute: &mut KeyedAttribute) -> bool { + visit_inner!(self.visitor.visit_keyed_attribute(attribute)); + + match &mut attribute.possible_value { + KeyedAttributeValue::None => self.visit_attribute_flag(&mut attribute.key), + KeyedAttributeValue::Binding(b) => self.visit_attribute_binding(&mut attribute.key, b), + KeyedAttributeValue::Value(v) => self.visit_attribute_value(&mut attribute.key, v), + } + } + fn visit_attribute_flag(&mut self, key: &mut NodeName) -> bool { + visit_inner!(self.visitor.visit_attribute_flag(key)); + true + } + fn visit_attribute_binding(&mut self, key: &mut NodeName, value: &mut FnBinding) -> bool { + visit_inner!(self.visitor.visit_attribute_binding(key, value)); + + for input in value.inputs.iter_mut() { + try_visit!(self.visit_rust_code(RustCode::Pat(input))) + } + true + } + fn visit_attribute_value( + &mut self, + key: &mut NodeName, + value: &mut AttributeValueExpr, + ) -> bool { + visit_inner!(self.visitor.visit_attribute_value(key, value)); + + self.visit_node_name(key); + self.visit_rust_code(RustCode::Expr(&mut value.value)) + } + + fn visit_invalid_block(&mut self, block: &mut InvalidBlock) -> bool { + visit_inner!(self.visitor.visit_invalid_block(block)); + + true + } + fn visit_node_name(&mut self, name: &mut NodeName) -> bool { + visit_inner!(self.visitor.visit_node_name(name)); + + true + } + fn visit_rust_code(&mut self, mut code: RustCode) -> bool { + { + // use rewrap because enum `RustCode` is not Copy + let rewrap = match &mut code { + RustCode::Block(b) => RustCode::Block(b), + RustCode::Expr(e) => RustCode::Expr(e), + RustCode::LitStr(l) => RustCode::LitStr(l), + RustCode::Pat(p) => RustCode::Pat(p), + }; + visit_inner!(self.visitor.visit_rust_code(rewrap)); + } + + match code { + RustCode::Block(b) => self.visitor.visit_block_mut(b), + RustCode::Expr(e) => self.visitor.visit_expr_mut(e), + RustCode::LitStr(l) => self.visitor.visit_lit_str_mut(l), + RustCode::Pat(p) => self.visitor.visit_pat_mut(p), + } + + true + } +} +/// Visitor entrypoint. +/// Visit nodes in array calling visitor methods. +/// Recursively visit nodes in children, and attributes. +/// +/// Return modified visitor back +pub fn visit_nodes(nodes: &mut [Node], visitor: V) -> V +where + C: CustomNode, + V: Visitor + syn::visit_mut::VisitMut, +{ + let mut visitor = Walker::::new(visitor); + for node in nodes { + visitor.visit_node(node); + } + visitor.visitor +} + +/// Visitor entrypoint. +/// Visit nodes in array calling visitor methods. +/// Recursively visit nodes in children, and attributes. +/// Provide custom handler that is used to visit custom nodes. +/// Custom handler should return true if visitor should continue to traverse, +/// and call visitor methods for its children. +/// +/// Return modified visitor back +pub fn visit_nodes_with_custom(nodes: &mut [Node], visitor: V) -> V +where + C: CustomNode, + V: Visitor + syn::visit_mut::VisitMut, + CW: CustomNodeWalker, +{ + let mut visitor = Walker::with_custom_handler::(visitor); + for node in nodes { + visitor.visit_node(node); + } + visitor.visitor +} + +/// Visit attributes in array calling visitor methods. +pub fn visit_attributes(attributes: &mut [NodeAttribute], visitor: V) -> V +where + V: Visitor + syn::visit_mut::VisitMut, + Walker: Visitor, +{ + let mut visitor = Walker::new(visitor); + for attribute in attributes { + visitor.visit_attribute(attribute); + } + visitor.visitor +} +#[cfg(test)] +mod tests { + + use quote::{quote, ToTokens}; + use syn::parse_quote; + + use super::*; + use crate::Infallible; + #[test] + fn collect_node_names() { + #[derive(Default)] + struct TestVisitor { + collected_names: Vec, + } + impl Visitor for TestVisitor { + fn visit_node_name(&mut self, name: &mut NodeName) -> bool { + self.collected_names.push(name.clone()); + true + } + } + // empty impl + impl syn::visit_mut::VisitMut for TestVisitor {} + + let stream = quote! { +
+ + +
+ + + }; + let mut nodes = crate::parse2(stream).unwrap(); + let visitor = visit_nodes(&mut nodes, TestVisitor::default()); + // convert node_names to string; + let node_names = visitor + .collected_names + .iter() + .map(|name| name.to_string()) + .collect::>(); + + assert_eq!( + node_names, + vec!["div", "span", "span", "span", "span", "div", "foo", "key", "foo"] + ); + } + + #[test] + fn collect_node_elements() { + #[derive(Default)] + struct TestVisitor { + collected_names: Vec, + } + impl Visitor for TestVisitor { + fn visit_element(&mut self, node: &mut NodeElement) -> bool { + self.collected_names.push(node.open_tag.name.clone()); + true + } + } + // empty impl + impl syn::visit_mut::VisitMut for TestVisitor {} + + let stream = quote! { +
+ + +
+ + + }; + let mut nodes = crate::parse2(stream).unwrap(); + let visitor = visit_nodes(&mut nodes, TestVisitor::default()); + // convert node_names to string; + let node_names = visitor + .collected_names + .iter() + .map(|name| name.to_string()) + .collect::>(); + + assert_eq!(node_names, vec!["div", "span", "span", "foo"]); + } + + #[test] + fn collect_rust_blocks() { + #[derive(Default)] + struct TestVisitor { + collected_blocks: Vec, + } + // empty impl + impl Visitor for TestVisitor {} + impl syn::visit_mut::VisitMut for TestVisitor { + fn visit_block_mut(&mut self, i: &mut syn::Block) { + self.collected_blocks.push(i.clone()); + } + } + + let stream = quote! { +
+ { let block = "in node position"; } + + +
+ + }; + let mut nodes = crate::parse2(stream).unwrap(); + let visitor = visit_nodes(&mut nodes, TestVisitor::default()); + // convert node_names to string; + let blocks = visitor + .collected_blocks + .iter() + .map(|block| block.to_token_stream().to_string()) + .collect::>(); + + assert_eq!( + blocks, + vec![ + "{ let block = \"in node position\" ; }", + "{ block_in_attr_position = foo }", + "{ block_in_value }", + ] + ); + } + + #[test] + fn collect_raw_text() { + #[derive(Default)] + struct TestVisitor { + collected_raw_text: Vec>, + } + impl Visitor for TestVisitor { + fn visit_raw_node(&mut self, node: &mut RawText) -> bool { + let raw = node.clone().convert_custom::(); + self.collected_raw_text.push(raw); + true + } + } + // empty impl + impl syn::visit_mut::VisitMut for TestVisitor {} + + let stream = quote! { + +
+ Some raw text + And text after span +
+ + + }; + let mut nodes = crate::parse2(stream).unwrap(); + let visitor = visit_nodes(&mut nodes, TestVisitor::default()); + // convert collected_raw_text to string; + let raw_text = visitor + .collected_raw_text + .iter() + .map(|raw| raw.to_string_best()) + .collect::>(); + + assert_eq!( + raw_text, + vec!["Other raw text", "Some raw text", "And text after span",] + ); + } + + #[test] + fn collect_string_literals() { + #[derive(Default)] + struct TestVisitor { + collected_literals: Vec, + } + impl Visitor for TestVisitor {} + impl syn::visit_mut::VisitMut for TestVisitor { + fn visit_lit_str_mut(&mut self, i: &mut syn::LitStr) { + self.collected_literals.push(i.clone()); + } + } + + let stream = quote! { + +
+ "Some raw text" + "And text after span" +
+ + + }; + let mut nodes = crate::parse2(stream).unwrap(); + let visitor = visit_nodes(&mut nodes, TestVisitor::default()); + // convert collected_literals to string; + let literals = visitor + .collected_literals + .iter() + .map(|lit| lit.value()) + .collect::>(); + + assert_eq!( + literals, + vec!["Some raw text", "And text after span", "comment"] + ); + } + + #[test] + fn modify_text_visitor() { + struct TestVisitor; + impl Visitor for TestVisitor { + fn visit_text_node(&mut self, node: &mut NodeText) -> bool { + *node = parse_quote!("modified"); + true + } + } + impl syn::visit_mut::VisitMut for TestVisitor {} + + let mut visitor = Walker::new(TestVisitor); + + let tokens = quote! { +
+ "Some raw text" + "And text after span" +
+ }; + let mut nodes = crate::parse2(tokens).unwrap(); + for node in &mut nodes { + visitor.visit_node(node); + } + let result = quote! { + #(#nodes)* + }; + assert_eq!( + result.to_string(), + quote! { +
+ "modified" + "modified" +
+ } + .to_string() + ); + } +} diff --git a/tests/custom_node.rs b/tests/custom_node.rs index 98969c6..26eb4bf 100644 --- a/tests/custom_node.rs +++ b/tests/custom_node.rs @@ -1,8 +1,8 @@ -use quote::{quote, ToTokens, TokenStreamExt}; +use quote::{quote, TokenStreamExt}; use rstml::{ atoms::{self, OpenTag, OpenTagEnd}, node::{CustomNode, Node, NodeElement}, - recoverable::Recoverable, + recoverable::{ParseRecoverable, Recoverable}, Parser, ParserConfig, }; use syn::{parse_quote, Expr, Token}; @@ -17,23 +17,14 @@ struct If { body: Vec, close_tag: Option, } - -impl CustomNode for If { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - ToTokens::to_tokens(&self, tokens) - } - - fn peek_element(input: syn::parse::ParseStream) -> bool { - input.peek(Token![<]) && input.peek2(Token![if]) - } - - fn parse_element( +impl ParseRecoverable for If { + fn parse_recoverable( parser: &mut rstml::recoverable::RecoverableContext, input: syn::parse::ParseStream, ) -> Option { let token_lt = OpenTag::parse_start_tag(parser, input)?; let token_if = parser.parse_simple(input)?; - let (condition, open_tag_end): (_, OpenTagEnd) = parser.parse_simple_with_ending(input)?; + let (condition, open_tag_end): (_, OpenTagEnd) = parser.parse_simple_until(input)?; let (body, close_tag) = if open_tag_end.token_solidus.is_none() { // Passed to allow parsing of close_tag NodeElement::parse_children( @@ -62,6 +53,12 @@ impl CustomNode for If { } } +impl CustomNode for If { + fn peek_element(input: syn::parse::ParseStream) -> bool { + input.peek(Token![<]) && input.peek2(Token![if]) + } +} + #[test] fn custom_node() { let actual: Recoverable> = parse_quote! { diff --git a/tests/test.rs b/tests/test.rs index 88794da..79aaf32 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -5,11 +5,20 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use rstml::{ node::{ - KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, NodeElement, NodeName, NodeType, + CustomNode, KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, NodeElement, + NodeName, NodeType, }, - parse2, Parser, ParserConfig, + parse2, + recoverable::{ParseRecoverable, RecoverableContext}, + Parser, ParserConfig, +}; +use syn::{ + bracketed, + parse::ParseStream, + parse_quote, + token::{Bracket, Colon}, + Block, LifetimeParam, Pat, PatType, Token, TypeParam, }; -use syn::{parse_quote, token::Colon, Block, LifetimeParam, Pat, PatType, Token, TypeParam}; #[test] fn test_single_empty_element() -> Result<()> { @@ -73,6 +82,61 @@ fn test_single_element_with_unquoted_text_simple() -> Result<()> { Ok(()) } +#[test] +fn test_css_selector_unquoted_text() -> Result<()> { + let tokens = quote! { + // Note two spaces between bar and baz + --css-selector & with @strange + .puncts + }; + + let nodes = parse2(tokens)?; + let Node::RawText(child) = &nodes[0] else { + panic!("expected child") + }; + + // We can't use source text if token stream was created with quote!. + assert_eq!( + child.to_token_stream_string(), + "- - css - selector & with @ strange + . puncts" + ); + assert_eq!(child.to_token_stream_string(), child.to_string_best()); + Ok(()) +} + +#[test] +fn test_css_selector_unquoted_text_string() -> Result<()> { + let tokens = TokenStream::from_str( + r#" + + "#, + ) + .unwrap(); + + let nodes = parse2(tokens)?; + let Node::RawText(child) = get_element_child(&nodes, 0, 0) else { + panic!("expected child") + }; + + // source text should be available + assert_eq!( + child.to_source_text(true).unwrap(), + " --css-selector & with @strange + .puncts " + ); + assert_eq!( + child.to_source_text(false).unwrap(), + "--css-selector & with @strange + .puncts" + ); + + // without source text - it will return invalid css + assert_eq!( + child.to_token_stream_string(), + "-- css - selector & with @ strange + . puncts" + ); + // When source is available, best should + assert_eq!(child.to_string_best(), child.to_source_text(true).unwrap()); + Ok(()) +} + #[test] fn test_single_element_with_unquoted_text_advance() -> Result<()> { let tokens = TokenStream::from_str( @@ -96,6 +160,38 @@ fn test_single_element_with_unquoted_text_advance() -> Result<()> { Ok(()) } +#[derive(Clone, Debug)] +struct TestCustomNode { + bracket: Bracket, + data: TokenStream, +} + +impl ToTokens for TestCustomNode { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.bracket.surround(tokens, |c| self.data.to_tokens(c)) + } +} + +impl ParseRecoverable for TestCustomNode { + fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option { + let inner_parser = |_parser: &mut RecoverableContext, input: ParseStream| { + let content; + let bracket = bracketed!(content in input); + Ok(Some(TestCustomNode { + bracket, + data: content.parse()?, + })) + }; + parser.parse_mixed_fn(input, inner_parser)? + } +} + +impl CustomNode for TestCustomNode { + fn peek_element(input: ParseStream) -> bool { + input.peek(Bracket) + } +} + macro_rules! test_unquoted { ($($name:ident => $constructor: expr => Node::$getter:ident($bind:ident) => $check:expr ;)* ) => { $( @@ -106,13 +202,12 @@ macro_rules! test_unquoted { let tokens = TokenStream::from_str( concat!(" bar bar ", $constructor, " baz baz ") ).unwrap(); + let nodes = Parser::new(ParserConfig::default().custom_node::()).parse_simple(tokens)?; + let Node::RawText(child1) = get_element_child(&*nodes, 0, 0) else { panic!("expected unquoted child") }; - let nodes = parse2(tokens)?; - let Node::RawText(child1) = get_element_child(&nodes, 0, 0) else { panic!("expected unquoted child") }; - - let Node::$getter($bind) = get_element_child(&nodes, 0, 1) else { panic!("expected matcher child") }; + let Node::$getter($bind) = get_element_child(&*nodes, 0, 1) else { panic!("expected matcher child") }; - let Node::RawText(child3) = get_element_child(&nodes, 0, 2) else { panic!("expected unquoted child") }; + let Node::RawText(child3) = get_element_child(&*nodes, 0, 2) else { panic!("expected unquoted child") }; // source text should be available assert_eq!(child1.to_source_text(true).unwrap(), " bar bar "); @@ -166,7 +261,9 @@ test_unquoted!( assert!(child.close_tag.is_none()); }; - + custom_node => "[bracketed text]" => Node::Custom(v) => { + assert_eq!(v.data.to_string(), "bracketed text"); + }; ); #[test] @@ -893,15 +990,15 @@ fn test_consecutive_puncts_in_name() { assert_eq!(name.to_string(), "a--::..d"); } -fn get_element(nodes: &[Node], element_index: usize) -> &NodeElement { +fn get_element(nodes: &[Node], element_index: usize) -> &NodeElement { let Some(Node::Element(element)) = nodes.get(element_index) else { panic!("expected element") }; element } -fn get_element_attribute( - nodes: &[Node], +fn get_element_attribute( + nodes: &[Node], element_index: usize, attribute_index: usize, ) -> &KeyedAttribute { @@ -916,7 +1013,11 @@ fn get_element_attribute( attribute } -fn get_element_child(nodes: &[Node], element_index: usize, child_index: usize) -> &Node { +fn get_element_child( + nodes: &[Node], + element_index: usize, + child_index: usize, +) -> &Node { let Some(Node::Element(element)) = nodes.get(element_index) else { panic!("expected element") };