Feat: docs, code frm percy
This commit is contained in:
commit
2b9c8d09d9
72 files changed
+5130
No files matched your search
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user