Feat: docs, code frm percy

This commit is contained in:
Jonathan Kelley committed 2021-01-14 02:56:41 -05:00
commit 2b9c8d09d9
72 files changed
+5130

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
[package]
name = "html-macro"
version = "0.1.9"
description = "html macro"
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
license = "MIT/Apache-2.0"
repository = "https://github.com/chinedufn/percy"
documentation = "https://chinedufn.github.io/percy/api/html_macro/"
edition = "2018"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "0.4", features = ["span-locations"] }
quote = "0.6.11"
syn = { version = "0.15", features = ["full", "extra-traits"] }
html-validation = {path = "../html-validation", version = "0.1.2"}
+20
View File
@@ -0,0 +1,20 @@
# html-macro
```rust
use html_macro::*;
fn main () {
let component = html! { <div id='component'>Some component</div> };
let text_var = "You can interpolate text variables";
let html = html! {
<div onclick=|_ev: web_sys::MouseEvent| {}>
You can type text right into the elements
{ component }
{ text_var }
</div>
};
println!("{}", node);
}
```
+55
View File
@@ -0,0 +1,55 @@
extern crate proc_macro;
use crate::parser::HtmlParser;
use crate::tag::Tag;
use syn::parse::{Parse, ParseStream, Result};
use syn::parse_macro_input;
mod parser;
mod tag;
/// Used to generate VirtualNode's from a TokenStream.
///
/// html! { <div> Welcome to the html! procedural macro! </div> }
#[proc_macro]
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let parsed = parse_macro_input!(input as Html);
let mut html_parser = HtmlParser::new();
let parsed_tags_len = parsed.tags.len();
// Iterate over all of our parsed tags and push them into our HtmlParser one by one.
//
// As we go out HtmlParser will maintain some heuristics about what we've done so far
// since that will sometimes inform how to parse the next token.
for (idx, tag) in parsed.tags.iter().enumerate() {
let mut next_tag = None;
if parsed_tags_len - 1 > idx {
next_tag = Some(&parsed.tags[idx + 1])
}
html_parser.push_tag(tag, next_tag);
}
html_parser.finish().into()
}
#[derive(Debug)]
struct Html {
tags: Vec<Tag>,
}
impl Parse for Html {
fn parse(input: ParseStream) -> Result<Self> {
let mut tags = Vec::new();
while !input.is_empty() {
let tag: Tag = input.parse()?;
tags.push(tag);
}
Ok(Html { tags })
}
}
+102
View File
@@ -0,0 +1,102 @@
use crate::parser::HtmlParser;
use crate::tag::{Tag, TagKind};
use proc_macro2::Span;
use quote::quote;
use syn::spanned::Spanned;
use syn::Block;
impl HtmlParser {
/// Parse an incoming Tag::Braced text node
pub(crate) fn parse_braced(
&mut self,
block: &Box<Block>,
brace_span: &Span,
next_tag: Option<&Tag>,
) {
// We'll check to see if there is a space between this block and the previous open
// tag's closing brace.
//
// If so we'll then check if the node in this block is a text node. If it is we'll
// insert a single white space before it.
//
// let some_var = "hello"
// let another_var = "world";
//
// html! { <span>{some_var}</span> } -> would not get a " " inserted
//
// html! { <span> {some_var}</span> } -> would get a " " inserted
let mut insert_whitespace_before_text = false;
if let Some(open_tag_end) = self.recent_span_locations.most_recent_open_tag_end.as_ref() {
if self.last_tag_kind == Some(TagKind::Open)
&& self.separated_by_whitespace(open_tag_end, brace_span)
{
insert_whitespace_before_text = true;
}
}
// If
// 1. The next tag is a closing tag or another braced block
// 2. There is space between this brace and that next tag / braced block
//
// Then
// We'll insert some spacing after this brace.
//
// This ensures that we properly maintain spacing between two neighboring braced
// text nodes
//
// html! { <div>{ This Brace } { Space WILL be inserted }</div>
// -> <div>This Brace Space WILL be inserted</div>
//
// html! { <div>{ This Brace }{ Space WILL NOT be inserted }</div>
// -> <div>This BraceSpace WILL NOT be inserted</div>
let insert_whitespace_after_text = match next_tag {
Some(Tag::Close {
first_angle_bracket_span,
..
}) => self.separated_by_whitespace(brace_span, &first_angle_bracket_span),
Some(Tag::Braced {
brace_span: next_brace_span,
..
}) => self.separated_by_whitespace(brace_span, &next_brace_span),
_ => false,
};
// TODO: Only allow one statement per block. Put a quote_spanned! compiler error if
// there is more than 1 statement. Add a UI test for this.
block.stmts.iter().for_each(|stmt| {
if self.current_node_idx == 0 {
// Here we handle a block being the root node of an `html!` call
//
// html { { some_node } }
let node = quote! {
let node_0: VirtualNode = #stmt.into();
};
self.push_tokens(node);
} else {
self.parse_statement(stmt);
if insert_whitespace_before_text {
let node = self.current_virtual_node_ident(stmt.span());
let insert_whitespace = quote! {
#node.first().insert_space_before_text();
};
self.push_tokens(insert_whitespace);
}
if insert_whitespace_after_text {
let node = self.current_virtual_node_ident(stmt.span());
let insert_whitespace = quote! {
#node.last().insert_space_after_text();
};
self.push_tokens(insert_whitespace);
}
}
});
self.set_most_recent_block_start(brace_span.clone());
}
}
@@ -0,0 +1,49 @@
use crate::parser::{is_self_closing, HtmlParser};
use proc_macro2::Ident;
use quote::quote_spanned;
impl HtmlParser {
/// Parse an incoming Tag::Close
pub(crate) fn parse_close_tag(&mut self, name: &Ident) {
let parent_stack = &mut self.parent_stack;
let close_span = name.span();
let close_tag = name.to_string();
// For example, this should have been <br /> instead of </br>
if is_self_closing(&close_tag) {
let error = format!(
r#"{} is a self closing tag. Try "<{}>" or "<{} />""#,
close_tag, close_tag, close_tag
);
let error = quote_spanned! {close_span=> {
compile_error!(#error);
}};
self.push_tokens(error);
return;
}
let last_open_tag = parent_stack.pop().expect("Last open tag");
let last_open_tag = last_open_tag.1.to_string();
// TODO: 2 compile_error!'s one pointing to the open tag and one pointing to the
// closing tag. Update the ui test accordingly
//
// ex: if div != strong
if last_open_tag != close_tag {
let error = format!(
r#"Wrong closing tag. Try changing "{}" into "{}""#,
close_tag, last_open_tag
);
let error = quote_spanned! {close_span=> {
compile_error!(#error);
}};
// TODO: Abort early if we find an error. So we should be returning
// a Result.
self.push_tokens(error);
}
}
}
+270
View File
@@ -0,0 +1,270 @@
use crate::tag::TagKind;
use crate::Tag;
use quote::{quote, quote_spanned};
use std::collections::HashMap;
use syn::export::Span;
use syn::spanned::Spanned;
use syn::{Ident, Stmt};
mod braced;
mod close_tag;
mod open_tag;
mod statement;
mod text;
pub enum NodesToPush<'a> {
Stmt(&'a Stmt),
TokenStream(&'a Stmt, proc_macro2::TokenStream),
}
/// Used to parse [`Tag`]s that we've parsed and build a tree of `VirtualNode`s
///
/// [`Tag`]: enum.Tag.html
pub struct HtmlParser {
/// As we parse our macro tokens we'll generate new tokens to return back into the compiler
/// when we're done.
tokens: Vec<proc_macro2::TokenStream>,
/// Everytime we encounter a new node we'll use the current_node_idx to name it.
/// Then we'll increment the current_idx by one.
/// This gives every node that we encounter a unique name that we can use to find
/// it later when we want to push child nodes into parent nodes
current_node_idx: usize,
/// The order that we encountered nodes while parsing.
node_order: Vec<usize>,
/// Each time we encounter a new node that could possible be a parent node
/// we push it's node index onto the stack.
///
/// Text nodes cannot be parent nodes.
parent_stack: Vec<(usize, Ident)>,
/// Key -> index of the parent node within the HTML tree
/// Value -> vector of child node indices
parent_to_children: HashMap<usize, Vec<usize>>,
/// The locations of the most recent spans that we parsed.
/// Used to determine whether or not to put space around text nodes.
recent_span_locations: RecentSpanLocations,
/// The last kind of tag that we parsed.
/// Used to determine whether or not to put space around text nodes.
last_tag_kind: Option<TagKind>,
}
/// TODO: I've hit a good stopping point... but we can clean these methods up / split them up
/// a bit...
impl HtmlParser {
/// Create a new HtmlParser
pub fn new() -> HtmlParser {
let mut parent_to_children: HashMap<usize, Vec<usize>> = HashMap::new();
parent_to_children.insert(0, vec![]);
HtmlParser {
tokens: vec![],
current_node_idx: 0,
node_order: vec![],
parent_stack: vec![],
parent_to_children,
recent_span_locations: RecentSpanLocations::default(),
last_tag_kind: None,
}
}
/// Generate the tokens for the incoming Tag and update our parser's heuristics that keep
/// track of information about what we've parsed.
pub fn push_tag(&mut self, tag: &Tag, next_tag: Option<&Tag>) {
match tag {
Tag::Open {
name,
attrs,
closing_bracket_span,
is_self_closing,
..
} => {
self.parse_open_tag(name, closing_bracket_span, attrs, *is_self_closing);
self.last_tag_kind = Some(TagKind::Open);
}
Tag::Close { name, .. } => {
self.parse_close_tag(name);
self.last_tag_kind = Some(TagKind::Close);
}
Tag::Text {
text,
start_span,
end_span,
} => {
self.parse_text(text, start_span.unwrap(), end_span.unwrap(), next_tag);
self.last_tag_kind = Some(TagKind::Text);
}
Tag::Braced { block, brace_span } => {
self.parse_braced(block, brace_span, next_tag);
self.last_tag_kind = Some(TagKind::Braced);
}
};
}
/// 1. Pop a node off the stack
/// 2. Look up all of it's children in parent_to_children
/// 3. Append the children to this node
/// 4. Move on to the next node (as in, go back to step 1)
pub fn finish(&mut self) -> proc_macro2::TokenStream {
let node_order = &mut self.node_order;
let parent_to_children = &mut self.parent_to_children;
let tokens = &mut self.tokens;
if node_order.len() > 1 {
for _ in 0..(node_order.len()) {
let parent_idx = node_order.pop().unwrap();
// TODO: Figure out how to really use spans
let parent_name =
Ident::new(format!("node_{}", parent_idx).as_str(), Span::call_site());
let parent_to_children_indices = match parent_to_children.get(&parent_idx) {
Some(children) => children,
None => continue,
};
if parent_to_children_indices.len() > 0 {
for child_idx in parent_to_children_indices.iter() {
let children =
Ident::new(format!("node_{}", child_idx).as_str(), Span::call_site());
let unreachable = quote_spanned!(Span::call_site() => {
unreachable!("Non-elements cannot have children");
});
let push_children = quote! {
if let Some(ref mut element_node) = #parent_name.as_velement_mut() {
element_node.children.extend(#children.into_iter());
} else {
#unreachable;
}
};
tokens.push(push_children);
}
}
}
}
// Create a virtual node tree
let node = quote! {
{
#(#tokens)*
// Root node is always named node_0
node_0
}
};
node
}
/// Add more tokens to our tokens that we'll eventually return to the compiler.
fn push_tokens(&mut self, tokens: proc_macro2::TokenStream) {
self.tokens.push(tokens);
}
/// Set the location of the most recent start tag's ending LineColumn
fn set_most_recent_open_tag_end(&mut self, span: Span) {
self.recent_span_locations.most_recent_open_tag_end = Some(span);
}
/// Set the location of the most recent start tag's ending LineColumn
fn set_most_recent_block_start(&mut self, span: Span) {
self.recent_span_locations.most_recent_block_start = Some(span);
}
/// Determine whether or not there is any space between the end of the first
/// span and the beginning of the second span.
///
/// There is space if they are on separate lines or if they have different columns.
///
/// html! { <div>Hello</div> } <--- no space between end of div and Hello
///
/// html! { <div> Hello</div> } <--- space between end of div and Hello
fn separated_by_whitespace(&self, first_span: &Span, second_span: &Span) -> bool {
if first_span.end().line != second_span.end().line {
return true;
}
second_span.start().column - first_span.end().column > 0
}
/// Create a new identifier for a VirtualNode and increment our node_idx so that next
/// time we call this our node will get a different name.
fn new_virtual_node_ident(&mut self, span: Span) -> Ident {
let node_name = format!("node_{}", self.current_node_idx);
let node_ident = Ident::new(node_name.as_str(), span);
// TODO: Increment before creating the new node, not after.
// This way the current virtual node ident won't need to do strange subtraction
self.current_node_idx += 1;
node_ident
}
/// Get the Ident for the current (last created) virtual node, without incrementing
/// the node index.
fn current_virtual_node_ident(&self, span: Span) -> Ident {
// TODO: Increment before creating the new node, not after.
// This way the current virtual node ident won't need to do strange subtraction
let node_name = format!("node_{}", self.current_node_idx - 1);
Ident::new(node_name.as_str(), span)
}
/// Generate virtual node tokens for a statement that came from in between braces
///
/// examples:
///
/// html! { <div> { some_var_in_braces } </div>
/// html! { <div> { some_other_variable } </div>
fn push_iterable_nodes(&mut self, nodes: NodesToPush) {
let node_idx = self.current_node_idx;
match nodes {
NodesToPush::Stmt(stmt) => {
let node_ident = self.new_virtual_node_ident(stmt.span());
self.push_tokens(quote! {
let mut #node_ident: IterableNodes = (#stmt).into();
});
}
NodesToPush::TokenStream(stmt, tokens) => {
let node_ident = self.new_virtual_node_ident(stmt.span());
self.push_tokens(quote! {
let mut #node_ident: IterableNodes = #tokens.into();
});
}
}
let parent_idx = *&self.parent_stack[self.parent_stack.len() - 1].0;
self.parent_to_children
.get_mut(&parent_idx)
.expect("Parent of these iterable nodes")
.push(node_idx);
self.node_order.push(node_idx);
}
}
/// Keep track of the locations of different kinds of tokens that we encounter.
///
/// This helps us determine whether or not to insert space before or after text tokens
/// in cases such as:
///
/// ```ignore
/// html! { <div> { Hello World } </div>
/// html! { <div>{Hello World}</div>
/// ```
#[derive(Default)]
struct RecentSpanLocations {
most_recent_open_tag_end: Option<Span>,
most_recent_block_start: Option<Span>,
}
fn is_self_closing(tag: &str) -> bool {
html_validation::is_self_closing(tag)
}
fn is_valid_tag(tag: &str) -> bool {
html_validation::is_valid_tag(tag)
}
+152
View File
@@ -0,0 +1,152 @@
use crate::parser::{is_self_closing, is_valid_tag, HtmlParser};
use crate::tag::Attr;
use proc_macro2::{Ident, Span};
use quote::{quote, quote_spanned};
use syn::Expr;
impl HtmlParser {
/// Parse an incoming Tag::Open
pub(crate) fn parse_open_tag(
&mut self,
name: &Ident,
closing_span: &Span,
attrs: &Vec<Attr>,
is_self_closing_tag: bool,
) {
self.set_most_recent_open_tag_end(closing_span.clone());
let idx = &mut self.current_node_idx;
let parent_to_children = &mut self.parent_to_children;
let parent_stack = &mut self.parent_stack;
let tokens = &mut self.tokens;
let node_order = &mut self.node_order;
// The root node is named `node_0`. All of it's descendants are node_1.. node_2.. etc.
// This just comes from the `idx` variable
// TODO: Not sure what the span is supposed to be so I just picked something..
let var_name_node = Ident::new(format!("node_{}", idx).as_str(), name.span());
let html_tag = format!("{}", name);
let is_html_tag = is_valid_tag(&html_tag);
// TODO: Maybe this could be split up into two functions at some point, would have to pass
// a lot of vars around though, which isn't very nice.
if is_html_tag {
let node = quote! {
let mut #var_name_node = VirtualNode::element(#html_tag);
};
tokens.push(node);
for attr in attrs.iter() {
let key = format!("{}", attr.key);
let value = &attr.value;
match value {
Expr::Closure(closure) => {
// TODO: Use this to decide Box<FnMut(_, _, _, ...)
// After we merge the DomUpdater
let _arg_count = closure.inputs.len();
// NOTE: Closures don't work on non wasm32 targets so we only add
// events on wasm32 targets.
let add_closure = quote! {
#[cfg(target_arch = "wasm32")]
{
let closure = Closure::wrap(
Box::new(#value) as Box<FnMut(_)>
);
let closure_rc = std::rc::Rc::new(closure);
#var_name_node.as_velement_mut().expect("Not an element")
.events.0.insert(#key.to_string(), closure_rc);
}
};
tokens.push(add_closure);
}
_ => {
let insert_attribute = quote! {
#var_name_node.as_velement_mut().expect("Not an element")
.attrs.insert(#key.to_string(), #value.to_string());
};
tokens.push(insert_attribute);
}
};
}
} else if !html_tag.chars().next().unwrap().is_uppercase() {
let error = format!(
r#"{} is not a valid HTML tag.
If you are trying to use a valid HTML tag, perhaps there's a typo?
If you are trying to use a custom component, please capitalize the component name.
custom components: https://chinedufn.github.io/percy/html-macro/custom-components/index.html"#,
html_tag,
);
let span = name.span();
let invalid_tag_name_error = quote_spanned! {span=> {
compile_error!(#error);
}};
tokens.push(invalid_tag_name_error);
let node = quote! {
let mut #var_name_node = VirtualNode::text("error");
};
tokens.push(node);
} else {
let var_name_component = Ident::new(format!("component_{}", idx).as_str(), name.span());
let component_ident = Ident::new(format!("{}", html_tag).as_str(), name.span());
let component_props: Vec<proc_macro2::TokenStream> = attrs
.into_iter()
.map(|attr| {
let key = Ident::new(format!("{}", attr.key).as_str(), name.span());
let value = &attr.value;
quote! {
#key: #value,
}
})
.collect();
let node = quote! {
let mut #var_name_component = #component_ident { #(#component_props),* };
let mut #var_name_node = #var_name_component.render();
};
tokens.push(node);
}
// The first open tag that we see is our root node so we won't worry about
// giving it a parent
if *idx == 0 {
node_order.push(0);
if !is_self_closing(&html_tag) && !is_self_closing_tag {
parent_stack.push((0, name.clone()));
}
*idx += 1;
return;
}
let parent_idx = *&parent_stack[parent_stack.len() - 1].0;
if !is_self_closing(&html_tag) && !is_self_closing_tag {
parent_stack.push((*idx, name.clone()));
}
node_order.push(*idx);
parent_to_children
.get_mut(&parent_idx)
.expect("Parent of this node")
.push(*idx);
parent_to_children.insert(*idx, vec![]);
*idx += 1;
}
}
@@ -0,0 +1,84 @@
use crate::parser::{HtmlParser, NodesToPush};
use quote::quote;
use syn::{Stmt, Expr, ExprIf};
impl HtmlParser {
/// Parse an incoming syn::Stmt node inside a block
pub(crate) fn parse_statement(
&mut self,
stmt: &Stmt,
) {
// Here we handle a block being a descendant within some html! call.
//
// The descendant should implement Into<IterableNodes>
//
// html { <div> { some_node } </div> }
match stmt {
Stmt::Expr(expr) => {
self.parse_expr(stmt, expr);
},
_ => {
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
}
};
}
/// Parse an incoming syn::Expr node inside a block
pub(crate) fn parse_expr(
&mut self,
stmt: &Stmt,
expr: &Expr
) {
match expr {
Expr::If(expr_if) => {
self.expand_if(stmt, expr_if);
},
_ => {
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
}
}
}
/// Expand an incoming Expr::If block
/// This enables us to use JSX-style conditions inside of blocks such as
/// the following example.
///
/// # Examples
///
/// ```rust,ignore
/// html! {
/// <div>
/// {if condition_is_true {
/// html! { <span>Hello World</span> }
/// }}
/// </div>
/// }
/// ```
///
/// Traditionally this would be possible as an if statement in rust is an
/// expression, so the then, and the else block have to return matching types.
/// Here we identify whether the block is missing the else and fill it in with
/// a blank VirtualNode::text("")
pub(crate) fn expand_if(
&mut self,
stmt: &Stmt,
expr_if: &ExprIf
) {
// Has else branch, we can parse the expression as normal.
if let Some(_else_branch) = &expr_if.else_branch {
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
} else {
let condition = &expr_if.cond;
let block = &expr_if.then_branch;
let tokens = quote! {
if #condition {
#block.into()
} else {
VirtualNode::text("")
}
};
self.push_iterable_nodes(NodesToPush::TokenStream(stmt, tokens));
}
}
}
+96
View File
@@ -0,0 +1,96 @@
use crate::parser::HtmlParser;
use crate::tag::{Tag, TagKind};
use proc_macro2::{Ident, Span};
use quote::quote;
impl HtmlParser {
/// Parse an incoming Tag::Text text node
pub(crate) fn parse_text(
&mut self,
text: &str,
text_start: Span,
text_end: Span,
next_tag: Option<&Tag>,
) {
let mut text = text.to_string();
if self.should_insert_space_before_text(&text_start) {
text = " ".to_string() + &text;
}
let should_insert_space_after_text = match next_tag {
Some(Tag::Close {
first_angle_bracket_span,
..
}) => self.separated_by_whitespace(&text_end, first_angle_bracket_span),
Some(Tag::Braced { brace_span, .. }) => {
self.separated_by_whitespace(&text_end, brace_span)
}
Some(Tag::Open {
open_bracket_span, ..
}) => self.separated_by_whitespace(&text_end, open_bracket_span),
_ => false,
};
if should_insert_space_after_text {
text += " ";
}
let idx = &mut self.current_node_idx;
let parent_to_children = &mut self.parent_to_children;
let parent_stack = &mut self.parent_stack;
let tokens = &mut self.tokens;
let node_order = &mut self.node_order;
if *idx == 0 {
node_order.push(0);
// TODO: This is just a consequence of bad code. We're pushing this to make
// things work but in reality a text node isn't a parent ever.
// Just need to make the code DRY / refactor so that we can make things make
// sense vs. just bolting things together.
parent_stack.push((0, Ident::new("unused", Span::call_site())));
}
let var_name = Ident::new(format!("node_{}", idx).as_str(), Span::call_site());
let text_node = quote! {
let mut #var_name = VirtualNode::text(#text);
};
tokens.push(text_node);
if *idx == 0 {
*idx += 1;
return;
}
let parent_idx = &parent_stack[parent_stack.len() - 1];
node_order.push(*idx);
parent_to_children
.get_mut(&parent_idx.0)
.expect("Parent of this text node")
.push(*idx);
*idx += 1;
}
/// If the last TagKind was a block or an open tag we check to see if there is space
/// between this text and that tag. If so we insert some space before this text.
fn should_insert_space_before_text(&self, text_start: &Span) -> bool {
if self.last_tag_kind == Some(TagKind::Braced) {
let most_recent_block_start = self.recent_span_locations.most_recent_block_start;
let most_recent_block_start = most_recent_block_start.as_ref().unwrap();
self.separated_by_whitespace(most_recent_block_start, text_start)
} else if self.last_tag_kind == Some(TagKind::Open) {
let most_recent_open_tag_end =
self.recent_span_locations.most_recent_open_tag_end.as_ref();
let most_recent_open_tag_end = most_recent_open_tag_end.unwrap();
self.separated_by_whitespace(most_recent_open_tag_end, text_start)
} else {
false
}
}
}
+310
View File
@@ -0,0 +1,310 @@
use proc_macro2::{Span, TokenStream, TokenTree};
use syn::parse::{Parse, ParseStream, Result};
use syn::spanned::Spanned;
use syn::token::Brace;
use syn::{braced, Block, Expr, Ident, Token};
/// The different kinds of tokens that we parse.
///
/// TODO: A better name than tag since not all of these are tags
#[derive(Debug)]
pub enum Tag {
/// <div id="app" class=*CSS>
/// <br />
Open {
name: Ident,
attrs: Vec<Attr>,
open_bracket_span: Span,
closing_bracket_span: Span,
is_self_closing: bool,
},
/// </div>
Close {
name: Ident,
first_angle_bracket_span: Span,
},
/// html! { <div> Hello World </div> }
///
/// -> Hello world
///
/// start_span -> the span for the first token within the text
/// end_span -> the span for the last token within the text
Text {
text: String,
start_span: Option<Span>,
end_span: Option<Span>,
},
/// let text_var = VirtualNode::text("3");
///
/// let iter_nodes =
/// vec![
/// html!{ <div></div> },
/// html! {<span> </span>}
/// ];
///
/// html! {
/// <div>
/// Here are some examples of blocks
/// { text_var }
/// { iter_nodes }
/// { html! { <div> </div> }
/// </div>
/// }
Braced { block: Box<Block>, brace_span: Span },
}
/// The different kinds of tokens that we parse.
///
/// TODO: A better name than tag since not all of these are tags
#[derive(Debug, Eq, PartialEq)]
pub enum TagKind {
Open,
Close,
Text,
Braced,
}
/// id="my-id"
/// class="some classes"
/// etc...
#[derive(Debug)]
pub struct Attr {
pub key: Ident,
pub value: Expr,
}
impl Parse for Tag {
fn parse(input: ParseStream) -> Result<Self> {
let mut input = input;
// If it starts with a `<` it's either an open or close tag.
// ex: <div>
// ex: </em>
if input.peek(Token![<]) {
let first_angle_bracket_span = input.parse::<Token![<]>()?;
let first_angle_bracket_span = first_angle_bracket_span.span();
let optional_close: Option<Token![/]> = input.parse()?;
let is_open_tag = optional_close.is_none();
if is_open_tag {
return parse_open_tag(&mut input, first_angle_bracket_span);
} else {
return parse_close_tag(&mut input, first_angle_bracket_span);
}
}
// { node_inside_block }
if input.peek(Brace) {
return parse_block(&mut input);
}
return parse_text_node(&mut input);
}
}
/// `<div id="app" class=*CSS>`
fn parse_open_tag(input: &mut ParseStream, open_bracket_span: Span) -> Result<Tag> {
let name: Ident = input.parse()?;
let attrs = parse_attributes(input)?;
let is_self_closing: Option<Token![/]> = input.parse()?;
let is_self_closing = is_self_closing.is_some();
let closing_bracket = input.parse::<Token![>]>()?;
let closing_bracket_span = closing_bracket.span();
Ok(Tag::Open {
name,
attrs,
open_bracket_span,
closing_bracket_span,
is_self_closing
})
}
/// Parse the attributes starting from something like:
/// id="app" class=*CSS>
///
/// As soon as we see
/// >
/// We know that the element has no more attributes and our loop will end
fn parse_attributes(input: &mut ParseStream) -> Result<Vec<Attr>> {
let mut attrs = Vec::new();
// Do we see an identifier such as `id`? If so proceed
while input.peek(Ident)
|| input.peek(Token![async])
|| input.peek(Token![for])
|| input.peek(Token![loop])
|| input.peek(Token![type])
{
// <link rel="stylesheet" type="text/css"
// .. async, for, loop, type need to be handled specially since they are keywords
let maybe_async_key: Option<Token![async]> = input.parse()?;
let maybe_for_key: Option<Token![for]> = input.parse()?;
let maybe_loop_key: Option<Token![loop]> = input.parse()?;
let maybe_type_key: Option<Token![type]> = input.parse()?;
let key = if maybe_async_key.is_some() {
Ident::new("async", maybe_async_key.unwrap().span())
} else if maybe_for_key.is_some() {
Ident::new("for", maybe_for_key.unwrap().span())
} else if maybe_loop_key.is_some() {
Ident::new("loop", maybe_loop_key.unwrap().span())
} else if maybe_type_key.is_some() {
Ident::new("type", maybe_type_key.unwrap().span())
} else {
input.parse()?
};
// =
input.parse::<Token![=]>()?;
// Continue parsing tokens until we see the next attribute or a closing > tag
let mut value_tokens = TokenStream::new();
loop {
let tt: TokenTree = input.parse()?;
value_tokens.extend(Some(tt));
let has_attrib_key = input.peek(Ident)
|| input.peek(Token![async])
|| input.peek(Token![for])
|| input.peek(Token![loop])
|| input.peek(Token![type]);
let peek_start_of_next_attr = has_attrib_key && input.peek2(Token![=]);
let peek_end_of_tag = input.peek(Token![>]);
let peek_self_closing = input.peek(Token![/]);
if peek_end_of_tag || peek_start_of_next_attr || peek_self_closing {
break;
}
}
let value: Expr = syn::parse2(value_tokens)?;
attrs.push(Attr { key, value });
}
Ok(attrs)
}
/// </div>
fn parse_close_tag(input: &mut ParseStream, first_angle_bracket_span: Span) -> Result<Tag> {
let name: Ident = input.parse()?;
input.parse::<Token![>]>()?;
Ok(Tag::Close {
name,
first_angle_bracket_span,
})
}
fn parse_block(input: &mut ParseStream) -> Result<Tag> {
let content;
let brace_token = braced!(content in input);
let brace_span = brace_token.span;
let block_expr = content.call(Block::parse_within)?;
let block = Box::new(Block {
brace_token,
stmts: block_expr,
});
Ok(Tag::Braced { block, brace_span })
}
/// Parse a sequence of tokens until we run into a closing tag
/// html! { <div> Hello World </div> }
/// or a brace
/// html! { <div> Hello World { Braced } </div>
///
/// So, in the second case, there would be two VText nodes created. "Hello World" and "Braced".
///
/// Later in parser/text.rs we'll look at how close the VText nodes are to their neighboring tags
/// to determine whether or not to insert spacing.
///
/// So, in the examples above, since the opening "<div>" has a space after it we'll later transform
/// "Hello World" into " Hello World" in parser/tag.rs
fn parse_text_node(input: &mut ParseStream) -> Result<Tag> {
// Continue parsing tokens until we see a closing tag <
let _text_tokens = TokenStream::new();
let mut text = "".to_string();
let mut idx = 0;
let mut start_span = None;
let mut most_recent_span: Option<Span> = None;
loop {
if input.is_empty() {
break;
}
let tt: TokenTree = input.parse()?;
if idx == 0 {
start_span = Some(tt.span());
most_recent_span = Some(tt.span());
}
// TODO: Properly handle whitespace and new lines
// https://github.com/chinedufn/percy/pull/97#discussion_r263039215
if idx != 0 {
if let Some(most_recent_span) = most_recent_span {
let current_span_start = tt.span().start();
let most_recent_span_end = most_recent_span.end();
let spans_on_different_lines = current_span_start.line != most_recent_span_end.line;
// Contraptions such as "Aren't" give the "'" and the "t" the
// same span, even though they get parsed separately when calling
// input.parse::<TokenTree>().
// As in - it takes two input.parse calls to get the "'" and "t",
// even though they have the same span.
// This might be a bug in syn - but regardless we address this by
// not inserting a space in this case.
let span_comes_before_previous_span = current_span_start.column
< most_recent_span_end.column
&& !spans_on_different_lines;
// Spans are on different lines, insert space
if spans_on_different_lines {
text += " ";
} else if !span_comes_before_previous_span
&& current_span_start.column - most_recent_span_end.column > 0
{
text += " ";
}
}
}
text += &tt.to_string();
most_recent_span = Some(tt.span());
let peek_closing_tag = input.peek(Token![<]);
let peek_start_block = input.peek(Brace);
if peek_closing_tag || peek_start_block {
break;
}
idx += 1;
}
Ok(Tag::Text {
text,
start_span,
end_span: most_recent_span,
})
}