wip: remove HTML macro and add custom fields

This commit is contained in:
Jonathan Kelley 2021-10-27 16:41:00 -04:00
parent a42711a324
commit 9f7eb0f600
9 changed files with 295 additions and 763 deletions

32
docs/src/concepts/old Normal file
View file

@ -0,0 +1,32 @@
## All the VNode types
VNodes can be any of:
- **Element**: a container with a tag name, namespace, attributes, children, and event listeners
- **Text**: bump allocated text derived from string formatting
- **Fragments**: a container of elements with no parent
- **Suspended**: a container for nodes that aren't yet ready to be rendered
- **Anchor**: a special type of node that is only available when fragments have no children
In practice, only elements and text can be initialized directly while other node types can only be created through hooks or NodeFactory methods.
## Bump Arena Allocation
To speed up the process of building our elements and text, Dioxus uses a special type of memory allocator tuned for large batches of small allocations called a Bump Arena. We use the `bumpalo` allocator which was initially developed for Dioxus' spiritual predecessor: `Dodrio.`
- Bumpalo: [https://github.com/fitzgen/bumpalo](https://github.com/fitzgen/bumpalo)
- Dodrio: [https://github.com/fitzgen/dodrio](https://github.com/fitzgen/dodrio)
In other frontend frameworks for Rust, nearly every string is allocated using the global allocator. This means that strings in Rust do not benefit from the immutable string interning optimizations that JavaScript engines employ. By using a smaller, faster, more limited allocator, we can increase framework performance, bypassing even the naive WasmBindgen benchmarks for very quick renders.
It's important to note that VNodes are not `'static` - the VNode definition has a lifetime attached to it:
```rust, ignore
enum VNode<'bump> {
VElement { tag: &'static str, children: &'bump [VNode<'bump>] },
VText { content: &'bump str },
// other VNodes ....
}
```
Because VNodes use a bump allocator as their memory backing, they can only be created through the `NodeFactory` API - which we'll cover in the next chapter. This particular detail is important to understand because "rendering" VNodes produces a lifetime attached to the bump arena - which must be explicitly declared when dealing with components that borrow data from their parents.

View file

@ -1,64 +1,99 @@
# VNodes and Elements
# Declaring your first UI with Elements
At the heart of Dioxus is the concept of an "element" - a container that can have children, properties, event handlers, and other important attributes. Dioxus only knows how to render the `VNode` datastructure - an Enum variant of an Element, Text, Components, Fragments, and Anchors.
Every user interface you've ever used is just a symphony of tiny widgets working together to abstract over larger complex functions. In Dioxus, we call these tiny widgets "Elements." Using Components, you can easily compose Elements into larger groups to form even larger structures: Apps.
Because Dioxus is meant for the Web and uses WebView as a desktop and mobile renderer, almost all elements in Dioxus share properties with their HTML counterpart. When we declare our elements, we'll do so using HTML semantics:
Because Dioxus is mostly used with HTML/CSS renderers, the default Element "collection" is HTML. Provided the `html` feature is not disabled, we can declare Elements using the `rsx!` macro:
```rust
#use dioxus::prelude::*;
rsx!(
div {}
)
```
As you might expect, we can render this call using Dioxus-SSR to produce valid HTML:
```rust
#use dioxus::prelude::*;
dioxus::ssr::render_lazy(rsx!(
div {}
))
```
Produces:
```html
<div></div>
```
## Composing Elements
Every element has a set of properties that can be rendered in different ways. In particular, each Element may contain other Elements. To achieve this, we can simply declare new Elements within the parent:
```rust
#use dioxus::prelude::*;
rsx!(
div {
h1 {}
h2 {}
p {}
}
)
```
With the default configuration, any Element defined within the `dioxus-html` crate can be declared in this way. To create your own new elements, see the `Custom Elements` Advanced Guide.
## Text Elements
Dioxus also supports a special type of Element: Text. Text Elements do not accept children, but rather just text denoted with double quotes.
```rust
rsx! (
"hello world"
)
```
Text Elements can be composed within other Elements:
```rust
rsx! (
div {
h1 { "hello world" }
p { "Some body content" }
}
)
```
Text can also be formatted with any value that implements `Display`. We use f-string formatting - a "coming soon" feature for stable Rust that is familiar for Python and JavaScript users:
```rust
let name = "Bob";
rsx! ( "hello {name}" )
```
## Attributes
Every Element in your User Interface will have some sort of properties that the renderer will use when drawing to the screen. These might inform the renderer if the component should be hidden, what its background color should be, or to give it a specific name or ID.
To do this, we simply use the familiar struct-style syntax that Rust provides us. Commas are optional:
```rust
rsx!(
div {
"hello world"
hidden: true,
background_color: "blue",
class: "card color-{mycolor}"
}
)
```
As you would expect, this snippet would generate a simple hello-world div. In fact, we can render these nodes directly with the SSR crate:
Each field is defined as a method on the element in the `dioxus-html` crate. This prevents you from misspelling a field name and lets us provide inline documentation. When you need to use a field not defined as a method, you have two options:
1) file an issue if the attribute _should_ be enabled
2) add a custom attribute on-the-fly
To use custom attributes, simply put the attribute name in quotes followed by a colon:
```rust
dioxus::ssr::render_lazy(rsx!(
rsx!(
div {
"hello world"
"custom_attr": "important data here"
}
))
)
```
And produce the corresponding html structure:
```html
<div>hello world</div>
```
## Listeners
Our structure declared above is made of two variants of the `VNode` datastructure:
- A VElement with a tagname of `div`
- A VText with contents of `"hello world"`
## All the VNode types
VNodes can be any of:
- **Element**: a container with a tag name, namespace, attributes, children, and event listeners
- **Text**: bump allocated text derived from string formatting
- **Fragments**: a container of elements with no parent
- **Suspended**: a container for nodes that aren't yet ready to be rendered
- **Anchor**: a special type of node that is only available when fragments have no children
In practice, only elements and text can be initialized directly while other node types can only be created through hooks or NodeFactory methods.
## Bump Arena Allocation
To speed up the process of building our elements and text, Dioxus uses a special type of memory allocator tuned for large batches of small allocations called a Bump Arena. We use the `bumpalo` allocator which was initially developed for Dioxus' spiritual predecessor: `Dodrio.`
- Bumpalo: [https://github.com/fitzgen/bumpalo](https://github.com/fitzgen/bumpalo)
- Dodrio: [https://github.com/fitzgen/dodrio](https://github.com/fitzgen/dodrio)
In other frontend frameworks for Rust, nearly every string is allocated using the global allocator. This means that strings in Rust do not benefit from the immutable string interning optimizations that JavaScript engines employ. By using a smaller, faster, more limited allocator, we can increase framework performance, bypassing even the naive WasmBindgen benchmarks for very quick renders.
It's important to note that VNodes are not `'static` - the VNode definition has a lifetime attached to it:
```rust, ignore
enum VNode<'bump> {
VElement { tag: &'static str, children: &'bump [VNode<'bump>] },
VText { content: &'bump str },
// other VNodes ....
}
```
Because VNodes use a bump allocator as their memory backing, they can only be created through the `NodeFactory` API - which we'll cover in the next chapter. This particular detail is important to understand because "rendering" VNodes produces a lifetime attached to the bump arena - which must be explicitly declared when dealing with components that borrow data from their parents.
## Arbitrary Tokens

View file

@ -182,158 +182,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
/// ```
#[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody<AS_RSX>>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
/// The html! macro makes it easy for developers to write jsx-style markup in their components.
///
/// ## Complete Reference Guide:
/// ```
/// const Example: FC<()> = |(cx, props)|{
/// let formatting = "formatting!";
/// let formatting_tuple = ("a", "b");
/// let lazy_fmt = format_args!("lazily formatted text");
/// cx.render(html! {
/// <div>
/// <div />
/// <div> </div>
/// <h1>"Some text"</h1>
/// <h1>"Some text with {formatting}"</h1>
/// <h1>"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"</h1>
/// <h2>
/// "Multiple"
/// "Text"
/// "Blocks"
/// "Use comments as separators in html"
/// </h2>
/// <div>
/// <h1>"multiple"</h1>
/// <h2>"nested"</h2>
/// <h3>"elements"</h3>
/// </div>
/// <div class="my special div">
/// <h1>"Headers and attributes!"</h1>
/// </div>
/// <div class: lazy_fmt, id=format_args!("attributes can be passed lazily with std::fmt::Arguments")>
/// <div class: {
/// const WORD: &str = "expressions";
/// format_args!("Arguments can be passed in through curly braces for complex {}", WORD)
/// } />
/// </div>
/// {rsx!(p { "More templating!" })}
/// {html!(<p>"Even HTML templating!!"</p>)}
/// {(0..10).map(|i| html!(<li>"{i}"</li>))}
/// {{
/// let data = std::collections::HashMap::<&'static str, &'static str>::new();
/// // Iterators *should* have keys when you can provide them.
/// // Keys make your app run faster. Make sure your keys are stable, unique, and predictable.
/// // Using an "ID" associated with your data is a good idea.
/// data.into_iter().map(|(k, v)| rsx!(<li key="{k}"> "{v}" </li>))
/// }}
/// // Matching
/// // Matching will throw a Rust error about "no two closures are the same type"
/// // To fix this, call "render" method or use the "in" syntax to produce VNodes.
/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros)
/// {match true {
/// true => rsx!(cx, <h1>"Top text"</h1>),
/// false => rsx!(cx, <h1>"Bottom text"</h1>),
/// }}
///
/// // Conditional rendering
/// // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals.
/// // You can convert a bool condition to rsx! with .then and .or
/// {true.then(|| html!(<div />))}
///
/// // True conditions need to be rendered (same reasons as matching)
/// {if true {
/// html!(cx, <h1>"Top text"</h1>)
/// } else {
/// html!(cx, <h1>"Bottom text"</h1>)
/// }}
///
/// // returning "None" is a bit noisy... but rare in practice
/// {None as Option<()>}
///
/// // Use the Dioxus type-alias for less noise
/// {NONE_ELEMENT}
///
/// // can also just use empty fragments
/// <Fragment />
///
/// // Fragments let you insert groups of nodes without a parent.
/// // This lets you make components that insert elements as siblings without a container.
/// <div> "A" </div>
/// <Fragment>
/// <div> "B" </div>
/// <div> "C" </div>
/// <Fragment>
/// "D"
/// <Fragment>
/// "heavily nested fragments is an antipattern"
/// "they cause Dioxus to do unnecessary work"
/// "don't use them carelessly if you can help it"
/// </Fragment>
/// </Fragment
/// </Fragment>
///
/// // Components
/// // Can accept any paths
/// // Notice how you still get syntax highlighting and IDE support :)
/// <Baller />
/// <baller::Baller />
/// <crate::baller::Baller />
///
/// // Can take properties
/// <Taller a="asd" />
///
/// // Can take optional properties
/// <Taller a="asd" />
///
/// // Can pass in props directly as an expression
/// {{
/// let props = TallerProps {a: "hello"};
/// html!(<Taller ..{props} />)
/// }}
///
/// // Spreading can also be overridden manually
/// <Taller {..TallerProps { a: "ballin!" }} a="not ballin!" />
///
/// // Can take children too!
/// <Taller a="asd">
/// <div> "hello world!" </div>
/// </Taller>
/// }
/// })
/// };
///
/// mod baller {
/// use super::*;
/// pub struct BallerProps {}
///
/// /// This component totally balls
/// pub fn Baller(cx: Context<()>) -> DomTree {
/// todo!()
/// }
/// }
///
/// #[derive(Debug, PartialEq, Props)]
/// pub struct TallerProps {
/// a: &'static str,
/// }
///
/// /// This component is taller than most :)
/// pub fn Taller(cx: Context<TallerProps>) -> DomTree {
/// let b = true;
/// todo!()
/// }
/// ```
#[proc_macro]
pub fn html(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody<AS_HTML>>(s) {
match syn::parse::<rsx::CallBody>(s) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}

View file

@ -16,26 +16,22 @@ use syn::{
};
#[allow(clippy::large_enum_variant)]
pub enum AmbiguousElement<const AS: HtmlOrRsx> {
Element(Element<AS>),
Component(Component<AS>),
pub enum AmbiguousElement {
Element(Element),
Component(Component),
}
impl Parse for AmbiguousElement<AS_RSX> {
impl Parse for AmbiguousElement {
fn parse(input: ParseStream) -> Result<Self> {
// Try to parse as an absolute path and immediately defer to the componetn
if input.peek(Token![::]) {
return input
.parse::<Component<AS_RSX>>()
.map(AmbiguousElement::Component);
return input.parse::<Component>().map(AmbiguousElement::Component);
}
// If not an absolute path, then parse the ident and check if it's a valid tag
if let Ok(pat) = input.fork().parse::<syn::Path>() {
if pat.segments.len() > 1 {
return input
.parse::<Component<AS_RSX>>()
.map(AmbiguousElement::Component);
return input.parse::<Component>().map(AmbiguousElement::Component);
}
}
@ -45,45 +41,16 @@ impl Parse for AmbiguousElement<AS_RSX> {
let first_char = name_str.chars().next().unwrap();
if first_char.is_ascii_uppercase() {
input
.parse::<Component<AS_RSX>>()
.map(AmbiguousElement::Component)
input.parse::<Component>().map(AmbiguousElement::Component)
} else {
input
.parse::<Element<AS_RSX>>()
.map(AmbiguousElement::Element)
input.parse::<Element>().map(AmbiguousElement::Element)
}
} else {
Err(Error::new(input.span(), "Not a valid Html tag"))
}
}
}
impl Parse for AmbiguousElement<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(Token![<]) {
let forked = input.fork();
forked.parse::<Token![<]>().unwrap();
let tag = forked.parse::<Ident>()?;
let name_str = tag.to_string();
let first_char = name_str.chars().next().unwrap();
if first_char.is_ascii_uppercase() {
input
.parse::<Component<AS_HTML>>()
.map(AmbiguousElement::Component)
} else {
input
.parse::<Element<AS_HTML>>()
.map(AmbiguousElement::Element)
}
} else {
Err(Error::new(input.span(), "Not a valid Html tag"))
}
}
}
impl<const AS: HtmlOrRsx> ToTokens for AmbiguousElement<AS> {
impl ToTokens for AmbiguousElement {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
AmbiguousElement::Element(el) => el.to_tokens(tokens),

View file

@ -7,30 +7,16 @@ use syn::{
use super::*;
pub struct CallBody<const AS: HtmlOrRsx> {
pub struct CallBody {
custom_context: Option<Ident>,
roots: Vec<BodyNode<AS>>,
roots: Vec<BodyNode>,
}
/// The custom rusty variant of parsing rsx!
impl Parse for CallBody<AS_RSX> {
impl Parse for CallBody {
fn parse(input: ParseStream) -> Result<Self> {
let custom_context = try_parse_custom_context(input)?;
let (_, roots, _) = BodyConfig::<AS_RSX>::new_call_body().parse_component_body(input)?;
Ok(Self {
custom_context,
roots,
})
}
}
/// The HTML variant of parsing rsx!
impl Parse for CallBody<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
let custom_context = try_parse_custom_context(input)?;
// parsing the contents is almost like parsing the inner of any element, but with no props
let (_, roots, _) = BodyConfig::<AS_HTML>::new_call_body().parse_component_body(input)?;
let (_, roots, _) = BodyConfig::new_call_body().parse_component_body(input)?;
Ok(Self {
custom_context,
roots,
@ -50,7 +36,7 @@ fn try_parse_custom_context(input: ParseStream) -> Result<Option<Ident>> {
}
/// Serialize the same way, regardless of flavor
impl<const A: HtmlOrRsx> ToTokens for CallBody<A> {
impl ToTokens for CallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let inner = if self.roots.len() == 1 {
let inner = &self.roots[0];

View file

@ -22,15 +22,15 @@ use syn::{
token, Error, Expr, ExprClosure, Ident, Result, Token,
};
pub struct Component<const AS: HtmlOrRsx> {
pub struct Component {
// accept any path-like argument
name: syn::Path,
body: Vec<ComponentField<AS>>,
children: Vec<BodyNode<AS>>,
body: Vec<ComponentField>,
children: Vec<BodyNode>,
manual_props: Option<Expr>,
}
impl Parse for Component<AS_RSX> {
impl Parse for Component {
fn parse(stream: ParseStream) -> Result<Self> {
// let name = s.parse::<syn::ExprPath>()?;
// todo: look into somehow getting the crate/super/etc
@ -41,7 +41,7 @@ impl Parse for Component<AS_RSX> {
let content: ParseBuffer;
syn::braced!(content in stream);
let cfg: BodyConfig<AS_RSX> = BodyConfig {
let cfg: BodyConfig = BodyConfig {
allow_children: true,
allow_fields: true,
allow_manual_props: true,
@ -57,80 +57,14 @@ impl Parse for Component<AS_RSX> {
})
}
}
impl Parse for Component<AS_HTML> {
fn parse(stream: ParseStream) -> Result<Self> {
let _l_tok = stream.parse::<Token![<]>()?;
let name = syn::Path::parse_mod_style(stream)?;
let mut manual_props = None;
let mut body: Vec<ComponentField<AS_HTML>> = vec![];
let mut children: Vec<BodyNode<AS_HTML>> = vec![];
if stream.peek(Token![..]) {
stream.parse::<Token![..]>()?;
manual_props = Some(stream.parse::<Expr>()?);
}
while !stream.peek(Token![>]) {
// self-closing
if stream.peek(Token![/]) {
stream.parse::<Token![/]>()?;
stream.parse::<Token![>]>()?;
return Ok(Self {
name,
manual_props,
body,
children,
});
}
body.push(stream.parse::<ComponentField<AS_HTML>>()?);
}
stream.parse::<Token![>]>()?;
'parsing: loop {
if stream.peek(Token![<]) && stream.peek2(Token![/]) {
break 'parsing;
}
// [1] Break if empty
if stream.is_empty() {
break 'parsing;
}
children.push(stream.parse::<BodyNode<AS_HTML>>()?);
}
// closing element
stream.parse::<Token![<]>()?;
stream.parse::<Token![/]>()?;
let close = syn::Path::parse_mod_style(stream)?;
if close != name {
return Err(Error::new_spanned(
close,
"closing element does not match opening",
));
}
stream.parse::<Token![>]>()?;
Ok(Self {
name,
body,
children,
manual_props,
})
}
}
pub struct BodyConfig<const AS: HtmlOrRsx> {
pub struct BodyConfig {
pub allow_fields: bool,
pub allow_children: bool,
pub allow_manual_props: bool,
}
impl<const AS: HtmlOrRsx> BodyConfig<AS> {
impl BodyConfig {
/// The configuration to parse the root
pub fn new_call_body() -> Self {
Self {
@ -141,17 +75,13 @@ impl<const AS: HtmlOrRsx> BodyConfig<AS> {
}
}
impl BodyConfig<AS_RSX> {
impl BodyConfig {
// todo: unify this body parsing for both elements and components
// both are style rather ad-hoc, though components are currently more configured
pub fn parse_component_body(
&self,
content: &ParseBuffer,
) -> Result<(
Vec<ComponentField<AS_RSX>>,
Vec<BodyNode<AS_RSX>>,
Option<Expr>,
)> {
) -> Result<(Vec<ComponentField>, Vec<BodyNode>, Option<Expr>)> {
let mut body = Vec::new();
let mut children = Vec::new();
let mut manual_props = None;
@ -178,7 +108,7 @@ impl BodyConfig<AS_RSX> {
"Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.",
));
}
body.push(content.parse::<ComponentField<AS_RSX>>()?);
body.push(content.parse::<ComponentField>()?);
} else {
if !self.allow_children {
return Err(Error::new(
@ -186,65 +116,7 @@ impl BodyConfig<AS_RSX> {
"This item is not allowed to accept children.",
));
}
children.push(content.parse::<BodyNode<AS_RSX>>()?);
}
// consume comma if it exists
// we don't actually care if there *are* commas between attrs
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok((body, children, manual_props))
}
}
impl BodyConfig<AS_HTML> {
// todo: unify this body parsing for both elements and components
// both are style rather ad-hoc, though components are currently more configured
pub fn parse_component_body(
&self,
content: &ParseBuffer,
) -> Result<(
Vec<ComponentField<AS_HTML>>,
Vec<BodyNode<AS_HTML>>,
Option<Expr>,
)> {
let mut body = Vec::new();
let mut children = Vec::new();
let mut manual_props = None;
'parsing: loop {
// [1] Break if empty
if content.is_empty() {
break 'parsing;
}
if content.peek(Token![..]) {
if !self.allow_manual_props {
return Err(Error::new(
content.span(),
"Props spread syntax is not allowed in this context. \nMake to only use the elipsis `..` in Components.",
));
}
content.parse::<Token![..]>()?;
manual_props = Some(content.parse::<Expr>()?);
} else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
if !self.allow_fields {
return Err(Error::new(
content.span(),
"Property fields is not allowed in this context. \nMake to only use fields in Components or Elements.",
));
}
body.push(content.parse::<ComponentField<AS_HTML>>()?);
} else {
if !self.allow_children {
return Err(Error::new(
content.span(),
"This item is not allowed to accept children.",
));
}
children.push(content.parse::<BodyNode<AS_HTML>>()?);
children.push(content.parse::<BodyNode>()?);
}
// consume comma if it exists
@ -257,7 +129,7 @@ impl BodyConfig<AS_HTML> {
}
}
impl<const AS: HtmlOrRsx> ToTokens for Component<AS> {
impl ToTokens for Component {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
@ -327,7 +199,7 @@ impl<const AS: HtmlOrRsx> ToTokens for Component<AS> {
}
// the struct's fields info
pub struct ComponentField<const AS: HtmlOrRsx> {
pub struct ComponentField {
name: Ident,
content: ContentField,
}
@ -354,28 +226,7 @@ impl ToTokens for ContentField {
}
}
impl Parse for ComponentField<AS_RSX> {
fn parse(input: ParseStream) -> Result<Self> {
let name = Ident::parse_any(input)?;
input.parse::<Token![:]>()?;
let name_str = name.to_string();
let content = if name_str.starts_with("on") {
if input.peek(token::Brace) {
let content;
syn::braced!(content in input);
ContentField::OnHandlerRaw(content.parse()?)
} else {
ContentField::OnHandler(input.parse()?)
}
} else {
ContentField::ManExpr(input.parse::<Expr>()?)
};
Ok(Self { name, content })
}
}
impl Parse for ComponentField<AS_HTML> {
impl Parse for ComponentField {
fn parse(input: ParseStream) -> Result<Self> {
let name = Ident::parse_any(input)?;
input.parse::<Token![:]>()?;
@ -397,7 +248,7 @@ impl Parse for ComponentField<AS_HTML> {
}
}
impl<const AS: HtmlOrRsx> ToTokens for ComponentField<AS> {
impl ToTokens for ComponentField {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let ComponentField { name, content, .. } = self;
tokens.append_all(quote! {

View file

@ -11,16 +11,16 @@ use syn::{
// =======================================
// Parse the VNode::Element type
// =======================================
pub struct Element<const AS: HtmlOrRsx> {
pub struct Element {
name: Ident,
key: Option<LitStr>,
attributes: Vec<ElementAttr<AS>>,
listeners: Vec<ElementAttr<AS>>,
children: Vec<BodyNode<AS>>,
attributes: Vec<ElementAttr>,
listeners: Vec<ElementAttr>,
children: Vec<BodyNode>,
_is_static: bool,
}
impl Parse for Element<AS_RSX> {
impl Parse for Element {
fn parse(stream: ParseStream) -> Result<Self> {
let name = Ident::parse(stream)?;
@ -28,29 +28,75 @@ impl Parse for Element<AS_RSX> {
let content: ParseBuffer;
syn::braced!(content in stream);
let mut attributes: Vec<ElementAttr<AS_RSX>> = vec![];
let mut listeners: Vec<ElementAttr<AS_RSX>> = vec![];
let mut children: Vec<BodyNode<AS_RSX>> = vec![];
let mut attributes: Vec<ElementAttr> = vec![];
let mut listeners: Vec<ElementAttr> = vec![];
let mut children: Vec<BodyNode> = vec![];
let mut key = None;
let mut el_ref = None;
'parsing: loop {
// [1] Break if empty
if content.is_empty() {
break 'parsing;
}
while !content.is_empty() {
if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
parse_rsx_element_field(
&content,
&mut attributes,
&mut listeners,
&mut key,
&mut el_ref,
name.clone(),
)?;
let name = Ident::parse_any(stream)?;
let name_str = name.to_string();
stream.parse::<Token![:]>()?;
if name_str.starts_with("on") {
if stream.peek(token::Brace) {
let content;
syn::braced!(content in stream);
listeners.push(ElementAttr::EventTokens {
name,
tokens: content.parse()?,
});
} else {
children.push(content.parse::<BodyNode<AS_RSX>>()?);
listeners.push(ElementAttr::EventClosure {
name,
closure: content.parse()?,
});
};
} else {
match name_str.as_str() {
"key" => {
key = Some(stream.parse()?);
}
"classes" => {
todo!("custom class list not supported")
}
"namespace" => {
todo!("custom namespace not supported")
}
"node_ref" => {
el_ref = Some(stream.parse::<Expr>()?);
}
_ => {
if stream.peek(LitStr) {
listeners.push(ElementAttr::AttrText {
name,
value: content.parse()?,
});
} else {
listeners.push(ElementAttr::AttrExpression {
name,
value: content.parse()?,
});
}
}
}
}
} else if content.peek(LitStr) && content.peek2(Token![:]) {
let name = content.parse::<LitStr>()?;
content.parse::<Token![:]>()?;
if content.peek(LitStr) {
let value = content.parse::<LitStr>()?;
attributes.push(ElementAttr::CustomAttrText { name, value });
} else {
let value = content.parse::<Expr>()?;
attributes.push(ElementAttr::CustomAttrExpression { name, value });
}
} else {
children.push(content.parse::<BodyNode>()?);
}
// consume comma if it exists
@ -71,118 +117,17 @@ impl Parse for Element<AS_RSX> {
}
}
impl Parse for Element<AS_HTML> {
fn parse(stream: ParseStream) -> Result<Self> {
let _l_tok = stream.parse::<Token![<]>()?;
let el_name = Ident::parse(stream)?;
let mut attributes: Vec<ElementAttr<AS_HTML>> = vec![];
let mut listeners: Vec<ElementAttr<AS_HTML>> = vec![];
let mut children: Vec<BodyNode<AS_HTML>> = vec![];
let key = None;
while !stream.peek(Token![>]) {
// self-closing
if stream.peek(Token![/]) {
stream.parse::<Token![/]>()?;
stream.parse::<Token![>]>()?;
return Ok(Self {
name: el_name,
key: None,
attributes,
_is_static: false,
listeners,
children,
});
}
let name = Ident::parse_any(stream)?;
let name_str = name.to_string();
stream.parse::<Token![=]>()?;
if name_str.starts_with("on") {
let inner;
syn::braced!(inner in stream);
let toks = inner.parse::<Expr>()?;
let ty = AttrType::EventTokens(toks);
listeners.push(ElementAttr {
element_name: el_name.clone(),
name,
value: ty,
namespace: None,
})
} else {
match name_str.as_str() {
"style" => {}
"key" => {}
_ => {
// "classes" | "namespace" | "ref" | _ => {
let ty = if stream.peek(LitStr) {
let rawtext = stream.parse::<LitStr>().unwrap();
AttrType::BumpText(rawtext)
} else {
// like JSX, we expect raw expressions
let inner;
syn::braced!(inner in stream);
let toks = inner.parse::<Expr>()?;
AttrType::FieldTokens(toks)
};
attributes.push(ElementAttr {
element_name: el_name.clone(),
name,
value: ty,
namespace: None,
})
}
}
};
}
stream.parse::<Token![>]>()?;
'parsing: loop {
if stream.peek(Token![<]) && stream.peek2(Token![/]) {
break 'parsing;
}
// [1] Break if empty
if stream.is_empty() {
break 'parsing;
}
children.push(stream.parse::<BodyNode<AS_HTML>>()?);
}
// closing element
stream.parse::<Token![<]>()?;
stream.parse::<Token![/]>()?;
let close = Ident::parse_any(stream)?;
if close != el_name {
return Err(Error::new_spanned(
close,
"closing element does not match opening",
));
}
stream.parse::<Token![>]>()?;
Ok(Self {
key,
name: el_name,
attributes,
children,
listeners,
_is_static: false,
})
}
}
impl<const AS: HtmlOrRsx> ToTokens for Element<AS> {
impl ToTokens for Element {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let attr = &self.attributes;
let childs = &self.children;
let listeners = &self.listeners;
let attr = self.attributes.iter().map(|x| ElementAttrNamed {
attr: x,
el_name: name,
});
let key = match &self.key {
Some(ty) => quote! { Some(format_args_f!(#ty)) },
None => quote! { None },
@ -200,164 +145,73 @@ impl<const AS: HtmlOrRsx> ToTokens for Element<AS> {
}
}
/// =======================================
/// Parse a VElement's Attributes
/// =======================================
struct ElementAttr<const AS: HtmlOrRsx> {
element_name: Ident,
name: Ident,
value: AttrType,
namespace: Option<String>,
enum ElementAttr {
// attribute: "valuee {}"
AttrText { name: Ident, value: LitStr },
// attribute: true,
AttrExpression { name: Ident, value: Expr },
// "attribute": "value {}"
CustomAttrText { name: LitStr, value: LitStr },
// "attribute": true,
CustomAttrExpression { name: LitStr, value: Expr },
// onclick: move |_| {}
EventClosure { name: Ident, closure: ExprClosure },
// onclick: {}
EventTokens { name: Ident, tokens: Expr },
}
enum AttrType {
BumpText(LitStr),
FieldTokens(Expr),
EventTokens(Expr),
Event(ExprClosure),
}
// We parse attributes and dump them into the attribute vec
// This is because some tags might be namespaced (IE style)
// These dedicated tags produce multiple name-spaced attributes
fn parse_rsx_element_field(
stream: ParseStream,
attrs: &mut Vec<ElementAttr<AS_RSX>>,
listeners: &mut Vec<ElementAttr<AS_RSX>>,
key: &mut Option<LitStr>,
el_ref: &mut Option<Expr>,
element_name: Ident,
) -> Result<()> {
let name = Ident::parse_any(stream)?;
let name_str = name.to_string();
stream.parse::<Token![:]>()?;
// Return early if the field is a listener
if name_str.starts_with("on") {
// remove the "on" bit
let ty = if stream.peek(token::Brace) {
let content;
syn::braced!(content in stream);
// Try to parse directly as a closure
let fork = content.fork();
if let Ok(event) = fork.parse::<ExprClosure>() {
content.advance_to(&fork);
AttrType::Event(event)
} else {
AttrType::EventTokens(content.parse()?)
}
} else {
AttrType::Event(stream.parse()?)
};
listeners.push(ElementAttr {
name,
value: ty,
namespace: None,
element_name,
});
return Ok(());
}
let ty: AttrType = match name_str.as_str() {
// short circuit early if style is using the special syntax
"style" if stream.peek(token::Brace) => {
let inner;
syn::braced!(inner in stream);
while !inner.is_empty() {
let name = Ident::parse_any(&inner)?;
inner.parse::<Token![:]>()?;
let ty = if inner.peek(LitStr) {
let rawtext = inner.parse::<LitStr>().unwrap();
AttrType::BumpText(rawtext)
} else {
let toks = inner.parse::<Expr>()?;
AttrType::FieldTokens(toks)
};
if inner.peek(Token![,]) {
let _ = inner.parse::<Token![,]>();
}
attrs.push(ElementAttr {
name,
value: ty,
namespace: Some("style".to_string()),
element_name: element_name.clone(),
});
}
return Ok(());
}
"key" => {
*key = Some(stream.parse::<LitStr>()?);
return Ok(());
}
"classes" => {
todo!("custom class list not supported")
}
"namespace" => {
todo!("custom namespace not supported")
}
"node_ref" => {
*el_ref = Some(stream.parse::<Expr>()?);
return Ok(());
}
// Fall through
_ => {
if stream.peek(LitStr) {
let rawtext = stream.parse::<LitStr>().unwrap();
AttrType::BumpText(rawtext)
} else {
let toks = stream.parse::<Expr>()?;
AttrType::FieldTokens(toks)
}
}
};
// consume comma if it exists
// we don't actually care if there *are* commas between attrs
if stream.peek(Token![,]) {
let _ = stream.parse::<Token![,]>();
}
attrs.push(ElementAttr {
name,
value: ty,
namespace: None,
element_name,
});
Ok(())
}
impl<const AS: HtmlOrRsx> ToTokens for ElementAttr<AS> {
impl ToTokens for ElementAttr {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let el_name = &self.element_name;
let nameident = &self.name;
// weird requirment
todo!()
}
}
// TODO: wire up namespace
let _name_str = self.name.to_string();
let _namespace = match &self.namespace {
Some(t) => quote! { Some(#t) },
None => quote! { None },
struct ElementAttrNamed<'a> {
el_name: &'a Ident,
attr: &'a ElementAttr,
}
impl ToTokens for ElementAttrNamed<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let ElementAttrNamed { el_name, attr } = *self;
let toks = match attr {
ElementAttr::AttrText { name, value } => {
quote! {
dioxus_elements::#el_name.#name(__cx, format_args_f!(#value))
}
}
ElementAttr::AttrExpression { name, value } => {
quote! {
dioxus_elements::#el_name.#name(__cx, #value)
}
}
ElementAttr::CustomAttrText { name, value } => {
quote! { __cx.attr( #name, format_args_f!(#value), None, false ) }
}
ElementAttr::CustomAttrExpression { name, value } => {
quote! { __cx.attr( #name, format_args_f!(#value), None, false ) }
}
ElementAttr::EventClosure { name, closure } => {
quote! {
dioxus::events::on::#name(__cx, #closure)
}
}
ElementAttr::EventTokens { name, tokens } => {
quote! {
dioxus::events::on::#name(__cx, #tokens)
}
}
};
match &self.value {
AttrType::BumpText(value) => tokens.append_all(quote! {
dioxus_elements::#el_name.#nameident(__cx, format_args_f!(#value))
}),
AttrType::FieldTokens(exp) => tokens.append_all(quote! {
dioxus_elements::#el_name.#nameident(__cx, #exp)
}),
AttrType::Event(event) => tokens.append_all(quote! {
dioxus::events::on::#nameident(__cx, #event)
}),
AttrType::EventTokens(event) => tokens.append_all(quote! {
dioxus::events::on::#nameident(__cx, #event)
}),
}
tokens.append_all(toks);
}
}

View file

@ -19,11 +19,11 @@ use {
},
};
pub struct Fragment<const AS: HtmlOrRsx> {
children: Vec<AmbiguousElement<AS>>,
pub struct Fragment {
children: Vec<AmbiguousElement>,
}
impl Parse for Fragment<AS_RSX> {
impl Parse for Fragment {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Ident>()?;
@ -33,7 +33,7 @@ impl Parse for Fragment<AS_RSX> {
let content: ParseBuffer;
syn::braced!(content in input);
while !content.is_empty() {
content.parse::<AmbiguousElement<AS_RSX>>()?;
content.parse::<AmbiguousElement>()?;
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
@ -43,27 +43,7 @@ impl Parse for Fragment<AS_RSX> {
}
}
impl Parse for Fragment<AS_HTML> {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Ident>()?;
let children = Vec::new();
// parse the guts
let content: ParseBuffer;
syn::braced!(content in input);
while !content.is_empty() {
content.parse::<AmbiguousElement<AS_HTML>>()?;
if content.peek(Token![,]) {
let _ = content.parse::<Token![,]>();
}
}
Ok(Self { children })
}
}
impl<const AS: HtmlOrRsx> ToTokens for Fragment<AS> {
impl ToTokens for Fragment {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let childs = &self.children;
let children = quote! {

View file

@ -10,13 +10,13 @@ use syn::{
// ==============================================
// Parse any div {} as a VElement
// ==============================================
pub enum BodyNode<const AS: HtmlOrRsx> {
Element(AmbiguousElement<AS>),
pub enum BodyNode {
Element(AmbiguousElement),
Text(TextNode),
RawExpr(Expr),
}
impl Parse for BodyNode<AS_RSX> {
impl Parse for BodyNode {
fn parse(stream: ParseStream) -> Result<Self> {
// Supposedly this approach is discouraged due to inability to return proper errors
// TODO: Rework this to provide more informative errors
@ -31,33 +31,11 @@ impl Parse for BodyNode<AS_RSX> {
return Ok(BodyNode::Text(stream.parse::<TextNode>()?));
}
Ok(BodyNode::Element(
stream.parse::<AmbiguousElement<AS_RSX>>()?,
))
}
}
impl Parse for BodyNode<AS_HTML> {
fn parse(stream: ParseStream) -> Result<Self> {
// Supposedly this approach is discouraged due to inability to return proper errors
// TODO: Rework this to provide more informative errors
if stream.peek(token::Brace) {
let content;
syn::braced!(content in stream);
return Ok(BodyNode::RawExpr(content.parse::<Expr>()?));
}
if stream.peek(LitStr) {
return Ok(BodyNode::Text(stream.parse::<TextNode>()?));
}
Ok(BodyNode::Element(
stream.parse::<AmbiguousElement<AS_HTML>>()?,
))
Ok(BodyNode::Element(stream.parse::<AmbiguousElement>()?))
}
}
impl<const AS: HtmlOrRsx> ToTokens for BodyNode<AS> {
impl ToTokens for BodyNode {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match &self {
BodyNode::Element(el) => el.to_tokens(tokens),