From 92d889b57afbb9025188c40307c17d48b64eb36b Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Mon, 31 Jul 2023 15:50:49 -0700 Subject: [PATCH 01/11] Add common conversion traits for Node types --- html-node-core/src/lib.rs | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/html-node-core/src/lib.rs b/html-node-core/src/lib.rs index 212da30..139a072 100644 --- a/html-node-core/src/lib.rs +++ b/html-node-core/src/lib.rs @@ -139,6 +139,24 @@ impl Display for Comment { } } +impl From for Comment { + fn from(comment: String) -> Self { + Self { comment } + } +} + +impl From<&str> for Comment { + fn from(comment: &str) -> Self { + comment.to_owned().into() + } +} + +impl From for Node { + fn from(comment: Comment) -> Self { + Self::Comment(comment) + } +} + /// A doctype. /// /// ```html @@ -161,6 +179,24 @@ impl Display for Doctype { } } +impl From for Doctype { + fn from(syntax: String) -> Self { + Self { syntax } + } +} + +impl From<&str> for Doctype { + fn from(syntax: &str) -> Self { + syntax.to_owned().into() + } +} + +impl From for Node { + fn from(doctype: Doctype) -> Self { + Self::Doctype(doctype) + } +} + /// A fragment. /// /// ```html @@ -187,6 +223,12 @@ impl Display for Fragment { } } +impl From for Node { + fn from(fragment: Fragment) -> Self { + Self::Fragment(fragment) + } +} + /// An element. /// /// ```html @@ -254,6 +296,12 @@ impl Display for Element { } } +impl From for Node { + fn from(element: Element) -> Self { + Self::Element(element) + } +} + /// A text node. /// /// ```html @@ -279,6 +327,24 @@ impl Display for Text { } } +impl From for Text { + fn from(text: String) -> Self { + Self { text } + } +} + +impl From<&str> for Text { + fn from(text: &str) -> Self { + text.to_owned().into() + } +} + +impl From for Node { + fn from(text: Text) -> Self { + Self::Text(text) + } +} + /// An unsafe text node. /// /// # Warning @@ -298,6 +364,24 @@ impl Display for UnsafeText { } } +impl From for UnsafeText { + fn from(text: String) -> Self { + Self { text } + } +} + +impl From<&str> for UnsafeText { + fn from(text: &str) -> Self { + text.to_owned().into() + } +} + +impl From for Node { + fn from(text: UnsafeText) -> Self { + Self::UnsafeText(text) + } +} + /// Writes the children of a node. /// /// If the formatter is in alternate mode, then the children are put on their From 35c7be0eb76e3e03f9a12203d40e9bf66fdeefa3 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Mon, 31 Jul 2023 20:57:09 -0700 Subject: [PATCH 02/11] Reorganize from impls --- html-node-core/src/lib.rs | 136 ++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 72 deletions(-) diff --git a/html-node-core/src/lib.rs b/html-node-core/src/lib.rs index 139a072..8a8cb26 100644 --- a/html-node-core/src/lib.rs +++ b/html-node-core/src/lib.rs @@ -98,6 +98,42 @@ where } } +impl From for Node { + fn from(comment: Comment) -> Self { + Self::Comment(comment) + } +} + +impl From for Node { + fn from(doctype: Doctype) -> Self { + Self::Doctype(doctype) + } +} + +impl From for Node { + fn from(fragment: Fragment) -> Self { + Self::Fragment(fragment) + } +} + +impl From for Node { + fn from(element: Element) -> Self { + Self::Element(element) + } +} + +impl From for Node { + fn from(text: Text) -> Self { + Self::Text(text) + } +} + +impl From for Node { + fn from(text: UnsafeText) -> Self { + Self::UnsafeText(text) + } +} + impl Display for Node { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match &self { @@ -139,21 +175,14 @@ impl Display for Comment { } } -impl From for Comment { - fn from(comment: String) -> Self { - Self { comment } - } -} - -impl From<&str> for Comment { - fn from(comment: &str) -> Self { - comment.to_owned().into() - } -} - -impl From for Node { - fn from(comment: Comment) -> Self { - Self::Comment(comment) +impl From for Comment +where + C: Into, +{ + fn from(comment: C) -> Self { + Self { + comment: comment.into(), + } } } @@ -179,21 +208,14 @@ impl Display for Doctype { } } -impl From for Doctype { - fn from(syntax: String) -> Self { - Self { syntax } - } -} - -impl From<&str> for Doctype { - fn from(syntax: &str) -> Self { - syntax.to_owned().into() - } -} - -impl From for Node { - fn from(doctype: Doctype) -> Self { - Self::Doctype(doctype) +impl From for Doctype +where + S: Into, +{ + fn from(syntax: S) -> Self { + Self { + syntax: syntax.into(), + } } } @@ -223,12 +245,6 @@ impl Display for Fragment { } } -impl From for Node { - fn from(fragment: Fragment) -> Self { - Self::Fragment(fragment) - } -} - /// An element. /// /// ```html @@ -296,12 +312,6 @@ impl Display for Element { } } -impl From for Node { - fn from(element: Element) -> Self { - Self::Element(element) - } -} - /// A text node. /// /// ```html @@ -327,21 +337,12 @@ impl Display for Text { } } -impl From for Text { - fn from(text: String) -> Self { - Self { text } - } -} - -impl From<&str> for Text { - fn from(text: &str) -> Self { - text.to_owned().into() - } -} - -impl From for Node { - fn from(text: Text) -> Self { - Self::Text(text) +impl From for Text +where + T: Into, +{ + fn from(text: T) -> Self { + Self { text: text.into() } } } @@ -364,21 +365,12 @@ impl Display for UnsafeText { } } -impl From for UnsafeText { - fn from(text: String) -> Self { - Self { text } - } -} - -impl From<&str> for UnsafeText { - fn from(text: &str) -> Self { - text.to_owned().into() - } -} - -impl From for Node { - fn from(text: UnsafeText) -> Self { - Self::UnsafeText(text) +impl From for UnsafeText +where + T: Into, +{ + fn from(text: T) -> Self { + Self { text: text.into() } } } From 5b3237eb4d32fa9f0f42cfdb64539db10b7b6cc0 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Mon, 31 Jul 2023 23:09:55 -0700 Subject: [PATCH 03/11] Add pretty printing wrapper. --- html-node-core/src/lib.rs | 2 ++ html-node-core/src/pretty.rs | 22 ++++++++++++++++++++++ html-node/src/lib.rs | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 html-node-core/src/pretty.rs diff --git a/html-node-core/src/lib.rs b/html-node-core/src/lib.rs index 8a8cb26..1ef8e43 100644 --- a/html-node-core/src/lib.rs +++ b/html-node-core/src/lib.rs @@ -9,6 +9,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod http; +#[allow(missing_docs)] +pub mod pretty; #[allow(missing_docs)] #[cfg(feature = "typed")] diff --git a/html-node-core/src/pretty.rs b/html-node-core/src/pretty.rs new file mode 100644 index 0000000..0010a8e --- /dev/null +++ b/html-node-core/src/pretty.rs @@ -0,0 +1,22 @@ +use std::fmt::{self, Display, Formatter}; + +use crate::Node; + +/// A wrapper around [`Node`] that is always pretty printed. +#[derive(Debug, Default)] +pub struct Pretty(pub Node); + +impl Display for Pretty { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:#}", self.0) + } +} + +impl From for Pretty +where + N: Into, +{ + fn from(node: N) -> Self { + Self(node.into()) + } +} diff --git a/html-node/src/lib.rs b/html-node/src/lib.rs index 8441711..4f37e22 100644 --- a/html-node/src/lib.rs +++ b/html-node/src/lib.rs @@ -118,7 +118,7 @@ mod macros; #[cfg(feature = "typed")] pub mod typed; -pub use html_node_core::{Comment, Doctype, Element, Fragment, Node, Text, UnsafeText}; +pub use html_node_core::{pretty, Comment, Doctype, Element, Fragment, Node, Text, UnsafeText}; /// The HTML to [`Node`] macro. /// /// See the [crate-level documentation](crate) for more information. From 09198e75a830578da6cb37c0b79f74da18d08807 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Mon, 31 Jul 2023 23:16:43 -0700 Subject: [PATCH 04/11] Add axum support for pretty wrapper. --- html-node-core/src/http.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/html-node-core/src/http.rs b/html-node-core/src/http.rs index 1ef4132..561f305 100644 --- a/html-node-core/src/http.rs +++ b/html-node-core/src/http.rs @@ -2,11 +2,17 @@ mod axum { use axum::response::{Html, IntoResponse, Response}; - use crate::Node; + use crate::{pretty::Pretty, Node}; impl IntoResponse for Node { fn into_response(self) -> Response { Html(self.to_string()).into_response() } } + + impl IntoResponse for Pretty { + fn into_response(self) -> Response { + Html(self.to_string()).into_response() + } + } } From f526886c437fc5db008d012e3c8c84a2e8bfeabb Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Tue, 1 Aug 2023 14:35:55 -0700 Subject: [PATCH 05/11] Reorganize giant lib into separate node definitions. --- html-node-core/Cargo.toml | 30 +- html-node-core/src/http.rs | 5 +- html-node-core/src/lib.rs | 318 ++---------------- html-node-core/src/node.rs | 51 +++ html-node-core/src/node/comment.rs | 36 ++ html-node-core/src/node/doctype.rs | 37 ++ html-node-core/src/node/element.rs | 90 +++++ html-node-core/src/node/fragment.rs | 59 ++++ html-node-core/src/node/text.rs | 38 +++ html-node-core/src/node/unsafe_text.rs | 34 ++ html-node-core/src/pretty.rs | 5 +- html-node-core/src/{typed/mod.rs => typed.rs} | 0 html-node/Cargo.toml | 39 +-- html-node/examples/axum.rs | 10 + html-node/src/lib.rs | 4 +- 15 files changed, 438 insertions(+), 318 deletions(-) create mode 100644 html-node-core/src/node.rs create mode 100644 html-node-core/src/node/comment.rs create mode 100644 html-node-core/src/node/doctype.rs create mode 100644 html-node-core/src/node/element.rs create mode 100644 html-node-core/src/node/fragment.rs create mode 100644 html-node-core/src/node/text.rs create mode 100644 html-node-core/src/node/unsafe_text.rs rename html-node-core/src/{typed/mod.rs => typed.rs} (100%) diff --git a/html-node-core/Cargo.toml b/html-node-core/Cargo.toml index fb99267..8e73fbe 100644 --- a/html-node-core/Cargo.toml +++ b/html-node-core/Cargo.toml @@ -1,28 +1,30 @@ [package] name = "html-node-core" -authors.workspace = true -categories.workspace = true + +authors.workspace = true +categories.workspace = true description.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -axum = { version = "0.6", optional = true, default-features = false } +axum = { version = "0.6", optional = true, default-features = false } serde = { version = "1.0", optional = true, features = ["derive"] } html-escape = "0.2" -paste = "1.0.14" +paste = "1.0.14" [features] -axum = ["dep:axum"] -typed = [] -serde = ["dep:serde"] +axum = ["dep:axum"] +pretty = [] +serde = ["dep:serde"] +typed = [] diff --git a/html-node-core/src/http.rs b/html-node-core/src/http.rs index 561f305..8cd1d6c 100644 --- a/html-node-core/src/http.rs +++ b/html-node-core/src/http.rs @@ -2,7 +2,9 @@ mod axum { use axum::response::{Html, IntoResponse, Response}; - use crate::{pretty::Pretty, Node}; + #[cfg(feature = "pretty")] + use crate::pretty::Pretty; + use crate::Node; impl IntoResponse for Node { fn into_response(self) -> Response { @@ -10,6 +12,7 @@ mod axum { } } + #[cfg(feature = "pretty")] impl IntoResponse for Pretty { fn into_response(self) -> Response { Html(self.to_string()).into_response() diff --git a/html-node-core/src/lib.rs b/html-node-core/src/lib.rs index 1ef8e43..f0ef793 100644 --- a/html-node-core/src/lib.rs +++ b/html-node-core/src/lib.rs @@ -8,16 +8,23 @@ #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +/// HTTP Server integrations. mod http; -#[allow(missing_docs)] + +/// [`crate::Node`] variant definitions. +mod node; + +/// Pretty printing utilities. +#[cfg(feature = "pretty")] pub mod pretty; -#[allow(missing_docs)] +/// Typed HTML Nodes. #[cfg(feature = "typed")] pub mod typed; use std::fmt::{self, Display, Formatter}; +pub use self::node::*; #[cfg(feature = "typed")] use self::typed::TypedElement; @@ -86,6 +93,32 @@ impl Node { pub fn from_typed(element: E, children: Option>) -> Self { element.into_node(children) } + + /// Wrap the node in a pretty-printing wrapper. + #[cfg(feature = "pretty")] + #[must_use] + pub fn pretty(self) -> pretty::Pretty { + self.into() + } +} + +impl Default for Node { + fn default() -> Self { + Self::EMPTY + } +} + +impl Display for Node { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match &self { + Self::Comment(comment) => comment.fmt(f), + Self::Doctype(doctype) => doctype.fmt(f), + Self::Fragment(fragment) => fragment.fmt(f), + Self::Element(element) => element.fmt(f), + Self::Text(text) => text.fmt(f), + Self::UnsafeText(unsafe_text) => unsafe_text.fmt(f), + } + } } impl From for Node @@ -94,9 +127,7 @@ where N: Into, { fn from(iter: I) -> Self { - Self::Fragment(Fragment { - children: iter.into_iter().map(Into::into).collect(), - }) + Self::Fragment(iter.into()) } } @@ -135,280 +166,3 @@ impl From for Node { Self::UnsafeText(text) } } - -impl Display for Node { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match &self { - Self::Comment(comment) => comment.fmt(f), - Self::Doctype(doctype) => doctype.fmt(f), - Self::Fragment(fragment) => fragment.fmt(f), - Self::Element(element) => element.fmt(f), - Self::Text(text) => text.fmt(f), - Self::UnsafeText(unsafe_text) => unsafe_text.fmt(f), - } - } -} - -impl Default for Node { - fn default() -> Self { - Self::EMPTY - } -} - -/// A comment. -/// -/// ```html -/// -/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Comment { - /// The text of the comment. - /// - /// ```html - /// - /// ``` - pub comment: String, -} - -impl Display for Comment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "", self.comment) - } -} - -impl From for Comment -where - C: Into, -{ - fn from(comment: C) -> Self { - Self { - comment: comment.into(), - } - } -} - -/// A doctype. -/// -/// ```html -/// -/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Doctype { - /// The value of the doctype. - /// - /// ```html - /// - /// ``` - pub syntax: String, -} - -impl Display for Doctype { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "", self.syntax) - } -} - -impl From for Doctype -where - S: Into, -{ - fn from(syntax: S) -> Self { - Self { - syntax: syntax.into(), - } - } -} - -/// A fragment. -/// -/// ```html -/// <> -/// I'm in a fragment! -/// -/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Fragment { - /// The children of the fragment. - /// - /// ```html - /// <> - /// - /// I'm another child! - /// - pub children: Vec, -} - -impl Display for Fragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write_children(f, &self.children, true) - } -} - -/// An element. -/// -/// ```html -///
-/// I'm in an element! -///
-/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Element { - /// The name of the element. - /// - /// ```html - /// - /// ``` - pub name: String, - - /// The attributes of the element. - /// - /// ```html - ///
- /// ``` - pub attributes: Vec<(String, Option)>, - - /// The children of the element. - /// - /// ```html - ///
- /// - /// I'm another child! - ///
- /// ``` - pub children: Option>, -} - -impl Element { - /// Create a new [`Element`] from a [`TypedElement`]. - #[cfg(feature = "typed")] - pub fn from_typed(element: E, children: Option>) -> Self { - element.into_element(children) - } -} - -impl Display for Element { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "<{}", self.name)?; - - for (key, value) in &self.attributes { - write!(f, " {key}")?; - - if let Some(value) = value { - let encoded_value = html_escape::encode_double_quoted_attribute(value); - write!(f, r#"="{encoded_value}""#)?; - } - } - write!(f, ">")?; - - if let Some(children) = &self.children { - write_children(f, children, false)?; - - write!(f, "", self.name)?; - }; - - Ok(()) - } -} - -/// A text node. -/// -/// ```html -///
-/// I'm a text node! -///
-#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Text { - /// The text of the node. - /// - /// ```html - ///
- /// text - ///
- pub text: String, -} - -impl Display for Text { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let encoded_value = html_escape::encode_text_minimal(&self.text); - write!(f, "{encoded_value}") - } -} - -impl From for Text -where - T: Into, -{ - fn from(text: T) -> Self { - Self { text: text.into() } - } -} - -/// An unsafe text node. -/// -/// # Warning -/// -/// [`UnsafeText`] is not escaped when rendered, and as such, can allow -/// for XSS attacks. Use with caution! -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct UnsafeText { - /// The text of the node. - pub text: String, -} - -impl Display for UnsafeText { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.text) - } -} - -impl From for UnsafeText -where - T: Into, -{ - fn from(text: T) -> Self { - Self { text: text.into() } - } -} - -/// Writes the children of a node. -/// -/// If the formatter is in alternate mode, then the children are put on their -/// own lines. -/// -/// If alternate mode is enabled and `is_fragment` is false, then each line -/// is indented by 4 spaces. -fn write_children(f: &mut Formatter<'_>, children: &[Node], is_fragment: bool) -> fmt::Result { - if f.alternate() { - let mut children_iter = children.iter(); - - if is_fragment { - if let Some(first_child) = children_iter.next() { - write!(f, "{first_child:#}")?; - - for child in children_iter { - write!(f, "\n{child:#}")?; - } - } - } else { - for child_str in children_iter.map(|child| format!("{child:#}")) { - for line in child_str.lines() { - write!(f, "\n {line}")?; - } - } - - // exit inner block - writeln!(f)?; - } - } else { - for child in children { - child.fmt(f)?; - } - } - Ok(()) -} diff --git a/html-node-core/src/node.rs b/html-node-core/src/node.rs new file mode 100644 index 0000000..b347340 --- /dev/null +++ b/html-node-core/src/node.rs @@ -0,0 +1,51 @@ +use std::fmt::{self, Display, Formatter}; + +mod comment; +mod doctype; +mod element; +mod fragment; +mod text; +mod unsafe_text; + +pub use self::{ + comment::Comment, doctype::Doctype, element::Element, fragment::Fragment, text::Text, + unsafe_text::UnsafeText, +}; +use crate::Node; + +/// Writes the children of a node. +/// +/// If the formatter is in alternate mode, then the children are put on their +/// own lines. +/// +/// If alternate mode is enabled and `is_fragment` is false, then each line +/// is indented by 4 spaces. +fn write_children(f: &mut Formatter<'_>, children: &[Node], is_fragment: bool) -> fmt::Result { + if f.alternate() { + let mut children_iter = children.iter(); + + if is_fragment { + if let Some(first_child) = children_iter.next() { + write!(f, "{first_child:#}")?; + + for child in children_iter { + write!(f, "\n{child:#}")?; + } + } + } else { + for child_str in children_iter.map(|child| format!("{child:#}")) { + for line in child_str.lines() { + write!(f, "\n {line}")?; + } + } + + // exit inner block + writeln!(f)?; + } + } else { + for child in children { + child.fmt(f)?; + } + } + Ok(()) +} diff --git a/html-node-core/src/node/comment.rs b/html-node-core/src/node/comment.rs new file mode 100644 index 0000000..cae3bed --- /dev/null +++ b/html-node-core/src/node/comment.rs @@ -0,0 +1,36 @@ +use std::fmt::{self, Display, Formatter}; + +/// A comment. +/// +/// ```html +/// +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Comment { + /// The text of the comment. + /// + /// ```html + /// + /// ``` + pub comment: String, +} + +impl Display for Comment { + /// Format as an HTML comment. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "", self.comment) + } +} + +impl From for Comment +where + C: Into, +{ + /// Create a new comment from anything that can be converted into a string. + fn from(comment: C) -> Self { + Self { + comment: comment.into(), + } + } +} diff --git a/html-node-core/src/node/doctype.rs b/html-node-core/src/node/doctype.rs new file mode 100644 index 0000000..5c8bd6f --- /dev/null +++ b/html-node-core/src/node/doctype.rs @@ -0,0 +1,37 @@ +use std::fmt::{self, Display, Formatter}; + +/// A doctype. +/// +/// ```html +/// +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Doctype { + /// The value of the doctype. + /// + /// ```html + /// + /// ``` + pub syntax: String, +} + +impl Display for Doctype { + /// Format as an HTML doctype element. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "", self.syntax) + } +} + +impl From for Doctype +where + S: Into, +{ + /// Create a new doctype element with a syntax attribute set + /// from anything that can be converted into a string. + fn from(syntax: S) -> Self { + Self { + syntax: syntax.into(), + } + } +} diff --git a/html-node-core/src/node/element.rs b/html-node-core/src/node/element.rs new file mode 100644 index 0000000..8d9656d --- /dev/null +++ b/html-node-core/src/node/element.rs @@ -0,0 +1,90 @@ +use std::fmt::{self, Display, Formatter}; + +use super::write_children; +#[cfg(feature = "typed")] +use crate::typed::TypedElement; +use crate::Node; + +/// An element. +/// +/// ```html +///
+/// I'm in an element! +///
+/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Element { + /// The name of the element. + /// + /// ```html + /// + /// ``` + pub name: String, + + /// The attributes of the element. + /// + /// ```html + ///
+ /// ``` + pub attributes: Vec<(String, Option)>, + + /// The children of the element. + /// + /// ```html + ///
+ /// + /// I'm another child! + ///
+ /// ``` + pub children: Option>, +} + +#[cfg(feature = "typed")] +impl Element { + /// Create a new [`Element`] from a [`TypedElement`]. + pub fn from_typed(element: E, children: Option>) -> Self { + element.into_element(children) + } +} + +impl Display for Element { + /// Format as an HTML element. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "<{}", self.name)?; + + for (key, value) in &self.attributes { + write!(f, " {key}")?; + + if let Some(value) = value { + let encoded_value = html_escape::encode_double_quoted_attribute(value); + write!(f, r#"="{encoded_value}""#)?; + } + } + write!(f, ">")?; + + if let Some(children) = &self.children { + write_children(f, children, false)?; + + write!(f, "", self.name)?; + }; + + Ok(()) + } +} + +impl From for Element +where + N: Into, +{ + /// Create an HTML element directly from a string. + /// + /// This [`Element`] has no attributes and no children. + fn from(name: N) -> Self { + Self { + name: name.into(), + attributes: Vec::new(), + children: None, + } + } +} diff --git a/html-node-core/src/node/fragment.rs b/html-node-core/src/node/fragment.rs new file mode 100644 index 0000000..c826902 --- /dev/null +++ b/html-node-core/src/node/fragment.rs @@ -0,0 +1,59 @@ +use std::fmt::{self, Display, Formatter}; + +use super::write_children; +use crate::Node; + +/// A fragment. +/// +/// ```html +/// <> +/// I'm in a fragment! +/// +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Fragment { + /// The children of the fragment. + /// + /// ```html + /// <> + /// + /// I'm another child! + /// + pub children: Vec, +} + +impl Display for Fragment { + /// Format the fragment's childrent as HTML elements. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write_children(f, &self.children, true) + } +} + +impl FromIterator for Fragment +where + N: Into, +{ + /// Create a new fragment from an iterator of anything that + /// can be converted into a [`crate::Node`]. + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + Self { + children: iter.into_iter().map(Into::into).collect(), + } + } +} + +impl From for Fragment +where + I: IntoIterator, + N: Into, +{ + /// Create a new fragment from any iterator of anything that + /// can be converted into a [`crate::Node`]. + fn from(iter: I) -> Self { + Self::from_iter(iter) + } +} diff --git a/html-node-core/src/node/text.rs b/html-node-core/src/node/text.rs new file mode 100644 index 0000000..822496b --- /dev/null +++ b/html-node-core/src/node/text.rs @@ -0,0 +1,38 @@ +use std::fmt::{self, Display, Formatter}; + +/// A text node. +/// +/// ```html +///
+/// I'm a text node! +///
+#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Text { + /// The text of the node. + /// + /// ```html + ///
+ /// text + ///
+ pub text: String, +} + +impl Display for Text { + /// Format as HTML encoded string. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let encoded_value = html_escape::encode_text_minimal(&self.text); + write!(f, "{encoded_value}") + } +} + +impl From for Text +where + T: Into, +{ + /// Create a new text element from anything that can + /// be converted into a string. + fn from(text: T) -> Self { + Self { text: text.into() } + } +} diff --git a/html-node-core/src/node/unsafe_text.rs b/html-node-core/src/node/unsafe_text.rs new file mode 100644 index 0000000..f1a0672 --- /dev/null +++ b/html-node-core/src/node/unsafe_text.rs @@ -0,0 +1,34 @@ +use std::fmt::{self, Display, Formatter}; + +/// An unsafe text node. +/// +/// # Warning +/// +/// [`UnsafeText`] is not escaped when rendered, and as such, can allow +/// for XSS attacks. Use with caution! +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UnsafeText { + /// The text of the node. + pub text: String, +} + +impl Display for UnsafeText { + /// Unformatted text. + /// + /// This string is **not** HTML encoded! + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.text) + } +} + +impl From for UnsafeText +where + T: Into, +{ + /// Create a new unsafe text element from anything + /// that can be converted into a string. + fn from(text: T) -> Self { + Self { text: text.into() } + } +} diff --git a/html-node-core/src/pretty.rs b/html-node-core/src/pretty.rs index 0010a8e..5f45a77 100644 --- a/html-node-core/src/pretty.rs +++ b/html-node-core/src/pretty.rs @@ -3,10 +3,12 @@ use std::fmt::{self, Display, Formatter}; use crate::Node; /// A wrapper around [`Node`] that is always pretty printed. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Pretty(pub Node); impl Display for Pretty { + /// Format as a pretty printed HTML node. fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{:#}", self.0) } @@ -16,6 +18,7 @@ impl From for Pretty where N: Into, { + /// Create a new pretty wrapper around the given node. fn from(node: N) -> Self { Self(node.into()) } diff --git a/html-node-core/src/typed/mod.rs b/html-node-core/src/typed.rs similarity index 100% rename from html-node-core/src/typed/mod.rs rename to html-node-core/src/typed.rs diff --git a/html-node/Cargo.toml b/html-node/Cargo.toml index 9114475..fe617d1 100644 --- a/html-node/Cargo.toml +++ b/html-node/Cargo.toml @@ -1,40 +1,41 @@ [package] name = "html-node" -authors.workspace = true -categories.workspace = true -description.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true + documentation = "https://docs.rs/html-node" +authors.workspace = true +categories.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] - [[example]] -name = "axum" +name = "axum" required-features = ["axum"] [[example]] -name = "typed_custom_attributes" +name = "typed_custom_attributes" required-features = ["typed"] [dependencies] -html-node-core = { version = "0.2", path = "../html-node-core" } +html-node-core = { version = "0.2", path = "../html-node-core" } html-node-macro = { version = "0.2", path = "../html-node-macro" } - [dev-dependencies] -axum = "0.6" +axum = "0.6" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [features] -axum = ["html-node-core/axum"] -serde = ["html-node-core/serde"] -typed = ["html-node-core/typed", "html-node-macro/typed"] +axum = ["html-node-core/axum"] +pretty = ["html-node-core/pretty"] +serde = ["html-node-core/serde"] +typed = ["html-node-core/typed", "html-node-macro/typed"] diff --git a/html-node/examples/axum.rs b/html-node/examples/axum.rs index acbf4dc..8098fd7 100644 --- a/html-node/examples/axum.rs +++ b/html-node/examples/axum.rs @@ -5,6 +5,7 @@ use std::{ use axum::{extract::Query, routing::get, Router, Server}; use html_node::{html, text, Node}; +use html_node_core::pretty::Pretty; #[tokio::main] async fn main() { @@ -24,6 +25,7 @@ fn router() -> Router { .route("/about", get(about)) .route("/contact", get(contact)) .route("/greet", get(greet)) + .route("/pretty", get(pretty)) } fn layout(content: Node) -> Node { @@ -90,3 +92,11 @@ async fn greet(Query(params): Query>) -> Node {

{text!("hello, {name}")}!

}) } + +async fn pretty() -> Pretty { + Pretty(layout(html! { +
+

Pretty

+
+ })) +} diff --git a/html-node/src/lib.rs b/html-node/src/lib.rs index 4f37e22..ed273b5 100644 --- a/html-node/src/lib.rs +++ b/html-node/src/lib.rs @@ -118,7 +118,9 @@ mod macros; #[cfg(feature = "typed")] pub mod typed; -pub use html_node_core::{pretty, Comment, Doctype, Element, Fragment, Node, Text, UnsafeText}; +#[cfg(feature = "pretty")] +pub use html_node_core::pretty; +pub use html_node_core::{Comment, Doctype, Element, Fragment, Node, Text, UnsafeText}; /// The HTML to [`Node`] macro. /// /// See the [crate-level documentation](crate) for more information. From 5c170ad014b148ccb5f344ab900d03f0d555c72d Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Tue, 1 Aug 2023 22:20:17 -0700 Subject: [PATCH 06/11] basic, unformatted css --- html-node-macro/src/lib.rs | 39 +++++++++++++++++++++--- html-node-macro/src/node_handlers/mod.rs | 2 +- html-node/examples/axum.rs | 17 ++++++++++- html-node/src/lib.rs | 2 ++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/html-node-macro/src/lib.rs b/html-node-macro/src/lib.rs index 486d23a..300cb5c 100644 --- a/html-node-macro/src/lib.rs +++ b/html-node-macro/src/lib.rs @@ -7,24 +7,32 @@ mod node_handlers; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + os::linux::raw, +}; use node_handlers::{ handle_block, handle_comment, handle_doctype, handle_element, handle_fragment, handle_raw_text, handle_text, }; use proc_macro::TokenStream; -use proc_macro2::{Ident, TokenStream as TokenStream2}; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use proc_macro2_diagnostics::Diagnostic; -use quote::quote; +use quote::{quote, quote_spanned}; use rstml::{node::Node, Parser, ParserConfig}; -use syn::Type; +use syn::{spanned::Spanned, Type}; #[proc_macro] pub fn html(tokens: TokenStream) -> TokenStream { html_inner(tokens.into(), None) } +#[proc_macro] +pub fn style(tokens: TokenStream) -> TokenStream { + style_inner(tokens.into()) +} + #[cfg(feature = "typed")] #[proc_macro] pub fn typed_html(tokens: TokenStream) -> TokenStream { @@ -169,3 +177,26 @@ fn tokenize_nodes( (token_streams, diagnostics) } + +fn style_inner(tokens: TokenStream2) -> TokenStream { + let span = tokens.span(); + let raw_css = tokens + .into_iter() + .map(|t| t.to_string().split_whitespace().collect::()) + .collect::(); + + quote_spanned! { span=> + ::html_node::Node::Element( + ::html_node::Element { + name: ::std::convert::Into::<::std::string::String>::into("style"), + attributes: ::std::vec::Vec::new(), + children: ::std::option::Option::Some( + ::std::vec![ + ::html_node::Node::UnsafeText(#raw_css.into()) + ] + ) + } + ) + } + .into() +} diff --git a/html-node-macro/src/node_handlers/mod.rs b/html-node-macro/src/node_handlers/mod.rs index 901a4c1..5582110 100644 --- a/html-node-macro/src/node_handlers/mod.rs +++ b/html-node-macro/src/node_handlers/mod.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use proc_macro2::{Ident, Literal, TokenStream as TokenStream2}; use proc_macro2_diagnostics::{Diagnostic, SpanDiagnosticExt}; -use quote::{quote, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use rstml::node::{ KeyedAttribute, NodeAttribute, NodeBlock, NodeComment, NodeDoctype, NodeElement, NodeFragment, NodeName, NodeText, RawText, diff --git a/html-node/examples/axum.rs b/html-node/examples/axum.rs index 8098fd7..1e8db99 100644 --- a/html-node/examples/axum.rs +++ b/html-node/examples/axum.rs @@ -4,7 +4,7 @@ use std::{ }; use axum::{extract::Query, routing::get, Router, Server}; -use html_node::{html, text, Node}; +use html_node::{html, style, text, Node}; use html_node_core::pretty::Pretty; #[tokio::main] @@ -100,3 +100,18 @@ async fn pretty() -> Pretty {
})) } + +async fn css() -> Pretty { + Pretty(layout(html! { + { style! { + html { + margin: 0; + padding: 0; + height: 100vh; + } + } } +
+

Pretty

+
+ })) +} diff --git a/html-node/src/lib.rs b/html-node/src/lib.rs index ed273b5..0f6bab9 100644 --- a/html-node/src/lib.rs +++ b/html-node/src/lib.rs @@ -125,5 +125,7 @@ pub use html_node_core::{Comment, Doctype, Element, Fragment, Node, Text, Unsafe /// /// See the [crate-level documentation](crate) for more information. pub use html_node_macro::html; +/// CSS to [`Node`] macro. +pub use html_node_macro::style; pub use self::macros::*; From 087ed3af7568d3bde82e3272b7fc5377437c23c3 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Wed, 2 Aug 2023 15:12:07 -0700 Subject: [PATCH 07/11] Add styling macro. --- html-node-core/src/node/doctype.rs | 2 +- html-node-core/src/node/unsafe_text.rs | 2 +- html-node-macro/Cargo.toml | 32 ++-- html-node-macro/src/lib.rs | 29 +++- html-node-macro/src/node_handlers/mod.rs | 2 +- html-node/Cargo.toml | 13 +- html-node/examples/axum.rs | 15 -- html-node/examples/styling.rs | 191 +++++++++++++++++++++++ html-node/src/lib.rs | 36 ++++- html-node/tests/main.rs | 62 +++++++- 10 files changed, 336 insertions(+), 48 deletions(-) create mode 100644 html-node/examples/styling.rs diff --git a/html-node-core/src/node/doctype.rs b/html-node-core/src/node/doctype.rs index 5c8bd6f..11477b5 100644 --- a/html-node-core/src/node/doctype.rs +++ b/html-node-core/src/node/doctype.rs @@ -11,7 +11,7 @@ pub struct Doctype { /// The value of the doctype. /// /// ```html - /// + /// /// ``` pub syntax: String, } diff --git a/html-node-core/src/node/unsafe_text.rs b/html-node-core/src/node/unsafe_text.rs index f1a0672..da39eea 100644 --- a/html-node-core/src/node/unsafe_text.rs +++ b/html-node-core/src/node/unsafe_text.rs @@ -14,7 +14,7 @@ pub struct UnsafeText { } impl Display for UnsafeText { - /// Unformatted text. + /// Unescaped text. /// /// This string is **not** HTML encoded! fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { diff --git a/html-node-macro/Cargo.toml b/html-node-macro/Cargo.toml index 4f3fe44..318a628 100644 --- a/html-node-macro/Cargo.toml +++ b/html-node-macro/Cargo.toml @@ -1,15 +1,16 @@ [package] name = "html-node-macro" -authors.workspace = true -categories.workspace = true + +authors.workspace = true +categories.workspace = true description.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true [package.metadata.docs.rs] all-features = true @@ -19,12 +20,13 @@ version.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1" +proc-macro2 = "1" proc-macro2-diagnostics = { version = "0.10", default-features = false } -quote = "1" -rstml = { version = "0.11", default-features = false } -syn = "2" -syn_derive = { version = "0.1", optional = true } +quote = "1" +rstml = { version = "0.11", default-features = false } +syn = "2" +syn_derive = { version = "0.1", optional = true } [features] -typed = ["dep:syn_derive"] +basic-css = [] +typed = ["dep:syn_derive"] diff --git a/html-node-macro/src/lib.rs b/html-node-macro/src/lib.rs index 300cb5c..bf1e19a 100644 --- a/html-node-macro/src/lib.rs +++ b/html-node-macro/src/lib.rs @@ -7,27 +7,29 @@ mod node_handlers; -use std::{ - collections::{HashMap, HashSet}, - os::linux::raw, -}; +use std::collections::{HashMap, HashSet}; use node_handlers::{ handle_block, handle_comment, handle_doctype, handle_element, handle_fragment, handle_raw_text, handle_text, }; use proc_macro::TokenStream; -use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use proc_macro2::{Ident, TokenStream as TokenStream2}; use proc_macro2_diagnostics::Diagnostic; -use quote::{quote, quote_spanned}; +use quote::quote; +#[cfg(feature = "basic-css")] +use quote::quote_spanned; use rstml::{node::Node, Parser, ParserConfig}; -use syn::{spanned::Spanned, Type}; +#[cfg(feature = "basic-css")] +use syn::spanned::Spanned; +use syn::Type; #[proc_macro] pub fn html(tokens: TokenStream) -> TokenStream { html_inner(tokens.into(), None) } +#[cfg(feature = "basic-css")] #[proc_macro] pub fn style(tokens: TokenStream) -> TokenStream { style_inner(tokens.into()) @@ -178,11 +180,22 @@ fn tokenize_nodes( (token_streams, diagnostics) } +/// Naive conversion of a rust token stream into css content. +/// +/// Strips all whitespace from the given tokens, concatenates them into a +/// single string and returns a token stream of the given css content +/// wrapped in an HTML style tag. +#[cfg(feature = "basic-css")] fn style_inner(tokens: TokenStream2) -> TokenStream { let span = tokens.span(); let raw_css = tokens .into_iter() - .map(|t| t.to_string().split_whitespace().collect::()) + .map(|token_tree| { + token_tree + .to_string() + .split_whitespace() + .collect::() + }) .collect::(); quote_spanned! { span=> diff --git a/html-node-macro/src/node_handlers/mod.rs b/html-node-macro/src/node_handlers/mod.rs index 5582110..901a4c1 100644 --- a/html-node-macro/src/node_handlers/mod.rs +++ b/html-node-macro/src/node_handlers/mod.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use proc_macro2::{Ident, Literal, TokenStream as TokenStream2}; use proc_macro2_diagnostics::{Diagnostic, SpanDiagnosticExt}; -use quote::{quote, quote_spanned, ToTokens}; +use quote::{quote, ToTokens}; use rstml::node::{ KeyedAttribute, NodeAttribute, NodeBlock, NodeComment, NodeDoctype, NodeElement, NodeFragment, NodeName, NodeText, RawText, diff --git a/html-node/Cargo.toml b/html-node/Cargo.toml index fe617d1..7f0ed05 100644 --- a/html-node/Cargo.toml +++ b/html-node/Cargo.toml @@ -26,6 +26,10 @@ required-features = ["axum"] name = "typed_custom_attributes" required-features = ["typed"] +[[example]] +name = "styling" +required-features = ["basic-css"] + [dependencies] html-node-core = { version = "0.2", path = "../html-node-core" } html-node-macro = { version = "0.2", path = "../html-node-macro" } @@ -35,7 +39,8 @@ axum = "0.6" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [features] -axum = ["html-node-core/axum"] -pretty = ["html-node-core/pretty"] -serde = ["html-node-core/serde"] -typed = ["html-node-core/typed", "html-node-macro/typed"] +axum = ["html-node-core/axum"] +basic-css = ["html-node-macro/basic-css"] +pretty = ["html-node-core/pretty"] +serde = ["html-node-core/serde"] +typed = ["html-node-core/typed", "html-node-macro/typed"] diff --git a/html-node/examples/axum.rs b/html-node/examples/axum.rs index 1e8db99..4569550 100644 --- a/html-node/examples/axum.rs +++ b/html-node/examples/axum.rs @@ -100,18 +100,3 @@ async fn pretty() -> Pretty {
})) } - -async fn css() -> Pretty { - Pretty(layout(html! { - { style! { - html { - margin: 0; - padding: 0; - height: 100vh; - } - } } -
-

Pretty

-
- })) -} diff --git a/html-node/examples/styling.rs b/html-node/examples/styling.rs new file mode 100644 index 0000000..15627f9 --- /dev/null +++ b/html-node/examples/styling.rs @@ -0,0 +1,191 @@ +use html_node::{html, style}; + +fn main() { + bare_style(); + string_style(); + macro_style_unsupported_css(); + macro_style_supported_css(); +} + +/// Try and directly insert CSS into the style element. +/// +/// # Output +/// +/// ```text +/// Bare style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn bare_style() { + let node = html! { +
+ +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("Bare style:\n{node:#}"); +} + +/// Try and insert CSS into the style element via a string. +/// +/// # Output +/// +/// ```text +/// String style: +///
  • one
  • two
+/// +/// Pretty string style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn string_style() { + let node = html! { +
+ +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("String style:\n{node}"); + println!("Pretty string style:\n{node:#}"); +} + +/// Insert a style element and inner CSS content. +/// +/// The macro naively strips all whitespace from the CSS content, meaning the +/// shorthand version of outline as used below will still be rendered +/// incorrectly. See the next example for a workaround. +/// +/// Note that the `` tags are inserted by the macro. +/// +/// # Output +/// +/// ```text +/// Macro + unsupported CSS style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn macro_style_unsupported_css() { + let node = html! { +
+ { style! { + ul { + outline: 5px solid #CCDDFF; + padding-top: 15px; + } + } } +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("Macro + unsupported CSS style:\n{node:#}"); +} + +/// Insert a style element and inner CSS content, correctly. +/// +/// Since the macro strips all whitespace, use long-form CSS properties to +/// specify the needed selectors and rules. +/// +/// Note that the `` tags are inserted by the macro. +/// +/// # Output +/// +/// ```text +/// Macro + CSS style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn macro_style_supported_css() { + let node = html! { +
+ { style! { + ul { + outline-width: 5px; + outline-style: solid; + outline-color: #CCDDFF; + padding-top: 15px; + } + } } +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("Macro + CSS style:\n{node:#}"); +} diff --git a/html-node/src/lib.rs b/html-node/src/lib.rs index 0f6bab9..29415ab 100644 --- a/html-node/src/lib.rs +++ b/html-node/src/lib.rs @@ -67,6 +67,13 @@ //! //! ## Pretty-Printing //! +//! Pretty-printing is supported by default when formatting a [`Node`] using the +//! alternate formatter, specified by a `#` in the format string. +//! +//! If you want to avoid specifying the alternate formatter, enabling the +//! `pretty` feature will provide a convenience method [`Node::pretty()`] that +//! returns a wrapper around the node that will always be pretty-printed. +//! //! ```rust //! use html_node::{html, text}; //! @@ -100,10 +107,25 @@ //! \ //! "; //! -//! // note the `#` in the format string, which enables pretty-printing +//! // Note the `#` in the format string, which enables pretty-printing //! let formatted_html = format!("{html:#}"); //! //! assert_eq!(formatted_html, expected); +//! +//! # #[cfg(feature = "pretty")] +//! # { +//! // Wrap the HTML node in a pretty-printing wrapper. +//! let pretty = html.pretty(); +//! +//! // Get the pretty-printed HTML as a string by invoking the [`Display`][std::fmt::Display] trait. +//! let pretty_html_string = pretty.to_string(); +//! // Note the '#' is not required here. +//! let pretty_html_format = format!("{pretty}"); +//! +//! assert_eq!(pretty_html_string, expected); +//! assert_eq!(pretty_html_format, expected); +//! assert_eq!(pretty_html_string, pretty_html_format); +//! # } //! ``` #![warn(clippy::cargo)] @@ -125,7 +147,17 @@ pub use html_node_core::{Comment, Doctype, Element, Fragment, Node, Text, Unsafe /// /// See the [crate-level documentation](crate) for more information. pub use html_node_macro::html; -/// CSS to [`Node`] macro. +/// Experimental proc-macro allowing for the direct insertion of CSS into a +/// [`Node`]. +/// +/// This is a partial work-around for the fact that parsing raw CSS into a Rust +/// token-stream will clobber the CSS syntax, making it unparseable by the +/// browser, and leaving content unstyled. +/// +/// See the [styling example](../html-node/styling.rs) for more information on +/// how styles are parsed by this library and how the [`style!()] macro is used +/// to work-around some of the inevitable shortcomings. +#[cfg(feature = "basic-css")] pub use html_node_macro::style; pub use self::macros::*; diff --git a/html-node/tests/main.rs b/html-node/tests/main.rs index a6dc763..346c7c6 100644 --- a/html-node/tests/main.rs +++ b/html-node/tests/main.rs @@ -42,7 +42,7 @@ fn basic() { } #[test] -fn pretty_printed() { +fn pretty_printed_format() { let shopping_list = vec!["milk", "eggs", "bread"]; let html = html! { @@ -93,3 +93,63 @@ fn pretty_printed() { assert_eq!(pretty_html, expected); } + +#[cfg(feature = "pretty")] +#[test] +fn pretty_printed_helper() { + let pretty_html = html! { +
+
+

Pretty Printing Wrapper Test

+

This test should be pretty printed!

+
+
+ } + .pretty(); + + println!("Pretty helper:\n{pretty_html}"); + + let expected = r#"
+
+

+ Pretty Printing Wrapper Test +

+

+ This test should be + + pretty printed! + +

+
+
"#; + assert_eq!(expected, pretty_html.to_string()); +} + +#[cfg(feature = "basic-css")] +#[test] +fn css_style_macro() { + let style = html_node::style! { + #id { + outline-width: 5px; + outline-style: solid; + outline-color: #CCDDFF; + } + .class,div { + background-color: rgb(123, 253, 48); + } + }; + let expected = "\ + \ + "; + + assert_eq!(style.to_string(), expected); +} From 8c124a09c3b241b9651075c5ce1a57b168084b41 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Wed, 2 Aug 2023 15:21:28 -0700 Subject: [PATCH 08/11] Fix Cargo.toml formatting. Bump version. --- Cargo.toml | 2 +- html-node-core/Cargo.toml | 23 ++++++++++----------- html-node-macro/Cargo.toml | 31 ++++++++++++++-------------- html-node/Cargo.toml | 42 +++++++++++++++++++------------------- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 42a78fd..e539a13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,4 @@ resolver = "2" license = "MIT" readme = "README.md" repository = "https://github.com/vidhanio/html-node" - version = "0.2.0" + version = "0.3.0" diff --git a/html-node-core/Cargo.toml b/html-node-core/Cargo.toml index 8e73fbe..a57e764 100644 --- a/html-node-core/Cargo.toml +++ b/html-node-core/Cargo.toml @@ -1,27 +1,26 @@ [package] name = "html-node-core" - -authors.workspace = true -categories.workspace = true +authors.workspace = true +categories.workspace = true description.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -axum = { version = "0.6", optional = true, default-features = false } +axum = { version = "0.6", optional = true, default-features = false } serde = { version = "1.0", optional = true, features = ["derive"] } html-escape = "0.2" -paste = "1.0.14" +paste = "1.0.14" [features] axum = ["dep:axum"] diff --git a/html-node-macro/Cargo.toml b/html-node-macro/Cargo.toml index 318a628..079b021 100644 --- a/html-node-macro/Cargo.toml +++ b/html-node-macro/Cargo.toml @@ -1,16 +1,15 @@ [package] name = "html-node-macro" - -authors.workspace = true -categories.workspace = true +authors.workspace = true +categories.workspace = true description.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true [package.metadata.docs.rs] all-features = true @@ -20,13 +19,13 @@ version.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1" +proc-macro2 = "1" proc-macro2-diagnostics = { version = "0.10", default-features = false } -quote = "1" -rstml = { version = "0.11", default-features = false } -syn = "2" -syn_derive = { version = "0.1", optional = true } +quote = "1" +rstml = { version = "0.11", default-features = false } +syn = "2" +syn_derive = { version = "0.1", optional = true } [features] basic-css = [] -typed = ["dep:syn_derive"] +typed = ["dep:syn_derive"] diff --git a/html-node/Cargo.toml b/html-node/Cargo.toml index 7f0ed05..661455e 100644 --- a/html-node/Cargo.toml +++ b/html-node/Cargo.toml @@ -1,46 +1,46 @@ [package] name = "html-node" - -documentation = "https://docs.rs/html-node" - -authors.workspace = true -categories.workspace = true +authors.workspace = true +categories.workspace = true description.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -version.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true +documentation = "https://docs.rs/html-node" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + [[example]] -name = "axum" +name = "axum" required-features = ["axum"] [[example]] -name = "typed_custom_attributes" +name = "typed_custom_attributes" required-features = ["typed"] [[example]] -name = "styling" +name = "styling" required-features = ["basic-css"] [dependencies] -html-node-core = { version = "0.2", path = "../html-node-core" } +html-node-core = { version = "0.2", path = "../html-node-core" } html-node-macro = { version = "0.2", path = "../html-node-macro" } + [dev-dependencies] -axum = "0.6" +axum = "0.6" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [features] -axum = ["html-node-core/axum"] +axum = ["html-node-core/axum"] basic-css = ["html-node-macro/basic-css"] -pretty = ["html-node-core/pretty"] -serde = ["html-node-core/serde"] -typed = ["html-node-core/typed", "html-node-macro/typed"] +pretty = ["html-node-core/pretty"] +serde = ["html-node-core/serde"] +typed = ["html-node-core/typed", "html-node-macro/typed"] From 9a664aa59df8afd402f8701e41af195d5f853b11 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Wed, 2 Aug 2023 15:39:46 -0700 Subject: [PATCH 09/11] Always use current core and macro crates. --- html-node/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/html-node/Cargo.toml b/html-node/Cargo.toml index 661455e..cc2255a 100644 --- a/html-node/Cargo.toml +++ b/html-node/Cargo.toml @@ -30,8 +30,8 @@ name = "styling" required-features = ["basic-css"] [dependencies] -html-node-core = { version = "0.2", path = "../html-node-core" } -html-node-macro = { version = "0.2", path = "../html-node-macro" } +html-node-core = { path = "../html-node-core" } +html-node-macro = { path = "../html-node-macro" } [dev-dependencies] From b041aa11b1b0d84cd92ebf4177ee3eb4388ae782 Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Wed, 2 Aug 2023 19:18:13 -0700 Subject: [PATCH 10/11] Remove css macro. --- html-node-macro/Cargo.toml | 1 - html-node-macro/src/lib.rs | 44 -------- html-node/Cargo.toml | 5 - html-node/examples/axum.rs | 2 +- html-node/examples/styling.rs | 191 ---------------------------------- html-node/src/lib.rs | 12 --- html-node/tests/main.rs | 29 ------ 7 files changed, 1 insertion(+), 283 deletions(-) delete mode 100644 html-node/examples/styling.rs diff --git a/html-node-macro/Cargo.toml b/html-node-macro/Cargo.toml index 079b021..4f3fe44 100644 --- a/html-node-macro/Cargo.toml +++ b/html-node-macro/Cargo.toml @@ -27,5 +27,4 @@ syn = "2" syn_derive = { version = "0.1", optional = true } [features] -basic-css = [] typed = ["dep:syn_derive"] diff --git a/html-node-macro/src/lib.rs b/html-node-macro/src/lib.rs index bf1e19a..486d23a 100644 --- a/html-node-macro/src/lib.rs +++ b/html-node-macro/src/lib.rs @@ -17,11 +17,7 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, TokenStream as TokenStream2}; use proc_macro2_diagnostics::Diagnostic; use quote::quote; -#[cfg(feature = "basic-css")] -use quote::quote_spanned; use rstml::{node::Node, Parser, ParserConfig}; -#[cfg(feature = "basic-css")] -use syn::spanned::Spanned; use syn::Type; #[proc_macro] @@ -29,12 +25,6 @@ pub fn html(tokens: TokenStream) -> TokenStream { html_inner(tokens.into(), None) } -#[cfg(feature = "basic-css")] -#[proc_macro] -pub fn style(tokens: TokenStream) -> TokenStream { - style_inner(tokens.into()) -} - #[cfg(feature = "typed")] #[proc_macro] pub fn typed_html(tokens: TokenStream) -> TokenStream { @@ -179,37 +169,3 @@ fn tokenize_nodes( (token_streams, diagnostics) } - -/// Naive conversion of a rust token stream into css content. -/// -/// Strips all whitespace from the given tokens, concatenates them into a -/// single string and returns a token stream of the given css content -/// wrapped in an HTML style tag. -#[cfg(feature = "basic-css")] -fn style_inner(tokens: TokenStream2) -> TokenStream { - let span = tokens.span(); - let raw_css = tokens - .into_iter() - .map(|token_tree| { - token_tree - .to_string() - .split_whitespace() - .collect::() - }) - .collect::(); - - quote_spanned! { span=> - ::html_node::Node::Element( - ::html_node::Element { - name: ::std::convert::Into::<::std::string::String>::into("style"), - attributes: ::std::vec::Vec::new(), - children: ::std::option::Option::Some( - ::std::vec![ - ::html_node::Node::UnsafeText(#raw_css.into()) - ] - ) - } - ) - } - .into() -} diff --git a/html-node/Cargo.toml b/html-node/Cargo.toml index cc2255a..d7fe0cf 100644 --- a/html-node/Cargo.toml +++ b/html-node/Cargo.toml @@ -25,10 +25,6 @@ required-features = ["axum"] name = "typed_custom_attributes" required-features = ["typed"] -[[example]] -name = "styling" -required-features = ["basic-css"] - [dependencies] html-node-core = { path = "../html-node-core" } html-node-macro = { path = "../html-node-macro" } @@ -40,7 +36,6 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [features] axum = ["html-node-core/axum"] -basic-css = ["html-node-macro/basic-css"] pretty = ["html-node-core/pretty"] serde = ["html-node-core/serde"] typed = ["html-node-core/typed", "html-node-macro/typed"] diff --git a/html-node/examples/axum.rs b/html-node/examples/axum.rs index 4569550..8098fd7 100644 --- a/html-node/examples/axum.rs +++ b/html-node/examples/axum.rs @@ -4,7 +4,7 @@ use std::{ }; use axum::{extract::Query, routing::get, Router, Server}; -use html_node::{html, style, text, Node}; +use html_node::{html, text, Node}; use html_node_core::pretty::Pretty; #[tokio::main] diff --git a/html-node/examples/styling.rs b/html-node/examples/styling.rs deleted file mode 100644 index 15627f9..0000000 --- a/html-node/examples/styling.rs +++ /dev/null @@ -1,191 +0,0 @@ -use html_node::{html, style}; - -fn main() { - bare_style(); - string_style(); - macro_style_unsupported_css(); - macro_style_supported_css(); -} - -/// Try and directly insert CSS into the style element. -/// -/// # Output -/// -/// ```text -/// Bare style: -///
-/// -///
    -///
  • -/// one -///
  • -///
  • -/// two -///
  • -///
-///
-/// ``` -fn bare_style() { - let node = html! { -
- -
    -
  • one
  • -
  • two
  • -
-
- }; - - println!("Bare style:\n{node:#}"); -} - -/// Try and insert CSS into the style element via a string. -/// -/// # Output -/// -/// ```text -/// String style: -///
  • one
  • two
-/// -/// Pretty string style: -///
-/// -///
    -///
  • -/// one -///
  • -///
  • -/// two -///
  • -///
-///
-/// ``` -fn string_style() { - let node = html! { -
- -
    -
  • one
  • -
  • two
  • -
-
- }; - - println!("String style:\n{node}"); - println!("Pretty string style:\n{node:#}"); -} - -/// Insert a style element and inner CSS content. -/// -/// The macro naively strips all whitespace from the CSS content, meaning the -/// shorthand version of outline as used below will still be rendered -/// incorrectly. See the next example for a workaround. -/// -/// Note that the `` tags are inserted by the macro. -/// -/// # Output -/// -/// ```text -/// Macro + unsupported CSS style: -///
-/// -///
    -///
  • -/// one -///
  • -///
  • -/// two -///
  • -///
-///
-/// ``` -fn macro_style_unsupported_css() { - let node = html! { -
- { style! { - ul { - outline: 5px solid #CCDDFF; - padding-top: 15px; - } - } } -
    -
  • one
  • -
  • two
  • -
-
- }; - - println!("Macro + unsupported CSS style:\n{node:#}"); -} - -/// Insert a style element and inner CSS content, correctly. -/// -/// Since the macro strips all whitespace, use long-form CSS properties to -/// specify the needed selectors and rules. -/// -/// Note that the `` tags are inserted by the macro. -/// -/// # Output -/// -/// ```text -/// Macro + CSS style: -///
-/// -///
    -///
  • -/// one -///
  • -///
  • -/// two -///
  • -///
-///
-/// ``` -fn macro_style_supported_css() { - let node = html! { -
- { style! { - ul { - outline-width: 5px; - outline-style: solid; - outline-color: #CCDDFF; - padding-top: 15px; - } - } } -
    -
  • one
  • -
  • two
  • -
-
- }; - - println!("Macro + CSS style:\n{node:#}"); -} diff --git a/html-node/src/lib.rs b/html-node/src/lib.rs index 29415ab..979019e 100644 --- a/html-node/src/lib.rs +++ b/html-node/src/lib.rs @@ -147,17 +147,5 @@ pub use html_node_core::{Comment, Doctype, Element, Fragment, Node, Text, Unsafe /// /// See the [crate-level documentation](crate) for more information. pub use html_node_macro::html; -/// Experimental proc-macro allowing for the direct insertion of CSS into a -/// [`Node`]. -/// -/// This is a partial work-around for the fact that parsing raw CSS into a Rust -/// token-stream will clobber the CSS syntax, making it unparseable by the -/// browser, and leaving content unstyled. -/// -/// See the [styling example](../html-node/styling.rs) for more information on -/// how styles are parsed by this library and how the [`style!()] macro is used -/// to work-around some of the inevitable shortcomings. -#[cfg(feature = "basic-css")] -pub use html_node_macro::style; pub use self::macros::*; diff --git a/html-node/tests/main.rs b/html-node/tests/main.rs index 346c7c6..70a0ec9 100644 --- a/html-node/tests/main.rs +++ b/html-node/tests/main.rs @@ -124,32 +124,3 @@ fn pretty_printed_helper() { "#; assert_eq!(expected, pretty_html.to_string()); } - -#[cfg(feature = "basic-css")] -#[test] -fn css_style_macro() { - let style = html_node::style! { - #id { - outline-width: 5px; - outline-style: solid; - outline-color: #CCDDFF; - } - .class,div { - background-color: rgb(123, 253, 48); - } - }; - let expected = "\ - \ - "; - - assert_eq!(style.to_string(), expected); -} From b94f53be140f190741b01e86284e5541474b95ba Mon Sep 17 00:00:00 2001 From: Conner Blair Date: Wed, 2 Aug 2023 19:52:35 -0700 Subject: [PATCH 11/11] Revert "Remove css macro." This reverts commit b041aa11b1b0d84cd92ebf4177ee3eb4388ae782. --- html-node-macro/Cargo.toml | 1 + html-node-macro/src/lib.rs | 44 ++++++++ html-node/Cargo.toml | 5 + html-node/examples/axum.rs | 2 +- html-node/examples/styling.rs | 191 ++++++++++++++++++++++++++++++++++ html-node/src/lib.rs | 12 +++ html-node/tests/main.rs | 29 ++++++ 7 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 html-node/examples/styling.rs diff --git a/html-node-macro/Cargo.toml b/html-node-macro/Cargo.toml index 4f3fe44..079b021 100644 --- a/html-node-macro/Cargo.toml +++ b/html-node-macro/Cargo.toml @@ -27,4 +27,5 @@ syn = "2" syn_derive = { version = "0.1", optional = true } [features] +basic-css = [] typed = ["dep:syn_derive"] diff --git a/html-node-macro/src/lib.rs b/html-node-macro/src/lib.rs index 486d23a..bf1e19a 100644 --- a/html-node-macro/src/lib.rs +++ b/html-node-macro/src/lib.rs @@ -17,7 +17,11 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, TokenStream as TokenStream2}; use proc_macro2_diagnostics::Diagnostic; use quote::quote; +#[cfg(feature = "basic-css")] +use quote::quote_spanned; use rstml::{node::Node, Parser, ParserConfig}; +#[cfg(feature = "basic-css")] +use syn::spanned::Spanned; use syn::Type; #[proc_macro] @@ -25,6 +29,12 @@ pub fn html(tokens: TokenStream) -> TokenStream { html_inner(tokens.into(), None) } +#[cfg(feature = "basic-css")] +#[proc_macro] +pub fn style(tokens: TokenStream) -> TokenStream { + style_inner(tokens.into()) +} + #[cfg(feature = "typed")] #[proc_macro] pub fn typed_html(tokens: TokenStream) -> TokenStream { @@ -169,3 +179,37 @@ fn tokenize_nodes( (token_streams, diagnostics) } + +/// Naive conversion of a rust token stream into css content. +/// +/// Strips all whitespace from the given tokens, concatenates them into a +/// single string and returns a token stream of the given css content +/// wrapped in an HTML style tag. +#[cfg(feature = "basic-css")] +fn style_inner(tokens: TokenStream2) -> TokenStream { + let span = tokens.span(); + let raw_css = tokens + .into_iter() + .map(|token_tree| { + token_tree + .to_string() + .split_whitespace() + .collect::() + }) + .collect::(); + + quote_spanned! { span=> + ::html_node::Node::Element( + ::html_node::Element { + name: ::std::convert::Into::<::std::string::String>::into("style"), + attributes: ::std::vec::Vec::new(), + children: ::std::option::Option::Some( + ::std::vec![ + ::html_node::Node::UnsafeText(#raw_css.into()) + ] + ) + } + ) + } + .into() +} diff --git a/html-node/Cargo.toml b/html-node/Cargo.toml index d7fe0cf..cc2255a 100644 --- a/html-node/Cargo.toml +++ b/html-node/Cargo.toml @@ -25,6 +25,10 @@ required-features = ["axum"] name = "typed_custom_attributes" required-features = ["typed"] +[[example]] +name = "styling" +required-features = ["basic-css"] + [dependencies] html-node-core = { path = "../html-node-core" } html-node-macro = { path = "../html-node-macro" } @@ -36,6 +40,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [features] axum = ["html-node-core/axum"] +basic-css = ["html-node-macro/basic-css"] pretty = ["html-node-core/pretty"] serde = ["html-node-core/serde"] typed = ["html-node-core/typed", "html-node-macro/typed"] diff --git a/html-node/examples/axum.rs b/html-node/examples/axum.rs index 8098fd7..4569550 100644 --- a/html-node/examples/axum.rs +++ b/html-node/examples/axum.rs @@ -4,7 +4,7 @@ use std::{ }; use axum::{extract::Query, routing::get, Router, Server}; -use html_node::{html, text, Node}; +use html_node::{html, style, text, Node}; use html_node_core::pretty::Pretty; #[tokio::main] diff --git a/html-node/examples/styling.rs b/html-node/examples/styling.rs new file mode 100644 index 0000000..15627f9 --- /dev/null +++ b/html-node/examples/styling.rs @@ -0,0 +1,191 @@ +use html_node::{html, style}; + +fn main() { + bare_style(); + string_style(); + macro_style_unsupported_css(); + macro_style_supported_css(); +} + +/// Try and directly insert CSS into the style element. +/// +/// # Output +/// +/// ```text +/// Bare style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn bare_style() { + let node = html! { +
+ +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("Bare style:\n{node:#}"); +} + +/// Try and insert CSS into the style element via a string. +/// +/// # Output +/// +/// ```text +/// String style: +///
  • one
  • two
+/// +/// Pretty string style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn string_style() { + let node = html! { +
+ +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("String style:\n{node}"); + println!("Pretty string style:\n{node:#}"); +} + +/// Insert a style element and inner CSS content. +/// +/// The macro naively strips all whitespace from the CSS content, meaning the +/// shorthand version of outline as used below will still be rendered +/// incorrectly. See the next example for a workaround. +/// +/// Note that the `` tags are inserted by the macro. +/// +/// # Output +/// +/// ```text +/// Macro + unsupported CSS style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn macro_style_unsupported_css() { + let node = html! { +
+ { style! { + ul { + outline: 5px solid #CCDDFF; + padding-top: 15px; + } + } } +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("Macro + unsupported CSS style:\n{node:#}"); +} + +/// Insert a style element and inner CSS content, correctly. +/// +/// Since the macro strips all whitespace, use long-form CSS properties to +/// specify the needed selectors and rules. +/// +/// Note that the `` tags are inserted by the macro. +/// +/// # Output +/// +/// ```text +/// Macro + CSS style: +///
+/// +///
    +///
  • +/// one +///
  • +///
  • +/// two +///
  • +///
+///
+/// ``` +fn macro_style_supported_css() { + let node = html! { +
+ { style! { + ul { + outline-width: 5px; + outline-style: solid; + outline-color: #CCDDFF; + padding-top: 15px; + } + } } +
    +
  • one
  • +
  • two
  • +
+
+ }; + + println!("Macro + CSS style:\n{node:#}"); +} diff --git a/html-node/src/lib.rs b/html-node/src/lib.rs index 979019e..29415ab 100644 --- a/html-node/src/lib.rs +++ b/html-node/src/lib.rs @@ -147,5 +147,17 @@ pub use html_node_core::{Comment, Doctype, Element, Fragment, Node, Text, Unsafe /// /// See the [crate-level documentation](crate) for more information. pub use html_node_macro::html; +/// Experimental proc-macro allowing for the direct insertion of CSS into a +/// [`Node`]. +/// +/// This is a partial work-around for the fact that parsing raw CSS into a Rust +/// token-stream will clobber the CSS syntax, making it unparseable by the +/// browser, and leaving content unstyled. +/// +/// See the [styling example](../html-node/styling.rs) for more information on +/// how styles are parsed by this library and how the [`style!()] macro is used +/// to work-around some of the inevitable shortcomings. +#[cfg(feature = "basic-css")] +pub use html_node_macro::style; pub use self::macros::*; diff --git a/html-node/tests/main.rs b/html-node/tests/main.rs index 70a0ec9..346c7c6 100644 --- a/html-node/tests/main.rs +++ b/html-node/tests/main.rs @@ -124,3 +124,32 @@ fn pretty_printed_helper() { "#; assert_eq!(expected, pretty_html.to_string()); } + +#[cfg(feature = "basic-css")] +#[test] +fn css_style_macro() { + let style = html_node::style! { + #id { + outline-width: 5px; + outline-style: solid; + outline-color: #CCDDFF; + } + .class,div { + background-color: rgb(123, 253, 48); + } + }; + let expected = "\ + \ + "; + + assert_eq!(style.to_string(), expected); +}