diff --git a/Cargo.toml b/Cargo.toml index aea25860a..a3c3f09fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ tokio = { version = "1.16.1", features = ["full"] } reqwest = { version = "0.11.9", features = ["json"] } fern = { version = "0.6.0", features = ["colored"] } thiserror = "1.0.30" -env_logger = "0.9.0" +env_logger = "0.10.0" simple_logger = "4.0.0" [profile.release] diff --git a/docs/guide/examples/hello_world_ssr.rs b/docs/guide/examples/hello_world_ssr.rs new file mode 100644 index 000000000..0f8db0b46 --- /dev/null +++ b/docs/guide/examples/hello_world_ssr.rs @@ -0,0 +1,63 @@ +#![allow(unused)] +#![allow(non_snake_case)] +// ANCHOR: all + +// ANCHOR: main +#![allow(non_snake_case)] +use axum::{response::Html, routing::get, Router}; +// import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types +use dioxus::prelude::*; + +#[tokio::main] +async fn main() { + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on http://{}", addr); + + axum::Server::bind(&addr) + .serve( + Router::new() + .route("/", get(app_endpoint)) + .into_make_service(), + ) + .await + .unwrap(); +} + +// ANCHOR_END: main + +// ANCHOR: endpoint +async fn app_endpoint() -> Html { + // render the rsx! macro to HTML + Html(dioxus_ssr::render_lazy(rsx! { + div { "hello world!" } + })) +} +// ANCHOR_END: endpoint + +// ANCHOR: second_endpoint +async fn second_app_endpoint() -> Html { + // create a component that renders a div with the text "hello world" + fn app(cx: Scope) -> Element { + cx.render(rsx!(div { "hello world" })) + } + // create a VirtualDom with the app component + let mut app = VirtualDom::new(app); + // rebuild the VirtualDom before rendering + let _ = app.rebuild(); + + // render the VirtualDom to HTML + Html(dioxus_ssr::render(&app)) +} +// ANCHOR_END: second_endpoint + +// ANCHOR: component +// define a component that renders a div with the text "Hello, world!" +fn App(cx: Scope) -> Element { + cx.render(rsx! { + div { + "Hello, world!" + } + }) +} +// ANCHOR_END: component +// ANCHOR_END: all diff --git a/docs/guide/src/en/getting_started/ssr.md b/docs/guide/src/en/getting_started/ssr.md index f78623003..7a1954191 100644 --- a/docs/guide/src/en/getting_started/ssr.md +++ b/docs/guide/src/en/getting_started/ssr.md @@ -40,54 +40,30 @@ tokio = { version = "1.15.0", features = ["full"] } Now, set up your Axum app to respond on an endpoint. ```rust -use axum::{response::Html, routing::get, Router}; -use dioxus::prelude::*; - -#[tokio::main] -async fn main() { - let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000)); - println!("listening on http://{}", addr); - - axum::Server::bind(&addr) - .serve( - Router::new() - .route("/", get(app_endpoint)) - .into_make_service(), - ) - .await - .unwrap(); -} +{{#include ../../../examples/hello_world_ssr.rs:main}} ``` And then add our endpoint. We can either render `rsx!` directly: ```rust -async fn app_endpoint() -> Html { - // render the rsx! macro to HTML - Html(dioxus_ssr::render_lazy(rsx! { - div { "hello world!" } - })) -} +{{#include ../../../examples/hello_world_ssr.rs:endpoint}} ``` Or we can render VirtualDoms. ```rust -async fn app_endpoint() -> Html { - // create a component that renders a div with the text "hello world" - fn app(cx: Scope) -> Element { - cx.render(rsx!(div { "hello world" })) - } - // create a VirtualDom with the app component - let mut app = VirtualDom::new(app); - // rebuild the VirtualDom before rendering - let _ = app.rebuild(); - - // render the VirtualDom to HTML - Html(dioxus_ssr::render_vdom(&app)) -} +{{#include ../../../examples/hello_world_ssr.rs:second_endpoint}} ``` +And then add our app component: + +```rust +{{#include ../../../examples/hello_world_ssr.rs:component}} +``` + +And that's it! + + ## Multithreaded Support The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. diff --git a/docs/guide/src/en/interactivity/hooks.md b/docs/guide/src/en/interactivity/hooks.md index 3539e5884..083f04b49 100644 --- a/docs/guide/src/en/interactivity/hooks.md +++ b/docs/guide/src/en/interactivity/hooks.md @@ -44,7 +44,7 @@ This is only possible because the two hooks are always called in the same order, 1. Hooks may be only used in components or other hooks (we'll get to that later) 2. On every call to the component function - 1. The same hooks must be called + 1. The same hooks must be called (except in the case of early returns, as explained later in the [Error Handling chapter](../best_practices/error_handling.md)) 2. In the same order 3. Hooks name's should start with `use_` so you don't accidentally confuse them with regular functions diff --git a/examples/web_component.rs b/examples/web_component.rs new file mode 100644 index 000000000..bd0f7eb15 --- /dev/null +++ b/examples/web_component.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + web-component { + "my-prop": "5%", + } + }) +} diff --git a/notes/CONTRIBUTING.md b/notes/CONTRIBUTING.md index e69de29bb..150b60c88 100644 --- a/notes/CONTRIBUTING.md +++ b/notes/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +On Linux, install the following packages: + +```bash +sudo apt install libgdk3.0-cil libatk1.0-dev libcairo2-dev libpango1.0-dev libgdk-pixbuf2.0-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev +``` + +Then run: + +```bash +cargo test --workspace --tests +``` diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 092676322..35e63bd9d 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -38,6 +38,7 @@ slab = "0.4" futures-util = "0.3.25" rfd = "0.11.3" +urlencoding = "2.1.2" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 2e407b258..c338a50cc 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -43,7 +43,7 @@ fn module_loader(root_name: &str) -> String { let rootname = "{root_name}"; let root = window.document.getElementById(rootname); if (root != null) {{ - window.interpreter = new Interpreter(root); + window.interpreter = new Interpreter(root, new InterpreterConfig(true)); window.ipc.postMessage(serializeIpcMessage("initialize")); }} @@ -87,7 +87,9 @@ pub(super) fn desktop_handler( } // Else, try to serve a file from the filesystem. - let path = PathBuf::from(request.uri().path().trim_start_matches('/')); + let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) + .expect("expected URL to be UTF-8 encoded"); + let path = PathBuf::from(&*decoded); // If the path is relative, we'll try to serve it from the assets directory. let mut asset = get_asset_root() diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index 5232bdb8b..e5cc05315 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -41,17 +41,10 @@ impl Shortcut { impl ShortcutRegistry { pub fn new(target: &EventLoopWindowTarget) -> Self { - let myself = Self { + Self { manager: Rc::new(RefCell::new(ShortcutManager::new(target))), shortcuts: Rc::new(RefCell::new(HashMap::new())), - }; - // prevent CTRL+R from reloading the page which breaks apps - let _ = myself.add_shortcut( - Some(ModifiersState::CONTROL), - KeyCode::KeyR, - Box::new(|| {}), - ); - myself + } } pub(crate) fn call_handlers(&self, id: AcceleratorId) { diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index bc2098285..8f0cc2184 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -35,7 +35,7 @@ log = "0.4.14" rand = { version = "0.8.4", features = ["small_rng"] } criterion = "0.3.5" thiserror = "1.0.30" -env_logger = "0.9.0" +env_logger = "0.10.0" tokio = { version = "1.21.2", features = ["full"] } # dioxus-edit-stream = { path = "../edit-stream" } diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs index d2fa3f213..95732c491 100644 --- a/packages/fermi/src/hooks/atom_ref.rs +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -70,6 +70,12 @@ impl UseAtomRef { self.value.borrow() } + /// This is silent operation + /// call `.force_update()` manually if required + pub fn with_mut_silent(&self, cb: impl FnOnce(&mut T)) { + cb(&mut *self.write_silent()) + } + pub fn write(&self) -> RefMut { self.root.force_update(self.ptr); self.value.borrow_mut() diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 581bf2f52..5e7da25ca 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"] [dependencies] # server functions -server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] } +server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", default-features = false, features = ["stable"] } dioxus_server_macro = { path = "server-macro" } # warp @@ -56,9 +56,11 @@ dioxus-hot-reload = { path = "../hot-reload" } web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] } [features] -default = ["hot-reload"] +default = ["hot-reload", "default-tls"] hot-reload = ["serde_json", "tokio-stream", "futures-util"] warp = ["dep:warp", "http-body", "ssr"] axum = ["dep:axum", "tower-http", "ssr"] salvo = ["dep:salvo", "ssr"] ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "hyper", "http"] +default-tls = ["server_fn/default-tls"] +rustls = ["server_fn/rustls"] diff --git a/packages/hot-reload/Cargo.toml b/packages/hot-reload/Cargo.toml index 2d88bf029..4809df480 100644 --- a/packages/hot-reload/Cargo.toml +++ b/packages/hot-reload/Cargo.toml @@ -18,7 +18,7 @@ dioxus-html = { path = "../html", features = ["hot-reload-context"], version = " interprocess = { version = "1.2.1" } notify = "5.0.0" -chrono = "0.4.23" +chrono = { version = "0.4.24", default-features = false, features = ["clock"] } serde_json = "1.0.91" serde = { version = "1", features = ["derive"] } execute = "0.2.11" diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index 478b8ca5c..4fd9016f1 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -6,10 +6,14 @@ use web_sys::Element; #[wasm_bindgen(module = "/src/interpreter.js")] extern "C" { + pub type InterpreterConfig; + #[wasm_bindgen(constructor)] + pub fn new(intercept_link_redirects: bool) -> InterpreterConfig; + pub type Interpreter; #[wasm_bindgen(constructor)] - pub fn new(arg: Element) -> Interpreter; + pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter; #[wasm_bindgen(method)] pub fn SaveTemplate(this: &Interpreter, template: JsValue); diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 3ea0a86d4..4c874d32b 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -53,8 +53,15 @@ class ListenerMap { } } +class InterpreterConfig { + constructor(intercept_link_redirects) { + this.intercept_link_redirects = intercept_link_redirects; + } +} + class Interpreter { - constructor(root) { + constructor(root, config) { + this.config = config; this.root = root; this.listeners = new ListenerMap(root); this.nodes = [root]; @@ -397,7 +404,7 @@ class Interpreter { ); } else { this.NewEventListener(edit.name, edit.id, bubbles, (event) => { - handler(event, edit.name, bubbles); + handler(event, edit.name, bubbles, this.config); }); } break; @@ -407,32 +414,34 @@ class Interpreter { // this handler is only provided on the desktop and liveview implementations since this // method is not used by the web implementation -function handler(event, name, bubbles) { +function handler(event, name, bubbles, config) { let target = event.target; if (target != null) { let preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); if (event.type === "click") { // todo call prevent default if it's the right type of event - let a_element = target.closest("a"); - if (a_element != null) { - event.preventDefault(); + if (config.intercept_link_redirects) { + let a_element = target.closest("a"); + if (a_element != null) { + event.preventDefault(); - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); + let elementShouldPreventDefault = + preventDefaultRequests && preventDefaultRequests.includes(`onclick`); + let aElementShouldPreventDefault = a_element.getAttribute( + `dioxus-prevent-default` + ); + let linkShouldPreventDefault = + aElementShouldPreventDefault && + aElementShouldPreventDefault.includes(`onclick`); - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) - ); + if (!elementShouldPreventDefault && !linkShouldPreventDefault) { + const href = a_element.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined) { + window.ipc.postMessage( + serializeIpcMessage("browser_open", { href }) + ); + } } } } diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 354b0f28c..302e4c486 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -46,7 +46,7 @@ once_cell = "1.17.1" # actix-ws = { version = "0.2.5", optional = true } [dev-dependencies] -pretty_env_logger = { version = "0.4.0" } +pretty_env_logger = { version = "0.5.0" } tokio = { version = "1.22.0", features = ["full"] } dioxus = { path = "../dioxus", version = "0.3.0" } warp = "0.3.3" diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 594c764c2..e98d02a9c 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -8,7 +8,7 @@ function main() { class IPC { constructor(root) { // connect to the websocket - window.interpreter = new Interpreter(root); + window.interpreter = new Interpreter(root, new InterpreterConfig(false)); let ws = new WebSocket(WS_ADDR); diff --git a/packages/rsx-rosetta/src/lib.rs b/packages/rsx-rosetta/src/lib.rs index 64fed1e18..4f30c0081 100644 --- a/packages/rsx-rosetta/src/lib.rs +++ b/packages/rsx-rosetta/src/lib.rs @@ -1,6 +1,6 @@ use convert_case::{Case, Casing}; use dioxus_rsx::{ - BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, IfmtInput, + BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput, }; pub use html_parser::{Dom, Node}; use proc_macro2::{Ident, Span}; @@ -21,7 +21,7 @@ pub fn rsx_node_from_html(node: &Node) -> Option { Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))), Node::Element(el) => { let el_name = el.name.to_case(Case::Snake); - let el_name = Ident::new(el_name.as_str(), Span::call_site()); + let el_name = ElementName::Ident(Ident::new(el_name.as_str(), Span::call_site())); let mut attributes: Vec<_> = el .attributes diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index e47c2e02a..466200eca 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -1,9 +1,13 @@ +use std::fmt::{Display, Formatter}; + use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, Error, Expr, Ident, LitStr, Result, Token, }; @@ -12,7 +16,7 @@ use syn::{ // ======================================= #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct Element { - pub name: Ident, + pub name: ElementName, pub key: Option, pub attributes: Vec, pub children: Vec, @@ -22,7 +26,7 @@ pub struct Element { impl Parse for Element { fn parse(stream: ParseStream) -> Result { - let el_name = Ident::parse(stream)?; + let el_name = ElementName::parse(stream)?; // parse the guts let content: ParseBuffer; @@ -181,7 +185,7 @@ impl ToTokens for Element { tokens.append_all(quote! { __cx.element( - dioxus_elements::#name, + #name, __cx.bump().alloc([ #(#listeners),* ]), __cx.bump().alloc([ #(#attr),* ]), __cx.bump().alloc([ #(#children),* ]), @@ -191,6 +195,75 @@ impl ToTokens for Element { } } +#[derive(PartialEq, Eq, Clone, Debug, Hash)] +pub enum ElementName { + Ident(Ident), + Custom(LitStr), +} + +impl ElementName { + pub(crate) fn tag_name(&self) -> TokenStream2 { + match self { + ElementName::Ident(i) => quote! { dioxus_elements::#i::TAG_NAME }, + ElementName::Custom(s) => quote! { #s }, + } + } +} + +impl ElementName { + pub fn span(&self) -> Span { + match self { + ElementName::Ident(i) => i.span(), + ElementName::Custom(s) => s.span(), + } + } +} + +impl PartialEq<&str> for ElementName { + fn eq(&self, other: &&str) -> bool { + match self { + ElementName::Ident(i) => i == *other, + ElementName::Custom(s) => s.value() == *other, + } + } +} + +impl Display for ElementName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ElementName::Ident(i) => write!(f, "{}", i), + ElementName::Custom(s) => write!(f, "{}", s.value()), + } + } +} + +impl Parse for ElementName { + fn parse(stream: ParseStream) -> Result { + let raw = Punctuated::::parse_separated_nonempty(stream)?; + if raw.len() == 1 { + Ok(ElementName::Ident(raw.into_iter().next().unwrap())) + } else { + let span = raw.span(); + let tag = raw + .into_iter() + .map(|ident| ident.to_string()) + .collect::>() + .join("-"); + let tag = LitStr::new(&tag, span); + Ok(ElementName::Custom(tag)) + } + } +} + +impl ToTokens for ElementName { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ElementName::Ident(i) => tokens.append_all(quote! { dioxus_elements::#i }), + ElementName::Custom(s) => tokens.append_all(quote! { #s }), + } + } +} + #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub enum ElementAttr { /// `attribute: "value"` @@ -234,7 +307,7 @@ impl ElementAttr { #[derive(PartialEq, Eq, Clone, Debug, Hash)] pub struct ElementAttrNamed { - pub el_name: Ident, + pub el_name: ElementName, pub attr: ElementAttr, } @@ -242,24 +315,46 @@ impl ToTokens for ElementAttrNamed { fn to_tokens(&self, tokens: &mut TokenStream2) { let ElementAttrNamed { el_name, attr } = self; - tokens.append_all(match attr { + let ns = |name| match el_name { + ElementName::Ident(i) => quote! { dioxus_elements::#i::#name.1 }, + ElementName::Custom(_) => quote! { None }, + }; + let volitile = |name| match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.2 }, + ElementName::Custom(_) => quote! { false }, + }; + let attribute = |name: &Ident| match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.0 }, + ElementName::Custom(_) => { + let as_string = name.to_string(); + quote!(#as_string) + } + }; + + let attribute = match attr { ElementAttr::AttrText { name, value } => { + let ns = ns(name); + let volitile = volitile(name); + let attribute = attribute(name); quote! { __cx.attr( - dioxus_elements::#el_name::#name.0, + #attribute, #value, - dioxus_elements::#el_name::#name.1, - dioxus_elements::#el_name::#name.2 + #ns, + #volitile ) } } ElementAttr::AttrExpression { name, value } => { + let ns = ns(name); + let volitile = volitile(name); + let attribute = attribute(name); quote! { __cx.attr( - dioxus_elements::#el_name::#name.0, + #attribute, #value, - dioxus_elements::#el_name::#name.1, - dioxus_elements::#el_name::#name.2 + #ns, + #volitile ) } } @@ -288,7 +383,9 @@ impl ToTokens for ElementAttrNamed { dioxus_elements::events::#name(__cx, #tokens) } } - }); + }; + + tokens.append_all(attribute); } } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index d22b122e9..07c887e65 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -426,13 +426,25 @@ impl<'a> DynamicContext<'a> { match root { BodyNode::Element(el) => { let el_name = &el.name; + let ns = |name| match el_name { + ElementName::Ident(i) => quote! { dioxus_elements::#i::#name }, + ElementName::Custom(_) => quote! { None }, + }; let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { ElementAttr::AttrText { name, value } if value.is_static() => { let value = value.to_static().unwrap(); + let ns = ns(quote!(#name.1)); + let name = match el_name { + ElementName::Ident(_) => quote! { #el_name::#name.0 }, + ElementName::Custom(_) => { + let as_string = name.to_string(); + quote! { #as_string } + } + }; quote! { ::dioxus::core::TemplateAttribute::Static { - name: dioxus_elements::#el_name::#name.0, - namespace: dioxus_elements::#el_name::#name.1, + name: #name, + namespace: #ns, value: #value, // todo: we don't diff these so we never apply the volatile flag @@ -479,10 +491,13 @@ impl<'a> DynamicContext<'a> { let _opt = el.children.len() == 1; let children = quote! { #(#children),* }; + let ns = ns(quote!(NAME_SPACE)); + let el_name = el_name.tag_name(); + quote! { ::dioxus::core::TemplateNode::Element { - tag: dioxus_elements::#el_name::TAG_NAME, - namespace: dioxus_elements::#el_name::NAME_SPACE, + tag: #el_name, + namespace: #ns, attrs: &[ #attrs ], children: &[ #children ], } diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 0d5556552..54448c883 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -50,7 +50,16 @@ impl Parse for BodyNode { return Ok(BodyNode::Text(stream.parse()?)); } + // if this is a dash-separated path, it's a web component (custom element) let body_stream = stream.fork(); + if let Ok(ElementName::Custom(name)) = body_stream.parse::() { + if name.value().contains('-') && body_stream.peek(token::Brace) { + return Ok(BodyNode::Element(stream.parse::()?)); + } + } + + let body_stream = stream.fork(); + if let Ok(path) = body_stream.parse::() { // this is an Element if path match of: // - one ident