Merge pull request #425 from Demonthos/jk/rsx-refactor
Hot Reloading Rsx
This commit is contained in:
30 files changed
+3768
-285
No files matched your search
+9
-3
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
//! pretty printer for rsx!
|
||||
@@ -48,5 +48,4 @@ backtrace = "0.3"
|
||||
[features]
|
||||
default = []
|
||||
serialize = ["serde"]
|
||||
debug_vdom = []
|
||||
|
||||
debug_vdom = []
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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"));
|
||||
};
|
||||
}
|
||||
@@ -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(¤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<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) {
|
||||
@@ -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"] }
|
||||
@@ -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
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user