Skip to content

Commit

Permalink
RawHtml
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Oct 8, 2023
1 parent e4b130b commit c296205
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 3 deletions.
65 changes: 62 additions & 3 deletions htmx-macros/src/special_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use quote::{ToTokens, TokenStreamExt};
use rstml::node::CustomNode;
use rstml::recoverable::{ParseRecoverable, RecoverableContext};
use syn::parse::{ParseBuffer, ParseStream};
use syn::token::Brace;
use syn::{Expr, Token};
use syn::punctuated::Punctuated;
use syn::token::{Brace, Paren};
use syn::{Expr, ExprPath, Token};
use syn_derive::ToTokens;

use crate::htmx::{expand_node, expand_nodes};
Expand All @@ -25,6 +26,16 @@ macro_rules! braced {($name:ident in $parser:expr, $input:expr) => {{
brace.0
}};}

macro_rules! parenthesized {($name:ident in $parser:expr, $input:expr) => {{
let paren = $parser.save_diagnostics((|| {
let content;
let paren = syn::parenthesized!(content in $input);
Ok((paren, content))
})())?;
$name = paren.1;
paren.0
}};}

fn parse_nodes<'a>(
parser: &mut RecoverableContext,
input: impl Borrow<ParseBuffer<'a>>,
Expand Down Expand Up @@ -60,13 +71,15 @@ pub enum Special {
If(If),
For(For),
While(While),
FunctionCall(FunctionCall),
}
impl Special {
pub(crate) fn expand_node(self, htmx: &TokenStream, child: bool) -> Result {
match self {
Special::If(if_) => if_.expand_node(htmx, child),
Special::For(for_) => for_.expand_node(htmx, child),
Special::While(while_) => while_.expand_node(htmx, child),
Special::FunctionCall(function_call) => function_call.expand_node(htmx, child),
}
}
}
Expand All @@ -77,14 +90,21 @@ impl CustomNode for Special {
}

fn peek_element(input: ParseStream) -> bool {
input.peek(Token![if]) || input.peek(Token![for]) || input.peek(Token![while])
let fork = input.fork();
input.peek(Token![if])
|| input.peek(Token![for])
|| input.peek(Token![while])
|| fork.parse::<Token![<]>().is_ok()
&& fork.parse::<ExprPath>().is_ok()
&& fork.peek(Paren)
}

fn parse_element(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
match () {
() if input.peek(Token![if]) => parser.parse_recoverable(input).map(Self::If),
() if input.peek(Token![for]) => parser.parse_recoverable(input).map(Self::For),
() if input.peek(Token![while]) => parser.parse_recoverable(input).map(Self::While),
() if input.peek(Token![<]) => parser.parse_recoverable(input).map(Self::FunctionCall),
_ => unreachable!("`peek_element` should only peek valid keywords"),
}
}
Expand Down Expand Up @@ -263,3 +283,42 @@ impl ParseRecoverable for While {
})
}
}

#[derive(Debug, ToTokens)]
pub struct FunctionCall {
pub open_token: Token![<],
pub function: ExprPath,
#[syn(parenthesized)]
pub paren: Paren,
#[syn(in = paren)]
#[to_tokens(TokenStreamExt::append_all)]
pub args: Punctuated<Expr, Token![,]>,
pub slash: Token![/],
pub gt_token: Token![>],
}

impl FunctionCall {
fn expand_node(self, htmx: &TokenStream, child: bool) -> Result {
let Self { function, args, .. } = self;
let args = args.into_iter();
Ok(if child {
quote!($node = $node.child(&#function(#(Into::into(#args),)*));)
} else {
quote!(#htmx::ToHtml::write_to_html(&#function(#(Into::into(#args),)*), &mut $htmx);)
})
}
}

impl ParseRecoverable for FunctionCall {
fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
let args;
Some(Self {
open_token: parser.parse_simple(input)?,
function: parser.parse_simple(input)?,
paren: parenthesized!(args in parser, input),
args: parser.save_diagnostics(Punctuated::parse_terminated(&args))?,
slash: parser.parse_simple(input)?,
gt_token: parser.parse_simple(input)?,
})
}
}
37 changes: 37 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,40 @@ impl ToHtml for CustomElement {
write!(out.0, "</{}>", self.name).unwrap();
}
}

/// Puts content directly into HTML bypassing HTML-escaping.
///
/// ```
/// # use htmx::{htmx, RawHtml};
/// # insta::assert_display_snapshot!("doc-RawHtml",
/// htmx! {
/// "this < will be > escaped "
/// <RawHtml("This < will > not")/>
/// }
/// # );
/// ```
pub struct RawHtml<'a>(pub Cow<'a, str>);

impl<'a> RawHtml<'a> {
/// Creates a new `RawHtml`.
pub fn new(content: impl Into<Cow<'a, str>>) -> Self {
Self(content.into())
}
}

impl ToHtml for RawHtml<'_> {
fn write_to_html(&self, html: &mut Html) {
html.0.push_str(&self.0);
}

fn to_html(&self) -> Html {
Html(self.0.to_string())
}

fn into_html(self) -> Html
where
Self: Sized,
{
Html(self.0.into())
}
}
5 changes: 5 additions & 0 deletions src/snapshots/doctest_lib_rs__doc-RawHtml.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: src/lib.rs
expression: "htmx! { \"this < will be > escaped \" < RawHtml(\"This < will > not\") / > }"
---
<!DOCTYPE html>this &lt; will be &gt; escaped This < will > not
12 changes: 12 additions & 0 deletions tests/macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,15 @@ fn custom_element() {
.to_string()
);
}

#[test]
fn raw_html() {
use htmx::RawHtml;
insta::assert_snapshot!(
htmx! {
"this < will be > escaped "
<RawHtml("This < will > not")/>
}
.to_string()
);
}
5 changes: 5 additions & 0 deletions tests/snapshots/r#macro__raw_html.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: tests/macro.rs
expression: "htmx! {\n \"this < will be > escaped \" < RawHtml(\"This < will > not\") / >\n }.to_string()"
---
<!DOCTYPE html>this &lt; will be &gt; escaped This < will > not

0 comments on commit c296205

Please sign in to comment.