Skip to content

Commit

Permalink
feat(config)!: transform_block callback (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
stoically committed Jan 3, 2021
1 parent 7e31eb4 commit 02aa7cb
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 8 deletions.
95 changes: 92 additions & 3 deletions src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! RSX Parser
use proc_macro2::{TokenStream, TokenTree};
use proc_macro2::{Span, TokenStream, TokenTree};
use syn::{
braced,
ext::IdentExt,
Expand All @@ -13,11 +13,12 @@ use syn::{
use crate::{node::*, punctuation::*};

/// Configures the `Parser` behavior
#[derive(Debug, Clone, Default)]
#[derive(Default)]
pub struct ParserConfig {
flat_tree: bool,
number_of_top_level_nodes: Option<usize>,
type_of_top_level_nodes: Option<NodeType>,
transform_block: Option<Box<dyn Fn(ParseStream) -> Result<Option<TokenStream>>>>,
}

impl ParserConfig {
Expand All @@ -43,6 +44,46 @@ impl ParserConfig {
self.type_of_top_level_nodes = Some(node_type);
self
}

/// Transforms the `value` of all `NodeType::Block`s with the given closure
/// callback. The given `ParseStream` is the content of the block.
///
/// When `Some(TokenStream)` is returned, the `TokenStream` is parsed as
/// Rust block content. The `ParseStream` must be completely consumed in
/// this case (no tokens left).
///
/// If `None` is returned, the `ParseStream` is parsed as Rust block
/// content. The `ParseStream` isn't forked, so partial parsing inside the
/// transform callback will break this mechanism - fork if you want to avoid
/// breaking.
///
/// An example usage might be a custom syntax inside blocks which isn't
/// valid Rust. The given example simply translates the `%` character into
/// the string `percent`
///
/// ```rust
/// use quote::quote;
/// use syn::Token;
/// use syn_rsx::{parse2_with_config, ParserConfig};
///
/// let tokens = quote! {
/// <div>{%}</div>
/// };
///
/// let config = ParserConfig::new().transform_block(|input| {
/// input.parse::<Token![%]>()?;
/// Ok(Some(quote! { "percent" }))
/// });
///
/// parse2_with_config(tokens, config).unwrap();
/// ```
pub fn transform_block<F>(mut self, callback: F) -> Self
where
F: Fn(ParseStream) -> Result<Option<TokenStream>> + 'static,
{
self.transform_block = Some(Box::new(callback));
self
}
}

/// RSX Parser
Expand Down Expand Up @@ -120,7 +161,11 @@ impl Parser {
}

fn block(&self, input: ParseStream) -> Result<Node> {
let block = self.block_expr(input)?;
let block = if self.config.transform_block.is_some() {
self.block_transform(input)?
} else {
self.block_expr(input)?
};

Ok(Node {
name: None,
Expand All @@ -131,6 +176,50 @@ impl Parser {
})
}

fn block_transform(&self, input: ParseStream) -> Result<Expr> {
let transform_block = self.config.transform_block.as_ref().unwrap();

input.step(|cursor| {
if let Some((tree, next)) = cursor.token_tree() {
match tree {
TokenTree::Group(block_group) => {
let block_span = block_group.span();
let parser = move |block_content: ParseStream| match transform_block(
block_content,
) {
Ok(transformed_tokens) => match transformed_tokens {
Some(tokens) => {
let parser = move |input: ParseStream| {
Ok(self.block_content_to_block(input, block_span))
};
parser.parse2(tokens)?
}
None => self.block_content_to_block(block_content, block_span),
},
Err(error) => Err(error),
};
Ok((parser.parse2(block_group.stream())?, next))
}
_ => Err(cursor.error("unexpected: no Group in TokenTree found")),
}
} else {
Err(cursor.error("unexpected: no TokenTree found"))
}
})
}

fn block_content_to_block(&self, input: ParseStream, span: Span) -> Result<Expr> {
Ok(ExprBlock {
attrs: vec![],
label: None,
block: Block {
brace_token: Brace { span },
stmts: Block::parse_within(&input)?,
},
}
.into())
}

fn block_expr(&self, input: ParseStream) -> Result<Expr> {
let fork = input.fork();
let content;
Expand Down
55 changes: 50 additions & 5 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,12 @@ fn test_block_as_attribute() {

#[test]
fn test_number_of_top_level_nodes() {
let config = ParserConfig::new().number_of_top_level_nodes(2);

let tokens = quote! {
<div />
<div />
<div />
};
let nodes = parse2_with_config(tokens, config.clone());
let nodes = parse2_with_config(tokens, ParserConfig::new().number_of_top_level_nodes(2));
assert!(nodes.is_err());

let tokens = quote! {
Expand All @@ -129,13 +127,16 @@ fn test_number_of_top_level_nodes() {
</div>
<div />
};
let nodes = parse2_with_config(tokens, config.clone().flat_tree());
let nodes = parse2_with_config(
tokens,
ParserConfig::new().number_of_top_level_nodes(2).flat_tree(),
);
assert!(nodes.is_ok());

let tokens = quote! {
<div />
};
let nodes = parse2_with_config(tokens, config);
let nodes = parse2_with_config(tokens, ParserConfig::new().number_of_top_level_nodes(2));
assert!(nodes.is_err());
}

Expand All @@ -150,3 +151,47 @@ fn test_type_of_top_level_nodes() {

assert!(nodes.is_err())
}

#[test]
fn test_transform_block_some() {
use syn::{Expr, Lit, Stmt, Token};

let tokens = quote! {
<div>{%}</div>
};

let config = ParserConfig::new().transform_block(|input| {
input.parse::<Token![%]>()?;
Ok(Some(quote! { "percent" }))
});

let nodes = parse2_with_config(tokens, config).unwrap();

assert_eq!(
match &nodes[0].children[0].value {
Some(Expr::Block(expr)) => {
match &expr.block.stmts[0] {
Stmt::Expr(Expr::Lit(expr)) => match &expr.lit {
Lit::Str(lit_str) => Some(lit_str.value()),
_ => None,
},
_ => None,
}
}
_ => None,
},
Some("percent".to_owned())
)
}

#[test]
fn test_transform_block_none() {
let tokens = quote! {
<div>{"foo"}</div>
};

let config = ParserConfig::new().transform_block(|_| Ok(None));
let nodes = parse2_with_config(tokens, config);

assert!(nodes.is_ok())
}

0 comments on commit 02aa7cb

Please sign in to comment.