Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use procedural macro for parsing jsx-like syntax #500

Merged
merged 49 commits into from
Jun 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
b336704
cargo new --lib yew-html
jstarry May 8, 2019
3464e00
Add proc macro dependencies
jstarry May 8, 2019
f28bd90
Create macro for parsing lists
jstarry May 16, 2019
6c94426
Expand html list tests
jstarry May 16, 2019
dec10ed
Only allow one root html element
jstarry May 16, 2019
1d810c1
Add html-common and tokenize
jstarry May 16, 2019
6e59a79
Support html blocks
jstarry May 16, 2019
17f9759
Use cursor for peeking and remove HtmlListChildren
jstarry May 16, 2019
b1deca6
Reorder html list file
jstarry May 16, 2019
72f3007
Implement FromIterator for HtmlTree
jstarry May 16, 2019
4ce8005
Allow peek inside and add rudimentary tag support
jstarry May 16, 2019
6d0ffab
Support self closing tags
jstarry May 17, 2019
83746b1
Improve tag and list error messages
jstarry May 22, 2019
00de9bb
Support root literal nodes and braced literals
jstarry May 22, 2019
e082380
Integrate with yew virtual dom library
jstarry May 24, 2019
16821eb
Add example runnable from main crate
jstarry May 24, 2019
92d7a0b
Add empty list test case
jstarry May 24, 2019
14b58e3
Allow root level iterables
jstarry May 24, 2019
4f11996
Support root level expressions
jstarry May 24, 2019
1246ad5
Basic support for components
jstarry May 24, 2019
940400f
Merge branch 'master' of github.com:jstarry/yew-html-macro
jstarry May 25, 2019
22ec126
Merge html-common and html-impl crates
jstarry May 25, 2019
1605c15
Rename macro crates, add proc_macro feature switch, split off shared …
jstarry May 25, 2019
a7291b8
Add root component tests
jstarry May 25, 2019
3aca0d8
Support component props
jstarry May 26, 2019
26edd6e
Fix syn dependency version range
jstarry May 27, 2019
284a680
Implement tag attributes and overhaul prop parsing
jstarry May 27, 2019
9a8b0ad
Disallow type prop for components
jstarry May 27, 2019
44775c1
Support special tag attributes
jstarry May 27, 2019
1625a81
Add tag listener support
jstarry May 27, 2019
b07f279
Fix tag classes
jstarry May 28, 2019
5df053e
Fix select component
jstarry May 28, 2019
4011a7c
Use component transform trait to set props
jstarry May 28, 2019
58c69d3
Special handling for href
jstarry May 28, 2019
2858503
Add alias module for macro
therustmonk May 28, 2019
f02cfb3
Merge pull request #1 from DenisKolodin/proc-macro-fixes
jstarry May 28, 2019
a12a38f
Code cleanup and small fix for component type parsing
jstarry May 28, 2019
0ca34f9
Rename html tag file
jstarry May 28, 2019
0b08b80
Split off tag attributes code
jstarry May 28, 2019
eccee9c
Improve error messages
jstarry May 28, 2019
47f4b50
Add test for invalid list children
jstarry May 28, 2019
5615f53
Add more test cases and improve error messages
jstarry May 28, 2019
d3dac87
Fix build
jstarry May 28, 2019
e121b0c
Merge branch 'master' into proc-macro
jstarry Jun 3, 2019
1a7c2cf
Allow iterable nodes of all types
jstarry Jun 13, 2019
9f2b3e1
Allow setting href with types that impl Into<Href>
jstarry Jun 13, 2019
f9cf5ea
Bump example recursion limits
jstarry Jun 13, 2019
1e1eb6f
Code cleanup
jstarry Jun 27, 2019
02f2bf3
cargo clippy fixes
jstarry Jun 28, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ toml = { version = "0.4", optional = true }
serde_yaml = { version = "0.8.3", optional = true }
rmp-serde = { version = "0.13.7", optional = true }
serde_cbor = { version = "0.9.0", optional = true }
yew-macro = { path = "crates/macro", optional = true }
yew-shared = { path = "crates/shared" }

[dev-dependencies]
serde_derive = "1"
Expand All @@ -36,3 +38,32 @@ web_test = []
yaml = ["serde_yaml"]
msgpack = ["rmp-serde"]
cbor = ["serde_cbor"]
proc_macro = ["yew-macro"]

[workspace]
members = [
"crates/macro",
"crates/macro-impl",
"crates/shared",
"examples/counter",
"examples/crm",
"examples/custom_components",
"examples/dashboard",
"examples/file_upload",
"examples/fragments",
"examples/game_of_life",
"examples/inner_html",
"examples/js_callback",
"examples/large_table",
"examples/minimal",
"examples/mount_point",
"examples/multi_thread",
"examples/npm_and_rest",
"examples/routing",
"examples/server",
"examples/showcase",
"examples/textarea",
"examples/timer",
"examples/todomvc",
"examples/two_apps",
]
15 changes: 15 additions & 0 deletions crates/macro-impl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "yew-macro-impl"
version = "0.7.0"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
boolinator = "2.4.0"
lazy_static = "1.3.0"
proc-macro-hack = "0.5"
proc-macro2 = "0.4"
quote = "0.6"
syn = { version = "^0.15.34", features = ["full"] }
51 changes: 51 additions & 0 deletions crates/macro-impl/src/html_tree/html_block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use super::html_iterable::HtmlIterable;
use super::html_node::HtmlNode;
use crate::Peek;
use proc_macro2::Delimiter;
use quote::{quote, quote_spanned, ToTokens};
use syn::braced;
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::token;

pub struct HtmlBlock {
content: BlockContent,
brace: token::Brace,
}

enum BlockContent {
Node(HtmlNode),
Iterable(HtmlIterable),
}

impl Peek<()> for HtmlBlock {
fn peek(cursor: Cursor) -> Option<()> {
cursor.group(Delimiter::Brace).map(|_| ())
}
}

impl Parse for HtmlBlock {
fn parse(input: ParseStream) -> ParseResult<Self> {
let content;
let brace = braced!(content in input);
let content = if HtmlIterable::peek(content.cursor()).is_some() {
BlockContent::Iterable(content.parse()?)
} else {
BlockContent::Node(content.parse()?)
};

Ok(HtmlBlock { brace, content })
}
}

impl ToTokens for HtmlBlock {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let HtmlBlock { content, brace } = self;
let new_tokens = match content {
BlockContent::Iterable(html_iterable) => quote! {#html_iterable},
BlockContent::Node(html_node) => quote! {#html_node},
};

tokens.extend(quote_spanned! {brace.span=> #new_tokens});
}
}
215 changes: 215 additions & 0 deletions crates/macro-impl/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
use super::HtmlProp;
use super::HtmlPropSuffix;
use crate::Peek;
use boolinator::Boolinator;
use proc_macro2::Span;
use quote::{quote, quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::parse;
use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::spanned::Spanned;
use syn::{Ident, Token, Type};

pub struct HtmlComponent(HtmlComponentInner);

impl Peek<()> for HtmlComponent {
fn peek(cursor: Cursor) -> Option<()> {
let (punct, cursor) = cursor.punct()?;
(punct.as_char() == '<').as_option()?;

HtmlComponent::peek_type(cursor)
}
}

impl Parse for HtmlComponent {
fn parse(input: ParseStream) -> ParseResult<Self> {
let lt = input.parse::<Token![<]>()?;
let HtmlPropSuffix { stream, div, gt } = input.parse()?;
if div.is_none() {
return Err(syn::Error::new_spanned(
HtmlComponentTag { lt, gt },
"expected component tag be of form `< .. />`",
));
}

match parse(stream) {
Ok(comp) => Ok(HtmlComponent(comp)),
Err(err) => {
if err.to_string().starts_with("unexpected end of input") {
Err(syn::Error::new_spanned(div, err.to_string()))
} else {
Err(err)
}
}
}
}
}

impl ToTokens for HtmlComponent {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let HtmlComponentInner { ty, props } = &self.0;
let vcomp = Ident::new("__yew_vcomp", Span::call_site());
let vcomp_props = Ident::new("__yew_vcomp_props", Span::call_site());
let override_props = props.iter().map(|props| match props {
Props::List(ListProps(vec_props)) => {
let check_props = vec_props.iter().map(|HtmlProp { name, .. }| {
quote_spanned! { name.span()=> #vcomp_props.#name; }
});

let set_props = vec_props.iter().map(|HtmlProp { name, value }| {
quote_spanned! { value.span()=>
#vcomp_props.#name = ::yew::virtual_dom::vcomp::Transformer::transform(&mut #vcomp, #value);
}
});

quote! {
#(#check_props#set_props)*
}
}
Props::With(WithProps(props)) => {
quote_spanned! { props.span()=> #vcomp_props = #props; }
}
});

tokens.extend(quote_spanned! { ty.span()=> {
let (mut #vcomp_props, mut #vcomp) = ::yew::virtual_dom::VComp::lazy::<#ty>();
#(#override_props)*
#vcomp.set_props(#vcomp_props);
::yew::virtual_dom::VNode::VComp(#vcomp)
}});
}
}

impl HtmlComponent {
fn double_colon(mut cursor: Cursor) -> Option<Cursor> {
for _ in 0..2 {
let (punct, c) = cursor.punct()?;
(punct.as_char() == ':').as_option()?;
cursor = c;
}

Some(cursor)
}

fn peek_type(mut cursor: Cursor) -> Option<()> {
let mut type_str: String = "".to_owned();
let mut colons_optional = true;

loop {
let mut found_colons = false;
let mut post_colons_cursor = cursor;
if let Some(c) = Self::double_colon(post_colons_cursor) {
found_colons = true;
post_colons_cursor = c;
} else if !colons_optional {
break;
}

if let Some((ident, c)) = post_colons_cursor.ident() {
cursor = c;
if found_colons {
type_str += "::";
}
type_str += &ident.to_string();
} else {
break;
}

// only first `::` is optional
colons_optional = false;
}

(!type_str.is_empty()).as_option()?;
(type_str.to_lowercase() != type_str).as_option()
}
}

pub struct HtmlComponentInner {
ty: Type,
props: Option<Props>,
}

impl Parse for HtmlComponentInner {
fn parse(input: ParseStream) -> ParseResult<Self> {
let ty = input.parse()?;
// backwards compat
let _ = input.parse::<Token![:]>();

let props = if let Some(prop_type) = Props::peek(input.cursor()) {
match prop_type {
PropType::List => input.parse().map(Props::List).map(Some)?,
PropType::With => input.parse().map(Props::With).map(Some)?,
}
} else {
None
};

Ok(HtmlComponentInner { ty, props })
}
}

struct HtmlComponentTag {
lt: Token![<],
gt: Token![>],
}

impl ToTokens for HtmlComponentTag {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let HtmlComponentTag { lt, gt } = self;
tokens.extend(quote! {#lt#gt});
}
}

enum PropType {
List,
With,
}

enum Props {
List(ListProps),
With(WithProps),
}

impl Peek<PropType> for Props {
fn peek(cursor: Cursor) -> Option<PropType> {
let (ident, _) = cursor.ident()?;
let prop_type = if ident.to_string() == "with" {
PropType::With
} else {
PropType::List
};

Some(prop_type)
}
}

struct ListProps(Vec<HtmlProp>);
impl Parse for ListProps {
fn parse(input: ParseStream) -> ParseResult<Self> {
let mut props: Vec<HtmlProp> = Vec::new();
while HtmlProp::peek(input.cursor()).is_some() {
props.push(input.parse::<HtmlProp>()?);
}

for prop in &props {
if prop.name.to_string() == "type" {
return Err(syn::Error::new_spanned(&prop.name, "expected identifier"));
}
}

Ok(ListProps(props))
}
}

struct WithProps(Ident);
impl Parse for WithProps {
fn parse(input: ParseStream) -> ParseResult<Self> {
let with = input.parse::<Ident>()?;
if with.to_string() != "with" {
return Err(input.error("expected to find `with` token"));
}
let props = input.parse::<Ident>()?;
let _ = input.parse::<Token![,]>();
Ok(WithProps(props))
}
}
53 changes: 53 additions & 0 deletions crates/macro-impl/src/html_tree/html_iterable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::Peek;
use boolinator::Boolinator;
use proc_macro2::TokenStream;
use quote::{quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::spanned::Spanned;
use syn::{Expr, Token};

pub struct HtmlIterable(Expr);

impl Peek<()> for HtmlIterable {
fn peek(cursor: Cursor) -> Option<()> {
let (ident, _) = cursor.ident()?;
(ident.to_string() == "for").as_option()
}
}

impl Parse for HtmlIterable {
fn parse(input: ParseStream) -> ParseResult<Self> {
let for_token = input.parse::<Token![for]>()?;

match input.parse() {
Ok(expr) => Ok(HtmlIterable(expr)),
Err(err) => {
if err.to_string().starts_with("unexpected end of input") {
Err(syn::Error::new_spanned(
for_token,
"expected expression after `for`",
))
} else {
Err(err)
}
}
}
}
}

impl ToTokens for HtmlIterable {
fn to_tokens(&self, tokens: &mut TokenStream) {
let expr = &self.0;
let new_tokens = quote_spanned! {expr.span()=> {
let mut __yew_vlist = ::yew::virtual_dom::VList::new();
let __yew_nodes: &mut ::std::iter::Iterator<Item = _> = &mut(#expr);
for __yew_node in __yew_nodes.into_iter() {
__yew_vlist.add_child(__yew_node.into());
}
::yew::virtual_dom::VNode::from(__yew_vlist)
}};

tokens.extend(new_tokens);
}
}
Loading