From 698dfe26ed4160b9c4de5fb8af8400a041c3bc5f Mon Sep 17 00:00:00 2001 From: Ryan Balsdon Date: Thu, 13 Jul 2023 14:01:47 -0400 Subject: [PATCH 1/3] Reworked proc macros to allow for tests and added some tests --- macros/Cargo.toml | 2 + macros/src/grammar.lalrpop | 6 +- macros/src/lib.rs | 83 +- macros/src/span.rs | 8 +- macros/src/tests/declare-anchor-element.rs | 805 ++++++++++++++++++++ macros/src/tests/generate-anchor-element.rs | 20 + macros/src/tests/generate-button-event.rs | 6 + tests/main.rs | 129 ++++ 8 files changed, 1046 insertions(+), 13 deletions(-) create mode 100644 macros/src/tests/declare-anchor-element.rs create mode 100644 macros/src/tests/generate-anchor-element.rs create mode 100644 macros/src/tests/generate-button-event.rs diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 62ec8a7..931febc 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -20,6 +20,8 @@ lalrpop-util = "0.19" proc-macro2 = "1.0.54" quote = "1.0.26" console = "0.15.5" +rust-format = { version="0.3.4", features=["pretty_please"]} +pretty_assertions = "1.4.0" [build-dependencies] lalrpop = "0.19.9" diff --git a/macros/src/grammar.lalrpop b/macros/src/grammar.lalrpop index 172313e..198c83d 100644 --- a/macros/src/grammar.lalrpop +++ b/macros/src/grammar.lalrpop @@ -60,14 +60,14 @@ HtmlIdent: Ident = { "-")*> => { let mut init = init; init.push(last); - let (span, name) = init.into_iter().fold((None, String::new()), |(span, name), token| { + let (span, name): (Option, String) = init.into_iter().fold((None, String::new()), |(span, name), token| { ( match span { - None => Some(token.span().unstable()), + None => Some(token.span()), Some(span) => { #[cfg(can_join_spans)] { - span.join(token.span().unstable()) + span.join(token.span()) } #[cfg(not(can_join_spans))] { diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 298758c..ac11056 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,9 +1,10 @@ #![recursion_limit = "128"] #![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))] -extern crate proc_macro; +#[allow(unused_imports)] +use rust_format::Formatter; -use proc_macro::TokenStream; +extern crate proc_macro; mod config; mod declare; @@ -21,10 +22,13 @@ mod span; /// /// [axohtml]: https://docs.rs/axohtml/ #[proc_macro] -pub fn html(input: TokenStream) -> TokenStream { +pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + html_impl(input.into()).into() +} +fn html_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { let stream = lexer::unroll_stream(input.into(), false); let result = html::expand_html(&stream); - TokenStream::from(match result { + proc_macro2::TokenStream::from(match result { Err(err) => error::parse_error(&stream, &err), Ok((node, ty)) => match node.into_token_stream(&ty) { Err(err) => err, @@ -56,10 +60,14 @@ pub fn dodrio(input: TokenStream) -> TokenStream { /// This macro is used by `axohtml` internally to generate types and /// implementations for HTML elements. #[proc_macro] -pub fn declare_elements(input: TokenStream) -> TokenStream { +pub fn declare_elements(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + declare_elements_impl(input.into()).into() +} + +fn declare_elements_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { let stream = lexer::keywordise(lexer::unroll_stream(input.into(), true)); let result = declare::expand_declare(&stream); - TokenStream::from(match result { + proc_macro2::TokenStream::from(match result { Err(err) => error::parse_error(&stream, &err), Ok(decls) => { let mut out = proc_macro2::TokenStream::new(); @@ -70,3 +78,66 @@ pub fn declare_elements(input: TokenStream) -> TokenStream { } }) } + +#[test] +fn test_declare_anchor_element() { + use quote::quote; + + let input = quote!( + a { + download: String, + href: Uri, + hreflang: LanguageTag, + ping: SpacedList, + rel: SpacedList, + target: Target, + type: Mime, + } in [FlowContent, PhrasingContent, InteractiveContent] with FlowContent; + ); + + let output = declare_elements_impl(input); + + pretty_assertions::assert_eq!( + rust_format::PrettyPlease::default().format_str(output.to_string()).unwrap(), + include_str!("tests/declare-anchor-element.rs") + ) +} + +#[test] +fn test_html_anchor_element() { + use quote::quote; + + let input = quote!( + + "Visit W3Schools.com!" + + ); + + let output = html_impl(input); + let output = format!("fn html() {}", output.to_string()); + + pretty_assertions::assert_eq!( + rust_format::PrettyPlease::default().format_str(output.to_string()).unwrap(), + include_str!("tests/generate-anchor-element.rs") + ) +} + +#[test] +fn test_html_button_event() { + use quote::quote; + + let input = quote!( + + : String + ); + + let output = html_impl(input); + let output = format!("fn html() {}", output.to_string()); + + pretty_assertions::assert_eq!( + rust_format::PrettyPlease::default().format_str(output.to_string()).unwrap(), + include_str!("tests/generate-button-event.rs") + ) +} \ No newline at end of file diff --git a/macros/src/span.rs b/macros/src/span.rs index 6f1cb10..3c1c003 100644 --- a/macros/src/span.rs +++ b/macros/src/span.rs @@ -1,7 +1,7 @@ -pub fn from_unstable(span: proc_macro::Span) -> proc_macro2::Span { - let ident = proc_macro::Ident::new("_", span); - let tt = proc_macro::TokenTree::Ident(ident); - let tts = proc_macro::TokenStream::from(tt); +pub fn from_unstable(span: proc_macro2::Span) -> proc_macro2::Span { + let ident = proc_macro2::Ident::new("_", span); + let tt = proc_macro2::TokenTree::Ident(ident); + let tts = proc_macro2::TokenStream::from(tt); let tts2 = proc_macro2::TokenStream::from(tts); tts2.into_iter().next().unwrap().span() } diff --git a/macros/src/tests/declare-anchor-element.rs b/macros/src/tests/declare-anchor-element.rs new file mode 100644 index 0000000..d2ec972 --- /dev/null +++ b/macros/src/tests/declare-anchor-element.rs @@ -0,0 +1,805 @@ +pub struct Attrs_a { + pub r#accesskey: Option, + pub r#aria_autocomplete: Option, + pub r#aria_checked: Option, + pub r#aria_disabled: Option, + pub r#aria_errormessage: Option, + pub r#aria_expanded: Option, + pub r#aria_haspopup: Option, + pub r#aria_hidden: Option, + pub r#aria_invalid: Option, + pub r#aria_label: Option, + pub r#aria_modal: Option, + pub r#aria_multiline: Option, + pub r#aria_multiselectable: Option, + pub r#aria_orientation: Option, + pub r#aria_placeholder: Option, + pub r#aria_pressed: Option, + pub r#aria_readonly: Option, + pub r#aria_required: Option, + pub r#aria_selected: Option, + pub r#aria_sort: Option, + pub r#aria_valuemax: Option, + pub r#aria_valuemin: Option, + pub r#aria_valuenow: Option, + pub r#aria_valuetext: Option, + pub r#autocapitalize: Option, + pub r#class: Option, + pub r#contenteditable: Option, + pub r#contextmenu: Option, + pub r#dir: Option, + pub r#download: Option, + pub r#draggable: Option, + pub r#hidden: Option, + pub r#href: Option, + pub r#hreflang: Option, + pub r#id: Option, + pub r#is: Option, + pub r#lang: Option, + pub r#ping: Option>, + pub r#rel: Option>, + pub r#role: Option, + pub r#style: Option, + pub r#tabindex: Option, + pub r#target: Option, + pub r#title: Option, + pub r#type: Option, +} +pub struct a +where + T: crate::OutputType + Send, +{ + pub attrs: Attrs_a, + pub data_attributes: Vec<(&'static str, String)>, + pub aria_attributes: Vec<(&'static str, String)>, + pub events: T::Events, + pub children: Vec>>, +} +impl a +where + T: crate::OutputType + Send, +{ + pub fn new() -> Self { + a { + events: T::Events::default(), + attrs: Attrs_a { + r#accesskey: None, + r#aria_autocomplete: None, + r#aria_checked: None, + r#aria_disabled: None, + r#aria_errormessage: None, + r#aria_expanded: None, + r#aria_haspopup: None, + r#aria_hidden: None, + r#aria_invalid: None, + r#aria_label: None, + r#aria_modal: None, + r#aria_multiline: None, + r#aria_multiselectable: None, + r#aria_orientation: None, + r#aria_placeholder: None, + r#aria_pressed: None, + r#aria_readonly: None, + r#aria_required: None, + r#aria_selected: None, + r#aria_sort: None, + r#aria_valuemax: None, + r#aria_valuemin: None, + r#aria_valuenow: None, + r#aria_valuetext: None, + r#autocapitalize: None, + r#class: None, + r#contenteditable: None, + r#contextmenu: None, + r#dir: None, + r#download: None, + r#draggable: None, + r#hidden: None, + r#href: None, + r#hreflang: None, + r#id: None, + r#is: None, + r#lang: None, + r#ping: None, + r#rel: None, + r#role: None, + r#style: None, + r#tabindex: None, + r#target: None, + r#title: None, + r#type: None, + }, + data_attributes: Vec::new(), + aria_attributes: Vec::new(), + children: Vec::new(), + } + } +} +impl crate::dom::Node for a +where + T: crate::OutputType + Send, +{ + fn vnode(&'_ mut self) -> crate::dom::VNode<'_, T> { + let mut attributes = Vec::new(); + if let Some(ref value) = self.attrs.r#accesskey { + attributes.push(("accesskey", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_autocomplete { + attributes.push(("aria_autocomplete", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_checked { + attributes.push(("aria_checked", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_disabled { + attributes.push(("aria_disabled", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_errormessage { + attributes.push(("aria_errormessage", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_expanded { + attributes.push(("aria_expanded", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_haspopup { + attributes.push(("aria_haspopup", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_hidden { + attributes.push(("aria_hidden", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_invalid { + attributes.push(("aria_invalid", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_label { + attributes.push(("aria_label", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_modal { + attributes.push(("aria_modal", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_multiline { + attributes.push(("aria_multiline", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_multiselectable { + attributes.push(("aria_multiselectable", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_orientation { + attributes.push(("aria_orientation", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_placeholder { + attributes.push(("aria_placeholder", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_pressed { + attributes.push(("aria_pressed", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_readonly { + attributes.push(("aria_readonly", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_required { + attributes.push(("aria_required", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_selected { + attributes.push(("aria_selected", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_sort { + attributes.push(("aria_sort", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuemax { + attributes.push(("aria_valuemax", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuemin { + attributes.push(("aria_valuemin", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuenow { + attributes.push(("aria_valuenow", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuetext { + attributes.push(("aria_valuetext", value.to_string())); + } + if let Some(ref value) = self.attrs.r#autocapitalize { + attributes.push(("autocapitalize", value.to_string())); + } + if let Some(ref value) = self.attrs.r#class { + attributes.push(("class", value.to_string())); + } + if let Some(ref value) = self.attrs.r#contenteditable { + attributes.push(("contenteditable", value.to_string())); + } + if let Some(ref value) = self.attrs.r#contextmenu { + attributes.push(("contextmenu", value.to_string())); + } + if let Some(ref value) = self.attrs.r#dir { + attributes.push(("dir", value.to_string())); + } + if let Some(ref value) = self.attrs.r#download { + attributes.push(("download", value.to_string())); + } + if let Some(ref value) = self.attrs.r#draggable { + attributes.push(("draggable", value.to_string())); + } + if let Some(ref value) = self.attrs.r#hidden { + attributes.push(("hidden", value.to_string())); + } + if let Some(ref value) = self.attrs.r#href { + attributes.push(("href", value.to_string())); + } + if let Some(ref value) = self.attrs.r#hreflang { + attributes.push(("hreflang", value.to_string())); + } + if let Some(ref value) = self.attrs.r#id { + attributes.push(("id", value.to_string())); + } + if let Some(ref value) = self.attrs.r#is { + attributes.push(("is", value.to_string())); + } + if let Some(ref value) = self.attrs.r#lang { + attributes.push(("lang", value.to_string())); + } + if let Some(ref value) = self.attrs.r#ping { + attributes.push(("ping", value.to_string())); + } + if let Some(ref value) = self.attrs.r#rel { + attributes.push(("rel", value.to_string())); + } + if let Some(ref value) = self.attrs.r#role { + attributes.push(("role", value.to_string())); + } + if let Some(ref value) = self.attrs.r#style { + attributes.push(("style", value.to_string())); + } + if let Some(ref value) = self.attrs.r#tabindex { + attributes.push(("tabindex", value.to_string())); + } + if let Some(ref value) = self.attrs.r#target { + attributes.push(("target", value.to_string())); + } + if let Some(ref value) = self.attrs.r#title { + attributes.push(("title", value.to_string())); + } + if let Some(ref value) = self.attrs.r#type { + attributes.push(("type", value.to_string())); + } + attributes.extend(self.data_attributes.clone()); + attributes.extend(self.aria_attributes.clone()); + let mut children = Vec::new(); + for child in &mut self.children { + children.push(child.vnode()); + } + crate::dom::VNode::Element(crate::dom::VElement { + name: "a", + attributes, + events: &mut self.events, + children, + }) + } +} +impl crate::dom::Element for a +where + T: crate::OutputType + Send, +{ + fn name() -> &'static str { + "a" + } + fn attribute_names() -> &'static [&'static str] { + &[ + "accesskey", + "aria_autocomplete", + "aria_checked", + "aria_disabled", + "aria_errormessage", + "aria_expanded", + "aria_haspopup", + "aria_hidden", + "aria_invalid", + "aria_label", + "aria_modal", + "aria_multiline", + "aria_multiselectable", + "aria_orientation", + "aria_placeholder", + "aria_pressed", + "aria_readonly", + "aria_required", + "aria_selected", + "aria_sort", + "aria_valuemax", + "aria_valuemin", + "aria_valuenow", + "aria_valuetext", + "autocapitalize", + "class", + "contenteditable", + "contextmenu", + "dir", + "download", + "draggable", + "hidden", + "href", + "hreflang", + "id", + "is", + "lang", + "ping", + "rel", + "role", + "style", + "tabindex", + "target", + "title", + "type", + ] + } + fn required_children() -> &'static [&'static str] { + &[] + } + fn attributes(&self) -> Vec<(&'static str, String)> { + let mut out = Vec::new(); + if let Some(ref value) = self.attrs.r#accesskey { + out.push(("accesskey", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_autocomplete { + out.push(("aria_autocomplete", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_checked { + out.push(("aria_checked", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_disabled { + out.push(("aria_disabled", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_errormessage { + out.push(("aria_errormessage", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_expanded { + out.push(("aria_expanded", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_haspopup { + out.push(("aria_haspopup", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_hidden { + out.push(("aria_hidden", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_invalid { + out.push(("aria_invalid", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_label { + out.push(("aria_label", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_modal { + out.push(("aria_modal", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_multiline { + out.push(("aria_multiline", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_multiselectable { + out.push(("aria_multiselectable", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_orientation { + out.push(("aria_orientation", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_placeholder { + out.push(("aria_placeholder", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_pressed { + out.push(("aria_pressed", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_readonly { + out.push(("aria_readonly", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_required { + out.push(("aria_required", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_selected { + out.push(("aria_selected", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_sort { + out.push(("aria_sort", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuemax { + out.push(("aria_valuemax", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuemin { + out.push(("aria_valuemin", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuenow { + out.push(("aria_valuenow", value.to_string())); + } + if let Some(ref value) = self.attrs.r#aria_valuetext { + out.push(("aria_valuetext", value.to_string())); + } + if let Some(ref value) = self.attrs.r#autocapitalize { + out.push(("autocapitalize", value.to_string())); + } + if let Some(ref value) = self.attrs.r#class { + out.push(("class", value.to_string())); + } + if let Some(ref value) = self.attrs.r#contenteditable { + out.push(("contenteditable", value.to_string())); + } + if let Some(ref value) = self.attrs.r#contextmenu { + out.push(("contextmenu", value.to_string())); + } + if let Some(ref value) = self.attrs.r#dir { + out.push(("dir", value.to_string())); + } + if let Some(ref value) = self.attrs.r#download { + out.push(("download", value.to_string())); + } + if let Some(ref value) = self.attrs.r#draggable { + out.push(("draggable", value.to_string())); + } + if let Some(ref value) = self.attrs.r#hidden { + out.push(("hidden", value.to_string())); + } + if let Some(ref value) = self.attrs.r#href { + out.push(("href", value.to_string())); + } + if let Some(ref value) = self.attrs.r#hreflang { + out.push(("hreflang", value.to_string())); + } + if let Some(ref value) = self.attrs.r#id { + out.push(("id", value.to_string())); + } + if let Some(ref value) = self.attrs.r#is { + out.push(("is", value.to_string())); + } + if let Some(ref value) = self.attrs.r#lang { + out.push(("lang", value.to_string())); + } + if let Some(ref value) = self.attrs.r#ping { + out.push(("ping", value.to_string())); + } + if let Some(ref value) = self.attrs.r#rel { + out.push(("rel", value.to_string())); + } + if let Some(ref value) = self.attrs.r#role { + out.push(("role", value.to_string())); + } + if let Some(ref value) = self.attrs.r#style { + out.push(("style", value.to_string())); + } + if let Some(ref value) = self.attrs.r#tabindex { + out.push(("tabindex", value.to_string())); + } + if let Some(ref value) = self.attrs.r#target { + out.push(("target", value.to_string())); + } + if let Some(ref value) = self.attrs.r#title { + out.push(("title", value.to_string())); + } + if let Some(ref value) = self.attrs.r#type { + out.push(("type", value.to_string())); + } + for (key, value) in &self.data_attributes { + out.push((key, value.to_string())); + } + for (key, value) in &self.aria_attributes { + out.push((key, value.to_string())); + } + out + } +} +impl FlowContent for a +where + T: crate::OutputType + Send, +{} +impl PhrasingContent for a +where + T: crate::OutputType + Send, +{} +impl InteractiveContent for a +where + T: crate::OutputType + Send, +{} +impl std::fmt::Display for a +where + T: crate::OutputType + Send, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "<{}", "a")?; + if let Some(ref value) = self.attrs.r#accesskey { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "accesskey", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_autocomplete { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_autocomplete", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_checked { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_checked", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_disabled { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_disabled", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_errormessage { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_errormessage", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_expanded { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_expanded", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_haspopup { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_haspopup", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_hidden { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_hidden", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_invalid { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_invalid", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_label { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_label", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_modal { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_modal", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_multiline { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_multiline", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_multiselectable { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_multiselectable", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_orientation { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_orientation", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_placeholder { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_placeholder", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_pressed { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_pressed", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_readonly { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_readonly", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_required { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_required", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_selected { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_selected", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_sort { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_sort", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_valuemax { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_valuemax", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_valuemin { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_valuemin", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_valuenow { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_valuenow", value)?; + } + } + if let Some(ref value) = self.attrs.r#aria_valuetext { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "aria_valuetext", value)?; + } + } + if let Some(ref value) = self.attrs.r#autocapitalize { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "autocapitalize", value)?; + } + } + if let Some(ref value) = self.attrs.r#class { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "class", value)?; + } + } + if let Some(ref value) = self.attrs.r#contenteditable { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "contenteditable", value)?; + } + } + if let Some(ref value) = self.attrs.r#contextmenu { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "contextmenu", value)?; + } + } + if let Some(ref value) = self.attrs.r#dir { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "dir", value)?; + } + } + if let Some(ref value) = self.attrs.r#download { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "download", value)?; + } + } + if let Some(ref value) = self.attrs.r#draggable { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "draggable", value)?; + } + } + if let Some(ref value) = self.attrs.r#hidden { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "hidden", value)?; + } + } + if let Some(ref value) = self.attrs.r#href { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "href", value)?; + } + } + if let Some(ref value) = self.attrs.r#hreflang { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "hreflang", value)?; + } + } + if let Some(ref value) = self.attrs.r#id { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "id", value)?; + } + } + if let Some(ref value) = self.attrs.r#is { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "is", value)?; + } + } + if let Some(ref value) = self.attrs.r#lang { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "lang", value)?; + } + } + if let Some(ref value) = self.attrs.r#ping { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "ping", value)?; + } + } + if let Some(ref value) = self.attrs.r#rel { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "rel", value)?; + } + } + if let Some(ref value) = self.attrs.r#role { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "role", value)?; + } + } + if let Some(ref value) = self.attrs.r#style { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "style", value)?; + } + } + if let Some(ref value) = self.attrs.r#tabindex { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "tabindex", value)?; + } + } + if let Some(ref value) = self.attrs.r#target { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "target", value)?; + } + } + if let Some(ref value) = self.attrs.r#title { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "title", value)?; + } + } + if let Some(ref value) = self.attrs.r#type { + let value = crate::escape_html_attribute(value.to_string()); + if !value.is_empty() { + write!(f, " {}=\"{}\"", "type", value)?; + } + } + for (key, value) in &self.data_attributes { + write!( + f, " data-{}=\"{}\"", key, crate ::escape_html_attribute(value + .to_string()) + )?; + } + for (key, value) in &self.aria_attributes { + write!( + f, " aria-{}=\"{}\"", key, crate ::escape_html_attribute(value + .to_string()) + )?; + } + write!(f, "{}", self.events)?; + write!(f, ">")?; + for child in &self.children { + child.fmt(f)?; + } + write!(f, "", "a") + } +} +impl IntoIterator for a +where + T: crate::OutputType + Send, +{ + type Item = a; + type IntoIter = std::vec::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} +impl IntoIterator for Box> +where + T: crate::OutputType + Send, +{ + type Item = Box>; + type IntoIter = std::vec::IntoIter>>; + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} diff --git a/macros/src/tests/generate-anchor-element.rs b/macros/src/tests/generate-anchor-element.rs new file mode 100644 index 0000000..f10f065 --- /dev/null +++ b/macros/src/tests/generate-anchor-element.rs @@ -0,0 +1,20 @@ +fn html() { + let mut element = axohtml::elements::a::new(); + element + .attrs + .r#href = Some( + "https://www.w3schools.com" + .parse() + .unwrap_or_else(|err| { + eprintln!( + "ERROR: failed to parse attribute value: {}\nERROR: rebuild with nightly to print source location", + err + ); + panic!("failed to parse string literal"); + }), + ); + element + .children + .push(Box::new(axohtml::dom::TextNode::new("Visit W3Schools.com!".to_string()))); + Box::new(element) +} diff --git a/macros/src/tests/generate-button-event.rs b/macros/src/tests/generate-button-event.rs new file mode 100644 index 0000000..e6d8089 --- /dev/null +++ b/macros/src/tests/generate-button-event.rs @@ -0,0 +1,6 @@ +fn html() { + let mut element: axohtml::elements::button = axohtml::elements::button::new(); + element.children.push(Box::new(axohtml::dom::TextNode::new("Click me".to_string()))); + element.events.r#click = Some("alert(1)".into()); + Box::new(element) +} diff --git a/tests/main.rs b/tests/main.rs index 5bb3fbc..c1e99a2 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -23,3 +23,132 @@ fn ui() { compiletest::run_tests(&config); } + +#[test] +fn test_subtree() { + use axohtml::html; + use axohtml::dom::DOMTree; + + let output: DOMTree = html!( +

"This is HTML"

+ ); + + assert_eq!( + output.to_string(), + "

This is HTML

" + ) +} + +#[test] +fn test_button_onclick() { + use axohtml::html; + use axohtml::dom::DOMTree; + + let output: DOMTree = html!( + + : String + ); + + assert_eq!( + output.to_string(), + "" + ) +} + +#[test] +fn test_flowcontent_function() { + use axohtml::html; + use axohtml::dom::DOMTree; + use axohtml::elements::FlowContent; + + fn content() -> Box> { + return html!( +

+ "This is HTML" +

+ ) + } + + let output: DOMTree = html!( +
+ { content() } +
+ ); + + assert_eq!( + output.to_string(), + "

This is HTML

" + ) +} + +#[test] +fn test_phrasingcontent_function() { + use axohtml::{html, text}; + use axohtml::dom::DOMTree; + use axohtml::elements::PhrasingContent; + + fn italicized(content: &str) -> Box> { + return html!( + + { text!(content) } + + ) + } + + let output: DOMTree = html!( +

+ "This is " + { italicized("HTML") } +

+ ); + + assert_eq!( + output.to_string(), + "

This is HTML

" + ) +} + +#[test] +fn test_template_function() { + use axohtml::html; + use axohtml::dom::DOMTree; + use axohtml::elements::FlowContent; + + fn layout(body: impl FnOnce() -> Box>) -> DOMTree { + return html!( +
+

"Header"

+ { body() } +
+ ) + } + + let output: DOMTree = layout(|| {html!( +
+

"Subheading"

+

"This is content!"

+
+ )}); + + assert_eq!( + output.to_string(), + "

Header

Subheading

This is content!

" + ) +} + +#[test] +fn test_inline_script() { + use axohtml::{html, unsafe_text}; + use axohtml::dom::DOMTree; + + let output: DOMTree = html!( + + ); + + assert_eq!( + output.to_string(), + "" + ) +} From 2f811afc53d1ec7a07739e8b51fd15b9abe3d858 Mon Sep 17 00:00:00 2001 From: Ryan Balsdon Date: Wed, 26 Jul 2023 11:03:19 -0400 Subject: [PATCH 2/3] fix: clippy warnings about useless conversion --- macros/src/lib.rs | 12 ++++++------ macros/src/span.rs | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index ac11056..81576f4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -26,15 +26,15 @@ pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream { html_impl(input.into()).into() } fn html_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let stream = lexer::unroll_stream(input.into(), false); + let stream = lexer::unroll_stream(input, false); let result = html::expand_html(&stream); - proc_macro2::TokenStream::from(match result { + match result { Err(err) => error::parse_error(&stream, &err), Ok((node, ty)) => match node.into_token_stream(&ty) { Err(err) => err, Ok(success) => success, }, - }) + } } /// Construct a Dodrio node. @@ -65,9 +65,9 @@ pub fn declare_elements(input: proc_macro::TokenStream) -> proc_macro::TokenStre } fn declare_elements_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let stream = lexer::keywordise(lexer::unroll_stream(input.into(), true)); + let stream = lexer::keywordise(lexer::unroll_stream(input, true)); let result = declare::expand_declare(&stream); - proc_macro2::TokenStream::from(match result { + match result { Err(err) => error::parse_error(&stream, &err), Ok(decls) => { let mut out = proc_macro2::TokenStream::new(); @@ -76,7 +76,7 @@ fn declare_elements_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenS } out } - }) + } } #[test] diff --git a/macros/src/span.rs b/macros/src/span.rs index 3c1c003..2d77489 100644 --- a/macros/src/span.rs +++ b/macros/src/span.rs @@ -2,6 +2,5 @@ pub fn from_unstable(span: proc_macro2::Span) -> proc_macro2::Span { let ident = proc_macro2::Ident::new("_", span); let tt = proc_macro2::TokenTree::Ident(ident); let tts = proc_macro2::TokenStream::from(tt); - let tts2 = proc_macro2::TokenStream::from(tts); - tts2.into_iter().next().unwrap().span() + tts.into_iter().next().unwrap().span() } From 238556ee844660faea2c950856027ff255fc9ac7 Mon Sep 17 00:00:00 2001 From: Ryan Balsdon Date: Wed, 26 Jul 2023 11:05:29 -0400 Subject: [PATCH 3/3] fix: rust-fmt wants things prettier --- macros/src/lib.rs | 14 +++++++++---- tests/main.rs | 52 +++++++++++++++++++---------------------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 81576f4..f2f9eb4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -98,7 +98,9 @@ fn test_declare_anchor_element() { let output = declare_elements_impl(input); pretty_assertions::assert_eq!( - rust_format::PrettyPlease::default().format_str(output.to_string()).unwrap(), + rust_format::PrettyPlease::default() + .format_str(output.to_string()) + .unwrap(), include_str!("tests/declare-anchor-element.rs") ) } @@ -117,7 +119,9 @@ fn test_html_anchor_element() { let output = format!("fn html() {}", output.to_string()); pretty_assertions::assert_eq!( - rust_format::PrettyPlease::default().format_str(output.to_string()).unwrap(), + rust_format::PrettyPlease::default() + .format_str(output.to_string()) + .unwrap(), include_str!("tests/generate-anchor-element.rs") ) } @@ -137,7 +141,9 @@ fn test_html_button_event() { let output = format!("fn html() {}", output.to_string()); pretty_assertions::assert_eq!( - rust_format::PrettyPlease::default().format_str(output.to_string()).unwrap(), + rust_format::PrettyPlease::default() + .format_str(output.to_string()) + .unwrap(), include_str!("tests/generate-button-event.rs") ) -} \ No newline at end of file +} diff --git a/tests/main.rs b/tests/main.rs index c1e99a2..d1778ac 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -26,23 +26,20 @@ fn ui() { #[test] fn test_subtree() { - use axohtml::html; use axohtml::dom::DOMTree; + use axohtml::html; let output: DOMTree = html!(

"This is HTML"

); - assert_eq!( - output.to_string(), - "

This is HTML

" - ) + assert_eq!(output.to_string(), "

This is HTML

") } #[test] fn test_button_onclick() { - use axohtml::html; use axohtml::dom::DOMTree; + use axohtml::html; let output: DOMTree = html!( @@ -57,16 +54,16 @@ fn test_button_onclick() { #[test] fn test_flowcontent_function() { - use axohtml::html; use axohtml::dom::DOMTree; use axohtml::elements::FlowContent; + use axohtml::html; fn content() -> Box> { return html!(

"This is HTML"

- ) + ); } let output: DOMTree = html!( @@ -75,24 +72,21 @@ fn test_flowcontent_function() { ); - assert_eq!( - output.to_string(), - "

This is HTML

" - ) + assert_eq!(output.to_string(), "

This is HTML

") } #[test] fn test_phrasingcontent_function() { - use axohtml::{html, text}; use axohtml::dom::DOMTree; use axohtml::elements::PhrasingContent; + use axohtml::{html, text}; fn italicized(content: &str) -> Box> { return html!( { text!(content) } - ) + ); } let output: DOMTree = html!( @@ -102,17 +96,14 @@ fn test_phrasingcontent_function() {

); - assert_eq!( - output.to_string(), - "

This is HTML

" - ) + assert_eq!(output.to_string(), "

This is HTML

") } #[test] fn test_template_function() { - use axohtml::html; use axohtml::dom::DOMTree; use axohtml::elements::FlowContent; + use axohtml::html; fn layout(body: impl FnOnce() -> Box>) -> DOMTree { return html!( @@ -120,15 +111,17 @@ fn test_template_function() {

"Header"

{ body() } - ) + ); } - let output: DOMTree = layout(|| {html!( -
-

"Subheading"

-

"This is content!"

-
- )}); + let output: DOMTree = layout(|| { + html!( +
+

"Subheading"

+

"This is content!"

+
+ ) + }); assert_eq!( output.to_string(), @@ -138,8 +131,8 @@ fn test_template_function() { #[test] fn test_inline_script() { - use axohtml::{html, unsafe_text}; use axohtml::dom::DOMTree; + use axohtml::{html, unsafe_text}; let output: DOMTree = html!( ); - assert_eq!( - output.to_string(), - "" - ) + assert_eq!(output.to_string(), "") }