Merge pull request #425 from Demonthos/jk/rsx-refactor

Hot Reloading Rsx
This commit is contained in:
Jon Kelley authored and GitHub committed 2022-06-21 11:07:44 -04:00
commit 478fea58d9
30 files changed
+3768 -285

No files matched your search

+9 -3
View File
@@ -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
+1
View File
@@ -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)
+24
View File
@@ -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.
+8 -3
View File
@@ -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"]
-228
View File
@@ -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<TokenStream> {
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<Segment> = {
impl Parse for Segment {
fn parse(input: ParseStream<'_>) -> Result<Self> {
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::<Segment, Token![.]>::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<TokenStream, Token![.]> = 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<Expr>,
named_args: Vec<(Ident, Expr)>,
}
impl Parse for IfmtInput {
fn parse(input: ParseStream) -> Result<Self> {
let format_literal = input.parse()?;
let mut positional_args = vec![];
loop {
if input.parse::<Option<Token![,]>>()?.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<Target = T> + 'a
where
Drop: FnOnce(T),
{
use ::core::mem::ManuallyDrop;
struct Ret<T, Drop>(ManuallyDrop<T>, ManuallyDrop<Drop>)
where
Drop: FnOnce(T);
impl<T, Drop> ::core::ops::Drop for Ret<T, Drop>
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<T, Drop> ::core::ops::Deref for Ret<T, Drop>
where
Drop: FnOnce(T),
{
type Target = T;
#[inline]
fn deref(&'_ self) -> &'_ Self::Target {
&self.0
}
}
impl<T, Drop> ::core::ops::DerefMut for Ret<T, Drop>
where
Drop: FnOnce(T),
{
#[inline]
fn deref_mut(&'_ mut self) -> &'_ mut Self::Target {
&mut self.0
}
}
Ret(ManuallyDrop::new(x), ManuallyDrop::new(drop))
}
+37 -5
View File
@@ -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::<rsx::CallBody>(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()
}
}
}
-31
View File
@@ -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")
};
}
-1
View File
@@ -1 +0,0 @@
//! pretty printer for rsx!
+1 -2
View File
@@ -48,5 +48,4 @@ backtrace = "0.3"
[features]
default = []
serialize = ["serde"]
debug_vdom = []
debug_vdom = []
+8
View File
@@ -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);
}
}
}
}
+4 -1
View File
@@ -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" }
+88
View File
@@ -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<LocalSocketStream>,
) -> Option<LocalSocketStream> {
connection
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
.ok()
}
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_in_connection_handle = latest_in_connection.clone();
let latest_out_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_out_connection_handle = latest_out_connection.clone();
struct DesktopErrorHandler {
latest_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>>,
}
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
+12
View File
@@ -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" }
@@ -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<AngleBracketedGenericArguments>,
@@ -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),
@@ -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<LitStr>,
@@ -64,7 +65,7 @@ impl Parse for Element {
}
if content.parse::<Token![,]>().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::<Token![,]>().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,
+26
View File
@@ -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"));
};
}
+176
View File
@@ -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<TokenStream> {
// 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<Segment>,
}
impl FromStr for IfmtInput {
type Err = syn::Error;
fn from_str(input: &str) -> Result<Self> {
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(&current_captured)?,
});
break;
}
current_format_args.push(c);
}
break;
}
if c == '}' {
segments.push(Segment::Formatted {
format_args: String::new(),
segment: FormattedSegment::parse(&current_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<Expr>),
Ident(Ident),
}
impl FormattedSegment {
fn parse(input: &str) -> Result<Self> {
if let Ok(ident) = parse_str::<Ident>(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<Self> {
let input: LitStr = input.parse()?;
let input_str = input.value();
IfmtInput::from_str(&input_str)
}
}
@@ -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
@@ -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<Self> {
if stream.peek(LitStr) {
+23
View File
@@ -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"] }
+869
View File
@@ -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
/// <p class="note editorial">Above point sounds a bit obvious. Remove/rewrite?</p>
/// ```
///
/// ### 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",
}
}
@@ -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<IfmtInput>,
pub components: Vec<Component>,
pub iterators: Vec<BodyNode>,
pub captured_expressions: Vec<Expr>,
pub listeners: Vec<ElementAttrNamed>,
pub custom_context: Option<Ident>,
}
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<Self> {
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<Self> {
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<FormattedArg>,
}
/// A formated segment that has been resolved
pub struct FormattedArg {
pub expr: &'static str,
pub format_args: &'static str,
pub result: String,
}
File diff suppressed because it is too large. Load diff
+39
View File
@@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
use crate::CodeLocation;
/// An error produced when interperting the rsx
#[derive(Debug, Serialize, Deserialize)]
pub enum Error {
ParseError(ParseError),
RecompileRequiredError(RecompileReason),
}
#[derive(Debug, Serialize, Deserialize)]
pub enum RecompileReason {
CapturedVariable(String),
CapturedExpression(String),
CapturedComponent(String),
CapturedListener(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ParseError {
pub message: String,
pub location: CodeLocation,
}
impl ParseError {
pub fn new(error: syn::Error, mut location: CodeLocation) -> Self {
let message = error.to_string();
let syn_call_site = error.span().start();
location.line += syn_call_site.line as u32;
if syn_call_site.line == 0 {
location.column += syn_call_site.column as u32;
} else {
location.column = syn_call_site.column as u32;
}
location.column += 1;
ParseError { message, location }
}
}
+251
View File
@@ -0,0 +1,251 @@
use dioxus_core::{Attribute, AttributeValue, NodeFactory, VNode};
use dioxus_rsx::{BodyNode, CallBody, ElementAttr, IfmtInput, Segment};
use quote::ToTokens;
use quote::__private::Span;
use std::str::FromStr;
use syn::{parse2, parse_str, Expr};
use crate::attributes::attrbute_to_static_str;
use crate::captuered_context::{CapturedContext, IfmtArgs};
use crate::elements::element_to_static_str;
use crate::error::{Error, ParseError, RecompileReason};
fn resolve_ifmt(ifmt: &IfmtInput, captured: &IfmtArgs) -> Result<String, Error> {
let mut result = String::new();
for seg in &ifmt.segments {
match seg {
Segment::Formatted {
segment,
format_args,
} => {
let expr = segment.to_token_stream();
let expr: Expr = parse2(expr).unwrap();
let search = captured.named_args.iter().find(|fmted| {
parse_str::<Expr>(fmted.expr).unwrap() == expr
&& fmted.format_args == format_args
});
match search {
Some(formatted) => {
result.push_str(&formatted.result);
}
None => {
let expr_str = segment.to_token_stream().to_string();
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedExpression(format!(
"could not resolve {{{}:{}}}",
expr_str, format_args
)),
));
}
}
}
Segment::Literal(lit) => result.push_str(lit),
}
}
Ok(result)
}
pub fn build<'a>(
rsx: CallBody,
mut ctx: CapturedContext<'a>,
factory: &NodeFactory<'a>,
) -> Result<VNode<'a>, Error> {
let children_built = factory.bump().alloc(Vec::new());
for child in rsx.roots {
children_built.push(build_node(child, &mut ctx, factory)?);
}
if children_built.len() == 1 {
Ok(children_built.pop().unwrap())
} else {
Ok(factory.fragment_from_iter(children_built.iter()))
}
}
fn build_node<'a>(
node: BodyNode,
ctx: &mut CapturedContext<'a>,
factory: &NodeFactory<'a>,
) -> Result<VNode<'a>, Error> {
let bump = factory.bump();
match node {
BodyNode::Text(text) => {
let ifmt = IfmtInput::from_str(&text.value())
.map_err(|err| Error::ParseError(ParseError::new(err, ctx.location.clone())))?;
let text = bump.alloc(resolve_ifmt(&ifmt, &ctx.captured)?);
Ok(factory.text(format_args!("{}", text)))
}
BodyNode::Element(el) => {
let attributes: &mut Vec<Attribute> = bump.alloc(Vec::new());
for attr in &el.attributes {
match &attr.attr {
ElementAttr::AttrText { .. } | ElementAttr::CustomAttrText { .. } => {
let (name, value, span): (String, IfmtInput, Span) = match &attr.attr {
ElementAttr::AttrText { name, value } => (
name.to_string(),
IfmtInput::from_str(&value.value()).map_err(|err| {
Error::ParseError(ParseError::new(err, ctx.location.clone()))
})?,
name.span(),
),
ElementAttr::CustomAttrText { name, value } => (
name.value(),
IfmtInput::from_str(&value.value()).map_err(|err| {
Error::ParseError(ParseError::new(err, ctx.location.clone()))
})?,
name.span(),
),
_ => unreachable!(),
};
if let Some((name, namespace)) = attrbute_to_static_str(&name) {
let value = bump.alloc(resolve_ifmt(&value, &ctx.captured)?);
attributes.push(Attribute {
name,
value: AttributeValue::Text(value),
is_static: true,
is_volatile: false,
namespace,
});
} else {
return Err(Error::ParseError(ParseError::new(
syn::Error::new(span, format!("unknown attribute: {}", name)),
ctx.location.clone(),
)));
}
}
ElementAttr::AttrExpression { .. }
| ElementAttr::CustomAttrExpression { .. } => {
let (name, value) = match &attr.attr {
ElementAttr::AttrExpression { name, value } => {
(name.to_string(), value)
}
ElementAttr::CustomAttrExpression { name, value } => {
(name.value(), value)
}
_ => unreachable!(),
};
if let Some((_, resulting_value)) = ctx
.expressions
.iter()
.find(|(n, _)| parse_str::<Expr>(*n).unwrap() == *value)
{
if let Some((name, namespace)) = attrbute_to_static_str(&name) {
let value = bump.alloc(resulting_value.clone());
attributes.push(Attribute {
name,
value: AttributeValue::Text(value),
is_static: true,
is_volatile: false,
namespace,
});
}
} else {
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedExpression(
value.into_token_stream().to_string(),
),
));
}
}
_ => (),
};
}
let children = bump.alloc(Vec::new());
for child in el.children {
let node = build_node(child, ctx, factory)?;
children.push(node);
}
let listeners = bump.alloc(Vec::new());
for attr in el.attributes {
if let ElementAttr::EventTokens { .. } = attr.attr {
let expr: Expr = parse2(attr.to_token_stream()).map_err(|err| {
Error::ParseError(ParseError::new(err, ctx.location.clone()))
})?;
if let Some(idx) = ctx.listeners.iter().position(|(code, _)| {
if let Ok(parsed) = parse_str::<Expr>(*code) {
parsed == expr
} else {
false
}
}) {
let (_, listener) = ctx.listeners.remove(idx);
listeners.push(listener)
} else {
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedListener(expr.to_token_stream().to_string()),
));
}
}
}
let tag = bump.alloc(el.name.to_string());
if let Some((tag, ns)) = element_to_static_str(tag) {
match el.key {
None => Ok(factory.raw_element(
tag,
ns,
listeners,
attributes.as_slice(),
children.as_slice(),
None,
)),
Some(lit) => {
let ifmt: IfmtInput = parse_str(&lit.value()).map_err(|err| {
Error::ParseError(ParseError::new(err, ctx.location.clone()))
})?;
let key = bump.alloc(resolve_ifmt(&ifmt, &ctx.captured)?);
Ok(factory.raw_element(
tag,
ns,
listeners,
attributes.as_slice(),
children.as_slice(),
Some(format_args!("{}", key)),
))
}
}
} else {
Err(Error::ParseError(ParseError::new(
syn::Error::new(el.name.span(), format!("unknown element: {}", tag)),
ctx.location.clone(),
)))
}
}
BodyNode::Component(comp) => {
let expr: Expr = parse2(comp.to_token_stream())
.map_err(|err| Error::ParseError(ParseError::new(err, ctx.location.clone())))?;
if let Some(idx) = ctx.components.iter().position(|(code, _)| {
if let Ok(parsed) = parse_str::<Expr>(*code) {
parsed == expr
} else {
false
}
}) {
let (_, vnode) = ctx.components.remove(idx);
Ok(vnode)
} else {
Err(Error::RecompileRequiredError(
RecompileReason::CapturedComponent(comp.name.to_token_stream().to_string()),
))
}
}
BodyNode::RawExpr(iterator) => {
if let Some(idx) = ctx.iterators.iter().position(|(code, _)| {
if let Ok(parsed) = parse_str::<Expr>(*code) {
parsed == iterator
} else {
false
}
}) {
let (_, vnode) = ctx.iterators.remove(idx);
Ok(vnode)
} else {
Err(Error::RecompileRequiredError(
RecompileReason::CapturedExpression(iterator.to_token_stream().to_string()),
))
}
}
}
}
+182
View File
@@ -0,0 +1,182 @@
use captuered_context::CapturedContext;
use dioxus_core::{NodeFactory, SchedulerMsg, VNode};
use dioxus_hooks::UnboundedSender;
use error::{Error, ParseError};
use interperter::build;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{RwLock, RwLockReadGuard};
use syn::parse_str;
mod attributes;
pub mod captuered_context;
mod elements;
pub mod error;
mod interperter;
lazy_static! {
/// This a a global store of the current rsx text for each call to rsx
// Global mutable data is genrally not great, but it allows users to not worry about passing down the text RsxContex every time they switch to hot reloading.
pub static ref RSX_CONTEXT: RsxContext = RsxContext::new(RsxData::default());
}
// the location of the code relative to the current crate based on [std::panic::Location]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
/// Get the resolved rsx given the origional rsx, a captured context of dynamic components, and a factory to build the resulting node
#[cfg_attr(
not(debug_assertions),
deprecated(
note = "The hot reload feature is enabled in release mode. This feature should be disabled for production builds."
)
)]
pub fn resolve_scope<'a>(
location: CodeLocation,
rsx: &'static str,
captured: CapturedContext<'a>,
factory: NodeFactory<'a>,
) -> VNode<'a> {
let rsx_text_index = &*RSX_CONTEXT;
// only the insert the rsx text once
if !rsx_text_index.read().hm.contains_key(&location) {
rsx_text_index.insert(location.clone(), rsx.to_string());
}
if let Some(text) = {
let read = rsx_text_index.read();
// clone prevents deadlock on nested rsx calls
read.hm.get(&location).cloned()
} {
match interpert_rsx(factory, &text, captured) {
Ok(vnode) => vnode,
Err(err) => {
rsx_text_index.report_error(err);
factory.text(format_args!(""))
}
}
} else {
panic!("rsx: line number {:?} not found in rsx index", location);
}
}
fn interpert_rsx<'a, 'b>(
factory: dioxus_core::NodeFactory<'a>,
text: &str,
context: captuered_context::CapturedContext<'a>,
) -> Result<VNode<'a>, Error> {
build(
parse_str(text)
.map_err(|err| Error::ParseError(ParseError::new(err, context.location.clone())))?,
context,
&factory,
)
}
/// get the code location of the code that called this function
#[macro_export]
macro_rules! get_line_num {
() => {{
let line = line!();
let column = column!();
let file = file!();
#[cfg(windows)]
let file = env!("CARGO_MANIFEST_DIR").to_string() + "\\" + file!();
#[cfg(unix)]
let file = env!("CARGO_MANIFEST_DIR").to_string() + "/" + file!();
CodeLocation {
file: file.to_string(),
line: line,
column: column,
}
}};
}
/// A handle to the rsx context with interior mutability
#[derive(Debug)]
pub struct RsxContext {
data: RwLock<RsxData>,
}
/// A store of the text for the rsx macro for each call to rsx
#[derive(Default)]
pub struct RsxData {
pub hm: HashMap<CodeLocation, String>,
pub error_handler: Option<Box<dyn ErrorHandler>>,
pub scheduler_channel: Option<UnboundedSender<SchedulerMsg>>,
}
impl std::fmt::Debug for RsxData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RsxData").field("hm", &self.hm).finish()
}
}
impl RsxContext {
pub fn new(data: RsxData) -> Self {
Self {
data: RwLock::new(data),
}
}
/// Set the text for an rsx call at some location
pub fn insert(&self, loc: CodeLocation, text: String) {
let mut write = self.data.write().unwrap();
write.hm.insert(loc, text);
if let Some(channel) = &mut write.scheduler_channel {
channel.unbounded_send(SchedulerMsg::DirtyAll).unwrap()
}
}
/// Set the text for many rsx calls
pub fn extend(&self, msg: SetManyRsxMessage) {
let mut write = self.data.write().unwrap();
for rsx in msg.0 {
write.hm.insert(rsx.location, rsx.new_text);
}
if let Some(channel) = &mut write.scheduler_channel {
channel.unbounded_send(SchedulerMsg::DirtyAll).unwrap()
}
}
fn read(&self) -> RwLockReadGuard<RsxData> {
self.data.read().unwrap()
}
fn report_error(&self, error: Error) {
if let Some(handler) = &self.data.write().unwrap().error_handler {
handler.handle_error(error)
}
}
/// Set the handler for errors interperting the rsx
pub fn set_error_handler(&self, handler: impl ErrorHandler + 'static) {
self.data.write().unwrap().error_handler = Some(Box::new(handler));
}
/// Provide the scduler channel from [dioxus_code::VirtualDom::get_scheduler_channel].
/// The channel allows the interpreter to force re-rendering of the dom when the rsx is changed.
pub fn provide_scheduler_channel(&self, channel: UnboundedSender<SchedulerMsg>) {
self.data.write().unwrap().scheduler_channel = Some(channel)
}
}
/// A error handler for errors reported by the rsx interperter
pub trait ErrorHandler: Send + Sync {
fn handle_error(&self, err: Error);
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SetRsxMessage {
pub location: CodeLocation,
pub new_text: String,
}
/// Set many rsx texts at once to avoid duplicate errors
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SetManyRsxMessage(pub Vec<SetRsxMessage>);
+353
View File
@@ -0,0 +1,353 @@
use dioxus_core::prelude::*;
use dioxus_core_macro::*;
use dioxus_html as dioxus_elements;
use dioxus_rsx_interpreter::{
captuered_context::{CapturedContext, FormattedArg, IfmtArgs},
CodeLocation,
};
#[test]
#[allow(non_snake_case)]
fn render_basic() {
fn Base(cx: Scope) -> Element {
rsx!(cx, div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx!(div{"hello world"});
let location = CodeLocation {
file: String::new(),
line: 0,
column: 0,
};
let empty_context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
};
let interperted_vnodes = LazyNodes::new(|factory| {
dioxus_rsx_interpreter::resolve_scope(
location,
"div{\"hello world\"}",
empty_context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_nested() {
fn Base(cx: Scope) -> Element {
rsx!(cx, div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
p { "hello world" }
div {
p { "hello world" }
}
}
};
let location = CodeLocation {
file: String::new(),
line: 1,
column: 0,
};
let empty_context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
};
let interperted_vnodes = LazyNodes::new(|factory| {
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
p { "hello world" }
div {
p { "hello world" }
}
}"#,
empty_context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_component() {
fn Comp(cx: Scope) -> Element {
rsx!(cx, div {})
}
fn Base(cx: Scope) -> Element {
rsx!(cx, div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
Comp {}
}
};
let location = CodeLocation {
file: String::new(),
line: 2,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: vec![(
r#"__cx.component(Comp, fc_to_builder(Comp).build(), None, "Comp")"#,
factory.component(Comp, (), None, "Comp"),
)],
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
Comp {}
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_iterator() {
fn Base(cx: Scope) -> Element {
rsx!(cx, div {})
}
let dom = VirtualDom::new(Base);
let iter = (0..10).map(|i| dom.render_vnodes(rsx! {"{i}"}));
let static_vnodes = rsx! {
div {
iter
}
};
let location = CodeLocation {
file: String::new(),
line: 3,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: vec![(
r#"
(0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))"#,
factory.fragment_from_iter((0..10).map(|i| factory.text(format_args!("{i}")))),
)],
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
(0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_captured_variable() {
fn Base(cx: Scope) -> Element {
rsx!(cx, div {})
}
let dom = VirtualDom::new(Base);
let x = 10;
let static_vnodes = rsx! {
div {
"{x}"
}
};
let location = CodeLocation {
file: String::new(),
line: 4,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let context = CapturedContext {
captured: IfmtArgs {
named_args: vec![FormattedArg {
expr: "x",
format_args: "",
result: x.to_string(),
}],
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
"{x}"
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_listener() {
fn Base(cx: Scope) -> Element {
rsx!(cx, div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
onclick: |_| println!("clicked")
}
};
let location = CodeLocation {
file: String::new(),
line: 5,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let f = |_| println!("clicked");
let f = factory.bump().alloc(f);
let context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: vec![(
r#"dioxus_elements::on::onclick(__cx, |_| println!("clicked"))"#,
dioxus_elements::on::onclick(factory, f),
)],
location: location.clone(),
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
onclick: |_| println!("clicked")
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
fn check_eq<'a>(a: &'a VNode<'a>, b: &'a VNode<'a>) -> bool {
match (a, b) {
(VNode::Text(t_a), VNode::Text(t_b)) => t_a.text == t_b.text,
(VNode::Element(e_a), VNode::Element(e_b)) => {
e_a.attributes
.iter()
.zip(e_b.attributes.iter())
.all(|(a, b)| {
a.is_static == b.is_static
&& a.is_volatile == b.is_volatile
&& a.name == b.name
&& a.value == b.value
&& a.namespace == b.namespace
})
&& e_a
.children
.iter()
.zip(e_b.children.iter())
.all(|(a, b)| check_eq(a, b))
&& e_a.key == e_b.key
&& e_a.tag == e_b.tag
&& e_a.namespace == e_b.namespace
&& e_a
.listeners
.iter()
.zip(e_b.listeners.iter())
.all(|(a, b)| a.event == b.event)
}
(VNode::Fragment(f_a), VNode::Fragment(f_b)) => {
f_a.key == f_b.key
&& f_a
.children
.iter()
.zip(f_b.children.iter())
.all(|(a, b)| check_eq(a, b))
}
(VNode::Component(c_a), VNode::Component(c_b)) => {
c_a.can_memoize == c_b.can_memoize
&& c_a.key == c_b.key
&& c_a.fn_name == c_b.fn_name
&& c_a.user_fc == c_b.user_fc
}
(VNode::Placeholder(_), VNode::Placeholder(_)) => true,
_ => false,
}
}
+3
View File
@@ -16,6 +16,7 @@ dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
"web"
] }
dioxus-rsx-interpreter = { path = "../rsx_interpreter", version = "*", optional = true }
js-sys = "0.3.56"
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
@@ -30,6 +31,7 @@ futures-util = "0.3.19"
smallstr = "0.2.0"
serde-wasm-bindgen = "0.4.2"
futures-channel = "0.3.21"
serde_json = { version = "1.0", optional = true }
[dependencies.web-sys]
version = "0.3.56"
@@ -75,6 +77,7 @@ features = [
[features]
default = ["panic_hook"]
panic_hook = ["console_error_panic_hook"]
hot_reload = ["dioxus-rsx-interpreter", "web-sys/WebSocket", "web-sys/Location", "web-sys/MessageEvent", "web-sys/console", "serde_json"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
+64
View File
@@ -217,6 +217,70 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
let mut work_loop = ric_raf::RafLoop::new();
#[cfg(feature = "hot_reload")]
{
use dioxus_rsx_interpreter::error::Error;
use dioxus_rsx_interpreter::{ErrorHandler, SetManyRsxMessage, RSX_CONTEXT};
use futures_channel::mpsc::unbounded;
use futures_channel::mpsc::UnboundedSender;
use futures_util::StreamExt;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use web_sys::{MessageEvent, WebSocket};
let window = web_sys::window().unwrap();
let protocol = if window.location().protocol().unwrap() == "https:" {
"wss:"
} else {
"ws:"
};
let url = format!(
"{protocol}//{}/_dioxus/hot_reload",
window.location().host().unwrap()
);
let ws = WebSocket::new(&url).unwrap();
// change the rsx when new data is received
let cl = Closure::wrap(Box::new(|e: MessageEvent| {
if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
let msgs: SetManyRsxMessage = serde_json::from_str(&format!("{text}")).unwrap();
RSX_CONTEXT.extend(msgs);
}
}) as Box<dyn FnMut(MessageEvent)>);
ws.set_onmessage(Some(cl.as_ref().unchecked_ref()));
cl.forget();
let (error_channel_sender, mut error_channel_receiver) = unbounded();
struct WebErrorHandler {
sender: UnboundedSender<Error>,
}
impl ErrorHandler for WebErrorHandler {
fn handle_error(&self, err: dioxus_rsx_interpreter::error::Error) {
self.sender.unbounded_send(err).unwrap();
}
}
RSX_CONTEXT.set_error_handler(WebErrorHandler {
sender: error_channel_sender,
});
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
// forward stream to the websocket
dom.base_scope().spawn_forever(async move {
while let Some(err) = error_channel_receiver.next().await {
ws.send_with_str(serde_json::to_string(&err).unwrap().as_str())
.unwrap();
}
});
}
loop {
log::trace!("waiting for work");
// if virtualdom has nothing, wait for it to have something before requesting idle time
+11
View File
@@ -44,6 +44,11 @@ pub mod events {
pub use dioxus_html::{on::*, KeyCode};
}
pub use dioxus_rsx as rsx;
#[cfg(feature = "hot_reload")]
pub use dioxus_rsx_interpreter as rsx_interpreter;
pub mod prelude {
pub use crate::hooks::*;
pub use dioxus_core::prelude::*;
@@ -56,4 +61,10 @@ pub mod prelude {
#[cfg(feature = "fermi")]
pub use fermi::{use_atom_ref, use_init_atom_root, use_read, use_set, Atom, AtomRef};
#[cfg(feature = "hot_reload")]
pub use dioxus_rsx_interpreter::{
captuered_context::{CapturedContext, FormattedArg, IfmtArgs},
get_line_num, resolve_scope, CodeLocation, RsxContext,
};
}