Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
102 changes: 13 additions & 89 deletions packages/yew-macro/src/html_tree/html_for.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
use proc_macro2::{Ident, TokenStream};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::token::{For, In};
use syn::{Expr, Local, Pat, Stmt, Token, braced};
use syn::{Expr, Local, Pat, braced};

use super::{HtmlChildrenTree, ToNodeIterator};
use super::HtmlChildrenTree;
use super::html_loop::{emit_loop, parse_loop_body};
use crate::PeekValue;
use crate::html_tree::HtmlTree;

/// Determines if an expression is guaranteed to always return the same value anywhere.
fn is_contextless_pure(expr: &Expr) -> bool {
match expr {
Expr::Lit(_) => true,
Expr::Path(path) => path.path.get_ident().is_none(),
_ => false,
}
}

pub struct HtmlFor {
pat: Pat,
Expand All @@ -44,35 +35,8 @@ impl Parse for HtmlFor {
let body_stream;
braced!(body_stream in input);

let mut let_stmts = Vec::new();
while body_stream.peek(Token![let]) {
let stmt: Stmt = body_stream.parse()?;
match stmt {
Stmt::Local(local) => let_stmts.push(local),
_ => unreachable!("peeked Token![let] but parsed non-local statement"),
}
}
let (let_stmts, body, deprecations) = parse_loop_body(&body_stream, "for")?;

let body = HtmlChildrenTree::parse_delimited_with_nodes(&body_stream)?;
let deprecations = super::check_unnecessary_fragment(&body);
// TODO: more concise code by using if-let guards (MSRV 1.95)
for child in body.0.iter() {
let HtmlTree::Element(element) = child else {
continue;
};

let Some(key) = &element.props.special.key else {
continue;
};

if is_contextless_pure(&key.value) {
return Err(syn::Error::new(
key.value.span(),
"duplicate key for a node in a `for`-loop\nthis will create elements with \
duplicate keys if the loop iterates more than once",
));
}
}
Ok(Self {
pat,
iter,
Expand All @@ -92,53 +56,13 @@ impl ToTokens for HtmlFor {
body,
deprecations,
} = self;
let acc = Ident::new("__yew_v", iter.span());

let alloc_opt = body
.size_hint()
.filter(|&size| size > 1) // explicitly reserving space for 1 more element is redundant
.map(|size| quote!( #acc.reserve(#size) ));

let vlist_gen = match body.fully_keyed() {
Some(true) => quote! {
::yew::virtual_dom::VList::__macro_new(
#acc,
::std::option::Option::None,
::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed
)
},
Some(false) => quote! {
::yew::virtual_dom::VList::__macro_new(
#acc,
::std::option::Option::None,
::yew::virtual_dom::FullyKeyedState::KnownMissingKeys
)
},
None => quote! {
::yew::virtual_dom::VList::with_children(#acc, ::std::option::Option::None)
},
};

let body = body
.0
.iter()
.map(|child| match child.to_node_iterator_stream() {
Some(child) => {
quote!( #acc.extend(#child) )
}
_ => {
quote!( #acc.push(::std::convert::Into::into(#child)) )
}
});

tokens.extend(quote!({
#deprecations
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
::std::iter::Iterator::for_each(
::std::iter::IntoIterator::into_iter(#iter),
|#pat| { #(#let_stmts)* #alloc_opt; #(#body);* }
);
#vlist_gen
}))
let header = quote!(for #pat in #iter);
tokens.extend(emit_loop(
header,
iter.span(),
let_stmts,
body,
deprecations,
));
}
}
114 changes: 114 additions & 0 deletions packages/yew-macro/src/html_tree/html_loop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::parse::ParseStream;
use syn::spanned::Spanned;
use syn::{Expr, Local, Stmt, Token};

use super::{HtmlChildrenTree, HtmlTree, ToNodeIterator};

/// Determines if an expression is guaranteed to always return the same value anywhere.
pub(super) fn is_contextless_pure(expr: &Expr) -> bool {
match expr {
Expr::Lit(_) => true,
Expr::Path(path) => path.path.get_ident().is_none(),
_ => false,
}
}

/// Parse leading `let` bindings from a loop body, then the remaining children.
/// Also runs duplicate-key detection keyed to `loop_kind` (e.g. "for", "while").
pub(super) fn parse_loop_body(
body_stream: ParseStream,
loop_kind: &str,
) -> syn::Result<(Vec<Local>, HtmlChildrenTree, TokenStream)> {
let mut let_stmts = Vec::new();
while body_stream.peek(Token![let]) {
let stmt: Stmt = body_stream.parse()?;
match stmt {
Stmt::Local(local) => let_stmts.push(local),
_ => unreachable!("peeked Token![let] but parsed non-local statement"),
}
}

let body = HtmlChildrenTree::parse_delimited_with_nodes(body_stream)?;
let deprecations = super::check_unnecessary_fragment(&body);
// TODO: more concise code by using if-let guards (MSRV 1.95)
for child in body.0.iter() {
let HtmlTree::Element(element) = child else {
continue;
};

let Some(key) = &element.props.special.key else {
continue;
};

if is_contextless_pure(&key.value) {
return Err(syn::Error::new(
key.value.span(),
format!(
"duplicate key for a node in a `{loop_kind}`-loop\nthis will create elements \
with duplicate keys if the loop iterates more than once"
),
));
}
}

Ok((let_stmts, body, deprecations))
}

/// Emit a loop that accumulates its body children into a `VList`.
///
/// `loop_header` is the native Rust loop syntax without its body, e.g.
/// `for #pat in #iter` or `while #cond`. `span` is used to place the internal
/// accumulator identifier.
pub(super) fn emit_loop(
loop_header: TokenStream,
span: Span,
let_stmts: &[Local],
body: &HtmlChildrenTree,
deprecations: &TokenStream,
) -> TokenStream {
let acc = Ident::new("__yew_v", span);

let alloc_opt = body
.size_hint()
.filter(|&size| size > 1) // explicitly reserving space for 1 more element is redundant
.map(|size| quote!( #acc.reserve(#size) ));

let vlist_gen = match body.fully_keyed() {
Some(true) => quote! {
::yew::virtual_dom::VList::__macro_new(
#acc,
::std::option::Option::None,
::yew::virtual_dom::FullyKeyedState::KnownFullyKeyed
)
},
Some(false) => quote! {
::yew::virtual_dom::VList::__macro_new(
#acc,
::std::option::Option::None,
::yew::virtual_dom::FullyKeyedState::KnownMissingKeys
)
},
None => quote! {
::yew::virtual_dom::VList::with_children(#acc, ::std::option::Option::None)
},
};

let body_streams = body.0.iter().map(|child| match child {
HtmlTree::Break(_) | HtmlTree::Continue(_) => quote!( #child ),
_ => match child.to_node_iterator_stream() {
Some(stream) => quote!( #acc.extend(#stream) ),
_ => quote!( #acc.push(::std::convert::Into::into(#child)) ),
},
});

quote!({
#deprecations
let mut #acc = ::std::vec::Vec::<::yew::virtual_dom::VNode>::new();
#loop_header {
#(#let_stmts)* #alloc_opt; #(#body_streams);*
}
#vlist_gen
})
}
78 changes: 78 additions & 0 deletions packages/yew-macro/src/html_tree/html_while.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::token::While;
use syn::{Expr, Local, braced};

use super::HtmlChildrenTree;
use super::html_loop::{emit_loop, parse_loop_body};
use crate::PeekValue;

pub struct HtmlWhile {
cond: Box<Expr>,
let_stmts: Vec<Local>,
body: HtmlChildrenTree,
deprecations: TokenStream,
}

impl PeekValue<()> for HtmlWhile {
fn peek(cursor: Cursor) -> Option<()> {
let (ident, _) = cursor.ident()?;
(ident == "while").then_some(())
}
}

impl Parse for HtmlWhile {
fn parse(input: ParseStream) -> syn::Result<Self> {
While::parse(input)?;
let cond = Box::new(input.call(Expr::parse_without_eager_brace)?);
match &*cond {
Expr::Block(syn::ExprBlock { block, .. }) if block.stmts.is_empty() => {
return Err(syn::Error::new(
cond.span(),
"missing condition for `while` expression",
));
}
_ => {}
}
if input.is_empty() {
return Err(syn::Error::new(
cond.span(),
"this `while` expression has a condition, but no block",
));
}

let body_stream;
braced!(body_stream in input);

let (let_stmts, body, deprecations) = parse_loop_body(&body_stream, "while")?;

Ok(Self {
cond,
let_stmts,
body,
deprecations,
})
}
}

impl ToTokens for HtmlWhile {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
cond,
let_stmts,
body,
deprecations,
} = self;
let header = quote!(while #cond);
tokens.extend(emit_loop(
header,
cond.span(),
let_stmts,
body,
deprecations,
));
}
}
Loading
Loading