diff --git a/Cargo.toml b/Cargo.toml index 86a952e7f..7db59226c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] -rust-version = "1.56.0" +rust-version = "1.60.0" [dependencies] dioxus-core = { path = "./packages/core", version = "^0.2.1" } @@ -26,6 +26,8 @@ dioxus-router = { path = "./packages/router", version = "^0.2.3", optional = tru dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true } dioxus-tui = { path = "./packages/tui", version = "^0.2.2", optional = true } +dioxus-rsx = { path = "./packages/rsx", optional = true } +dioxus-rsx-interpreter = { path = "./packages/rsx_interpreter", optional = true } dioxus-liveview = { path = "./packages/liveview", version = "^0.1.0", optional = true } dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true } @@ -38,15 +40,16 @@ dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^ [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] -web = ["dioxus-web", "dioxus-router/web"] +web = ["dioxus-web", "dioxus-router?/web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] tui = ["dioxus-tui"] liveview = ["dioxus-liveview"] +hot_reload = ["dioxus-core-macro/hot_reload", "dioxus-rsx-interpreter", "dioxus-desktop?/hot_reload", "dioxus-web?/hot_reload"] native-core = ["dioxus-native-core", "dioxus-native-core-macro"] @@ -64,6 +67,8 @@ members = [ "packages/fermi", "packages/tui", "packages/liveview", + "packages/rsx", + "packages/rsx_interpreter", "packages/native-core", "packages/native-core-macro", ] @@ -86,6 +91,7 @@ criterion = "0.3.5" thiserror = "1.0.30" env_logger = "0.9.0" + [[bench]] name = "create" harness = false diff --git a/benches/tui_update.rs b/benches/tui_update.rs index d46921a8d..811c40739 100644 --- a/benches/tui_update.rs +++ b/benches/tui_update.rs @@ -80,11 +80,11 @@ fn Grid(cx: Scope) -> Element { counts.with_mut(|c| { let i = *count.current(); c[i] += 1; - c[i] = c[i] % 360; + c[i] %= 360; }); count.with_mut(|i| { *i += 1; - *i = *i % (size * size); + *i %= size * size; }); } diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 18f351bfa..3918268d3 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -21,6 +21,7 @@ - [Custom Renderer](guide/custom_renderer.md) - [Server-side components](guide/server_side_components.md) - [Bundling and Distributing](guide/bundline.md) + - [Hot Reloading Rsx](guide/hot_reloading.md) - [Reference Guide](reference/reference.md) - [Anti-patterns](reference/anti.md) diff --git a/docs/reference/src/guide/hot_reloading.md b/docs/reference/src/guide/hot_reloading.md new file mode 100644 index 000000000..eff0c8a54 --- /dev/null +++ b/docs/reference/src/guide/hot_reloading.md @@ -0,0 +1,24 @@ +# Hot Reloading +1. Hot reloading allows much faster iteration times inside of rsx calls by interperting them and streaming the edits. +2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program. +3. Currently the cli only implements hot reloading for the web renderer. + +# Setup +Install [dioxus-cli](https://github.com/DioxusLabs/cli). +Enable the hot_reload feature on dioxus: +```toml +dioxus = { version = "*", features = ["web", "hot_reload"] } +``` + +# Usage +1. run: +``` +dioxus serve --hot-reload +``` +2. change some code within a rsx macro +3. open your localhost in a browser +4. save and watch the style change without recompiling + +# Limitations +1. The interperter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression. +2. Components and Iterators can contain abritary rust code, and will trigger a full recompile when changed. \ No newline at end of file diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs new file mode 100644 index 000000000..31d2e1ce0 --- /dev/null +++ b/examples/tui_buttons.rs @@ -0,0 +1,93 @@ +use dioxus::{events::KeyCode, prelude::*}; + +fn main() { + dioxus::tui::launch(app); +} + +#[derive(PartialEq, Props)] +struct ButtonProps { + color_offset: u32, + layer: u16, +} + +#[allow(non_snake_case)] +fn Button(cx: Scope) -> Element { + let toggle = use_state(&cx, || false); + let hovered = use_state(&cx, || false); + + let hue = cx.props.color_offset % 255; + let saturation = if *toggle.get() { 50 } else { 25 } + if *hovered.get() { 50 } else { 25 }; + let brightness = saturation / 2; + let color = format!("hsl({hue}, {saturation}, {brightness})"); + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + background_color: "{color}", + tabindex: "{cx.props.layer}", + onkeydown: |e| { + if let KeyCode::Space = e.data.key_code{ + toggle.modify(|f| !f); + } + }, + onclick: |_| { + toggle.modify(|f| !f); + }, + onmouseenter: |_|{ + hovered.set(true); + }, + onmouseleave: |_|{ + hovered.set(false); + }, + justify_content: "center", + align_items: "center", + display: "flex", + flex_direction: "column", + + p{"tabindex: {cx.props.layer}"} + } + }) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + display: "flex", + flex_direction: "column", + width: "100%", + height: "100%", + + (1..8).map(|y| + cx.render(rsx!{ + div{ + display: "flex", + flex_direction: "row", + width: "100%", + height: "100%", + (1..8).map(|x|{ + if (x + y) % 2 == 0{ + cx.render(rsx!{ + div{ + width: "100%", + height: "100%", + background_color: "rgb(100, 100, 100)", + } + }) + } + else{ + let layer = (x + y) % 3; + cx.render(rsx!{ + Button{ + color_offset: x * y, + layer: layer as u16, + } + }) + } + }) + } + }) + ) + } + }) +} diff --git a/examples/tui_keys.rs b/examples/tui_keys.rs index 138eec2fe..91c42e30e 100644 --- a/examples/tui_keys.rs +++ b/examples/tui_keys.rs @@ -11,9 +11,9 @@ fn main() { fn app(cx: Scope) -> Element { let key = use_state(&cx, || "".to_string()); - let mouse = use_state(&cx, || ScreenPoint::zero()); + let mouse = use_state(&cx, ScreenPoint::zero); let count = use_state(&cx, || 0); - let buttons = use_state(&cx, || MouseButtonSet::empty()); + let buttons = use_state(&cx, MouseButtonSet::empty); let mouse_clicked = use_state(&cx, || false); cx.render(rsx! { diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 358f5edf0..80502cf66 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,12 +15,17 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -proc-macro-error = "1" -proc-macro2 = { version = "1.0.6" } +proc-macro2 = { version = "1.0" } quote = "1.0" -syn = { version = "1.0.11", features = ["full", "extra-traits"] } +syn = { version = "1.0", features = ["full", "extra-traits"] } +dioxus-rsx = { path = "../rsx" } +dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true } # testing [dev-dependencies] rustversion = "1.0" trybuild = "1.0" + +[features] +default = [] +hot_reload = ["dioxus-rsx-interpreter"] \ No newline at end of file diff --git a/packages/core-macro/src/ifmt.rs b/packages/core-macro/src/ifmt.rs deleted file mode 100644 index 912d78f7e..000000000 --- a/packages/core-macro/src/ifmt.rs +++ /dev/null @@ -1,228 +0,0 @@ -use ::quote::{quote, ToTokens}; -use ::std::ops::Not; -use ::syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - *, -}; -use proc_macro2::TokenStream; - -pub fn format_args_f_impl(input: IfmtInput) -> Result { - let IfmtInput { - mut format_literal, - mut positional_args, - mut named_args, - } = input; - - let s = format_literal.value(); - let out_format_literal = &mut String::with_capacity(s.len()); - - let mut iterator = s.char_indices().peekable(); - while let Some((i, c)) = iterator.next() { - out_format_literal.push(c); - if c != '{' { - continue; - } - // encountered `{`, let's see if it was `{{` - if let Some(&(_, '{')) = iterator.peek() { - let _ = iterator.next(); - out_format_literal.push('{'); - continue; - } - let (end, colon_or_closing_brace) = - iterator - .find(|&(_, c)| c == '}' || c == ':') - .expect(concat!( - "Invalid format string literal\n", - "note: if you intended to print `{`, ", - "you can escape it using `{{`", - )); - // We use defer to ensure all the `continue`s append the closing char. - let mut out_format_literal = defer(&mut *out_format_literal, |it| { - it.push(colon_or_closing_brace) - }); - let out_format_literal: &mut String = *out_format_literal; - let mut arg = s[i + 1..end].trim(); - if let Some("=") = arg.get(arg.len().saturating_sub(1)..) { - assert_eq!( - out_format_literal.pop(), // Remove the opening brace - Some('{'), - ); - arg = &arg[..arg.len() - 1]; - out_format_literal.push_str(arg); - out_format_literal.push_str(" = {"); - } - if arg.is_empty() { - continue; - } - - enum Segment { - Ident(Ident), - LitInt(LitInt), - } - let segments: Vec = { - impl Parse for Segment { - fn parse(input: ParseStream<'_>) -> Result { - let lookahead = input.lookahead1(); - if lookahead.peek(Ident) { - input.parse().map(Segment::Ident) - } else if lookahead.peek(LitInt) { - input.parse().map(Segment::LitInt) - } else { - Err(lookahead.error()) - } - } - } - match ::syn::parse::Parser::parse_str( - Punctuated::::parse_separated_nonempty, - arg, - ) { - Ok(segments) => segments.into_iter().collect(), - Err(err) => return Err(err), - } - }; - match segments.len() { - 0 => unreachable!("`parse_separated_nonempty` returned empty"), - 1 => { - out_format_literal.push_str(arg); - match { segments }.pop().unwrap() { - Segment::LitInt(_) => { - // found something like `{0}`, let `format_args!` - // handle it. - continue; - } - Segment::Ident(ident) => { - // if `ident = ...` is not yet among the extra args - if named_args.iter().all(|(it, _)| *it != ident) { - named_args.push(( - ident.clone(), - parse_quote!(#ident), // Expr - )); - } - } - } - } - _ => { - ::std::fmt::Write::write_fmt( - out_format_literal, - format_args!("{}", positional_args.len()), - ) - .expect("`usize` or `char` Display impl cannot panic"); - let segments: Punctuated = segments - .into_iter() - .map(|it| match it { - Segment::Ident(ident) => ident.into_token_stream(), - Segment::LitInt(literal) => literal.into_token_stream(), - }) - .collect(); - positional_args.push(parse_quote! { - #segments - }) - } - } - } - - let named_args = named_args.into_iter().map(|(ident, expr)| { - quote! { - #ident = #expr - } - }); - format_literal = LitStr::new(out_format_literal, format_literal.span()); - - Ok(quote! { - format_args!( - #format_literal - #(, #positional_args)* - #(, #named_args)* - ) - }) -} - -#[allow(dead_code)] // dumb compiler does not see the struct being used... -pub struct IfmtInput { - format_literal: LitStr, - positional_args: Vec, - named_args: Vec<(Ident, Expr)>, -} - -impl Parse for IfmtInput { - fn parse(input: ParseStream) -> Result { - let format_literal = input.parse()?; - let mut positional_args = vec![]; - loop { - if input.parse::>()?.is_none() { - return Ok(Self { - format_literal, - positional_args, - named_args: vec![], - }); - } - if input.peek(Ident) && input.peek2(Token![=]) && input.peek3(Token![=]).not() { - // Found a positional parameter - break; - } - positional_args.push(input.parse()?); - } - let named_args = Punctuated::<_, Token![,]>::parse_terminated_with(input, |input| { - Ok({ - let name: Ident = input.parse()?; - let _: Token![=] = input.parse()?; - let expr: Expr = input.parse()?; - (name, expr) - }) - })? - .into_iter() - .collect(); - Ok(Self { - format_literal, - positional_args, - named_args, - }) - } -} - -pub fn defer<'a, T: 'a, Drop: 'a>(x: T, drop: Drop) -> impl ::core::ops::DerefMut + 'a -where - Drop: FnOnce(T), -{ - use ::core::mem::ManuallyDrop; - struct Ret(ManuallyDrop, ManuallyDrop) - where - Drop: FnOnce(T); - impl ::core::ops::Drop for Ret - where - Drop: FnOnce(T), - { - fn drop(&'_ mut self) { - use ::core::ptr; - unsafe { - // # Safety - // - // - This is the canonical example of using `ManuallyDrop`. - let value = ManuallyDrop::into_inner(ptr::read(&self.0)); - let drop = ManuallyDrop::into_inner(ptr::read(&self.1)); - drop(value); - } - } - } - impl ::core::ops::Deref for Ret - where - Drop: FnOnce(T), - { - type Target = T; - #[inline] - fn deref(&'_ self) -> &'_ Self::Target { - &self.0 - } - } - impl ::core::ops::DerefMut for Ret - where - Drop: FnOnce(T), - { - #[inline] - fn deref_mut(&'_ mut self) -> &'_ mut Self::Target { - &mut self.0 - } - } - Ret(ManuallyDrop::new(x), ManuallyDrop::new(drop)) -} diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 8407a3733..56d82876f 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -2,14 +2,15 @@ use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; -mod ifmt; mod inlineprops; mod props; -mod rsx; + +// mod rsx; +use dioxus_rsx as rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { - use ifmt::*; + use rsx::*; let item = parse_macro_input!(input as IfmtInput); format_args_f_impl(item) .unwrap_or_else(|err| err.to_compile_error()) @@ -31,12 +32,43 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// ``` #[doc = include_str!("../../../examples/rsx_usage.rs")] /// ``` -#[proc_macro_error::proc_macro_error] #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { + #[cfg(feature = "hot_reload")] + let rsx_text = s.to_string(); match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(stream) => stream.to_token_stream().into(), + Ok(body) => { + #[cfg(feature = "hot_reload")] + { + use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder; + + match CapturedContextBuilder::from_call_body(body) { + Ok(captured) => { + let lazy = quote::quote! { + LazyNodes::new(move |__cx|{ + let code_location = get_line_num!(); + let captured = #captured; + let text = #rsx_text; + + resolve_scope(code_location, text, captured, __cx) + }) + }; + if let Some(cx) = captured.custom_context { + quote::quote! { + #cx.render(#lazy) + } + .into() + } else { + lazy.into() + } + } + Err(err) => err.into_compile_error().into(), + } + } + #[cfg(not(feature = "hot_reload"))] + body.to_token_stream().into() + } } } diff --git a/packages/core-macro/src/rsx/errors.rs b/packages/core-macro/src/rsx/errors.rs deleted file mode 100644 index 476f42eff..000000000 --- a/packages/core-macro/src/rsx/errors.rs +++ /dev/null @@ -1,31 +0,0 @@ -macro_rules! missing_trailing_comma { - ($span:expr) => { - proc_macro_error::emit_error!($span, "missing trailing comma") - }; -} - -macro_rules! attr_after_element { - ($span:expr) => { - proc_macro_error::emit_error!( - $span, - "expected element"; - help = "move the attribute above all the children and text elements" - ) - }; -} - -macro_rules! component_path_cannot_have_arguments { - ($span:expr) => { - proc_macro_error::abort!( - $span, - "expected a path without arguments"; - help = "try remove the path arguments" - ) - }; -} - -macro_rules! invalid_component_path { - ($span:expr) => { - proc_macro_error::abort!($span, "Invalid component path syntax") - }; -} diff --git a/packages/core-macro/src/rsx/pretty.rs b/packages/core-macro/src/rsx/pretty.rs deleted file mode 100644 index 26ad54006..000000000 --- a/packages/core-macro/src/rsx/pretty.rs +++ /dev/null @@ -1 +0,0 @@ -//! pretty printer for rsx! diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index fc697df12..b3a8007f0 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -48,5 +48,4 @@ backtrace = "0.3" [features] default = [] serialize = ["serde"] -debug_vdom = [] - +debug_vdom = [] \ No newline at end of file diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 333fef513..37c30742a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -125,6 +125,9 @@ pub enum SchedulerMsg { /// Immediate updates from Components that mark them as dirty Immediate(ScopeId), + /// Mark all components as dirty and update them + DirtyAll, + /// New tasks from components that should be polled when the next poll is ready NewTask(ScopeId), } @@ -390,6 +393,11 @@ impl VirtualDom { SchedulerMsg::Immediate(s) => { self.dirty_scopes.insert(s); } + SchedulerMsg::DirtyAll => { + for id in self.scopes.scopes.borrow().keys() { + self.dirty_scopes.insert(*id); + } + } } } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index e61120d64..de07bb88a 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" } +dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true } serde = "1.0.136" serde_json = "1.0.79" @@ -32,6 +33,8 @@ webbrowser = "0.7.1" mime_guess = "2.0.3" dunce = "1.0.2" +interprocess = { version = "1.1.1", optional = true } + [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" @@ -41,7 +44,7 @@ tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] tray = ["wry/tray"] - +hot_reload = ["dioxus-rsx-interpreter", "interprocess"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 2fa1355d2..b6a954b01 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,4 +1,5 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; + use dioxus_core::*; use std::{ collections::HashMap, @@ -49,6 +50,93 @@ impl DesktopController { dom.base_scope().provide_context(window_context); + // allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel + #[cfg(feature = "hot_reload")] + { + use dioxus_rsx_interpreter::{ + error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT, + }; + use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; + use std::io::{BufRead, BufReader, Write}; + use std::time::Duration; + + fn handle_error( + connection: std::io::Result, + ) -> Option { + connection + .map_err(|error| eprintln!("Incoming connection failed: {}", error)) + .ok() + } + + let latest_in_connection: Arc>>> = + Arc::new(Mutex::new(None)); + let latest_in_connection_handle = latest_in_connection.clone(); + let latest_out_connection: Arc>>> = + Arc::new(Mutex::new(None)); + let latest_out_connection_handle = latest_out_connection.clone(); + + struct DesktopErrorHandler { + latest_connection: Arc>>>, + } + impl ErrorHandler for DesktopErrorHandler { + fn handle_error(&self, err: Error) { + if let Some(conn) = &mut *self.latest_connection.lock().unwrap() { + conn.get_mut() + .write_all( + (serde_json::to_string(&err).unwrap() + "\n").as_bytes(), + ) + .unwrap(); + } + } + } + + RSX_CONTEXT.set_error_handler(DesktopErrorHandler { + latest_connection: latest_out_connection_handle, + }); + RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel()); + + // connect to processes for incoming data + std::thread::spawn(move || { + if let Ok(listener) = LocalSocketListener::bind("@dioxusin") { + for conn in listener.incoming().filter_map(handle_error) { + *latest_in_connection_handle.lock().unwrap() = + Some(BufReader::new(conn)); + } + } + }); + + // connect to processes for outgoing errors + std::thread::spawn(move || { + if let Ok(listener) = LocalSocketListener::bind("@dioxusout") { + for conn in listener.incoming().filter_map(handle_error) { + *latest_out_connection.lock().unwrap() = Some(BufReader::new(conn)); + } + } + }); + + std::thread::spawn(move || { + loop { + if let Some(conn) = &mut *latest_in_connection.lock().unwrap() { + let mut buf = String::new(); + match conn.read_line(&mut buf) { + Ok(_) => { + let msgs: SetManyRsxMessage = + serde_json::from_str(&buf).unwrap(); + RSX_CONTEXT.extend(msgs); + } + Err(err) => { + if err.kind() != std::io::ErrorKind::WouldBlock { + break; + } + } + } + } + // give the error handler time to take the mutex + std::thread::sleep(Duration::from_millis(100)); + } + }); + } + let edits = dom.rebuild(); edit_queue diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index a69ac5fef..670b211c0 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -96,7 +96,7 @@ impl From<&MouseEvent> for MouseData { ElementPoint::new(e.offset_x().into(), e.offset_y().into()), PagePoint::new(e.page_x().into(), e.page_y().into()), ), - Some(MouseButton::from_web_code(e.button().into())), + Some(MouseButton::from_web_code(e.button())), decode_mouse_button_set(e.buttons()), modifiers, ) diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs index 3bcdee8c2..5983e304d 100644 --- a/packages/liveview/examples/axum.rs +++ b/packages/liveview/examples/axum.rs @@ -1,36 +1,29 @@ -use axum::{ - extract::ws::WebSocketUpgrade, response::Html, response::IntoResponse, routing::get, Extension, - Router, -}; +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; use dioxus_core::{Element, LazyNodes, Scope}; -use dioxus_liveview::Liveview; #[tokio::main] async fn main() { - #[cfg(feature = "axum")] - { - pretty_env_logger::init(); + pretty_env_logger::init(); - let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); - let view = dioxus_liveview::new(addr); - let body = view.body("Dioxus Liveview"); + let view = dioxus_liveview::new(addr); + let body = view.body("Dioxus Liveview"); - let app = Router::new() - .route("/", get(move || async { Html(body) })) - .route( - "/app", - get(move |ws: WebSocketUpgrade| async move { - ws.on_upgrade(move |socket| async move { - view.upgrade(socket, app).await; - }) - }), - ); - axum::Server::bind(&addr.to_string().parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); - } + let app = Router::new() + .route("/", get(move || async { Html(body) })) + .route( + "/app", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + view.upgrade_axum(socket, app).await; + }) + }), + ); + axum::Server::bind(&addr.to_string().parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); } fn app(cx: Scope) -> Element { diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 2c14d318b..dcece1ae8 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -5,28 +5,25 @@ use warp::Filter; #[tokio::main] async fn main() { - #[cfg(feature = "warp")] - { - pretty_env_logger::init(); + pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr = ([127, 0, 0, 1], 3030); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); - let body = view.body("Dioxus LiveView"); + // todo: compactify this routing under one liveview::app method + let view = liveview::new(addr); + let body = view.body("Dioxus LiveView"); - let routes = warp::path::end() - .map(move || warp::reply::html(body.clone())) - .or(warp::path("app") - .and(warp::ws()) - .and(warp::any().map(move || view.clone())) - .map(|ws: Ws, view: liveview::Liveview| { - ws.on_upgrade(|socket| async move { - view.upgrade(socket, app).await; - }) - })); - warp::serve(routes).run(addr).await; - } + let routes = warp::path::end() + .map(move || warp::reply::html(body.clone())) + .or(warp::path("app") + .and(warp::ws()) + .and(warp::any().map(move || view.clone())) + .map(|ws: Ws, view: liveview::Liveview| { + ws.on_upgrade(|socket| async move { + view.upgrade_warp(socket, app).await; + }) + })); + warp::serve(routes).run(addr).await; } fn app(cx: Scope) -> Element { diff --git a/packages/liveview/src/adapters/actix_adapter.rs b/packages/liveview/src/adapters/actix_adapter.rs deleted file mode 100644 index 8b1378917..000000000 --- a/packages/liveview/src/adapters/actix_adapter.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index 4122950bc..c60b267ea 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1,4 +1,4 @@ -use crate::{events, Liveview}; +use crate::events; use axum::extract::ws::{Message, WebSocket}; use dioxus_core::prelude::*; use futures_util::{ @@ -10,11 +10,16 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::task::LocalPoolHandle; impl crate::Liveview { - pub async fn upgrade(&self, ws: WebSocket, app: fn(Scope) -> Element) { + pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) { connect(ws, self.pool.clone(), app, ()).await; } - pub async fn upgrade_with_props(&self, ws: WebSocket, app: fn(Scope) -> Element, props: T) - where + + pub async fn upgrade_axum_with_props( + &self, + ws: WebSocket, + app: fn(Scope) -> Element, + props: T, + ) where T: Send + Sync + 'static, { connect(ws, self.pool.clone(), app, props).await; diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index f3cf3914b..398c7cdd9 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -7,10 +7,10 @@ use tokio_util::task::LocalPoolHandle; use warp::ws::{Message, WebSocket}; impl crate::Liveview { - pub async fn upgrade(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { + pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { connect(ws, self.pool.clone(), app, ()).await; } - pub async fn upgrade_with_props( + pub async fn upgrade_warp_with_props( &self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element, diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index f3ad90511..2f1cd3b9b 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -7,21 +7,10 @@ pub mod adapters { #[cfg(feature = "axum")] pub mod axum_adapter; - - #[cfg(feature = "actix")] - pub mod actix_adapter; } use std::net::SocketAddr; -#[cfg(feature = "warp")] -pub use adapters::warp_adapter::connect; - -#[cfg(feature = "axum")] -pub use adapters::axum_adapter::connect; - -#[cfg(feature = "actix")] -pub use adapters::actix_adapter::connect; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index 508754bf7..58fa3e3a9 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -211,7 +211,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream { ty: dioxus_native_core::state::MemberId, node: &'a dioxus_core::VNode<'a>, vdom: &'a dioxus_core::VirtualDom, - children: &Vec<&Self>, + children: &[&Self], ctx: &anymap::AnyMap, ) -> Option{ use dioxus_native_core::state::ChildDepState as _; @@ -225,7 +225,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream { ty - #sum_idents, node, vdom, - &children.iter().map(|p| &p.#child_state_idents).collect(), + &children.iter().map(|p| &p.#child_state_idents).collect::>(), ctx, ).map(|mut changed|{ for id in &mut changed.node_dep{ diff --git a/packages/native-core-macro/tests/peristant_iterator.rs b/packages/native-core-macro/tests/peristant_iterator.rs new file mode 100644 index 000000000..29df3afe5 --- /dev/null +++ b/packages/native-core-macro/tests/peristant_iterator.rs @@ -0,0 +1,342 @@ +use dioxus_native_core::{ + real_dom::{NodeType, RealDom}, + state::State, + utils::PersistantElementIter, +}; +use dioxus_native_core_macro::State; + +#[derive(State, Default, Clone)] +struct Empty {} + +#[test] +#[allow(unused_variables)] +fn traverse() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let mutations = vdom.create_vnodes(rsx! { + div{ + div{ + "hello" + p{ + "world" + } + "hello world" + } + } + }); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![mutations]); + + let mut iter = PersistantElementIter::new(); + let div_tag = "div".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + let text1 = "hello".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + let p_tag = "p".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text2 = "world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + let text3 = "hello world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_removes() { + use dioxus_core::VNode; + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + // this will end on the node that is removed + let mut iter1 = PersistantElementIter::new(); + // this will end on the after node that is removed + let mut iter2 = PersistantElementIter::new(); + // div + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "hello" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "world" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter2.next(&rdom).id(); + // "hello world" + iter2.next(&rdom).id(); + + iter1.prune(&update, &rdom); + iter2.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); + let div_tag = "div".to_string(); + let idx = iter2.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: div_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_before() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "div".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_after() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); +} diff --git a/packages/native-core-macro/tests/update_state.rs b/packages/native-core-macro/tests/update_state.rs index 5cd23732e..84ec5d530 100644 --- a/packages/native-core-macro/tests/update_state.rs +++ b/packages/native-core-macro/tests/update_state.rs @@ -92,6 +92,7 @@ impl NodeDepState for NodeDepCallCounter { } } +#[allow(clippy::vec_box)] #[derive(Debug, Clone, PartialEq, Default)] struct BubbledUpStateTester(Option, Vec>); impl ChildDepState for BubbledUpStateTester { diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index a6e011fa9..2319ca4ac 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -2,3 +2,4 @@ pub mod layout_attributes; pub mod node_ref; pub mod real_dom; pub mod state; +pub mod utils; diff --git a/packages/native-core/src/state.rs b/packages/native-core/src/state.rs index 7583e3ede..89c47e6e9 100644 --- a/packages/native-core/src/state.rs +++ b/packages/native-core/src/state.rs @@ -132,7 +132,7 @@ pub trait State: Default + Clone { ty: MemberId, node: &'a VNode<'a>, vdom: &'a dioxus_core::VirtualDom, - children: &Vec<&Self>, + children: &[&Self], ctx: &AnyMap, ) -> Option; /// This must be a valid resolution order. (no nodes updated before a state they rely on) diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs new file mode 100644 index 000000000..143149521 --- /dev/null +++ b/packages/native-core/src/utils.rs @@ -0,0 +1,230 @@ +use crate::{ + real_dom::{NodeType, RealDom}, + state::State, +}; +use dioxus_core::{DomEdit, ElementId, Mutations}; + +pub enum ElementProduced { + Progressed(ElementId), + Looped(ElementId), +} +impl ElementProduced { + pub fn id(&self) -> ElementId { + match self { + ElementProduced::Progressed(id) => *id, + ElementProduced::Looped(id) => *id, + } + } +} + +#[derive(Debug)] +enum NodePosition { + AtNode, + InChild(usize), +} + +impl NodePosition { + fn map(&self, mut f: impl FnMut(usize) -> usize) -> Self { + match self { + Self::AtNode => Self::AtNode, + Self::InChild(i) => Self::InChild(f(*i)), + } + } + + fn get_or_insert(&mut self, child_idx: usize) -> usize { + match self { + Self::AtNode => { + *self = Self::InChild(child_idx); + child_idx + } + Self::InChild(i) => *i, + } + } +} + +/// The focus system needs a iterator that can persist through changes in the [VirtualDom]. +/// Iterate through it with [ElementIter::next] [ElementIter::prev], and update it with [ElementIter::update] (with data from [`VirtualDom::work_with_deadline`]). +/// The iterator loops around when it reaches the end or the beginning. +pub struct PersistantElementIter { + // stack of elements and fragments + stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>, +} + +impl Default for PersistantElementIter { + fn default() -> Self { + PersistantElementIter { + stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)], + } + } +} + +impl PersistantElementIter { + pub fn new() -> Self { + Self::default() + } + + /// remove stale element refreneces + /// returns true if the focused element is removed + pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) -> bool { + let mut changed = false; + let ids_removed: Vec<_> = mutations + .edits + .iter() + .filter_map(|e| { + if let DomEdit::Remove { root } = e { + Some(*root) + } else { + None + } + }) + .collect(); + // if any element is removed in the chain, remove it and its children from the stack + if let Some(r) = self + .stack + .iter() + .position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id)) + { + self.stack.truncate(r); + changed = true; + } + // if a child is removed or inserted before or at the current element, update the child index + for (el_id, child_idx) in self.stack.iter_mut() { + if let NodePosition::InChild(child_idx) = child_idx { + if let NodeType::Element { children, .. } = &rdom[*el_id].node_type { + for m in &mutations.edits { + match m { + DomEdit::Remove { root } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx -= 1; + } + } + DomEdit::InsertBefore { root, n } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + DomEdit::InsertAfter { root, n } => { + if children + .iter() + .take(*child_idx) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + _ => (), + } + } + } + } + } + changed + } + + /// get the next element + pub fn next(&mut self, rdom: &RealDom) -> ElementProduced { + if self.stack.is_empty() { + let id = ElementId(0); + let new = (id, NodePosition::AtNode); + self.stack.push(new); + ElementProduced::Looped(id) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + *o_child_idx = o_child_idx.map(|i| i + 1); + // if we have children, go to the next child + let child_idx = o_child_idx.get_or_insert(0); + if child_idx >= children.len() { + self.pop(); + self.next(rdom) + } else { + let id = children[child_idx]; + if let NodeType::Element { .. } = &rdom[id].node_type { + self.stack.push((id, NodePosition::AtNode)); + } + ElementProduced::Progressed(id) + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + /// get the previous element + pub fn prev(&mut self, rdom: &RealDom) -> ElementProduced { + // recursively add the last child element to the stack + fn push_back( + stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>, + new_node: ElementId, + rdom: &RealDom, + ) -> ElementId { + match &rdom[new_node].node_type { + NodeType::Element { children, .. } => { + if children.is_empty() { + new_node + } else { + stack.push((new_node, NodePosition::InChild(children.len() - 1))); + push_back(stack, *children.last().unwrap(), rdom) + } + } + _ => new_node, + } + } + if self.stack.is_empty() { + let new_node = ElementId(0); + ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom)) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + // if we have children, go to the next child + if let NodePosition::InChild(0) = o_child_idx { + ElementProduced::Progressed(self.pop()) + } else { + *o_child_idx = o_child_idx.map(|i| i - 1); + if let NodePosition::InChild(child_idx) = o_child_idx { + if *child_idx >= children.len() || children.is_empty() { + self.pop(); + self.prev(rdom) + } else { + let new_node = children[*child_idx]; + ElementProduced::Progressed(push_back( + &mut self.stack, + new_node, + rdom, + )) + } + } else { + self.pop(); + self.prev(rdom) + } + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + fn pop(&mut self) -> ElementId { + self.stack.pop().unwrap().0 + } +} diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml new file mode 100644 index 000000000..24ef50c1d --- /dev/null +++ b/packages/rsx/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-rsx" +version = "0.0.0" +edition = "2018" +license = "MIT/Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = { version = "1.0" } +syn = { version = "1.0", features = ["full", "extra-traits"] } +quote = { version = "1.0" } diff --git a/packages/core-macro/src/rsx/component.rs b/packages/rsx/src/component.rs similarity index 95% rename from packages/core-macro/src/rsx/component.rs rename to packages/rsx/src/component.rs index 91c8c4903..90248ad1f 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/rsx/src/component.rs @@ -18,9 +18,12 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, AngleBracketedGenericArguments, Expr, Ident, LitStr, PathArguments, Result, Token, + spanned::Spanned, + token, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result, + Token, }; +#[derive(PartialEq, Eq)] pub struct Component { pub name: syn::Path, pub prop_gen_args: Option, @@ -39,7 +42,7 @@ impl Component { .take(path.segments.len() - 1) .any(|seg| seg.arguments != PathArguments::None) { - component_path_cannot_have_arguments!(path); + component_path_cannot_have_arguments!(path.span()); } // ensure last segment only have value of None or AngleBracketed @@ -47,7 +50,7 @@ impl Component { path.segments.last().unwrap().arguments, PathArguments::None | PathArguments::AngleBracketed(_) ) { - invalid_component_path!(path); + invalid_component_path!(path.span()); } Ok(()) @@ -190,12 +193,14 @@ impl ToTokens for Component { } // the struct's fields info +#[derive(PartialEq, Eq)] pub struct ComponentField { - name: Ident, - content: ContentField, + pub name: Ident, + pub content: ContentField, } -enum ContentField { +#[derive(PartialEq, Eq)] +pub enum ContentField { ManExpr(Expr), Formatted(LitStr), OnHandlerRaw(Expr), diff --git a/packages/core-macro/src/rsx/element.rs b/packages/rsx/src/element.rs similarity index 97% rename from packages/core-macro/src/rsx/element.rs rename to packages/rsx/src/element.rs index 96c7a2efc..38ab75575 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/rsx/src/element.rs @@ -4,12 +4,13 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, + Error, Expr, Ident, LitStr, Result, Token, }; // ======================================= // Parse the VNode::Element type // ======================================= +#[derive(PartialEq, Eq)] pub struct Element { pub name: Ident, pub key: Option, @@ -64,7 +65,7 @@ impl Parse for Element { } if content.parse::().is_err() { - missing_trailing_comma!(ident); + missing_trailing_comma!(ident.span()); } continue; } @@ -122,7 +123,7 @@ impl Parse for Element { // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { - missing_trailing_comma!(ident); + missing_trailing_comma!(ident.span()); } continue; } @@ -189,6 +190,7 @@ impl ToTokens for Element { } } +#[derive(PartialEq, Eq)] pub enum ElementAttr { /// attribute: "valuee {}" AttrText { name: Ident, value: LitStr }, @@ -208,6 +210,7 @@ pub enum ElementAttr { EventTokens { name: Ident, tokens: Expr }, } +#[derive(PartialEq, Eq)] pub struct ElementAttrNamed { pub el_name: Ident, pub attr: ElementAttr, diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs new file mode 100644 index 000000000..f89b28500 --- /dev/null +++ b/packages/rsx/src/errors.rs @@ -0,0 +1,26 @@ +macro_rules! missing_trailing_comma { + ($span:expr) => { + return Err(Error::new($span, "missing trailing comma")); + }; +} + +macro_rules! attr_after_element { + ($span:expr) => { + return Err(Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); + }; +} + +macro_rules! component_path_cannot_have_arguments { + ($span:expr) => { + return Err(Error::new( + $span, + "expected a path without arguments\n = try remove the path arguments", + )); + }; +} + +macro_rules! invalid_component_path { + ($span:expr) => { + return Err(Error::new($span, "Invalid component path syntax")); + }; +} diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs new file mode 100644 index 000000000..2d77a243f --- /dev/null +++ b/packages/rsx/src/ifmt.rs @@ -0,0 +1,176 @@ +use std::str::FromStr; + +use proc_macro2::{Span, TokenStream}; + +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + *, +}; + +pub fn format_args_f_impl(input: IfmtInput) -> Result { + // build format_literal + let mut format_literal = String::new(); + let mut expr_counter = 0; + for segment in input.segments.iter() { + match segment { + Segment::Literal(s) => format_literal += s, + Segment::Formatted { + format_args, + segment, + } => { + format_literal += "{"; + match segment { + FormattedSegment::Expr(_) => { + format_literal += &expr_counter.to_string(); + expr_counter += 1; + } + FormattedSegment::Ident(ident) => { + format_literal += &ident.to_string(); + } + } + format_literal += ":"; + format_literal += format_args; + format_literal += "}"; + } + } + } + + let positional_args = input.segments.iter().filter_map(|seg| { + if let Segment::Formatted { + segment: FormattedSegment::Expr(expr), + .. + } = seg + { + Some(expr) + } else { + None + } + }); + + let named_args = input.segments.iter().filter_map(|seg| { + if let Segment::Formatted { + segment: FormattedSegment::Ident(ident), + .. + } = seg + { + Some(quote! {#ident = #ident}) + } else { + None + } + }); + + Ok(quote! { + format_args!( + #format_literal + #(, #positional_args)* + #(, #named_args)* + ) + }) +} + +#[allow(dead_code)] // dumb compiler does not see the struct being used... +#[derive(Debug)] +pub struct IfmtInput { + pub segments: Vec, +} + +impl FromStr for IfmtInput { + type Err = syn::Error; + + fn from_str(input: &str) -> Result { + let mut chars = input.chars().peekable(); + let mut segments = Vec::new(); + let mut current_literal = String::new(); + while let Some(c) = chars.next() { + if c == '{' { + if let Some(c) = chars.next_if(|c| *c == '{') { + current_literal.push(c); + continue; + } + segments.push(Segment::Literal(current_literal)); + current_literal = String::new(); + let mut current_captured = String::new(); + while let Some(c) = chars.next() { + if c == ':' { + let mut current_format_args = String::new(); + for c in chars.by_ref() { + if c == '}' { + segments.push(Segment::Formatted { + format_args: current_format_args, + segment: FormattedSegment::parse(¤t_captured)?, + }); + break; + } + current_format_args.push(c); + } + break; + } + if c == '}' { + segments.push(Segment::Formatted { + format_args: String::new(), + segment: FormattedSegment::parse(¤t_captured)?, + }); + break; + } + current_captured.push(c); + } + } else { + current_literal.push(c); + } + } + segments.push(Segment::Literal(current_literal)); + Ok(Self { segments }) + } +} + +#[derive(Debug)] +pub enum Segment { + Literal(String), + Formatted { + format_args: String, + segment: FormattedSegment, + }, +} + +#[derive(Debug)] +pub enum FormattedSegment { + Expr(Box), + Ident(Ident), +} + +impl FormattedSegment { + fn parse(input: &str) -> Result { + if let Ok(ident) = parse_str::(input) { + if ident == input { + return Ok(Self::Ident(ident)); + } + } + // if let Ok(expr) = parse_str(&("{".to_string() + input + "}")) { + if let Ok(expr) = parse_str(input) { + Ok(Self::Expr(Box::new(expr))) + } else { + Err(Error::new( + Span::call_site(), + "Expected Ident or Expression", + )) + } + } +} + +impl ToTokens for FormattedSegment { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Expr(expr) => expr.to_tokens(tokens), + Self::Ident(ident) => ident.to_tokens(tokens), + } + } +} + +impl Parse for IfmtInput { + fn parse(input: ParseStream) -> Result { + let input: LitStr = input.parse()?; + let input_str = input.value(); + IfmtInput::from_str(&input_str) + } +} diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/rsx/src/lib.rs similarity index 99% rename from packages/core-macro/src/rsx/mod.rs rename to packages/rsx/src/lib.rs index 6b9dfc3dc..a5b8143f2 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/rsx/src/lib.rs @@ -16,13 +16,13 @@ mod errors; mod component; mod element; +mod ifmt; mod node; -pub mod pretty; - // Re-export the namespaces into each other pub use component::*; pub use element::*; +pub use ifmt::*; pub use node::*; // imports diff --git a/packages/core-macro/src/rsx/node.rs b/packages/rsx/src/node.rs similarity index 95% rename from packages/core-macro/src/rsx/node.rs rename to packages/rsx/src/node.rs index 1f1c19131..e8b754f13 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/rsx/src/node.rs @@ -15,6 +15,7 @@ Parse -> "text {with_args}" -> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr */ +#[derive(PartialEq, Eq)] pub enum BodyNode { Element(Element), Component(Component), @@ -22,6 +23,12 @@ pub enum BodyNode { RawExpr(Expr), } +impl BodyNode { + pub fn is_litstr(&self) -> bool { + matches!(self, BodyNode::Text(_)) + } +} + impl Parse for BodyNode { fn parse(stream: ParseStream) -> Result { if stream.peek(LitStr) { diff --git a/packages/rsx_interpreter/Cargo.toml b/packages/rsx_interpreter/Cargo.toml new file mode 100644 index 000000000..0b1f79a5d --- /dev/null +++ b/packages/rsx_interpreter/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dioxus-rsx-interpreter" +version = "0.1.0" +edition = "2021" +license = "MIT/Apache-2.0" + +[dependencies] +syn = { version = "1.0", features = ["extra-traits"] } +proc-macro2 = { version = "1.0.39", features = ["span-locations"] } +quote = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +lazy_static = "1.4.0" + +dioxus-rsx = { path = "../rsx", default-features = false } +dioxus-ssr = { path = "../ssr" } +dioxus-core = { path = "../core" } +dioxus-html = { path = "../html" } +dioxus-hooks = { path = "../hooks"} + +[dev-dependencies] +dioxus-core-macro = { path = "../core-macro" } +bumpalo = { version = "3.6", features = ["collections", "boxed"] } diff --git a/packages/rsx_interpreter/src/attributes.rs b/packages/rsx_interpreter/src/attributes.rs new file mode 100644 index 000000000..f8d19fddb --- /dev/null +++ b/packages/rsx_interpreter/src/attributes.rs @@ -0,0 +1,869 @@ +use crate::elements::*; + +// map the rsx name of the attribute to the html name of the attribute and the namespace that contains it +pub fn attrbute_to_static_str(attr: &str) -> Option<(&'static str, Option<&'static str>)> { + NO_NAMESPACE_ATTRIBUTES + .iter() + .find(|&a| *a == attr) + .map(|a| (*a, None)) + .or_else(|| { + STYLE_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, Some("style"))) + }) + .or_else(|| { + MAPPED_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + .or_else(|| { + svg::MAPPED_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + .or_else(|| { + ELEMENTS_WITH_MAPPED_ATTRIBUTES + .iter() + .find_map(|(_, attrs)| { + attrs + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + }) + .or_else(|| { + ELEMENTS_WITH_NAMESPACE + .iter() + .find_map(|(_, _, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None))) + }) + .or_else(|| { + ELEMENTS_WITHOUT_NAMESPACE + .iter() + .find_map(|(_, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None))) + }) +} + +macro_rules! no_namespace_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident; + )* + ) => { + pub const NO_NAMESPACE_ATTRIBUTES: &'static [&'static str] = &[ + $( + stringify!($name), + )* + ]; + }; +} + +macro_rules! style_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident: $lit:literal, + )* + ) => { + pub const STYLE_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ + $( + (stringify!($name), $lit), + )* + ]; + }; +} + +macro_rules! mapped_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident: $lit:literal, + )* + ) => { + pub const MAPPED_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ + $( + (stringify!($name), $lit), + )* + ("prevent_default", "dioxus-prevent-default"), + ]; + }; +} + +no_namespace_trait_methods! { + accesskey; + + /// The HTML class attribute is used to specify a class for an HTML element. + /// + /// ## Details + /// Multiple HTML elements can share the same class. + /// + /// The class global attribute is a space-separated list of the case-sensitive classes of the element. + /// Classes allow CSS and Javascript to select and access specific elements via the class selectors or + /// functions like the DOM method document.getElementsByClassName. + /// + /// ## Example + /// + /// ### HTML: + /// ```html + ///

Above point sounds a bit obvious. Remove/rewrite?

+ /// ``` + /// + /// ### CSS: + /// ```css + /// .note { + /// font-style: italic; + /// font-weight: bold; + /// } + /// + /// .editorial { + /// background: rgb(255, 0, 0, .25); + /// padding: 10px; + /// } + /// ``` + class; + contenteditable; + data; + dir; + draggable; + hidden; + id; + lang; + spellcheck; + style; + tabindex; + title; + translate; + + role; + + /// dangerous_inner_html is Dioxus's replacement for using innerHTML in the browser DOM. In general, setting + /// HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) + /// attack. So, you can set HTML directly from Dioxus, but you have to type out dangerous_inner_html to remind + /// yourself that it’s dangerous + dangerous_inner_html; +} + +// This macro creates an explicit method call for each of the style attributes. +// +// The left token specifies the name of the attribute in the rsx! macro, and the right string literal specifies the +// actual name of the attribute generated. +// +// This roughly follows the html spec +style_trait_methods! { + align_content: "align-content", + align_items: "align-items", + align_self: "align-self", + alignment_adjust: "alignment-adjust", + alignment_baseline: "alignment-baseline", + all: "all", + alt: "alt", + animation: "animation", + animation_delay: "animation-delay", + animation_direction: "animation-direction", + animation_duration: "animation-duration", + animation_fill_mode: "animation-fill-mode", + animation_iteration_count: "animation-iteration-count", + animation_name: "animation-name", + animation_play_state: "animation-play-state", + animation_timing_function: "animation-timing-function", + azimuth: "azimuth", + backface_visibility: "backface-visibility", + background: "background", + background_attachment: "background-attachment", + background_clip: "background-clip", + background_color: "background-color", + background_image: "background-image", + background_origin: "background-origin", + background_position: "background-position", + background_repeat: "background-repeat", + background_size: "background-size", + background_blend_mode: "background-blend-mode", + baseline_shift: "baseline-shift", + bleed: "bleed", + bookmark_label: "bookmark-label", + bookmark_level: "bookmark-level", + bookmark_state: "bookmark-state", + border: "border", + border_color: "border-color", + border_style: "border-style", + border_width: "border-width", + border_bottom: "border-bottom", + border_bottom_color: "border-bottom-color", + border_bottom_style: "border-bottom-style", + border_bottom_width: "border-bottom-width", + border_left: "border-left", + border_left_color: "border-left-color", + border_left_style: "border-left-style", + border_left_width: "border-left-width", + border_right: "border-right", + border_right_color: "border-right-color", + border_right_style: "border-right-style", + border_right_width: "border-right-width", + border_top: "border-top", + border_top_color: "border-top-color", + border_top_style: "border-top-style", + border_top_width: "border-top-width", + border_collapse: "border-collapse", + border_image: "border-image", + border_image_outset: "border-image-outset", + border_image_repeat: "border-image-repeat", + border_image_slice: "border-image-slice", + border_image_source: "border-image-source", + border_image_width: "border-image-width", + border_radius: "border-radius", + border_bottom_left_radius: "border-bottom-left-radius", + border_bottom_right_radius: "border-bottom-right-radius", + border_top_left_radius: "border-top-left-radius", + border_top_right_radius: "border-top-right-radius", + border_spacing: "border-spacing", + bottom: "bottom", + box_decoration_break: "box-decoration-break", + box_shadow: "box-shadow", + box_sizing: "box-sizing", + box_snap: "box-snap", + break_after: "break-after", + break_before: "break-before", + break_inside: "break-inside", + buffered_rendering: "buffered-rendering", + caption_side: "caption-side", + clear: "clear", + clear_side: "clear-side", + clip: "clip", + clip_path: "clip-path", + clip_rule: "clip-rule", + color: "color", + color_adjust: "color-adjust", + color_correction: "color-correction", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", + column_fill: "column-fill", + column_gap: "column-gap", + column_rule: "column-rule", + column_rule_color: "column-rule-color", + column_rule_style: "column-rule-style", + column_rule_width: "column-rule-width", + column_span: "column-span", + columns: "columns", + column_count: "column-count", + column_width: "column-width", + contain: "contain", + content: "content", + counter_increment: "counter-increment", + counter_reset: "counter-reset", + counter_set: "counter-set", + cue: "cue", + cue_after: "cue-after", + cue_before: "cue-before", + cursor: "cursor", + direction: "direction", + display: "display", + display_inside: "display-inside", + display_outside: "display-outside", + display_extras: "display-extras", + display_box: "display-box", + dominant_baseline: "dominant-baseline", + elevation: "elevation", + empty_cells: "empty-cells", + enable_background: "enable-background", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", + float: "float", + float_defer_column: "float-defer-column", + float_defer_page: "float-defer-page", + float_offset: "float-offset", + float_wrap: "float-wrap", + flow_into: "flow-into", + flow_from: "flow-from", + flex: "flex", + flex_basis: "flex-basis", + flex_grow: "flex-grow", + flex_shrink: "flex-shrink", + flex_flow: "flex-flow", + flex_direction: "flex-direction", + flex_wrap: "flex-wrap", + flood_color: "flood-color", + flood_opacity: "flood-opacity", + font: "font", + font_family: "font-family", + font_size: "font-size", + font_stretch: "font-stretch", + font_style: "font-style", + font_weight: "font-weight", + font_feature_settings: "font-feature-settings", + font_kerning: "font-kerning", + font_language_override: "font-language-override", + font_size_adjust: "font-size-adjust", + font_synthesis: "font-synthesis", + font_variant: "font-variant", + font_variant_alternates: "font-variant-alternates", + font_variant_caps: "font-variant-caps", + font_variant_east_asian: "font-variant-east-asian", + font_variant_ligatures: "font-variant-ligatures", + font_variant_numeric: "font-variant-numeric", + font_variant_position: "font-variant-position", + footnote_policy: "footnote-policy", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + grid: "grid", + grid_auto_flow: "grid-auto-flow", + grid_auto_columns: "grid-auto-columns", + grid_auto_rows: "grid-auto-rows", + grid_template: "grid-template", + grid_template_areas: "grid-template-areas", + grid_template_columns: "grid-template-columns", + grid_template_rows: "grid-template-rows", + grid_area: "grid-area", + grid_column: "grid-column", + grid_column_start: "grid-column-start", + grid_column_end: "grid-column-end", + grid_row: "grid-row", + grid_row_start: "grid-row-start", + grid_row_end: "grid-row-end", + hanging_punctuation: "hanging-punctuation", + height: "height", + hyphenate_character: "hyphenate-character", + hyphenate_limit_chars: "hyphenate-limit-chars", + hyphenate_limit_last: "hyphenate-limit-last", + hyphenate_limit_lines: "hyphenate-limit-lines", + hyphenate_limit_zone: "hyphenate-limit-zone", + hyphens: "hyphens", + icon: "icon", + image_orientation: "image-orientation", + image_resolution: "image-resolution", + image_rendering: "image-rendering", + ime: "ime", + ime_align: "ime-align", + ime_mode: "ime-mode", + ime_offset: "ime-offset", + ime_width: "ime-width", + initial_letters: "initial-letters", + inline_box_align: "inline-box-align", + isolation: "isolation", + justify_content: "justify-content", + justify_items: "justify-items", + justify_self: "justify-self", + kerning: "kerning", + left: "left", + letter_spacing: "letter-spacing", + lighting_color: "lighting-color", + line_box_contain: "line-box-contain", + line_break: "line-break", + line_grid: "line-grid", + line_height: "line-height", + line_slack: "line-slack", + line_snap: "line-snap", + list_style: "list-style", + list_style_image: "list-style-image", + list_style_position: "list-style-position", + list_style_type: "list-style-type", + margin: "margin", + margin_bottom: "margin-bottom", + margin_left: "margin-left", + margin_right: "margin-right", + margin_top: "margin-top", + marker: "marker", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_pattern: "marker-pattern", + marker_segment: "marker-segment", + marker_start: "marker-start", + marker_knockout_left: "marker-knockout-left", + marker_knockout_right: "marker-knockout-right", + marker_side: "marker-side", + marks: "marks", + marquee_direction: "marquee-direction", + marquee_play_count: "marquee-play-count", + marquee_speed: "marquee-speed", + marquee_style: "marquee-style", + mask: "mask", + mask_image: "mask-image", + mask_repeat: "mask-repeat", + mask_position: "mask-position", + mask_clip: "mask-clip", + mask_origin: "mask-origin", + mask_size: "mask-size", + mask_box: "mask-box", + mask_box_outset: "mask-box-outset", + mask_box_repeat: "mask-box-repeat", + mask_box_slice: "mask-box-slice", + mask_box_source: "mask-box-source", + mask_box_width: "mask-box-width", + mask_type: "mask-type", + max_height: "max-height", + max_lines: "max-lines", + max_width: "max-width", + min_height: "min-height", + min_width: "min-width", + mix_blend_mode: "mix-blend-mode", + nav_down: "nav-down", + nav_index: "nav-index", + nav_left: "nav-left", + nav_right: "nav-right", + nav_up: "nav-up", + object_fit: "object-fit", + object_position: "object-position", + offset_after: "offset-after", + offset_before: "offset-before", + offset_end: "offset-end", + offset_start: "offset-start", + opacity: "opacity", + order: "order", + orphans: "orphans", + outline: "outline", + outline_color: "outline-color", + outline_style: "outline-style", + outline_width: "outline-width", + outline_offset: "outline-offset", + overflow: "overflow", + overflow_x: "overflow-x", + overflow_y: "overflow-y", + overflow_style: "overflow-style", + overflow_wrap: "overflow-wrap", + padding: "padding", + padding_bottom: "padding-bottom", + padding_left: "padding-left", + padding_right: "padding-right", + padding_top: "padding-top", + page: "page", + page_break_after: "page-break-after", + page_break_before: "page-break-before", + page_break_inside: "page-break-inside", + paint_order: "paint-order", + pause: "pause", + pause_after: "pause-after", + pause_before: "pause-before", + perspective: "perspective", + perspective_origin: "perspective-origin", + pitch: "pitch", + pitch_range: "pitch-range", + play_during: "play-during", + pointer_events: "pointer-events", + position: "position", + quotes: "quotes", + region_fragment: "region-fragment", + resize: "resize", + rest: "rest", + rest_after: "rest-after", + rest_before: "rest-before", + richness: "richness", + right: "right", + ruby_align: "ruby-align", + ruby_merge: "ruby-merge", + ruby_position: "ruby-position", + scroll_behavior: "scroll-behavior", + scroll_snap_coordinate: "scroll-snap-coordinate", + scroll_snap_destination: "scroll-snap-destination", + scroll_snap_points_x: "scroll-snap-points-x", + scroll_snap_points_y: "scroll-snap-points-y", + scroll_snap_type: "scroll-snap-type", + shape_image_threshold: "shape-image-threshold", + shape_inside: "shape-inside", + shape_margin: "shape-margin", + shape_outside: "shape-outside", + shape_padding: "shape-padding", + shape_rendering: "shape-rendering", + size: "size", + speak: "speak", + speak_as: "speak-as", + speak_header: "speak-header", + speak_numeral: "speak-numeral", + speak_punctuation: "speak-punctuation", + speech_rate: "speech-rate", + stop_color: "stop-color", + stop_opacity: "stop-opacity", + stress: "stress", + string_set: "string-set", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", + tab_size: "tab-size", + table_layout: "table-layout", + text_align: "text-align", + text_align_all: "text-align-all", + text_align_last: "text-align-last", + text_anchor: "text-anchor", + text_combine_upright: "text-combine-upright", + text_decoration: "text-decoration", + text_decoration_color: "text-decoration-color", + text_decoration_line: "text-decoration-line", + text_decoration_style: "text-decoration-style", + text_decoration_skip: "text-decoration-skip", + text_emphasis: "text-emphasis", + text_emphasis_color: "text-emphasis-color", + text_emphasis_style: "text-emphasis-style", + text_emphasis_position: "text-emphasis-position", + text_emphasis_skip: "text-emphasis-skip", + text_height: "text-height", + text_indent: "text-indent", + text_justify: "text-justify", + text_orientation: "text-orientation", + text_overflow: "text-overflow", + text_rendering: "text-rendering", + text_shadow: "text-shadow", + text_size_adjust: "text-size-adjust", + text_space_collapse: "text-space-collapse", + text_spacing: "text-spacing", + text_transform: "text-transform", + text_underline_position: "text-underline-position", + text_wrap: "text-wrap", + top: "top", + touch_action: "touch-action", + transform: "transform", + transform_box: "transform-box", + transform_origin: "transform-origin", + transform_style: "transform-style", + transition: "transition", + transition_delay: "transition-delay", + transition_duration: "transition-duration", + transition_property: "transition-property", + unicode_bidi: "unicode-bidi", + vector_effect: "vector-effect", + vertical_align: "vertical-align", + visibility: "visibility", + voice_balance: "voice-balance", + voice_duration: "voice-duration", + voice_family: "voice-family", + voice_pitch: "voice-pitch", + voice_range: "voice-range", + voice_rate: "voice-rate", + voice_stress: "voice-stress", + voice_volumn: "voice-volumn", + volume: "volume", + white_space: "white-space", + widows: "widows", + width: "width", + will_change: "will-change", + word_break: "word-break", + word_spacing: "word-spacing", + word_wrap: "word-wrap", + wrap_flow: "wrap-flow", + wrap_through: "wrap-through", + writing_mode: "writing-mode", + gap: "gap", + list_styler_type: "list-style-type", + row_gap: "row-gap", + transition_timing_function: "transition-timing-function", + user_select: "user-select", + webkit_user_select: "-webkit-user-select", + z_index : "z-index", +} +mapped_trait_methods! { + aria_current: "aria-current", + aria_details: "aria-details", + aria_disabled: "aria-disabled", + aria_hidden: "aria-hidden", + aria_invalid: "aria-invalid", + aria_keyshortcuts: "aria-keyshortcuts", + aria_label: "aria-label", + aria_roledescription: "aria-roledescription", + // Widget Attributes + aria_autocomplete: "aria-autocomplete", + aria_checked: "aria-checked", + aria_expanded: "aria-expanded", + aria_haspopup: "aria-haspopup", + aria_level: "aria-level", + aria_modal: "aria-modal", + aria_multiline: "aria-multiline", + aria_multiselectable: "aria-multiselectable", + aria_orientation: "aria-orientation", + aria_placeholder: "aria-placeholder", + aria_pressed: "aria-pressed", + aria_readonly: "aria-readonly", + aria_required: "aria-required", + aria_selected: "aria-selected", + aria_sort: "aria-sort", + aria_valuemax: "aria-valuemax", + aria_valuemin: "aria-valuemin", + aria_valuenow: "aria-valuenow", + aria_valuetext: "aria-valuetext", + // Live Region Attributes + aria_atomic: "aria-atomic", + aria_busy: "aria-busy", + aria_live: "aria-live", + aria_relevant: "aria-relevant", + + aria_dropeffect: "aria-dropeffect", + aria_grabbed: "aria-grabbed", + // Relationship Attributes + aria_activedescendant: "aria-activedescendant", + aria_colcount: "aria-colcount", + aria_colindex: "aria-colindex", + aria_colspan: "aria-colspan", + aria_controls: "aria-controls", + aria_describedby: "aria-describedby", + aria_errormessage: "aria-errormessage", + aria_flowto: "aria-flowto", + aria_labelledby: "aria-labelledby", + aria_owns: "aria-owns", + aria_posinset: "aria-posinset", + aria_rowcount: "aria-rowcount", + aria_rowindex: "aria-rowindex", + aria_rowspan: "aria-rowspan", + aria_setsize: "aria-setsize", +} + +pub mod svg { + mapped_trait_methods! { + accent_height: "accent-height", + accumulate: "accumulate", + additive: "additive", + alignment_baseline: "alignment-baseline", + alphabetic: "alphabetic", + amplitude: "amplitude", + arabic_form: "arabic-form", + ascent: "ascent", + attributeName: "attributeName", + attributeType: "attributeType", + azimuth: "azimuth", + baseFrequency: "baseFrequency", + baseline_shift: "baseline-shift", + baseProfile: "baseProfile", + bbox: "bbox", + begin: "begin", + bias: "bias", + by: "by", + calcMode: "calcMode", + cap_height: "cap-height", + class: "class", + clip: "clip", + clipPathUnits: "clipPathUnits", + clip_path: "clip-path", + clip_rule: "clip-rule", + color: "color", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", + contentScriptType: "contentScriptType", + contentStyleType: "contentStyleType", + crossorigin: "crossorigin", + cursor: "cursor", + cx: "cx", + cy: "cy", + d: "d", + decelerate: "decelerate", + descent: "descent", + diffuseConstant: "diffuseConstant", + direction: "direction", + display: "display", + divisor: "divisor", + dominant_baseline: "dominant-baseline", + dur: "dur", + dx: "dx", + dy: "dy", + edgeMode: "edgeMode", + elevation: "elevation", + enable_background: "enable-background", + end: "end", + exponent: "exponent", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", + filterRes: "filterRes", + filterUnits: "filterUnits", + flood_color: "flood-color", + flood_opacity: "flood-opacity", + font_family: "font-family", + font_size: "font-size", + font_size_adjust: "font-size-adjust", + font_stretch: "font-stretch", + font_style: "font-style", + font_variant: "font-variant", + font_weight: "font-weight", + format: "format", + from: "from", + fr: "fr", + fx: "fx", + fy: "fy", + g1: "g1", + g2: "g2", + glyph_name: "glyph-name", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + glyphRef: "glyphRef", + gradientTransform: "gradientTransform", + gradientUnits: "gradientUnits", + hanging: "hanging", + height: "height", + href: "href", + hreflang: "hreflang", + horiz_adv_x: "horiz-adv-x", + horiz_origin_x: "horiz-origin-x", + id: "id", + ideographic: "ideographic", + image_rendering: "image-rendering", + _in: "_in", + in2: "in2", + intercept: "intercept", + k: "k", + k1: "k1", + k2: "k2", + k3: "k3", + k4: "k4", + kernelMatrix: "kernelMatrix", + kernelUnitLength: "kernelUnitLength", + kerning: "kerning", + keyPoints: "keyPoints", + keySplines: "keySplines", + keyTimes: "keyTimes", + lang: "lang", + lengthAdjust: "lengthAdjust", + letter_spacing: "letter-spacing", + lighting_color: "lighting-color", + limitingConeAngle: "limitingConeAngle", + local: "local", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_start: "marker_start", + markerHeight: "markerHeight", + markerUnits: "markerUnits", + markerWidth: "markerWidth", + mask: "mask", + maskContentUnits: "maskContentUnits", + maskUnits: "maskUnits", + mathematical: "mathematical", + max: "max", + media: "media", + method: "method", + min: "min", + mode: "mode", + name: "name", + numOctaves: "numOctaves", + offset: "offset", + opacity: "opacity", + operator: "operator", + order: "order", + orient: "orient", + orientation: "orientation", + origin: "origin", + overflow: "overflow", + overline_position: "overline-position", + overline_thickness: "overline-thickness", + panose_1: "panose-1", + paint_order: "paint-order", + path: "path", + pathLength: "pathLength", + patternContentUnits: "patternContentUnits", + patternTransform: "patternTransform", + patternUnits: "patternUnits", + ping: "ping", + pointer_events: "pointer-events", + points: "points", + pointsAtX: "pointsAtX", + pointsAtY: "pointsAtY", + pointsAtZ: "pointsAtZ", + preserveAlpha: "preserveAlpha", + preserveAspectRatio: "preserveAspectRatio", + primitiveUnits: "primitiveUnits", + r: "r", + radius: "radius", + referrerPolicy: "referrerPolicy", + refX: "refX", + refY: "refY", + rel: "rel", + rendering_intent: "rendering-intent", + repeatCount: "repeatCount", + repeatDur: "repeatDur", + requiredExtensions: "requiredExtensions", + requiredFeatures: "requiredFeatures", + restart: "restart", + result: "result", + role: "role", + rotate: "rotate", + rx: "rx", + ry: "ry", + scale: "scale", + seed: "seed", + shape_rendering: "shape-rendering", + slope: "slope", + spacing: "spacing", + specularConstant: "specularConstant", + specularExponent: "specularExponent", + speed: "speed", + spreadMethod: "spreadMethod", + startOffset: "startOffset", + stdDeviation: "stdDeviation", + stemh: "stemh", + stemv: "stemv", + stitchTiles: "stitchTiles", + stop_color: "stop_color", + stop_opacity: "stop_opacity", + strikethrough_position: "strikethrough-position", + strikethrough_thickness: "strikethrough-thickness", + string: "string", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", + style: "style", + surfaceScale: "surfaceScale", + systemLanguage: "systemLanguage", + tabindex: "tabindex", + tableValues: "tableValues", + target: "target", + targetX: "targetX", + targetY: "targetY", + text_anchor: "text-anchor", + text_decoration: "text-decoration", + text_rendering: "text-rendering", + textLength: "textLength", + to: "to", + transform: "transform", + transform_origin: "transform-origin", + r#type: "_type", + u1: "u1", + u2: "u2", + underline_position: "underline-position", + underline_thickness: "underline-thickness", + unicode: "unicode", + unicode_bidi: "unicode-bidi", + unicode_range: "unicode-range", + units_per_em: "units-per-em", + v_alphabetic: "v-alphabetic", + v_hanging: "v-hanging", + v_ideographic: "v-ideographic", + v_mathematical: "v-mathematical", + values: "values", + vector_effect: "vector-effect", + version: "version", + vert_adv_y: "vert-adv-y", + vert_origin_x: "vert-origin-x", + vert_origin_y: "vert-origin-y", + view_box: "viewBox", + view_target: "viewTarget", + visibility: "visibility", + width: "width", + widths: "widths", + word_spacing: "word-spacing", + writing_mode: "writing-mode", + x: "x", + x_height: "x-height", + x1: "x1", + x2: "x2", + xmlns: "xmlns", + x_channel_selector: "xChannelSelector", + y: "y", + y1: "y1", + y2: "y2", + y_channel_selector: "yChannelSelector", + z: "z", + zoomAndPan: "zoomAndPan", + } +} diff --git a/packages/rsx_interpreter/src/captuered_context.rs b/packages/rsx_interpreter/src/captuered_context.rs new file mode 100644 index 000000000..8473771e9 --- /dev/null +++ b/packages/rsx_interpreter/src/captuered_context.rs @@ -0,0 +1,176 @@ +use dioxus_core::{Listener, VNode}; +use dioxus_rsx::{ + BodyNode, CallBody, Component, ElementAttr, ElementAttrNamed, IfmtInput, Segment, +}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{Expr, Ident, Result}; + +use crate::CodeLocation; +#[derive(Default)] +pub struct CapturedContextBuilder { + pub ifmted: Vec, + pub components: Vec, + pub iterators: Vec, + pub captured_expressions: Vec, + pub listeners: Vec, + pub custom_context: Option, +} + +impl CapturedContextBuilder { + pub fn extend(&mut self, other: CapturedContextBuilder) { + self.ifmted.extend(other.ifmted); + self.components.extend(other.components); + self.iterators.extend(other.iterators); + self.listeners.extend(other.listeners); + self.captured_expressions.extend(other.captured_expressions); + } + + pub fn from_call_body(body: CallBody) -> Result { + let mut new = Self { + custom_context: body.custom_context, + ..Default::default() + }; + for node in body.roots { + new.extend(Self::find_captured(node)?); + } + Ok(new) + } + + fn find_captured(node: BodyNode) -> Result { + let mut captured = CapturedContextBuilder::default(); + match node { + BodyNode::Element(el) => { + for attr in el.attributes { + match attr.attr { + ElementAttr::AttrText { value, .. } + | ElementAttr::CustomAttrText { value, .. } => { + let value_tokens = value.to_token_stream(); + let formated: IfmtInput = syn::parse2(value_tokens)?; + captured.ifmted.push(formated); + } + ElementAttr::AttrExpression { name: _, value } => { + captured.captured_expressions.push(value); + } + ElementAttr::CustomAttrExpression { name: _, value } => { + captured.captured_expressions.push(value); + } + ElementAttr::EventTokens { .. } => captured.listeners.push(attr), + } + } + + if let Some(key) = el.key { + let value_tokens = key.to_token_stream(); + let formated: IfmtInput = syn::parse2(value_tokens)?; + captured.ifmted.push(formated); + } + + for child in el.children { + captured.extend(Self::find_captured(child)?); + } + } + BodyNode::Component(comp) => { + captured.components.push(comp); + } + BodyNode::Text(t) => { + let tokens = t.to_token_stream(); + let formated: IfmtInput = syn::parse2(tokens).unwrap(); + captured.ifmted.push(formated); + } + BodyNode::RawExpr(_) => captured.iterators.push(node), + } + Ok(captured) + } +} + +impl ToTokens for CapturedContextBuilder { + fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { + let CapturedContextBuilder { + ifmted, + components, + iterators, + captured_expressions, + listeners, + custom_context: _, + } = self; + let listeners_str = listeners + .iter() + .map(|comp| comp.to_token_stream().to_string()); + let compontents_str = components + .iter() + .map(|comp| comp.to_token_stream().to_string()); + let iterators_str = iterators.iter().map(|node| match node { + BodyNode::RawExpr(expr) => expr.to_token_stream().to_string(), + _ => unreachable!(), + }); + let captured: Vec<_> = ifmted + .iter() + .flat_map(|input| input.segments.iter()) + .filter_map(|seg| match seg { + Segment::Formatted { + format_args, + segment, + } => { + let expr = segment.to_token_stream(); + let as_string = expr.to_string(); + let format_expr = if format_args.is_empty() { + "{".to_string() + format_args + "}" + } else { + "{".to_string() + ":" + format_args + "}" + }; + Some(quote! { + FormattedArg{ + expr: #as_string, + format_args: #format_args, + result: format!(#format_expr, #expr) + } + }) + } + _ => None, + }) + .collect(); + let captured_attr_expressions_text = captured_expressions + .iter() + .map(|e| format!("{}", e.to_token_stream())); + tokens.append_all(quote! { + CapturedContext { + captured: IfmtArgs{ + named_args: vec![#(#captured),*] + }, + components: vec![#((#compontents_str, #components)),*], + iterators: vec![#((#iterators_str, #iterators)),*], + expressions: vec![#((#captured_attr_expressions_text, #captured_expressions.to_string())),*], + listeners: vec![#((#listeners_str, #listeners)),*], + location: code_location.clone() + } + }) + } +} + +pub struct CapturedContext<'a> { + // map of the variable name to the formated value + pub captured: IfmtArgs, + // map of the attribute name and element path to the formated value + // pub captured_attribute_values: IfmtArgs, + // the only thing we can update in component is the children + pub components: Vec<(&'static str, VNode<'a>)>, + // we can't reasonably interpert iterators, so they are staticly inserted + pub iterators: Vec<(&'static str, VNode<'a>)>, + // map expression to the value resulting from the expression + pub expressions: Vec<(&'static str, String)>, + // map listener code to the resulting listener + pub listeners: Vec<(&'static str, Listener<'a>)>, + // used to provide better error messages + pub location: CodeLocation, +} + +pub struct IfmtArgs { + // All expressions that have been resolved + pub named_args: Vec, +} + +/// A formated segment that has been resolved +pub struct FormattedArg { + pub expr: &'static str, + pub format_args: &'static str, + pub result: String, +} diff --git a/packages/rsx_interpreter/src/elements.rs b/packages/rsx_interpreter/src/elements.rs new file mode 100644 index 000000000..0668f57ff --- /dev/null +++ b/packages/rsx_interpreter/src/elements.rs @@ -0,0 +1,1377 @@ +// map the rsx name of the element to the html name of the element and the namespace that contains it +pub fn element_to_static_str(element: &str) -> Option<(&'static str, Option<&'static str>)> { + ELEMENTS_WITH_MAPPED_ATTRIBUTES + .iter() + .find(|(el, _)| *el == element) + .map(|(el, _)| (*el, None)) + .or_else(|| { + ELEMENTS_WITH_NAMESPACE + .iter() + .find(|(el, _, _)| *el == element) + .map(|(el, ns, _)| (*el, Some(*ns))) + }) + .or_else(|| { + ELEMENTS_WITHOUT_NAMESPACE + .iter() + .find(|(el, _)| *el == element) + .map(|(el, _)| (*el, None)) + }) +} + +macro_rules! builder_constructors { + ( + $( + $(#[$attr:meta])* + $name:ident { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident, + )* + }; + )* + ) => { + pub const ELEMENTS_WITHOUT_NAMESPACE: &'static [(&'static str, &'static [&'static str])] = &[ + $( + ( + stringify!($name), + &[ + $( + stringify!($fil), + )* + ] + ), + )* + ]; + }; + + ( $( + $(#[$attr:meta])* + $name:ident <> $namespace:tt { + $($fil:ident: $vil:ident,)* + }; + )* ) => { + pub const ELEMENTS_WITH_NAMESPACE: &'static [(&'static str, &'static str, &'static [&'static str])] = &[ + $( + ( + stringify!($name), + stringify!($namespace), + &[ + $( + stringify!($fil), + )* + ] + ), + )* + ]; + }; +} +pub const ELEMENTS_WITH_MAPPED_ATTRIBUTES: &[(&str, &[(&str, &str)])] = &[ + ("script", &[("r#type", "type"), ("r#script", "script")]), + ("button", &[("r#type", "type")]), + ("select", &[("value", "value")]), + ("option", &[("selected", "selected")]), + ("textarea", &[("value", "value")]), + ("label", &[("r#for", "for")]), + ("input", &[("r#type", "type"), ("value", "value")]), +]; + +// Organized in the same order as +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +// +// Does not include obsolete elements. +// +// This namespace represents a collection of modern HTML-5 compatiable elements. +// +// This list does not include obsolete, deprecated, experimental, or poorly supported elements. +builder_constructors! { + // Document metadata + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) + /// element. + /// + base { + href: Uri, + target: Target, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) + /// element. + head {}; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) + /// element. + link { + // as: Mime, + crossorigin: CrossOrigin, + href: Uri, + hreflang: LanguageTag, + media: String, // FIXME media query + rel: LinkType, + sizes: String, // FIXME + title: String, // FIXME + r#type: Mime, + integrity: String, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) + /// element. + meta { + charset: String, // FIXME IANA standard names + content: String, + http_equiv: HTTPEquiv, + name: Metadata, + }; + + /// Build a + /// [`