├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── README.md ├── leptos-mview-core ├── Cargo.toml ├── README.md └── src │ ├── ast.rs │ ├── ast │ ├── attribute.rs │ ├── attribute │ │ ├── directive.rs │ │ ├── kv.rs │ │ ├── selector.rs │ │ └── spread_attrs.rs │ ├── children.rs │ ├── doctype.rs │ ├── element.rs │ ├── ident.rs │ ├── tag.rs │ └── value.rs │ ├── error_ext.rs │ ├── expand.rs │ ├── expand │ ├── subroutines.rs │ └── utils.rs │ ├── kw.rs │ ├── lib.rs │ ├── parse.rs │ └── span.rs ├── leptos-mview-macro ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── src └── lib.rs └── tests ├── component.rs ├── html.rs ├── paren_children.rs ├── router.rs ├── slots.rs ├── spread.rs ├── ui.rs ├── ui ├── errors │ ├── com_builder_spans.rs │ ├── com_builder_spans.stderr │ ├── com_dyn_classes.rs │ ├── com_dyn_classes.stderr │ ├── invalid_child.rs │ ├── invalid_child.stderr │ ├── invalid_directive.rs │ ├── invalid_directive.stderr │ ├── invalid_value.rs │ ├── invalid_value.stderr │ ├── misc_partial.rs │ ├── misc_partial.stderr │ ├── no_children_after_closure.rs │ ├── no_children_after_closure.stderr │ ├── non_str_child.rs │ ├── non_str_child.stderr │ ├── return_expression.rs │ ├── return_expression.stderr │ ├── slot_builder_spans.rs │ ├── slot_builder_spans.stderr │ ├── slot_unsupported_dirs.rs │ ├── slot_unsupported_dirs.stderr │ ├── unsupported_attrs.rs │ ├── unsupported_attrs.stderr │ ├── unterminated_element.rs │ ├── unterminated_element.stderr │ ├── unterminated_element_error.rs │ ├── unterminated_element_error.stderr │ ├── use_directive.rs │ └── use_directive.stderr └── pass │ ├── many_braces.rs │ └── use_directive.rs ├── utils └── mod.rs └── values.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /tests/tmp.rs 4 | /.cargo/config.toml 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" 3 | format_code_in_doc_comments = true 4 | wrap_comments = true 5 | fn_single_line = true 6 | overflow_delimited_expr = true 7 | single_line_if_else_max_width = 80 8 | single_line_let_else_max_width = 80 9 | use_field_init_shorthand = true 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["leptos-mview-core", "leptos-mview-macro"] 3 | 4 | [workspace.package] 5 | version = "0.4.4" 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/blorbb/leptos-mview" 9 | 10 | [package] 11 | name = "leptos-mview" 12 | keywords = ["macro", "leptos", "view"] 13 | description = "A concise view macro for Leptos" 14 | readme = "README.md" 15 | version.workspace = true 16 | edition.workspace = true 17 | license.workspace = true 18 | repository.workspace = true 19 | 20 | [workspace.dependencies] 21 | syn = "2" 22 | quote = "1" 23 | proc-macro2 = "1" 24 | proc-macro-error2 = "2" 25 | 26 | # dev dependencies # 27 | trybuild = "1" 28 | # needs to use ssr for some view-to-HTML features to work. 29 | leptos = { version = "0.7.5", features = ["ssr", "nightly"] } 30 | leptos_router = { version = "0.7.5", features = ["ssr", "nightly"] } 31 | 32 | [dependencies] 33 | leptos-mview-macro = { path = "leptos-mview-macro", version = "0.4.4" } 34 | 35 | [dev-dependencies] 36 | trybuild.workspace = true 37 | leptos.workspace = true 38 | leptos_router.workspace = true 39 | leptos-mview = { path = ".", features = ["nightly"] } 40 | 41 | [features] 42 | nightly = ["leptos-mview-macro/nightly"] 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leptos mview 2 | 3 | [![crates.io](https://img.shields.io/crates/v/leptos-mview.svg)](https://crates.io/crates/leptos-mview) 4 | 5 | 6 | 7 | An alternative `view!` macro for [Leptos](https://github.com/leptos-rs/leptos/tree/main) inspired by [maud](https://maud.lambda.xyz/). 8 | 9 | ## Example 10 | 11 | A little preview of the syntax: 12 | 13 | ```rust 14 | use leptos::prelude::*; 15 | use leptos_mview::mview; 16 | 17 | #[component] 18 | fn MyComponent() -> impl IntoView { 19 | let (value, set_value) = signal(String::new()); 20 | let red_input = move || value().len() % 2 == 0; 21 | 22 | mview! { 23 | h1.title("A great website") 24 | br; 25 | 26 | input 27 | type="text" 28 | data-index=0 29 | class:red={red_input} 30 | prop:{value} 31 | on:change={move |ev| { 32 | set_value(event_target_value(&ev)) 33 | }}; 34 | 35 | Show 36 | when=[!value().is_empty()] 37 | fallback=[mview! { "..." }] 38 | ( 39 | Await 40 | future={fetch_from_db(value())} 41 | blocking 42 | |db_info| ( 43 | p("Things found: " strong({*db_info}) "!") 44 | p("Is bad: " f["{}", red_input()]) 45 | ) 46 | ) 47 | } 48 | } 49 | 50 | async fn fetch_from_db(data: String) -> usize { data.len() } 51 | ``` 52 | 53 |
54 | Explanation of the example: 55 | 56 | ```rust 57 | use leptos::prelude::*; 58 | use leptos_mview::mview; 59 | 60 | #[component] 61 | fn MyComponent() -> impl IntoView { 62 | let (value, set_value) = signal(String::new()); 63 | let red_input = move || value().len() % 2 == 0; 64 | 65 | mview! { 66 | // specify tags and attributes, children go in parentheses. 67 | // classes (and ids) can be added like CSS selectors. 68 | // same as `h1 class="title"` 69 | h1.title("A great website") 70 | // elements with no children end with a semi-colon 71 | br; 72 | 73 | input 74 | type="text" 75 | data-index=0 // kebab-cased identifiers supported 76 | class:red={red_input} // non-literal values must be wrapped in braces 77 | prop:{value} // shorthand! same as `prop:value={value}` 78 | on:change={move |ev| { // event handlers same as leptos 79 | set_value(event_target_value(&ev)) 80 | }}; 81 | 82 | Show 83 | // values wrapped in brackets `[body]` are expanded to `{move || body}` 84 | when=[!value().is_empty()] // `{move || !value().is_empty()}` 85 | fallback=[mview! { "..." }] // `{move || mview! { "..." }}` 86 | ( // I recommend placing children like this when attributes are multi-line 87 | Await 88 | future={fetch_from_db(value())} 89 | blocking // expanded to `blocking=true` 90 | // children take arguments with a 'closure' 91 | // this is very different to `let:db_info` in Leptos! 92 | |db_info| ( 93 | p("Things found: " strong({*db_info}) "!") 94 | // bracketed expansion works in children too! 95 | // this one also has a special prefix to add `format!` into the expansion! 96 | // {move || format!("{}", red_input()} 97 | p("Is bad: " f["{}", red_input()]) 98 | ) 99 | ) 100 | } 101 | } 102 | 103 | // fake async function 104 | async fn fetch_from_db(data: String) -> usize { data.len() } 105 | ``` 106 | 107 |
108 | 109 | ## Purpose 110 | 111 | The `view!` macros in Leptos is often the largest part of a component, and can get extremely long when writing complex components. This macro aims to be as **concise** as possible, trying to **minimise unnecessary punctuation/words** and **shorten common patterns**. 112 | 113 | ## Compatibility 114 | 115 | This macro will be compatible with the latest stable release of Leptos. The macro references Leptos items using `::leptos::...`, no items are re-exported from this crate. Therefore, this crate will likely work with any Leptos version if no view-related items are changed. 116 | 117 | The below are the versions with which I have tested it to be working. It is likely that the macro works with more versions of Leptos. 118 | 119 | | `leptos_mview` version | Compatible `leptos` version | 120 | | ---------------------- | --------------------------- | 121 | | `0.1` | `0.5` | 122 | | `0.2` | `0.5`, `0.6` | 123 | | `0.3` | `0.6` | 124 | | `0.4` | `0.7` | 125 | 126 | This crate also has a feature `"nightly"` that enables better proc-macro diagnostics (simply enables the nightly feature in proc-macro-error2. Necessary while [this pr](https://github.com/GnomedDev/proc-macro-error-2/pull/5) is not yet merged). 127 | 128 | ## Syntax details 129 | 130 | ### Elements 131 | 132 | Elements have the following structure: 133 | 134 | 1. Element / component tag name / path (`div`, `App`, `component::Codeblock`). 135 | 2. Any classes or ids prefixed with a dot `.` or hash `#` respectively. 136 | 3. A space-separated list of attributes and directives (`class="primary"`, `on:click={...}`). 137 | 4. Children in parens or braces (`("hi")` or `{ "hi!" }`), or a semi-colon for no children (`;`). 138 | 139 | Example: 140 | ```rust 141 | mview! { 142 | div.primary(strong("hello world")) 143 | input type="text" on:input={handle_input}; 144 | MyComponent data=3 other="hi"; 145 | } 146 | ``` 147 | 148 | Adding generics is the same as in Leptos: add it directly after the component name, with or without the turbofish. 149 | 150 | ```rust 151 | #[component] 152 | pub fn GenericComponent(ty: PhantomData) -> impl IntoView { 153 | std::any::type_name::() 154 | } 155 | 156 | #[component] 157 | pub fn App() -> impl IntoView { 158 | mview! { 159 | // both with and without turbofish is supported 160 | GenericComponent:: ty={PhantomData}; 161 | GenericComponent ty={PhantomData}; 162 | GenericComponent ty={PhantomData}; 163 | } 164 | } 165 | ``` 166 | 167 | Note that due to [Reserving syntax](https://doc.rust-lang.org/edition-guide/rust-2021/reserving-syntax.html), the `#` for ids must have a space before it. 168 | 169 | ```rust 170 | mview! { 171 | nav #primary ("...") 172 | // not allowed: nav#primary ("...") 173 | } 174 | ``` 175 | 176 | Classes/ids created with the selector syntax can be mixed with the attribute `class="..."` and directive `class:a-class={signal}` as well. 177 | 178 | ### Slots 179 | 180 | [Slots](https://docs.rs/leptos/latest/leptos/attr.slot.html) ([another example](https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs)) are supported by prefixing the struct with `slot:` inside the parent's children. 181 | 182 | The name of the parameter in the component function must be the same as the slot's name, in snake case. 183 | 184 | Using the slots defined by the [`SlotIf` example linked](https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs): 185 | ```rust 186 | use leptos::prelude::*; 187 | use leptos_mview::mview; 188 | 189 | #[component] 190 | pub fn App() -> impl IntoView { 191 | let (count, set_count) = signal(0); 192 | let is_even = Signal::derive(move || count() % 2 == 0); 193 | let is_div5 = Signal::derive(move || count() % 5 == 0); 194 | let is_div7 = Signal::derive(move || count() % 7 == 0); 195 | 196 | mview! { 197 | SlotIf cond={is_even} ( 198 | slot:Then ("even") 199 | slot:ElseIf cond={is_div5} ("divisible by 5") 200 | slot:ElseIf cond={is_div7} ("divisible by 7") 201 | slot:Fallback ("odd") 202 | ) 203 | } 204 | } 205 | ``` 206 | 207 | ### Values 208 | 209 | There are (currently) 3 main types of values you can pass in: 210 | 211 | - **Literals** can be passed in directly to attribute values (like `data=3`, `class="main"`, `checked=true`). 212 | - However, children do not accept literal numbers or bools - only strings. 213 | ```rust 214 | // does NOT compile. 215 | mview! { p("this works " 0 " times: " true) } 216 | ``` 217 | 218 | - Everything else must be passed in as a **block**, including variables, closures, or expressions. 219 | ```rust 220 | mview! { 221 | input 222 | class="main" 223 | checked=true 224 | data-index=3 225 | type={input_type} 226 | on:input={move |_| handle_input(1)}; 227 | } 228 | ``` 229 | 230 | This is not valid: 231 | ```rust 232 | let input_type = "text"; 233 | // ❌ This is not valid! Wrap input_type in braces. 234 | mview! { input type=input_type } 235 | ``` 236 | 237 | - Values wrapped in **brackets** (like `value=[a_bool().to_string()]`) are shortcuts for a block with an empty closure `move || ...` (to `value={move || a_bool().to_string()}`). 238 | ```rust 239 | mview! { 240 | Show 241 | fallback=[()] // common for not wanting a fallback as `|| ()` 242 | when=[number() % 2 == 0] // `{move || number() % 2 == 0}` 243 | ( 244 | "number + 1 = " [number() + 1] // works in children too! 245 | ) 246 | } 247 | ``` 248 | 249 | - Note that this always expands to `move || ...`: for any closures that take an argument, use the full closure block instead. 250 | ```rust 251 | mview! { 252 | input type="text" on:click=[log!("THIS DOESNT WORK")]; 253 | } 254 | ``` 255 | 256 | Instead: 257 | ```rust 258 | mview! { 259 | input type="text" on:click={|_| log!("THIS WORKS!")}; 260 | } 261 | ``` 262 | 263 | The bracketed values can also have some special prefixes for even more common shortcuts! 264 | - Currently, the only one is `f` - e.g. `f["{:.2}", stuff()]`. Adding an `f` will add `format!` into the closure. This is equivalent to `[format!("{:.2}", stuff())]` or `{move || format!("{:.2}", stuff())}`. 265 | 266 | ### Attributes 267 | 268 | #### Key-value attributes 269 | 270 | Most attributes are `key=value` pairs. The `value` follows the rules from above. The `key` has a few variations: 271 | 272 | - Standard identifier: identifiers like `type`, `an_attribute`, `class`, `id` etc are valid keys. 273 | - Kebab-case identifier: identifiers can be kebab-cased, like `data-value`, `an-attribute`. 274 | - NOTE: on HTML elements, this will be put on the element as is: `div data-index="0";` becomes `
`. **On components**, hyphens are converted to underscores then passed into the component builder. 275 | 276 | For example, this component: 277 | ```rust 278 | #[component] 279 | fn Something(some_attribute: i32) -> impl IntoView { ... } 280 | ``` 281 | 282 | Can be used elsewhere like this: 283 | ```rust 284 | mview! { Something some-attribute=5; } 285 | ``` 286 | 287 | And the `some-attribute` will be passed in to the `some_attribute` argument. 288 | 289 | - Attribute shorthand: if the name of the attribute and value are the same, e.g. `class={class}`, you can replace this with `{class}` to mean the same thing. 290 | ```rust 291 | let class = "these are classes"; 292 | let id = "primary"; 293 | mview! { 294 | div {class} {id} ("this has 3 classes and id='primary'") 295 | } 296 | ``` 297 | 298 | See also: [kebab-case identifiers with attribute shorthand](#kebab-case-identifiers-with-attribute-shorthand) 299 | 300 | Note that the special `node_ref` or `ref` or `_ref` or `ref_` attribute in Leptos to bind the element to a variable is just `ref={variable}` in here. 301 | 302 | #### Boolean attributes 303 | 304 | Another shortcut is that boolean attributes can be written without adding `=true`. Watch out though! `checked` is **very different** to `{checked}`. 305 | ```rust 306 | // recommend usually adding #[prop(optional)] to all these 307 | #[component] 308 | fn LotsOfFlags(wide: bool, tall: bool, red: bool, curvy: bool, count: i32) -> impl IntoView {} 309 | 310 | mview! { LotsOfFlags wide tall red=false curvy count=3; } 311 | // same as... 312 | mview! { LotsOfFlags wide=true tall=true red=false curvy=true count=3; } 313 | ``` 314 | 315 | See also: [boolean attributes on HTML elements](#boolean-attributes-on-html-elements) 316 | 317 | #### Directives 318 | 319 | Some special attributes (distinguished by the `:`) called **directives** have special functionality. All have the same behaviour as Leptos. These include: 320 | - `class:class-name=[when to show]` 321 | - `style:style-key=[style value]` 322 | - `on:event={move |ev| event handler}` 323 | - `prop:property-name={signal}` 324 | - `attr:name={value}` 325 | - `clone:ident_to_clone` 326 | - `use:directive_name` or `use:directive_name={params}` 327 | - `bind:checked={rwsignal}` or `bind:value={(getter, setter)}` 328 | 329 | All of these directives except `clone` also support the attribute shorthand: 330 | 331 | ```rust 332 | let color = RwSignal::new("red".to_string()); 333 | let disabled = false; 334 | mview! { 335 | div style:{color} class:{disabled}; 336 | } 337 | ``` 338 | 339 | The `class` and `style` directives also support using string literals, for more complicated names. Make sure the string for `class:` doesn't have spaces, or it will panic! 340 | 341 | ```rust 342 | let yes = move || true; 343 | mview! { 344 | div class:"complex-[class]-name"={yes} 345 | style:"doesn't-exist"="white"; 346 | } 347 | ``` 348 | 349 | Note that the `use:` directive automatically calls `.into()` on its argument, consistent with behaviour from Leptos. 350 | 351 | ### Children 352 | 353 | You may have noticed that the `let:data` prop was missing from the previous section on directive attributes! 354 | 355 | This is replaced with a closure right before the children block. This way, you can pass in multiple arguments to the children more easily. 356 | 357 | ```rust 358 | mview! { 359 | Await 360 | future={async { 3 }} 361 | |monkeys| ( 362 | p({*monkeys} " little monkeys, jumping on the bed.") 363 | ) 364 | } 365 | ``` 366 | 367 | Note that you will usually need to add a `*` before the data you are using. If you forget that, rust-analyser will tell you to dereference here: `*{monkeys}`. This is obviously invalid - put it inside the braces. 368 | 369 | Children can be wrapped in either braces or parentheses, whichever you prefer. 370 | 371 | ```rust 372 | mview! { 373 | p { 374 | "my " strong("bold") " and " em("fancy") " text." 375 | } 376 | } 377 | ``` 378 | 379 | Summary from the previous section on values in case you missed it: children can be literal strings (not bools or numbers!), blocks with Rust code inside (`{*monkeys}`), or the closure shorthand `[number() + 1]`. 380 | 381 | Children with closures are also supported on slots. 382 | 383 | ## Extra details 384 | 385 | ### Kebab-case identifiers with attribute shorthand 386 | 387 | If an attribute shorthand has hyphens: 388 | - On components, both the key and value will be converted to underscores. 389 | ```rust 390 | let some_attribute = 5; 391 | mview! { Something {some-attribute}; } 392 | // same as... 393 | mview! { Something {some_attribute}; } 394 | // same as... 395 | mview! { Something some_attribute={some_attribute}; } 396 | ``` 397 | 398 | - On HTML elements, the key will keep hyphens, but the value will be turned into an identifier with underscores. 399 | ```rust 400 | let aria_label = "a good label"; 401 | mview! { input {aria-label}; } 402 | // same as... 403 | mview! { input aria-label={aria_label}; } 404 | ``` 405 | 406 | ### Boolean attributes on HTML elements 407 | 408 | Note the behaviour from Leptos: setting an HTML attribute to true adds the attribute with no value associated. 409 | ```rust 410 | view! { } 411 | ``` 412 | Becomes ``, NOT `checked="true"` or `data-smth="true"` or `not-here="false"`. 413 | 414 | To have the attribute have a value of the string "true" or "false", use `.to_string()` on the bool. Make sure that it's in a closure if you're working with signals too. 415 | ```rust 416 | let boolean_signal = RwSignal::new(true); 417 | mview! { input type="checkbox" checked=[boolean_signal().to_string()]; } 418 | // or, if you prefer 419 | mview! { input type="checkbox" checked=f["{}", boolean_signal()]; } 420 | ``` 421 | 422 | ## Contributing 423 | 424 | Please feel free to make a PR/issue if you have feature ideas/bugs to report/feedback :) 425 | 426 | 427 | 428 | 429 | -------------------------------------------------------------------------------- /leptos-mview-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leptos-mview-core" 3 | description = "Main implementation of leptos-mview" 4 | readme = "README.md" 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | syn.workspace = true 12 | quote.workspace = true 13 | proc-macro2.workspace = true 14 | proc-macro-error2.workspace = true 15 | -------------------------------------------------------------------------------- /leptos-mview-core/README.md: -------------------------------------------------------------------------------- 1 | This crate is an implementation detail. 2 | 3 | See `leptos-mview` for the macro instead. 4 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast.rs: -------------------------------------------------------------------------------- 1 | //! Custom ASTs. 2 | //! 3 | //! Only 'basic' ASTs (values, idents, etc.) with one obvious way of expanding 4 | //! them have a [`ToTokens`](quote::ToTokens) implementation. Other ASTs with 5 | //! context-specific expansions (like expanding differently in components or 6 | //! HTML elements) or complex ASTs have their expansion implementations in 7 | //! [`crate::expand`]. 8 | //! 9 | //! Most ASTs also implement [`Parse`](syn::parse::Parse). The point at which it 10 | //! errors should not be relied on - i.e. do not run: 11 | //! ```ignore 12 | //! if let Ok(x) = X::parse(input) { /* ... */ } 13 | //! ```` 14 | //! Instead, use [`rollback_err`](crate::recover::rollback_err) to avoid 15 | //! advancing the input if parsing fails. Note that some parse implementations 16 | //! use [`proc_macro_error::emit_error`] to try and recover from incomplete 17 | //! expressions, so use [`input.peek`](syn::parse::ParseBuffer::peek) where 18 | //! possible if layered errors are not desired. 19 | 20 | pub mod attribute; 21 | pub use attribute::{Attr, Attrs}; 22 | mod children; 23 | pub use children::*; 24 | mod element; 25 | pub use element::*; 26 | mod ident; 27 | pub use ident::*; 28 | mod tag; 29 | pub use tag::*; 30 | mod value; 31 | pub use value::*; 32 | mod doctype; 33 | pub use doctype::*; 34 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/attribute.rs: -------------------------------------------------------------------------------- 1 | pub mod directive; 2 | pub mod kv; 3 | pub mod selector; 4 | pub mod spread_attrs; 5 | 6 | use syn::{ 7 | ext::IdentExt, 8 | parse::{Parse, ParseStream}, 9 | Token, 10 | }; 11 | 12 | use self::{directive::Directive, kv::KvAttr, spread_attrs::SpreadAttr}; 13 | use crate::{error_ext::ResultExt, parse::rollback_err}; 14 | 15 | #[derive(Clone)] 16 | pub enum Attr { 17 | Kv(KvAttr), 18 | Directive(Directive), 19 | Spread(SpreadAttr), 20 | } 21 | 22 | impl Parse for Attr { 23 | fn parse(input: ParseStream) -> syn::Result { 24 | // ident then colon must be directive 25 | // just ident must be regular kv attribute 26 | // otherwise, try kv or spread 27 | if input.peek(syn::Ident::peek_any) && input.peek2(Token![:]) { 28 | // cannot be anything else, abort if fails 29 | let dir = Directive::parse(input).unwrap_or_abort(); 30 | Ok(Self::Directive(dir)) 31 | } else if input.peek(syn::Ident) { 32 | // definitely a k-v attribute 33 | let kv = KvAttr::parse(input)?; 34 | Ok(Self::Kv(kv)) 35 | } else if let Some(kv) = rollback_err(input, KvAttr::parse) { 36 | // k-v attributes don't necessarily start with ident, try the rest 37 | Ok(Self::Kv(kv)) 38 | } else if let Some(spread) = rollback_err(input, SpreadAttr::parse) { 39 | Ok(Self::Spread(spread)) 40 | } else { 41 | Err(input.error("no attribute found")) 42 | } 43 | } 44 | } 45 | 46 | /// A space-separated series of attributes. 47 | #[derive(Clone)] 48 | pub struct Attrs(Vec); 49 | 50 | impl std::ops::Deref for Attrs { 51 | type Target = [Attr]; 52 | fn deref(&self) -> &Self::Target { &self.0 } 53 | } 54 | 55 | impl Parse for Attrs { 56 | fn parse(input: ParseStream) -> syn::Result { 57 | let mut vec = Vec::new(); 58 | while let Some(inner) = rollback_err(input, Attr::parse) { 59 | vec.push(inner); 60 | } 61 | Ok(Self(vec)) 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use syn::parse_quote; 68 | 69 | use super::{Attr, KvAttr}; 70 | use crate::ast::Attrs; 71 | 72 | #[test] 73 | fn simple_kv_attr() { 74 | let input: KvAttr = parse_quote! { key = "value" }; 75 | assert_eq!(input.key().repr(), "key"); 76 | assert!(input.value().is_lit()); 77 | } 78 | 79 | #[test] 80 | fn parse_complex_attrs() { 81 | #[allow(non_local_definitions)] 82 | impl Attr { 83 | fn is_kv(&self) -> bool { matches!(self, Self::Kv(..)) } 84 | 85 | fn is_dir(&self) -> bool { matches!(self, Self::Directive(..)) } 86 | 87 | fn is_spread(&self) -> bool { matches!(self, Self::Spread(..)) } 88 | } 89 | 90 | let attrs: Attrs = parse_quote! { 91 | key-1 = "value" 92 | a-long-thing=[some()] 93 | style:--var-2={move || true} 94 | class:{disabled} 95 | {checked} 96 | {..spread} 97 | }; 98 | assert!(attrs[0].is_kv()); 99 | assert!(attrs[1].is_kv()); 100 | assert!(attrs[2].is_dir()); 101 | assert!(attrs[3].is_dir()); 102 | assert!(attrs[4].is_kv()); 103 | assert!(attrs[5].is_spread()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/attribute/directive.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | ext::IdentExt, 3 | parse::{Parse, ParseStream}, 4 | Token, 5 | }; 6 | 7 | use crate::{ 8 | ast::{BracedKebabIdent, KebabIdentOrStr, Value}, 9 | parse::rollback_err, 10 | }; 11 | 12 | /// A special attribute like `on:click={...}`. 13 | /// 14 | /// # Examples 15 | /// ```ignore 16 | /// input type="checkbox" on:input={handle_input}; 17 | /// ^^^^^^^^^^^^^^^^^^^^^^^ 18 | /// button class:primary={primary} style:color="grey"; 19 | /// ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ 20 | /// ``` 21 | /// The shorthand syntax is also supported on the argument of directives: 22 | /// ```ignore 23 | /// button class:{primary} style:color="grey"; 24 | /// ``` 25 | /// 26 | /// If an extra `:modifier` is added, there will also be a modifier. 27 | /// ```ignore 28 | /// button on:click:undelegated={on_click}; 29 | /// ``` 30 | /// `on:{click}:undelegated` also works for the shorthand. 31 | #[derive(Clone)] 32 | pub struct Directive { 33 | pub(crate) dir: syn::Ident, 34 | pub(crate) key: KebabIdentOrStr, 35 | pub(crate) modifier: Option, // on:event:undelegated 36 | pub(crate) value: Option, 37 | } 38 | 39 | impl Parse for Directive { 40 | fn parse(input: ParseStream) -> syn::Result { 41 | let name = syn::Ident::parse_any(input)?; 42 | ::parse(input)?; 43 | 44 | let try_parse_modifier = |input| { 45 | rollback_err(input, ::parse) 46 | .is_some() 47 | .then(|| syn::Ident::parse_any(input)) 48 | .transpose() 49 | }; 50 | 51 | let key: KebabIdentOrStr; 52 | let value: Option; 53 | let modifier: Option; 54 | 55 | if input.peek(syn::token::Brace) { 56 | // on:{click}:undelegated 57 | let ident = BracedKebabIdent::parse(input)?; 58 | key = KebabIdentOrStr::KebabIdent(ident.ident().clone()); 59 | value = Some(ident.into_block_value()); 60 | modifier = try_parse_modifier(input)?; 61 | } else { 62 | // on:click:undelegated={on_click} 63 | key = KebabIdentOrStr::parse(input)?; 64 | modifier = try_parse_modifier(input)?; 65 | value = rollback_err(input, ::parse) 66 | .map(|eq| Value::parse_or_emit_err(input, eq.span)); 67 | }; 68 | 69 | Ok(Self { 70 | dir: name, 71 | key, 72 | modifier, 73 | value, 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/attribute/kv.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::{parse::Parse, Token}; 3 | 4 | use crate::{ 5 | ast::{BracedKebabIdent, KebabIdent, Value}, 6 | parse::rollback_err, 7 | span, 8 | }; 9 | 10 | /// A `key = value` type of attribute. 11 | /// 12 | /// This can either be a normal `key = value`, a shorthand `{key}`, or a 13 | /// boolean attribute `checked`. 14 | /// 15 | /// # Examples 16 | /// ```ignore 17 | /// input type="checkbox" data-index=1 checked; 18 | /// ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^ 19 | /// ``` 20 | /// Directives are not included. 21 | /// ```ignore 22 | /// input on:input={handle_input} type="text"; 23 | /// ^^^not included^^^^^^^^ ^included^^ 24 | /// ``` 25 | #[derive(Clone)] 26 | pub struct KvAttr { 27 | key: KebabIdent, 28 | value: Value, 29 | } 30 | 31 | impl KvAttr { 32 | pub const fn new(key: KebabIdent, value: Value) -> Self { Self { key, value } } 33 | 34 | pub const fn key(&self) -> &KebabIdent { &self.key } 35 | 36 | pub const fn value(&self) -> &Value { &self.value } 37 | 38 | pub fn span(&self) -> Span { span::join(self.key().span(), self.value().span()) } 39 | } 40 | 41 | impl Parse for KvAttr { 42 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 43 | let (ident, value) = if input.peek(syn::token::Brace) { 44 | let braced_ident = BracedKebabIdent::parse(input)?; 45 | ( 46 | braced_ident.ident().clone(), 47 | braced_ident.into_block_value(), 48 | ) 49 | } else { 50 | let ident = KebabIdent::parse(input)?; 51 | if let Some(eq) = rollback_err(input, ::parse) { 52 | let value = Value::parse_or_emit_err(input, eq.span); 53 | (ident, value) 54 | } else { 55 | let value = Value::new_true(); 56 | (ident, value) 57 | } 58 | }; 59 | 60 | Ok(Self::new(ident, value)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/attribute/selector.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | parse::{Parse, ParseStream}, 3 | Token, 4 | }; 5 | 6 | use crate::{ast::KebabIdent, parse::rollback_err}; 7 | 8 | /// A shorthand for adding class or ids to an element. 9 | /// 10 | /// Classes are added with a preceding `.`, ids with a `#`. 11 | /// 12 | /// # Example 13 | /// ```ignore 14 | /// div.a-class.small.big.big-big; 15 | /// ``` 16 | /// 17 | /// The `#` before the id needs a space before it due to 18 | /// [Reserving syntax](https://doc.rust-lang.org/edition-guide/rust-2021/reserving-syntax.html) 19 | /// since Rust 2021. 20 | /// ```ignore 21 | /// div #important .more-classes #another-id .claaass 22 | /// ``` 23 | #[derive(Clone)] 24 | pub enum SelectorShorthand { 25 | Id { 26 | pound_symbol: Token![#], 27 | id: KebabIdent, 28 | }, 29 | Class { 30 | dot_symbol: Token![.], 31 | class: KebabIdent, 32 | }, 33 | } 34 | 35 | impl SelectorShorthand { 36 | pub const fn ident(&self) -> &KebabIdent { 37 | match self { 38 | Self::Id { id, .. } => id, 39 | Self::Class { class, .. } => class, 40 | } 41 | } 42 | 43 | pub fn prefix(&self) -> proc_macro2::Punct { 44 | let (char, span) = match self { 45 | Self::Id { pound_symbol, .. } => ('#', pound_symbol.span), 46 | Self::Class { dot_symbol, .. } => ('.', dot_symbol.span), 47 | }; 48 | let mut punct = proc_macro2::Punct::new(char, proc_macro2::Spacing::Alone); 49 | punct.set_span(span); 50 | punct 51 | } 52 | 53 | // pub fn span(&self) -> Span { span::join(self.prefix().span(), 54 | // self.ident().span()) } 55 | } 56 | 57 | impl Parse for SelectorShorthand { 58 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 59 | if let Some(dot) = rollback_err(input, ::parse) { 60 | let class = KebabIdent::parse(input)?; 61 | Ok(Self::Class { 62 | dot_symbol: dot, 63 | class, 64 | }) 65 | } else if let Some(pound) = rollback_err(input, ::parse) { 66 | let id = KebabIdent::parse(input)?; 67 | Ok(Self::Id { 68 | pound_symbol: pound, 69 | id, 70 | }) 71 | } else { 72 | Err(input.error("no class or id shorthand found")) 73 | } 74 | } 75 | } 76 | 77 | #[derive(Clone, Default)] 78 | pub struct SelectorShorthands(Vec); 79 | 80 | impl std::ops::Deref for SelectorShorthands { 81 | type Target = [SelectorShorthand]; 82 | fn deref(&self) -> &Self::Target { &self.0 } 83 | } 84 | impl Parse for SelectorShorthands { 85 | fn parse(input: ParseStream) -> syn::Result { 86 | let mut vec = Vec::new(); 87 | while let Some(inner) = rollback_err(input, SelectorShorthand::parse) { 88 | vec.push(inner); 89 | } 90 | 91 | Ok(Self(vec)) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::{SelectorShorthand, SelectorShorthands}; 98 | 99 | #[derive(PartialEq, Eq)] 100 | enum SelectorKind { 101 | Class, 102 | Id, 103 | } 104 | 105 | #[test] 106 | fn multiple() { 107 | let stream = ".class.another-class #id #id2 .wow-class #ida"; 108 | let selectors: SelectorShorthands = syn::parse_str(stream).unwrap(); 109 | let result = [ 110 | (SelectorKind::Class, "class"), 111 | (SelectorKind::Class, "another-class"), 112 | (SelectorKind::Id, "id"), 113 | (SelectorKind::Id, "id2"), 114 | (SelectorKind::Class, "wow-class"), 115 | (SelectorKind::Id, "ida"), 116 | ] 117 | .into_iter(); 118 | for (selector, result) in selectors.iter().zip(result) { 119 | match selector { 120 | SelectorShorthand::Id { id, .. } => { 121 | assert!( 122 | result.0 == SelectorKind::Id, 123 | "{} should not be an id", 124 | id.repr() 125 | ); 126 | assert_eq!(result.1, id.repr()); 127 | } 128 | SelectorShorthand::Class { class, .. } => { 129 | assert!( 130 | result.0 == SelectorKind::Class, 131 | "{} should not be a class", 132 | class.repr() 133 | ); 134 | assert_eq!(result.1, class.repr()); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/attribute/spread_attrs.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use syn::{ 3 | parse::{Parse, ParseStream}, 4 | Token, 5 | }; 6 | 7 | use crate::parse::{extract_braced, rollback_err}; 8 | 9 | /// A spread attribute like `{..attrs}`. 10 | /// 11 | /// The spread after the `..` can be any expression. 12 | #[derive(Clone)] 13 | pub struct SpreadAttr { 14 | braces: syn::token::Brace, 15 | dotdot: Token![..], 16 | rest: TokenStream, 17 | } 18 | 19 | impl Parse for SpreadAttr { 20 | fn parse(input: ParseStream) -> syn::Result { 21 | // try parse spread attributes `{..attrs}` 22 | let (braces, stream) = extract_braced(input)?; 23 | 24 | if let Some(dotdot) = rollback_err(&stream, ::parse) { 25 | let rest = stream.parse::().unwrap(); 26 | 27 | Ok(Self { 28 | braces, 29 | dotdot, 30 | rest, 31 | }) 32 | } else { 33 | Err(input.error("invalid spread attribute")) 34 | } 35 | } 36 | } 37 | 38 | impl SpreadAttr { 39 | /// Returns the `..` in the spread attr 40 | pub const fn dotdot(&self) -> &Token![..] { &self.dotdot } 41 | 42 | /// Returns the expression after the `..`. 43 | pub const fn expr(&self) -> &TokenStream { &self.rest } 44 | 45 | /// Returns the span of the wrapping braces. 46 | pub fn span(&self) -> Span { self.braces.span.join() } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use syn::parse_quote; 52 | 53 | use super::SpreadAttr; 54 | 55 | #[test] 56 | fn compiles() { let _: SpreadAttr = parse_quote!({ ..a }); } 57 | } 58 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/children.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use proc_macro_error2::emit_error; 3 | use quote::{quote, ToTokens}; 4 | use syn::{ 5 | ext::IdentExt, 6 | parse::{Parse, ParseStream}, 7 | parse_quote, Token, 8 | }; 9 | 10 | use super::{Doctype, Element}; 11 | use crate::{ 12 | ast::Value, 13 | error_ext::SynErrorExt, 14 | kw, 15 | parse::{self, rollback_err}, 16 | }; 17 | 18 | /// A child that is an actual HTML value (i.e. not a slot). 19 | /// 20 | /// Use [`Child`] to try and parse these. 21 | pub enum NodeChild { 22 | Value(Value), 23 | Element(Element), 24 | Doctype(Doctype), 25 | } 26 | 27 | impl ToTokens for NodeChild { 28 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 29 | let child_tokens = match self { 30 | Self::Value(v) => v.into_token_stream(), 31 | Self::Element(e) => e.into_token_stream(), 32 | Self::Doctype(d) => d.into_token_stream(), 33 | }; 34 | tokens.extend(quote! { 35 | #child_tokens 36 | }); 37 | } 38 | } 39 | 40 | impl NodeChild { 41 | pub fn span(&self) -> Span { 42 | match self { 43 | Self::Value(v) => v.span(), 44 | Self::Element(e) => e.tag().span(), 45 | Self::Doctype(d) => d.span(), 46 | } 47 | } 48 | } 49 | 50 | /// Possible child items inside a component. 51 | /// 52 | /// If the child is a `Value::Lit`, this lit must be a string. Parsing will 53 | /// abort if the lit is not a string. 54 | /// 55 | /// Children can either be a [`NodeChild`] (i.e. an actual element), or a slot. 56 | /// Slots are distinguished by prefixing the child with `slot:`. 57 | pub enum Child { 58 | Node(NodeChild), 59 | Slot(kw::slot, Element), 60 | } 61 | 62 | impl Parse for Child { 63 | fn parse(input: ParseStream) -> syn::Result { 64 | if let Some(value) = rollback_err(input, Value::parse) { 65 | // only allow literals if they are a string. 66 | if let Value::Lit(ref lit) = value { 67 | if let syn::Lit::Str(_) = lit { 68 | Ok(Self::Node(NodeChild::Value(value))) 69 | } else { 70 | emit_error!(lit.span(), "only string literals are allowed in children"); 71 | Ok(Self::Node(NodeChild::Value(Value::Lit(parse_quote!(""))))) 72 | } 73 | } else { 74 | Ok(Self::Node(NodeChild::Value(value))) 75 | } 76 | // parse slot: make sure its not a qualified path (slot::) 77 | } else if input.peek(kw::slot) && input.peek2(Token![:]) && !input.peek2(Token![::]) { 78 | let slot = kw::slot::parse(input).unwrap(); 79 | ::parse(input).unwrap(); 80 | let elem = Element::parse(input)?; 81 | Ok(Self::Slot(slot, elem)) 82 | } else if input.peek(syn::Ident::peek_any) { 83 | let elem = Element::parse(input)?; 84 | Ok(Self::Node(NodeChild::Element(elem))) 85 | } else if let Some(doctype) = rollback_err(input, Doctype::parse) { 86 | Ok(Self::Node(NodeChild::Doctype(doctype))) 87 | } else { 88 | Err(input.error("invalid child: expected literal, block, bracket or element")) 89 | } 90 | } 91 | } 92 | 93 | /// A space-separated series of children. 94 | /// 95 | /// Parsing does not include the surrounding braces. 96 | /// If no children are present, an empty vector will be stored. 97 | pub struct Children(Vec); 98 | 99 | impl std::ops::Deref for Children { 100 | type Target = [Child]; 101 | fn deref(&self) -> &Self::Target { &self.0 } 102 | } 103 | 104 | impl Parse for Children { 105 | fn parse(input: ParseStream) -> syn::Result { 106 | let mut vec = Vec::new(); 107 | 108 | loop { 109 | if input.is_empty() { 110 | break; 111 | } 112 | match Child::parse(input) { 113 | Ok(child) => vec.push(child), 114 | Err(e) => { 115 | if input.peek(Token![;]) { 116 | // an extra semi-colon: just skip it and keep parsing 117 | emit_error!( 118 | e.span(), "extra semi-colon found"; 119 | help="remove this semi-colon" 120 | ); 121 | ::parse(input).unwrap(); 122 | } else { 123 | e.emit_as_error(); 124 | // skip the rest of the tokens 125 | // need to consume all tokens otherwise an error is made on drop 126 | parse::take_rest(input); 127 | } 128 | } 129 | }; 130 | } 131 | 132 | Ok(Self(vec)) 133 | } 134 | } 135 | 136 | impl Children { 137 | pub fn into_vec(self) -> Vec { self.0 } 138 | 139 | /// Returns an iterator of all children that are not slots. 140 | pub fn node_children(&self) -> impl Iterator { 141 | self.0.iter().filter_map(|child| match child { 142 | Child::Node(node) => Some(node), 143 | Child::Slot(..) => None, 144 | }) 145 | } 146 | 147 | /// Returns an iterator of all children that are slots. 148 | pub fn slot_children(&self) -> impl Iterator { 149 | self.0.iter().filter_map(|child| match child { 150 | Child::Node(_) => None, 151 | Child::Slot(_, elem) => Some(elem), 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/doctype.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use proc_macro_error2::emit_error; 3 | use quote::{quote, quote_spanned, ToTokens}; 4 | use syn::{ 5 | parse::{Parse, ParseStream}, 6 | Token, 7 | }; 8 | 9 | use crate::{parse::rollback_err, span}; 10 | 11 | /// The `!DOCTYPE html;` element. 12 | /// 13 | /// This will successfully parse as soon as a `!` is found at a child position. 14 | /// If the rest is not given, errors will be shown with hints on how to complete 15 | /// it. 16 | pub struct Doctype { 17 | bang: Token![!], 18 | doctype: Option, 19 | html: Option, 20 | semi: Option, 21 | } 22 | 23 | impl Doctype { 24 | pub fn span(&self) -> Span { 25 | let last_tok = self 26 | .semi 27 | .map(|s| s.span) 28 | .or(self.html.as_ref().map(|h| h.span())) 29 | .or(self.doctype.as_ref().map(|d| d.span())) 30 | .unwrap_or(self.bang.span); 31 | 32 | span::join(self.bang.span, last_tok) 33 | } 34 | } 35 | 36 | impl Parse for Doctype { 37 | fn parse(input: ParseStream) -> syn::Result { 38 | let Some(bang) = rollback_err(input, ::parse) else { 39 | return Err(input.error("expected ! to start DOCTYPE")); 40 | }; 41 | 42 | Ok(Self { 43 | bang, 44 | doctype: rollback_err(input, syn::Ident::parse), 45 | html: rollback_err(input, syn::Ident::parse), 46 | semi: rollback_err(input, ::parse), 47 | }) 48 | } 49 | } 50 | 51 | impl ToTokens for Doctype { 52 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 53 | let doctype_span = self 54 | .doctype 55 | .as_ref() 56 | .map(|d| d.span()) 57 | .unwrap_or(self.bang.span); 58 | let html_span = self.html.as_ref().map(|h| h.span()).unwrap_or(doctype_span); 59 | 60 | if self 61 | .doctype 62 | .as_ref() 63 | .is_none_or(|d| d.to_string() != "DOCTYPE") 64 | { 65 | emit_error!( 66 | doctype_span, 67 | "expected `DOCTYPE` after `!`"; 68 | help = "Add `!DOCTYPE html;`" 69 | ); 70 | } else if self.html.as_ref().is_none_or(|h| h.to_string() != "html") { 71 | emit_error!( 72 | html_span, 73 | "expected `html` after `!DOCTYPE`"; 74 | help = "Add `!DOCTYPE html;`" 75 | ); 76 | } else if self.semi.is_none() { 77 | emit_error!( 78 | html_span, 79 | "expected `;` after `!DOCTYPE html`"; 80 | help = "Add `!DOCTYPE html;`" 81 | ) 82 | } 83 | 84 | let doctype_fn = quote_spanned!(doctype_span=> doctype); 85 | let eq = quote_spanned!(self.bang.span=> =); 86 | // there will never be an error on these so call site is fine 87 | let partial_doctype = self 88 | .doctype 89 | .clone() 90 | .unwrap_or(syn::Ident::new("DOCTYPE", Span::call_site())); 91 | let partial_html = self 92 | .html 93 | .clone() 94 | .unwrap_or(syn::Ident::new("html", Span::call_site())); 95 | 96 | // don't span "html" so they aren't string colored. 97 | // "html" can't have an error anyways. 98 | tokens.extend(quote! { 99 | { 100 | // suggest autocomplete DOCTYPE and html 101 | #[allow(non_snake_case)] 102 | let DOCTYPE = (); 103 | let html = (); 104 | let _: () #eq #partial_doctype; 105 | let _: () = #partial_html; 106 | ::leptos::tachys::html::#doctype_fn("html") 107 | } 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/element.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{TokenStream, TokenTree}; 2 | use proc_macro_error2::emit_error; 3 | use quote::{ToTokens, TokenStreamExt}; 4 | use syn::{ 5 | parse::{Parse, ParseStream}, 6 | Token, 7 | }; 8 | 9 | use super::{attribute::selector::SelectorShorthands, Attrs, Children, Tag}; 10 | use crate::{ 11 | expand::{component_to_tokens, xml_to_tokens}, 12 | parse::{self, rollback_err}, 13 | span, 14 | }; 15 | 16 | /// A HTML or custom component. 17 | /// 18 | /// Consists of: 19 | /// 1. [`tag`](Tag): The HTML/SVG/MathML element name, or leptos component name. 20 | /// 2. [`selectors`](SelectorShorthands): Shortcut ways of writing `class="..."` 21 | /// or `id="..."`. A list of classes or ids prefixed with a `.` or `#` 22 | /// respectively. 23 | /// 3. [`attrs`](Attrs): A space-separated list of attributes. 24 | /// 4. [`children_args`](TokenStream): Optional arguments for the children, 25 | /// placed in closure pipes `|...|` immediately before the children block. 26 | /// The closure pipes **are included** in the stored [`TokenStream`]. 27 | /// 5. [`children`](Children): Either no children (ends with `;`) or a children 28 | /// block `{ ... }` that contains more elements/values. 29 | /// 30 | /// Syntax mostly looks like this: 31 | /// ```text 32 | /// div class="blue" { "hello!" } 33 | /// ^^^ ^^^^^^^^^^^^ ^^^^^^^^ 34 | /// tag attributes children 35 | /// ``` 36 | /// 37 | /// If the element ends in a semicolon, `children` is `None`. 38 | /// ```text 39 | /// input type="text"; 40 | /// br; 41 | /// ``` 42 | /// 43 | /// Whether the element is a slot or not is distinguished by 44 | /// [`Child`](crate::ast::Child). 45 | pub struct Element { 46 | tag: Tag, 47 | selectors: SelectorShorthands, 48 | attrs: Attrs, 49 | children_args: Option, 50 | children: Option, 51 | } 52 | 53 | impl Parse for Element { 54 | fn parse(input: ParseStream) -> syn::Result { 55 | let tag = Tag::parse(input)?; 56 | let selectors = SelectorShorthands::parse(input)?; 57 | let attrs = Attrs::parse(input)?; 58 | 59 | if rollback_err(input, ::parse).is_some() { 60 | // no children, terminated by semicolon. 61 | Ok(Self::new(tag, selectors, attrs, None, None)) 62 | } else if input.is_empty() { 63 | // allow no ending token if its the last child 64 | // makes for better editing experience when writing sequentially, 65 | // as syntax highlighting/autocomplete doesn't work if macro 66 | // can't fully compile. 67 | emit_error!( 68 | tag.span(), "unterminated element"; 69 | help = "add a `;` to terminate the element with no children" 70 | ); 71 | Ok(Self::new(tag, selectors, attrs, None, None)) 72 | } else if input.peek(syn::token::Brace) || input.peek(syn::token::Paren) { 73 | let children = if input.peek(syn::token::Brace) { 74 | parse::braced::(input)?.1 75 | } else { 76 | parse::parenthesized::(input)?.1 77 | }; 78 | 79 | Ok(Self::new(tag, selectors, attrs, None, Some(children))) 80 | } else if input.peek(Token![|]) { 81 | // extra args for the children 82 | let args = parse_closure_args(input)?; 83 | let children = if input.peek(syn::token::Brace) { 84 | Some(parse::braced::(input)?.1) 85 | } else if input.peek(syn::token::Paren) { 86 | Some(parse::parenthesized::(input)?.1) 87 | } else { 88 | // continue trying to parse as if there are no children 89 | emit_error!( 90 | input.span(), 91 | "expected children block after closure arguments" 92 | ); 93 | None 94 | }; 95 | Ok(Self::new(tag, selectors, attrs, Some(args), children)) 96 | } else { 97 | // add error at the unknown token 98 | // continue trying to parse as if there are no children 99 | emit_error!(input.span(), "unknown attribute"); 100 | emit_error!( 101 | span::join(tag.span(), input.span()), "child elements not found"; 102 | help = "add a `;` at the end to terminate the element" 103 | ); 104 | Ok(Self::new(tag, selectors, attrs, None, None)) 105 | } 106 | } 107 | } 108 | 109 | impl ToTokens for Element { 110 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 111 | tokens.extend(xml_to_tokens(self).unwrap_or_else(|| { 112 | component_to_tokens::(self).expect("element should be a component") 113 | })); 114 | } 115 | } 116 | 117 | impl Element { 118 | pub const fn new( 119 | tag: Tag, 120 | selectors: SelectorShorthands, 121 | attrs: Attrs, 122 | children_args: Option, 123 | children: Option, 124 | ) -> Self { 125 | Self { 126 | tag, 127 | selectors, 128 | attrs, 129 | children_args, 130 | children, 131 | } 132 | } 133 | 134 | pub const fn tag(&self) -> &Tag { &self.tag } 135 | 136 | pub const fn selectors(&self) -> &SelectorShorthands { &self.selectors } 137 | 138 | pub const fn attrs(&self) -> &Attrs { &self.attrs } 139 | 140 | pub const fn children_args(&self) -> Option<&TokenStream> { self.children_args.as_ref() } 141 | 142 | pub const fn children(&self) -> Option<&Children> { self.children.as_ref() } 143 | } 144 | 145 | /// Parses closure arguments like `|binding|` or `|(index, item)|`. 146 | /// 147 | /// Patterns are supported within the closure. 148 | /// 149 | /// # Parsing 150 | /// If the first pipe is not found, an [`Err`] will be returned. Otherwise, 151 | /// tokens are parsed until a second `|` is found. Errors if a second `|` is not 152 | /// found. 153 | /// 154 | /// This is ok because closure params take a 155 | /// [*PatternNoTopAlt*](https://doc.rust-lang.org/beta/reference/expressions/closure-expr.html), 156 | /// so no other `|` characters are allowed within a pattern that is outside of a 157 | /// nested group. 158 | fn parse_closure_args(input: ParseStream) -> syn::Result { 159 | let first_pipe = ::parse(input)?; 160 | 161 | let mut tokens = TokenStream::new(); 162 | first_pipe.to_tokens(&mut tokens); 163 | 164 | loop { 165 | // parse until second `|` is found 166 | if let Some(pipe) = rollback_err(input, ::parse) { 167 | pipe.to_tokens(&mut tokens); 168 | break Ok(tokens); 169 | } else if let Some(tt) = rollback_err(input, TokenTree::parse) { 170 | tokens.append(tt); 171 | } else { 172 | break Err(syn::Error::new_spanned( 173 | first_pipe, 174 | "closure arguments not closed", 175 | )); 176 | } 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | 183 | use super::Element; 184 | 185 | #[test] 186 | fn full_element() { 187 | let input = r#"div class="test" checked data-index=[index] { "child" span { "child2" } }"#; 188 | let element: Element = syn::parse_str(input).unwrap(); 189 | assert_eq!(element.attrs().len(), 3); 190 | assert!(element.children().is_some()); 191 | assert_eq!(element.children().unwrap().len(), 2); 192 | } 193 | 194 | #[test] 195 | fn no_child_element() { 196 | let input = r#"input type="text";"#; 197 | let element: Element = syn::parse_str(input).unwrap(); 198 | assert_eq!(element.attrs().len(), 1); 199 | assert!(element.children().is_none()); 200 | } 201 | 202 | #[test] 203 | fn no_child_or_attrs() { 204 | let input = "br;"; 205 | let element: Element = syn::parse_str(input).unwrap(); 206 | assert_eq!(element.attrs().len(), 0); 207 | assert!(element.children.is_none()); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/ident.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use proc_macro_error2::emit_error; 3 | use quote::{quote, ToTokens}; 4 | use syn::{ 5 | ext::IdentExt, 6 | parse::{Parse, ParseStream}, 7 | token::Brace, 8 | Token, 9 | }; 10 | 11 | use super::Value; 12 | use crate::{ 13 | parse::{self, rollback_err}, 14 | span, 15 | }; 16 | 17 | /// A kebab-cased identifier. 18 | /// 19 | /// The identifier must start with a letter, underscore or dash. The rest of 20 | /// the identifier can have numbers as well. Rust keywords are also allowed. 21 | /// 22 | /// Because whitespace is ignored in macros, and a dash is usually interpreted 23 | /// as subtraction, spaces between each segment is allowed but will be ignored. 24 | /// 25 | /// Valid [`KebabIdent`]s include `one`, `two-bits`, `--css-variable`, 26 | /// `blue-100`, `-0`, `--a---b_c`, `_a`; but does not include `3d-thing`. 27 | /// 28 | /// Equality and hashing are implemented and only based on the repr, not the 29 | /// spans. 30 | /// 31 | /// # Parsing 32 | /// If the next token is not a `-` or ident, an [`Err`] is returned and the 33 | /// [`ParseStream`] is not advanced. Otherwise, parsing will stop once the ident 34 | /// ends, and the `ParseStream` is advanced to after this kebab-ident. 35 | /// 36 | /// # Invariants 37 | /// The [`repr`](Self::repr) and [`spans`](Self::spans) fields are not empty. To 38 | /// construct a new [`KebabIdent`], use the [`From`] 39 | /// implementation or parse one with the [`Parse`] implementation. 40 | #[derive(Clone)] 41 | pub struct KebabIdent { 42 | repr: String, 43 | spans: Vec, 44 | } 45 | 46 | impl KebabIdent { 47 | /// Returns the string representation of the identifier, in kebab-case. 48 | /// 49 | /// This is not a raw identifier, i.e. it does not start with "r#". 50 | pub fn repr(&self) -> &str { self.repr.as_ref() } 51 | 52 | /// Returns the span of this [`KebabIdent`]. 53 | /// 54 | /// The span of the first and last 'section' (dash, ident or lit int) are 55 | /// joined. This only works on nightly, so only the first section's span is 56 | /// returned on stable. 57 | pub fn span(&self) -> Span { 58 | span::join( 59 | self.spans[0], 60 | *self.spans.last().expect("kebab ident should not be empty"), 61 | ) 62 | } 63 | 64 | /// Returns an iterator of every span in this [`KebabIdent`]. 65 | /// 66 | /// Spans usually need to be owned, so an iterator that produces owned spans 67 | /// is returned. 68 | pub fn spans(&self) -> impl ExactSizeIterator + '_ { self.spans.iter().copied() } 69 | 70 | /// Converts this ident to a `syn::LitStr` of the ident's repr with the 71 | /// appropriate span. 72 | pub fn to_lit_str(&self) -> syn::LitStr { syn::LitStr::new(self.repr(), self.span()) } 73 | 74 | /// Expands this ident to its string literal, along with dummy items to make 75 | /// each segment the same color as a variable. 76 | /// 77 | /// **NOTE:** The string itself won't be spanned to this [`KebabIdent`]. 78 | /// Make sure that where this is used will always take a string and never 79 | /// errors. 80 | /// 81 | /// The [`TokenStream`] returned is a block expression, so make sure that 82 | /// blocks can be used in the context where this is expanded. 83 | pub fn to_str_colored(&self) -> TokenStream { 84 | let dummy_items = span::color_all(self.spans()); 85 | let string = self.repr(); 86 | quote! { 87 | {#(#dummy_items)* #string} 88 | } 89 | } 90 | 91 | /// Converts this ident to a `syn::Ident` with the appropriate span, by 92 | /// replacing all `-`s with `_`. 93 | /// 94 | /// The span will only be the first 'section' on stable, but correctly 95 | /// covers the full ident on nightly. See [`KebabIdent::span`] for more 96 | /// details. 97 | /// 98 | /// The ident will also be a raw identifier. 99 | pub fn to_snake_ident(&self) -> syn::Ident { 100 | let snake_string = self.repr().replace('-', "_"); 101 | // This will always be valid as the first 'section' must be a `-` or rust ident, 102 | // which means it starts with `_` or another valid identifier beginning. The int 103 | // literals within the ident (e.g. between `-`s, like `blue-100`) are allowed 104 | // since the ident does not start with a number. 105 | syn::Ident::new_raw(&snake_string, self.span()) 106 | } 107 | } 108 | 109 | impl Parse for KebabIdent { 110 | fn parse(input: ParseStream) -> syn::Result { 111 | let mut repr = String::new(); 112 | let mut spans = Vec::new(); 113 | 114 | // Start with `-` or letter. 115 | if let Some(ident) = rollback_err(input, syn::Ident::parse_any) { 116 | // only store the non-raw representation: in expansion, 117 | // this should expand to a raw ident. 118 | repr.push_str(&ident.unraw().to_string()); 119 | spans.push(ident.span()); 120 | } else if let Some(dash) = rollback_err(input, ::parse) { 121 | repr.push('-'); 122 | spans.push(dash.span); 123 | } else { 124 | return Err(input.error("expected a kebab-cased ident")); 125 | }; 126 | 127 | // Whether we are parsing the second token now. 128 | // Can't just check if `repr == "-"` as it will cause an infinite 129 | // loop if the ident is only `-`. 130 | let mut is_second_token = true; 131 | 132 | // Parse any `-` and idents. 133 | loop { 134 | // After every loop, the next ident should be a `-`. 135 | // Otherwise, this means it was two idents separated by a space, 136 | // e.g. `one two`. 137 | if rollback_err(input, ::parse).is_some() { 138 | repr.push('-'); 139 | } else if !(is_second_token && repr == "-") { 140 | // unless the ident starts with a single `-`, then the next 141 | // token can be an ident or number. 142 | break; 143 | } 144 | 145 | is_second_token = false; 146 | 147 | // add ident or number 148 | if let Some(ident) = rollback_err(input, syn::Ident::parse_any) { 149 | let unraw = ident.unraw(); 150 | if ident != unraw { 151 | emit_error!(ident.span(), "invalid raw identifier within kebab-ident"); 152 | } 153 | repr.push_str(&unraw.to_string()); 154 | spans.push(ident.span()); 155 | } else if let Some(int) = rollback_err(input, syn::LitInt::parse) { 156 | repr.push_str(&int.to_string()); 157 | spans.push(int.span()); 158 | }; 159 | } 160 | 161 | // both repr and spans are not empty due to the first-segment check 162 | Ok(Self { repr, spans }) 163 | } 164 | } 165 | 166 | impl From for KebabIdent { 167 | fn from(value: proc_macro2::Ident) -> Self { 168 | // repr is not empty as `proc_macro2::Ident` must be a valid Rust identifier, 169 | // and "" is not. 170 | Self { 171 | repr: value.unraw().to_string(), 172 | spans: vec![value.span()], 173 | } 174 | } 175 | } 176 | 177 | // eq and hash are only based on the repr 178 | 179 | impl PartialEq for KebabIdent { 180 | fn eq(&self, other: &Self) -> bool { self.repr == other.repr } 181 | } 182 | 183 | impl Eq for KebabIdent {} 184 | 185 | // Parse either a kebab-case ident or a str literal. 186 | #[derive(Clone)] 187 | pub enum KebabIdentOrStr { 188 | KebabIdent(KebabIdent), 189 | Str(syn::LitStr), 190 | } 191 | 192 | impl KebabIdentOrStr { 193 | pub fn to_lit_str(&self) -> syn::LitStr { 194 | match self { 195 | Self::KebabIdent(ident) => ident.to_lit_str(), 196 | Self::Str(s) => s.clone(), 197 | } 198 | } 199 | 200 | pub fn to_ident_or_emit(&self) -> syn::Ident { 201 | match self { 202 | Self::KebabIdent(i) => i.to_snake_ident(), 203 | Self::Str(s) => { 204 | emit_error!(s.span(), "expected identifier"); 205 | syn::Ident::new("__invalid_identifier_found_str", s.span()) 206 | } 207 | } 208 | } 209 | 210 | pub fn to_unspanned_string(&self) -> String { 211 | match self { 212 | Self::KebabIdent(kebab_ident) => kebab_ident.repr().to_string(), 213 | Self::Str(lit_str) => lit_str.value(), 214 | } 215 | } 216 | } 217 | 218 | impl Parse for KebabIdentOrStr { 219 | fn parse(input: ParseStream) -> syn::Result { 220 | if let Some(str) = rollback_err(input, |input| ::parse(input)) { 221 | Ok(Self::Str(str)) 222 | } else { 223 | Ok(Self::KebabIdent(KebabIdent::parse(input)?)) 224 | } 225 | } 226 | } 227 | 228 | /// Parses a braced kebab-cased ident like `{abc-123}` 229 | /// 230 | /// Equivalent to `parse::braced::(input)`, but provides a few 231 | /// methods to help with conversions. 232 | pub struct BracedKebabIdent { 233 | brace_token: Brace, 234 | ident: KebabIdent, 235 | } 236 | 237 | impl BracedKebabIdent { 238 | pub const fn new(brace: Brace, ident: KebabIdent) -> Self { 239 | Self { 240 | brace_token: brace, 241 | ident, 242 | } 243 | } 244 | 245 | pub const fn ident(&self) -> &KebabIdent { &self.ident } 246 | 247 | pub fn into_block_value(self) -> Value { 248 | Value::Block { 249 | tokens: self.ident.to_snake_ident().into_token_stream(), 250 | braces: self.brace_token, 251 | } 252 | } 253 | } 254 | 255 | impl Parse for BracedKebabIdent { 256 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 257 | let (brace, ident) = parse::braced::(input)?; 258 | Ok(Self::new(brace, ident)) 259 | } 260 | } 261 | 262 | #[cfg(test)] 263 | mod tests { 264 | use std::iter; 265 | 266 | use super::KebabIdent; 267 | 268 | #[test] 269 | fn valid_reprs() { 270 | let streams = [ 271 | "word", 272 | "two-words", 273 | "--var-abc", 274 | "-a-b", 275 | "let--a", 276 | "struct-b-", 277 | "blue-100", 278 | "blue-100a", 279 | "number-0xa1b2", 280 | "-", 281 | "-_-_a", 282 | "for", 283 | ]; 284 | 285 | for stream in streams { 286 | let ident: KebabIdent = syn::parse_str(stream).unwrap(); 287 | assert_eq!(ident.repr(), stream) 288 | } 289 | } 290 | 291 | #[test] 292 | fn invalid_reprs() { 293 | let streams = ["data-thing- =", "distinct idents"]; 294 | 295 | for stream in streams { 296 | let ident = syn::parse_str::(stream); 297 | assert!(ident.is_err()); 298 | } 299 | } 300 | 301 | #[test] 302 | fn different_reprs() { 303 | let streams = ["two - words", "- - a - b"]; 304 | 305 | for stream in streams { 306 | let ident = syn::parse_str::(stream).unwrap(); 307 | assert_eq!(ident.repr(), stream.replace(' ', "")); 308 | } 309 | } 310 | 311 | #[test] 312 | fn raw() { 313 | let raws = ["r#move", "move", "r#some-thing"]; 314 | let results = ["move", "move", "some-thing"]; 315 | for (stream, res) in iter::zip(raws, results) { 316 | let ident = syn::parse_str::(stream).unwrap(); 317 | assert_eq!(ident.repr(), res); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/tag.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::{ 3 | ext::IdentExt, 4 | parse::{Parse, ParseStream}, 5 | spanned::Spanned, 6 | Token, 7 | }; 8 | 9 | use crate::ast::KebabIdent; 10 | 11 | #[allow(clippy::doc_markdown)] 12 | /// The name of the element, like `div`, `path`, `For`, `leptos-island`, etc. 13 | /// 14 | /// All tags except web-components are parsed as a [`syn::Ident`]. 15 | /// Whether elements are an HTML, SVG or MathML tag is based on a list: SVG and 16 | /// MathML are searched for first, everything else is considered to be an HTML 17 | /// element. 18 | /// 19 | /// All web-components have a `-` in them, so they are parsed as a 20 | /// [`KebabIdent`]. 21 | /// 22 | /// All leptos components are in `UpperCamelCase`, so any tags 23 | /// that start with a capital letter are considered components. Generics are 24 | /// supported and stored in this enum, if there are any after a leptos 25 | /// component. Turbofish syntax (`Component::`) is not used, the generic is 26 | /// placed directly after (`Component`). 27 | /// 28 | /// See [`TagKind`] for a discriminant-only version of this enum. 29 | pub enum Tag { 30 | Html(syn::Ident), 31 | /// The generic will contain a leading `::`. 32 | Component(syn::Path), 33 | Svg(syn::Ident), 34 | Math(syn::Ident), 35 | WebComponent(KebabIdent), 36 | } 37 | 38 | impl Tag { 39 | /// Returns the [`Span`] of the tag identifier. 40 | /// 41 | /// Component generics are not included in this span. 42 | pub fn span(&self) -> Span { 43 | match self { 44 | Self::Html(ident) | Self::Svg(ident) | Self::Math(ident) => ident.span(), 45 | Self::WebComponent(ident) => ident.span(), 46 | Self::Component(path) => path.span(), 47 | } 48 | } 49 | 50 | /// Returns the [`TagKind`] of this tag. 51 | pub fn kind(&self) -> TagKind { 52 | match self { 53 | Tag::Html(_) => TagKind::Html, 54 | Tag::Component(_) => TagKind::Component, 55 | Tag::Svg(_) => TagKind::Svg, 56 | Tag::Math(_) => TagKind::Math, 57 | Tag::WebComponent(_) => TagKind::WebComponent, 58 | } 59 | } 60 | } 61 | 62 | impl Parse for Tag { 63 | fn parse(input: ParseStream) -> syn::Result { 64 | // peek 1 in case it's a leading :: 65 | // this will also include any generics 66 | // also look for generics without a full path 67 | if input.peek2(Token![::]) || input.peek(Token![::]) || input.peek2(Token![<]) { 68 | // this is a path segment: must be a component 69 | let path = syn::Path::parse(input)?; 70 | return Ok(Self::Component(path)); 71 | } 72 | 73 | let ident = KebabIdent::parse(input)?; 74 | let kind = TagKind::from(ident.repr()); 75 | Ok(match kind { 76 | TagKind::Html => Self::Html(ident.to_snake_ident()), 77 | TagKind::Component => Self::Component(syn::Path::from(ident.to_snake_ident().unraw())), 78 | TagKind::Svg => Self::Svg(ident.to_snake_ident()), 79 | TagKind::Math => Self::Math(ident.to_snake_ident()), 80 | TagKind::WebComponent => Self::WebComponent(ident), 81 | }) 82 | } 83 | } 84 | 85 | /// Discriminant-only enum for [`Tag`]. 86 | #[derive(Debug, PartialEq, Eq)] 87 | pub enum TagKind { 88 | Html, 89 | Component, 90 | Svg, 91 | Math, 92 | WebComponent, 93 | } 94 | 95 | impl From<&str> for TagKind { 96 | /// Figures out the kind of element the provided tag is. 97 | /// 98 | /// The [`&str`](str) passed in should be a valid tag identifier, i.e. a 99 | /// valid Rust ident or [`KebabIdent`]. 100 | fn from(value: &str) -> Self { 101 | if is_component(value) { 102 | Self::Component 103 | } else if is_svg_element(value) { 104 | Self::Svg 105 | } else if is_web_component(value) { 106 | Self::WebComponent 107 | } else if is_math_ml_element(value) { 108 | Self::Math 109 | } else { 110 | Self::Html 111 | } 112 | } 113 | } 114 | 115 | /// Whether the tag is a leptos component. 116 | /// 117 | /// Checks if the first character is uppercase. 118 | /// 119 | /// The [`&str`](str) passed in should be a valid tag identifier, i.e. a 120 | /// valid Rust ident or [`KebabIdent`]. 121 | #[rustfmt::skip] 122 | pub fn is_component(tag: &str) -> bool { 123 | tag.starts_with(|c: char| c.is_ascii_uppercase()) 124 | } 125 | 126 | /// Whether the tag is an SVG element. 127 | /// 128 | /// Checks based on a list. 129 | pub fn is_svg_element(tag: &str) -> bool { 130 | [ 131 | "animate", 132 | "animateMotion", 133 | "animateTransform", 134 | "circle", 135 | "clipPath", 136 | "defs", 137 | "desc", 138 | "discard", 139 | "ellipse", 140 | "feBlend", 141 | "feColorMatrix", 142 | "feComponentTransfer", 143 | "feComposite", 144 | "feConvolveMatrix", 145 | "feDiffuseLighting", 146 | "feDisplacementMap", 147 | "feDistantLight", 148 | "feDropShadow", 149 | "feFlood", 150 | "feFuncA", 151 | "feFuncB", 152 | "feFuncG", 153 | "feFuncR", 154 | "feGaussianBlur", 155 | "feImage", 156 | "feMerge", 157 | "feMergeNode", 158 | "feMorphology", 159 | "feOffset", 160 | "fePointLight", 161 | "feSpecularLighting", 162 | "feSpotLight", 163 | "feTile", 164 | "feTurbulence", 165 | "filter", 166 | "foreignObject", 167 | "g", 168 | "hatch", 169 | "hatchpath", 170 | "image", 171 | "line", 172 | "linearGradient", 173 | "marker", 174 | "mask", 175 | "metadata", 176 | "mpath", 177 | "path", 178 | "pattern", 179 | "polygon", 180 | "polyline", 181 | "radialGradient", 182 | "rect", 183 | "set", 184 | "stop", 185 | "svg", 186 | "switch", 187 | "symbol", 188 | "text", 189 | "textPath", 190 | "tspan", 191 | "use", 192 | "use_", 193 | "view", 194 | ] 195 | .binary_search(&tag) 196 | .is_ok() 197 | } 198 | 199 | /// Whether the tag is an SVG element. 200 | /// 201 | /// Checks based on a list. 202 | fn is_math_ml_element(tag: &str) -> bool { 203 | [ 204 | "annotation", 205 | "maction", 206 | "math", 207 | "menclose", 208 | "merror", 209 | "mfenced", 210 | "mfrac", 211 | "mi", 212 | "mmultiscripts", 213 | "mn", 214 | "mo", 215 | "mover", 216 | "mpadded", 217 | "mphantom", 218 | "mprescripts", 219 | "mroot", 220 | "mrow", 221 | "ms", 222 | "mspace", 223 | "msqrt", 224 | "mstyle", 225 | "msub", 226 | "msubsup", 227 | "msup", 228 | "mtable", 229 | "mtd", 230 | "mtext", 231 | "mtr", 232 | "munder", 233 | "munderover", 234 | "semantics", 235 | ] 236 | .binary_search(&tag) 237 | .is_ok() 238 | } 239 | 240 | /// Whether the tag is a web-component. 241 | /// 242 | /// The [`&str`](str) passed in should be a valid tag identifier, i.e. a 243 | /// valid Rust ident or [`KebabIdent`]. 244 | /// 245 | /// Returns `true` if the tag contains a `-` as all web-components require a 246 | /// `-`. 247 | pub fn is_web_component(tag: &str) -> bool { tag.contains('-') } 248 | -------------------------------------------------------------------------------- /leptos-mview-core/src/ast/value.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use proc_macro_error2::{emit_error, Diagnostic}; 3 | use quote::{quote, quote_spanned, ToTokens}; 4 | use syn::{ 5 | ext::IdentExt, 6 | parse::{Parse, ParseStream}, 7 | parse_quote, 8 | spanned::Spanned, 9 | }; 10 | 11 | use crate::parse::{self, rollback_err}; 12 | 13 | /// Interpolated Rust expressions within the macro. 14 | /// 15 | /// Block expressions like `{move || !is_red.get()}` are placed as so. 16 | /// 17 | /// Expressions within brackets are wrapped in a closure, e.g. `[!is_red.get()]` 18 | /// is expanded to `{move || !is_red.get()}`. 19 | /// 20 | /// Only literals can have no delimiter, to avoid ambiguity. 21 | /// 22 | /// Block and bracketed expressions are not parsed as [`syn::Expr`]s as the 23 | /// specific details of what is contained is not required (they are expanded 24 | /// as-is). Instead, a plain [`TokenStream`] is taken, which allows for invalid 25 | /// expressions. rust-analyzer can produce errors at the correct span using this 26 | /// `TokenStream`, and provides better autocompletion (e.g. when looking for 27 | /// methods by entering `something.`). 28 | #[derive(Clone)] 29 | pub enum Value { 30 | Lit(syn::Lit), 31 | // take a raw `TokenStream` instead of ExprBlock/etc for better r-a support 32 | // as invalid expressions aren't completely rejected 33 | Block { 34 | tokens: TokenStream, 35 | braces: syn::token::Brace, 36 | }, 37 | Bracket { 38 | tokens: TokenStream, 39 | brackets: syn::token::Bracket, 40 | prefixes: Option, 41 | }, 42 | } 43 | 44 | impl Parse for Value { 45 | fn parse(input: ParseStream) -> syn::Result { 46 | if input.peek(syn::token::Bracket) { 47 | let (brackets, tokens) = parse::bracketed_tokens(input).unwrap(); 48 | Ok(Self::Bracket { 49 | tokens, 50 | brackets, 51 | prefixes: None, 52 | }) 53 | // with prefixes like `f["{}", something]` 54 | } else if input.peek(syn::Ident::peek_any) && input.peek2(syn::token::Bracket) { 55 | let prefixes = syn::Ident::parse_any(input).unwrap(); 56 | let (brackets, tokens) = parse::bracketed_tokens(input).unwrap(); 57 | Ok(Self::Bracket { 58 | tokens, 59 | brackets, 60 | prefixes: Some(prefixes), 61 | }) 62 | } else if input.peek(syn::token::Brace) { 63 | let (braces, tokens) = parse::braced_tokens(input).unwrap(); 64 | Ok(Self::Block { tokens, braces }) 65 | } else if input.peek(syn::Lit) { 66 | let lit = syn::Lit::parse(input).unwrap(); 67 | Ok(Self::Lit(lit)) 68 | } else { 69 | Err(input.error("invalid value: expected bracket, block or literal")) 70 | } 71 | } 72 | } 73 | 74 | impl ToTokens for Value { 75 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 76 | tokens.extend(match self { 77 | Self::Lit(lit) => lit.into_token_stream(), 78 | // using the tokens as the span instead of the block provides better error messages 79 | // see test ui/errors/invalid_child 80 | Self::Block { tokens, braces } => { 81 | // fallback in case `tokens` is empty, span would be the whole call site 82 | let span = if tokens.is_empty() { braces.span.join() } else { tokens.span() }; 83 | quote_spanned!(span=> {#tokens}) 84 | } 85 | Self::Bracket { 86 | tokens, 87 | prefixes, 88 | brackets, 89 | } => { 90 | if let Some(prefixes) = prefixes { 91 | // only f[] is supported for now 92 | if prefixes == "f" { 93 | let format = quote_spanned!(prefixes.span()=> format!); 94 | quote_spanned!(brackets.span.join()=> move || ::std::#format(#tokens)) 95 | } else { 96 | emit_error!( 97 | prefixes.span(), 98 | "unsupported prefix: only `f` is supported." 99 | ); 100 | quote! {} 101 | } 102 | } else { 103 | quote_spanned!(brackets.span.join()=> move || {#tokens}) 104 | } 105 | } 106 | }); 107 | } 108 | } 109 | 110 | impl Value { 111 | /// Returns the [`Span`] of this [`Value`]. 112 | /// 113 | /// If the value is a block/bracket, the span includes the delimiters. 114 | pub fn span(&self) -> Span { 115 | match self { 116 | Self::Lit(lit) => lit.span(), 117 | Self::Block { braces, .. } => braces.span.join(), 118 | Self::Bracket { brackets, .. } => brackets.span.join(), 119 | } 120 | } 121 | 122 | /// Either parses a valid [`Value`], or inserts a `MissingValueAfterEq` 123 | /// never-type enum. 124 | pub fn parse_or_emit_err(input: ParseStream, fallback_span: Span) -> Self { 125 | if let Some(value) = rollback_err(input, Self::parse) { 126 | value 127 | } else { 128 | // avoid call-site span 129 | let span = if input.is_empty() { fallback_span } else { input.span() }; 130 | 131 | // incomplete typing; place a MissingValueAfterEq and continue 132 | let error = Diagnostic::spanned( 133 | span, 134 | proc_macro_error2::Level::Error, 135 | "expected value after =".to_string(), 136 | ); 137 | // if the token after the `=` is an ident, perhaps the user forgot to wrap in 138 | // braces. 139 | let error = if input.peek(syn::Ident) { 140 | error.help("you may have meant to wrap this in braces".to_string()) 141 | } else { 142 | error 143 | }; 144 | 145 | error.emit(); 146 | Self::Block { 147 | tokens: quote_spanned!(span => ::leptos_mview::MissingValueAfterEq), 148 | braces: syn::token::Brace(span), 149 | } 150 | } 151 | } 152 | 153 | /// Constructs self as a literal `true` with no span. 154 | pub fn new_true() -> Self { Self::Lit(parse_quote!(true)) } 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use std::collections::HashMap; 160 | 161 | use super::Value; 162 | 163 | /// Variant-only version of `Value` for quick checking. 164 | enum ValueKind { 165 | Lit, 166 | Block, 167 | Bracket, 168 | } 169 | 170 | // test only implementation, as it is not used anywhere else. 171 | impl Value { 172 | pub fn is_lit(&self) -> bool { matches!(self, Self::Lit(_)) } 173 | 174 | pub fn is_block(&self) -> bool { matches!(self, Self::Block { .. }) } 175 | 176 | pub fn is_bracketed(&self) -> bool { matches!(self, Self::Bracket { .. }) } 177 | } 178 | 179 | impl ValueKind { 180 | fn value_is(&self, value: Value) -> bool { 181 | match self { 182 | ValueKind::Lit => value.is_lit(), 183 | ValueKind::Block => value.is_block(), 184 | ValueKind::Bracket => value.is_bracketed(), 185 | } 186 | } 187 | } 188 | 189 | #[test] 190 | fn value_conversion() { 191 | let mut exprs = HashMap::new(); 192 | 193 | exprs.insert("\"hi\"", ValueKind::Lit); 194 | exprs.insert("1", ValueKind::Lit); 195 | exprs.insert("true", ValueKind::Lit); 196 | exprs.insert("{value}", ValueKind::Block); 197 | exprs.insert("{value; value2; value3}", ValueKind::Block); 198 | exprs.insert("[abc.get()]", ValueKind::Bracket); 199 | exprs.insert("{(aa,)}", ValueKind::Block); 200 | exprs.insert("[{a; b}]", ValueKind::Bracket); 201 | 202 | for (expr, kind) in exprs { 203 | let value = syn::parse_str(expr).unwrap(); 204 | assert!(kind.value_is(value)) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /leptos-mview-core/src/error_ext.rs: -------------------------------------------------------------------------------- 1 | //! `proc_macro_error` has not updated for `syn` v2, so the 2 | //! `.unwrap_or_abort()` and related extension methods do not work. 3 | //! 4 | //! A simplified version of the extension traits have been added here. 5 | 6 | use proc_macro_error2::{abort, emit_error}; 7 | 8 | pub trait ResultExt { 9 | type Ok; 10 | 11 | /// Behaves like `Result::unwrap`: if self is `Ok` yield the contained 12 | /// value, otherwise abort macro execution via `abort!`. 13 | fn unwrap_or_abort(self) -> Self::Ok; 14 | } 15 | 16 | impl ResultExt for Result { 17 | type Ok = T; 18 | 19 | fn unwrap_or_abort(self) -> T { 20 | match self { 21 | Ok(res) => res, 22 | Err(e) => abort!(e.span(), e.to_string()), 23 | } 24 | } 25 | } 26 | 27 | pub trait SynErrorExt { 28 | fn emit_as_error(self); 29 | } 30 | 31 | impl SynErrorExt for syn::Error { 32 | fn emit_as_error(self) { emit_error!(self.span(), "{}", self) } 33 | } 34 | -------------------------------------------------------------------------------- /leptos-mview-core/src/expand.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous functions to convert structs to [`TokenStream`]s. 2 | 3 | // putting specific `-> TokenStream` implementations here to have it all 4 | // grouped instead of scattered throughout struct impls. 5 | 6 | use std::collections::HashMap; 7 | 8 | use proc_macro2::{Span, TokenStream}; 9 | use proc_macro_error2::emit_error; 10 | use quote::{quote, quote_spanned}; 11 | use syn::{ext::IdentExt, parse_quote, parse_quote_spanned, spanned::Spanned}; 12 | 13 | use crate::ast::{ 14 | attribute::{directive::Directive, selector::SelectorShorthand}, 15 | Attr, Element, KebabIdent, KebabIdentOrStr, NodeChild, Tag, Value, 16 | }; 17 | 18 | /// Functions for specific parts of an element's expansion. 19 | mod subroutines; 20 | #[allow(clippy::wildcard_imports)] 21 | use subroutines::*; 22 | /// Small helper functions for converting types or emitting errors. 23 | mod utils; 24 | #[allow(clippy::wildcard_imports)] 25 | use utils::*; 26 | 27 | /// Converts the children into a `View::new()` token stream. 28 | /// 29 | /// Example: 30 | /// ```ignore 31 | /// "a" 32 | /// {var} 33 | /// "b" 34 | /// ``` 35 | /// 36 | /// Should expand to: 37 | /// ```ignore 38 | /// View::new(( 39 | /// {"a"}, 40 | /// {var}, 41 | /// {"b"}, 42 | /// )) 43 | /// ``` 44 | pub fn root_children_tokens<'a>( 45 | children: impl Iterator, 46 | span: Span, 47 | ) -> TokenStream { 48 | quote_spanned! { span=> 49 | ::leptos::prelude::View::new(( 50 | #( #children, )* 51 | )) 52 | } 53 | } 54 | 55 | // used for component children 56 | pub fn children_fragment_tokens<'a>( 57 | children: impl Iterator, 58 | span: Span, 59 | ) -> TokenStream { 60 | let children = children.collect::>(); 61 | let has_multiple_children = children.len() > 1; 62 | 63 | if has_multiple_children { 64 | quote_spanned! { span=> 65 | ( #( #children, )* ) 66 | } 67 | } else { 68 | quote_spanned! { span=> 69 | #( #children )* 70 | } 71 | } 72 | } 73 | 74 | /// Converts an xml (like html, svg or math) element to tokens. 75 | /// 76 | /// Returns `None` if the element is not an xml element (custom component). 77 | /// 78 | /// # Example 79 | /// ```ignore 80 | /// use leptos::prelude::*; 81 | /// use leptos_mview::mview; 82 | /// let div = create_node_ref::(); 83 | /// mview! { 84 | /// div 85 | /// class="component" 86 | /// style:color="black" 87 | /// ref={div} 88 | /// { 89 | /// "Hello " strong { "world" } 90 | /// } 91 | /// } 92 | /// ``` 93 | /// Expands to: 94 | /// ```ignore 95 | /// div() 96 | /// .class("component") 97 | /// .style(("color", "black")) 98 | /// .node_ref(div) 99 | /// .child(IntoRender::into_render("Hello ")) 100 | /// .child(IntoRender::into_render(strong().child("world"))) 101 | /// ``` 102 | pub fn xml_to_tokens(element: &Element) -> Option { 103 | let tag_path = match element.tag() { 104 | Tag::Component(..) => return None, 105 | Tag::Html(ident) => quote! { ::leptos::tachys::html::element::#ident() }, 106 | Tag::Svg(ident) => quote! { ::leptos::tachys::svg::element::#ident() }, 107 | Tag::Math(ident) => quote! { ::leptos::tachys::math::element::#ident() }, 108 | Tag::WebComponent(ident) => { 109 | let ident = ident.to_lit_str(); 110 | let custom = syn::Ident::new("custom", ident.span()); 111 | quote! { ::leptos::tachys::html::element::#custom(#ident) } 112 | } 113 | }; 114 | 115 | // add selector-style ids/classes (div.some-class #some-id) 116 | let selector_methods = xml_selectors_tokens(element.selectors()); 117 | 118 | // parse normal attributes first 119 | let mut attrs = TokenStream::new(); 120 | let mut spread_attrs = TokenStream::new(); 121 | // put directives at the end so conditional attributes like `class:` work 122 | // with `class="..."` attributes 123 | let mut directives = TokenStream::new(); 124 | 125 | for a in element.attrs().iter() { 126 | match a { 127 | Attr::Kv(attr) => attrs.extend(xml_kv_attribute_tokens(attr, element.tag().kind())), 128 | Attr::Directive(dir) => directives.extend(xml_directive_tokens(dir)), 129 | Attr::Spread(spread) => spread_attrs.extend(xml_spread_tokens(spread)), 130 | } 131 | } 132 | 133 | let children = element 134 | .children() 135 | .map(|children| xml_child_methods_tokens(children.node_children())); 136 | 137 | Some(quote! { 138 | #tag_path 139 | #attrs 140 | #directives 141 | #selector_methods 142 | #spread_attrs 143 | #children 144 | }) 145 | } 146 | 147 | /// Transforms a component into a `TokenStream` of a leptos component view. 148 | /// 149 | /// Returns `None` if `self.tag` is not a `Component`. 150 | /// 151 | /// The const generic switches between parsing a slot and regular leptos 152 | /// component, as the two implementations are very similar. 153 | /// 154 | /// Example builder expansion of a component: 155 | /// ```ignore 156 | /// leptos::component_view( 157 | /// &Com, 158 | /// leptos::component_props_builder(&Com) 159 | /// .num(3) 160 | /// .text("a".to_string()) 161 | /// .children(::leptos::ToChildren::to_children(move || { 162 | /// Fragment::lazy(|| [ 163 | /// "child", 164 | /// "child2", 165 | /// ]) 166 | /// })) 167 | /// .build() 168 | /// ) 169 | /// ``` 170 | /// 171 | /// Where the component has signature: 172 | /// 173 | /// ```ignore 174 | /// #[component] 175 | /// pub fn Com(num: u32, text: String, children: Children) -> impl IntoView { ... } 176 | /// ``` 177 | #[allow(clippy::too_many_lines)] 178 | pub fn component_to_tokens(element: &Element) -> Option { 179 | let Tag::Component(path) = element.tag() else { 180 | return None; 181 | }; 182 | let path = turbofishify(path.clone()); 183 | 184 | // collect a bunch of info about the element attributes // 185 | 186 | // attribute methods to add when building 187 | let mut attrs = TokenStream::new(); 188 | let mut directive_paths: Vec = Vec::new(); 189 | // the variables (idents) to clone before making children 190 | // in the form `let name = name.clone();` 191 | let mut clones = TokenStream::new(); 192 | 193 | // shorthands are not supported on slots 194 | if IS_SLOT { 195 | if let Some(first) = element.selectors().first() { 196 | emit_error!( 197 | first.prefix(), 198 | "selector shorthands are not supported on slots" 199 | ) 200 | } 201 | } else { 202 | // all the ids need to be collected together 203 | // as multiple attr:id=... creates multiple `id=...` attributes on teh element 204 | let mut ids = Vec::::new(); 205 | let mut first_pound_symbol = None; 206 | for sel in element.selectors().iter() { 207 | match sel { 208 | SelectorShorthand::Id { id, pound_symbol } => { 209 | first_pound_symbol.get_or_insert(*pound_symbol); 210 | ids.push(id.clone()); 211 | } 212 | SelectorShorthand::Class { class, dot_symbol } => { 213 | // desugar to class:the-class 214 | directive_paths.push( 215 | directive_to_any_attr_path(&Directive { 216 | dir: syn::Ident::new("class", dot_symbol.span), 217 | key: KebabIdentOrStr::KebabIdent(class.clone()), 218 | modifier: None, 219 | value: None, 220 | }) 221 | .expect("class directive is known"), 222 | ); 223 | } 224 | }; 225 | } 226 | // push all the ids as directive 227 | if let Some(first_pound_symbol) = first_pound_symbol { 228 | let joined_ids = ids 229 | .iter() 230 | .map(|ident| ident.repr()) 231 | .collect::>() 232 | .join(" "); 233 | // desugar to attr:id="the-id id2 id3" 234 | directive_paths.push( 235 | directive_to_any_attr_path(&Directive { 236 | dir: syn::Ident::new("attr", Span::call_site()), 237 | key: parse_quote_spanned! { first_pound_symbol.span=> id }, 238 | modifier: None, 239 | value: Some(Value::Lit(parse_quote!(#joined_ids))), 240 | }) 241 | .expect("attr directive is known"), 242 | ); 243 | } 244 | } 245 | 246 | element.attrs().iter().for_each(|a| match a { 247 | Attr::Kv(attr) => attrs.extend(component_kv_attribute_tokens(attr)), 248 | Attr::Spread(spread) => { 249 | if IS_SLOT { 250 | emit_error!(spread.span(), "spread syntax is not supported on slots"); 251 | } else { 252 | directive_paths.push(component_spread_tokens(spread)); 253 | } 254 | } 255 | Attr::Directive(dir) => match dir.dir.to_string().as_str() { 256 | // clone works on both components and slots 257 | "clone" => { 258 | emit_error_if_modifier(dir.modifier.as_ref()); 259 | clones.extend(component_clone_tokens(dir)); 260 | } 261 | // slots support no other directives 262 | other if IS_SLOT => { 263 | emit_error!(dir.dir.span(), "`{}:` is not supported on slots", other); 264 | } 265 | _ => { 266 | if let Some(path) = directive_to_any_attr_path(dir) { 267 | directive_paths.push(path); 268 | } else { 269 | emit_error!(dir.dir.span(), "unknown directive"); 270 | } 271 | } 272 | }, 273 | }); 274 | 275 | // convert the collected info into tokens // 276 | 277 | let children = element.children().map(|children| { 278 | let mut it = children.node_children().peekable(); 279 | // need to check that there are any element children at all, 280 | // as components that accept slots may not accept children. 281 | it.peek() 282 | .is_some() 283 | .then(|| component_children_tokens(it, element.children_args(), &clones)) 284 | }); 285 | 286 | let slot_children = element 287 | .children() 288 | .map(|children| slots_to_tokens(children.slot_children())); 289 | 290 | // if attributes are missing, an error is made in `.build()` by the component 291 | // builder. 292 | let build = quote_spanned!(path.span()=> .build()); 293 | 294 | if IS_SLOT { 295 | // Into is for turning a single slot into a vec![slot] if needed 296 | Some(quote! { 297 | ::std::convert::Into::into( 298 | #path::builder() 299 | #attrs 300 | #children 301 | #build 302 | ) 303 | }) 304 | } else { 305 | // this whole thing needs to be spanned to avoid errors occurring at the whole 306 | // call site. 307 | let component_props_builder = quote_spanned! { 308 | path.span()=> ::leptos::component::component_props_builder(&#path) 309 | }; 310 | 311 | let directive_paths = (!directive_paths.is_empty()).then(|| { 312 | quote! { 313 | .add_any_attr((#(#directive_paths,)*)) 314 | } 315 | }); 316 | 317 | Some(quote! { 318 | ::leptos::component::component_view( 319 | &#path, 320 | #component_props_builder 321 | #attrs 322 | #children 323 | #slot_children 324 | #build 325 | ) 326 | #directive_paths 327 | }) 328 | } 329 | } 330 | 331 | #[allow(clippy::doc_markdown)] 332 | /// Converts a list of slots to a bunch of methods to be called on the parent 333 | /// component. 334 | /// 335 | /// The iterator must have only elements that are slots. 336 | /// 337 | /// Slots are expanded from: 338 | /// ```ignore 339 | /// Tabs { 340 | /// slot:Tab label="tab1" { "content" } 341 | /// } 342 | /// ``` 343 | /// to: 344 | /// ```ignore 345 | /// leptos::component_props_builder(&Tabs) 346 | /// .tab(vec![ 347 | /// Tab::builder() 348 | /// .label("tab1") 349 | /// .children( /* expansion of "content" to a component child */ ) 350 | /// .build() 351 | /// .into() 352 | /// ]) 353 | /// ``` 354 | /// Where the slot's name is converted to snake_case for the method name. 355 | fn slots_to_tokens<'a>(children: impl Iterator) -> TokenStream { 356 | // collect to hashmap // 357 | 358 | // Mapping from the slot name (component, UpperCamelCase name, not snake_case) 359 | // to a vec of the each slot's expansion. 360 | let mut slot_children = HashMap::>::new(); 361 | for el in children { 362 | let Tag::Component(path) = el.tag() else { 363 | panic!("called `slots_to_tokens` on non-slot element") 364 | }; 365 | let slot_name = if let Some(ident) = path.get_ident() { 366 | ident.clone() 367 | } else { 368 | emit_error!(path.span(), "slot name must be a single ident, not a path"); 369 | continue; 370 | }; 371 | 372 | let slot_component = 373 | component_to_tokens::(el).expect("checked that element is a component"); 374 | slot_children 375 | .entry(slot_name) 376 | .or_default() 377 | .push(slot_component); 378 | } 379 | 380 | // convert to tokens // 381 | slot_children 382 | .into_iter() 383 | .map(|(slot_name, slot_tokens)| { 384 | let method = syn::Ident::new_raw( 385 | &utils::upper_camel_to_snake_case(&slot_name.unraw().to_string()), 386 | slot_name.span(), 387 | ); 388 | 389 | if slot_tokens.len() == 1 { 390 | // don't wrap in a vec 391 | quote! { 392 | .#method(#(#slot_tokens)*) 393 | } 394 | } else { 395 | quote! { 396 | .#method(<[_]>::into_vec(::std::boxed::Box::new([ 397 | #(#slot_tokens),* 398 | ]))) 399 | } 400 | } 401 | }) 402 | .collect() 403 | } 404 | -------------------------------------------------------------------------------- /leptos-mview-core/src/expand/subroutines.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use proc_macro_error2::emit_error; 3 | use quote::{quote, quote_spanned}; 4 | use syn::{ext::IdentExt, spanned::Spanned}; 5 | 6 | use crate::{ 7 | ast::{ 8 | attribute::{ 9 | directive::Directive, 10 | kv::KvAttr, 11 | selector::{SelectorShorthand, SelectorShorthands}, 12 | spread_attrs::SpreadAttr, 13 | }, 14 | KebabIdentOrStr, NodeChild, TagKind, Value, 15 | }, 16 | expand::{children_fragment_tokens, emit_error_if_modifier, utils}, 17 | }; 18 | 19 | //////////////////////////////////////////////////////////////// 20 | // ------------------- shared subroutines ------------------- // 21 | //////////////////////////////////////////////////////////////// 22 | 23 | /// Converts a `use:directive={value}` to a key (function) and value. 24 | /// 25 | /// ```text 26 | /// use:d => (d, ().into()) 27 | /// use:d={some_value} => (d, some_value.into()) 28 | /// ``` 29 | /// 30 | /// **Panics** if the provided directive is not `use:`. 31 | pub(super) fn use_directive_fn_value(u: &Directive) -> (syn::Ident, TokenStream) { 32 | let Directive { 33 | dir: use_token, 34 | key, 35 | modifier, 36 | value, 37 | } = u; 38 | assert_eq!(use_token, "use", "directive should be `use:`"); 39 | let directive_fn = key.to_ident_or_emit(); 40 | emit_error_if_modifier(modifier.as_ref()); 41 | 42 | let value = value.as_ref().map_or_else( 43 | || quote_spanned! {directive_fn.span()=> ().into() }, 44 | |val| quote! { ::std::convert::Into::into(#val) }, 45 | ); 46 | (directive_fn, value) 47 | } 48 | 49 | pub(super) fn event_listener_event_path(dir: &Directive) -> TokenStream { 50 | let Directive { 51 | dir, 52 | key, 53 | modifier, 54 | value: _, 55 | } = dir; 56 | assert_eq!(dir, "on", "directive should be `on:`"); 57 | 58 | let ev_name = match key { 59 | KebabIdentOrStr::KebabIdent(ident) => ident.to_snake_ident(), 60 | KebabIdentOrStr::Str(s) => { 61 | emit_error!(s.span(), "event type must be an identifier"); 62 | syn::Ident::new("invalid_event", s.span()) 63 | } 64 | }; 65 | 66 | if let Some(modifier) = modifier { 67 | if modifier == "undelegated" { 68 | quote! { 69 | ::leptos::tachys::html::event::#modifier( 70 | ::leptos::tachys::html::event::#ev_name 71 | ) 72 | } 73 | } else { 74 | emit_error!( 75 | modifier.span(), "unknown modifier"; 76 | help = ":undelegated is the only known modifier" 77 | ); 78 | quote! { ::leptos::tachys::html::event::#ev_name } 79 | } 80 | } else { 81 | quote! { ::leptos::tachys::html::event::#ev_name } 82 | } 83 | } 84 | 85 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 86 | enum AttributeKind { 87 | /// "class" 88 | Class, 89 | /// "style" 90 | Style, 91 | /// An attribute with a `-`, like data-* 92 | /// 93 | /// Excludes `aria-*` attributes. 94 | Custom, 95 | /// An attribute that should be added by a method so that it is checked. 96 | OtherChecked, 97 | } 98 | 99 | impl AttributeKind { 100 | pub fn is_custom(self) -> bool { self == Self::Custom } 101 | 102 | pub fn is_class_or_style(self) -> bool { matches!(self, Self::Class | Self::Style) } 103 | } 104 | 105 | impl From<&str> for AttributeKind { 106 | fn from(value: &str) -> Self { 107 | if value == "class" { 108 | Self::Class 109 | } else if value == "style" { 110 | Self::Style 111 | } else if value.contains('-') && !value.starts_with("aria-") { 112 | Self::Custom 113 | } else { 114 | Self::OtherChecked 115 | } 116 | } 117 | } 118 | 119 | /////////////////////////////////////////////////////////// 120 | // ------------------- html/xml only ------------------- // 121 | /////////////////////////////////////////////////////////// 122 | 123 | /// Converts element class/id selector shorthands into a series of `.classes` 124 | /// and `.id` calls. 125 | pub(super) fn xml_selectors_tokens(selectors: &SelectorShorthands) -> TokenStream { 126 | let (classes, ids): (Vec<_>, Vec<_>) = selectors 127 | .iter() 128 | .partition(|sel| matches!(sel, SelectorShorthand::Class { .. })); 129 | 130 | let class_methods = classes.iter().map(|class| { 131 | let method = syn::Ident::new("class", class.prefix().span()); 132 | let class_name = class.ident().to_str_colored(); 133 | quote! { .#method((#class_name, true)) } 134 | }); 135 | 136 | let id_methods = ids.iter().map(|id| { 137 | let method = syn::Ident::new("id", id.prefix().span()); 138 | let id_name = id.ident().to_str_colored(); 139 | quote! { .#method(#id_name) } 140 | }); 141 | 142 | quote! { #(#class_methods)* #(#id_methods)* } 143 | } 144 | 145 | pub(super) fn xml_kv_attribute_tokens(attr: &KvAttr, element_tag: TagKind) -> TokenStream { 146 | let key = attr.key(); 147 | let value = attr.value(); 148 | // special cases 149 | if key.repr() == "ref" { 150 | let node_ref = syn::Ident::new("node_ref", key.span()); 151 | quote! { .#node_ref(#value) } 152 | } else { 153 | // https://github.com/leptos-rs/leptos/blob/main/leptos_macro/src/view/mod.rs#L960 154 | // Use unchecked attributes if: 155 | // - it's not `class` nor `style`, and 156 | // - It's a custom web component or SVG element 157 | // - or it's a custom or data attribute (has `-` except for `aria-`) 158 | let attr_kind = AttributeKind::from(key.repr()); 159 | let is_web_or_svg = matches!(element_tag, TagKind::Svg | TagKind::WebComponent); 160 | 161 | if (is_web_or_svg || attr_kind.is_custom()) && !attr_kind.is_class_or_style() { 162 | // unchecked attribute 163 | // don't span the attribute to the string, unnecessary and makes it 164 | // string-colored 165 | let key = key.repr(); 166 | quote! { .attr(#key, ::leptos::prelude::IntoAttributeValue::into_attribute_value(#value)) } 167 | } else { 168 | // checked attribute 169 | let key = key.to_snake_ident(); 170 | quote! { .#key(#value) } 171 | } 172 | } 173 | } 174 | 175 | pub(super) fn xml_directive_tokens(directive: &Directive) -> TokenStream { 176 | let Directive { 177 | dir, 178 | key, 179 | modifier, 180 | value, 181 | } = directive; 182 | 183 | match dir.to_string().as_str() { 184 | "class" | "style" => { 185 | let key = key.to_lit_str(); 186 | emit_error_if_modifier(modifier.as_ref()); 187 | quote! { .#dir((#key, #value)) } 188 | } 189 | "prop" => { 190 | let key = key.to_lit_str(); 191 | emit_error_if_modifier(modifier.as_ref()); 192 | quote! { .#dir(#key, #value) } 193 | } 194 | "on" => { 195 | let event_path = event_listener_event_path(directive); 196 | quote! { .#dir(#event_path, #value) } 197 | } 198 | "use" => { 199 | let (fn_name, value) = use_directive_fn_value(directive); 200 | let directive = syn::Ident::new("directive", dir.span()); 201 | quote! { 202 | .#directive(#fn_name, #value) 203 | } 204 | } 205 | "attr" | "clone" => { 206 | emit_error!(dir.span(), "`{}:` is not supported on elements", dir); 207 | quote! {} 208 | } 209 | "bind" => { 210 | emit_error_if_modifier(modifier.as_ref()); 211 | let bind = syn::Ident::new("bind", dir.span()); 212 | let bound_attribute_name = utils::snake_case_to_upper_camel(key.to_ident_or_emit()); 213 | 214 | // https://github.com/leptos-rs/leptos/pull/3680/files 215 | // special case for `bind:group` 216 | if key.to_lit_str().value() == "group" { 217 | quote! { .#bind(::leptos::tachys::reactive_graph::bind::#bound_attribute_name, #value) } 218 | } else { 219 | quote! { .#bind(::leptos::attr::#bound_attribute_name, #value) } 220 | } 221 | } 222 | _ => { 223 | emit_error!(dir.span(), "unknown directive"); 224 | quote! {} 225 | } 226 | } 227 | } 228 | 229 | pub(super) fn xml_spread_tokens(attr: &SpreadAttr) -> TokenStream { 230 | let (dotdot, expr) = (attr.dotdot(), attr.expr()); 231 | let attrs = syn::Ident::new("add_any_attr", dotdot.span()); 232 | quote! { 233 | .#attrs(#expr) 234 | } 235 | } 236 | 237 | /// Converts the children to a series of `.child` calls. 238 | /// 239 | /// # Example 240 | /// ```ignore 241 | /// div { "a" {var} "b" } 242 | /// ``` 243 | /// Expands to: 244 | /// ```ignore 245 | /// div().child("a").child({var}).child("b") 246 | /// ``` 247 | pub(super) fn xml_child_methods_tokens<'a>( 248 | children: impl Iterator, 249 | ) -> TokenStream { 250 | let mut ts = TokenStream::new(); 251 | for child in children { 252 | let child_method = syn::Ident::new("child", child.span()); 253 | ts.extend(quote! { 254 | .#child_method(#child) 255 | }); 256 | } 257 | ts 258 | } 259 | 260 | //////////////////////////////////////////////////////////// 261 | // ------------------- component only ------------------- // 262 | //////////////////////////////////////////////////////////// 263 | 264 | pub(super) fn component_kv_attribute_tokens(attr: &KvAttr) -> TokenStream { 265 | let (key, value) = (attr.key().to_snake_ident(), attr.value()); 266 | quote_spanned! { attr.span()=> .#key(#value) } 267 | } 268 | 269 | /// Expands to a `let` statement `let to_clone = to_clone.clone();`. 270 | pub(super) fn component_clone_tokens(dir: &Directive) -> TokenStream { 271 | let to_clone = dir.key.to_ident_or_emit(); 272 | emit_error_if_modifier(dir.modifier.as_ref()); 273 | if let Some(value) = &dir.value { 274 | emit_error!(value.span(), "`clone:` does not take any values"); 275 | }; 276 | 277 | quote! { let #to_clone = #to_clone.clone(); } 278 | } 279 | 280 | /// Converts children to tokens for use by components. 281 | /// 282 | /// The expansion is generally: 283 | /// 284 | /// If there are no closure arguments, 285 | /// ```ignore 286 | /// .children({ 287 | /// // any clones 288 | /// let clone = clone.clone(); 289 | /// // the children themself 290 | /// leptos::ToChildren::to_children(move || { 291 | /// leptos::Fragment::lazy(|| { 292 | /// [ 293 | /// child1.into_view(), 294 | /// child2.into_view(), 295 | /// ].to_vec() 296 | /// }) 297 | /// }) 298 | /// }) 299 | /// ``` 300 | /// 301 | /// If there are closure arguments, 302 | /// ```ignore 303 | /// .children({ 304 | /// // any clones 305 | /// let clone = clone.clone(); 306 | /// // the children 307 | /// move |args| leptos::Fragment::lazy(|| { 308 | /// [ 309 | /// child1.into_view(), 310 | /// child2.into_view(), 311 | /// ].to_vec() 312 | /// }) 313 | /// }) 314 | /// ``` 315 | pub(super) fn component_children_tokens<'a>( 316 | children: impl Iterator, 317 | args: Option<&TokenStream>, 318 | clones: &TokenStream, 319 | ) -> TokenStream { 320 | let mut children = children.peekable(); 321 | let child_span = children 322 | .peek() 323 | // not sure why `child.span()` is calling `syn::spanned::Spanned` instead 324 | .map_or_else(Span::call_site, |child| (*child).span()); 325 | 326 | // span call site if there are no args so that the children don't get all the 327 | // `std` `vec!` etc docs. 328 | let children_fragment = 329 | children_fragment_tokens(children, args.map_or(Span::call_site(), Spanned::span)); 330 | 331 | // children with arguments take a `Fn(T) -> impl IntoView` 332 | // normal children (`Children`, `ChildrenFn`, ...) take 333 | // `ToChildren::to_children` 334 | let wrapped_fragment = if let Some(args) = args { 335 | // `args` includes the pipes 336 | quote_spanned!(args.span()=> move #args #children_fragment) 337 | } else { 338 | // this span is required for slots that take `Callback` but have been 339 | // given a regular `ChildrenFn` instead. 340 | let closure = quote_spanned!(child_span=> move || #children_fragment); 341 | quote! { 342 | ::leptos::children::ToChildren::to_children(#closure) 343 | } 344 | }; 345 | 346 | let children_method = quote_spanned!(child_span=> children); 347 | 348 | quote! { 349 | .#children_method({ 350 | #clones 351 | #wrapped_fragment 352 | }) 353 | } 354 | } 355 | 356 | // https://github.com/leptos-rs/leptos/blob/5947aa299e5299eb3dc75c58e28affb15e79b6ff/leptos_macro/src/view/mod.rs#L998 357 | 358 | /// Converts a directive on a component to a path to be used on 359 | /// `.add_any_attr(...)` 360 | /// 361 | /// Returns [`None`] if the directive is an unknown directive, or `clone`. 362 | /// 363 | /// Adding these directives to a component looks like: 364 | /// ```ignore 365 | /// View::new( 366 | /// leptos::component::component_view(.., ..) 367 | /// .add_any_attr(( 368 | /// leptos::tachys::html::class::class("something"), 369 | /// leptos::tachys::html::class::class(("conditional", true)), 370 | /// leptos::tachys::html::style::style(("position", "absolute")), 371 | /// leptos::tachys::html::attribute::contenteditable(true), 372 | /// leptos::tachys::html::attribute::custom::custom_attribute("data-index", 0), 373 | /// leptos::tachys::html::property::prop("value", "aaaa"), 374 | /// leptos::tachys::html::event::on( 375 | /// leptos::tachys::html::event::undelegated( 376 | /// leptos::tachys::html::event::click 377 | /// ), 378 | /// || () 379 | /// ), 380 | /// leptos::tachys::html::directive::directive(directive_name, ().into()) 381 | /// )) 382 | /// ) 383 | /// ``` 384 | pub(super) fn directive_to_any_attr_path(directive: &Directive) -> Option { 385 | let dir = &directive.dir; 386 | let path = match &*dir.to_string() { 387 | "class" | "style" => { 388 | // avoid making it string coloured 389 | let key = directive.key.to_unspanned_string(); 390 | let value = directive.value.clone().unwrap_or_else(Value::new_true); 391 | // to avoid spanning the directive to the module 392 | let dir_unspanned = syn::Ident::new(&dir.to_string(), Span::call_site()); 393 | quote! { 394 | ::leptos::tachys::html::#dir_unspanned::#dir((#key, #value)) 395 | } 396 | } 397 | "attr" => { 398 | let attr_kind = AttributeKind::from(&*directive.key.to_lit_str().value()); 399 | match attr_kind { 400 | AttributeKind::Class | AttributeKind::Style => { 401 | let class_or_style = directive.key.to_ident_or_emit(); 402 | let value = directive.value.clone().unwrap_or_else(Value::new_true); 403 | // to avoid spanning to the module name 404 | let class_or_style_unspanned = 405 | syn::Ident::new(&class_or_style.unraw().to_string(), Span::call_site()); 406 | quote! { 407 | ::leptos::tachys::html::#class_or_style_unspanned::#class_or_style(#value) 408 | } 409 | } 410 | AttributeKind::Custom => { 411 | let attr_name = directive.key.to_unspanned_string(); 412 | let value = directive.value.clone().unwrap_or_else(Value::new_true); 413 | quote! { 414 | ::leptos::tachys::html::attribute::custom::custom_attribute(#attr_name, #value) 415 | } 416 | } 417 | AttributeKind::OtherChecked => { 418 | let attr_name = directive.key.to_ident_or_emit(); 419 | let value = directive.value.clone().unwrap_or_else(Value::new_true); 420 | quote! { 421 | ::leptos::tachys::html::attribute::#attr_name(#value) 422 | } 423 | } 424 | } 425 | } 426 | "prop" => { 427 | let prop = directive.key.to_ident_or_emit(); 428 | let value = directive.value.clone().unwrap_or_else(Value::new_true); 429 | quote! { 430 | ::leptos::tachys::html::property::#prop(#value) 431 | } 432 | } 433 | "on" => { 434 | let event_path = event_listener_event_path(directive); 435 | let value = &directive.value; 436 | quote! { 437 | ::leptos::tachys::html::event::on(#event_path, #value) 438 | } 439 | } 440 | "use" => { 441 | let (fn_name, value) = use_directive_fn_value(directive); 442 | let directive_method = syn::Ident::new("directive", directive.dir.span()); 443 | quote! { 444 | ::leptos::tachys::html::directive::#directive_method( 445 | #fn_name, 446 | #value 447 | ) 448 | } 449 | } 450 | _ => return None, 451 | }; 452 | 453 | Some(path) 454 | } 455 | 456 | /// This should be added with all the other directives. 457 | /// 458 | /// Spread attrs are added as `.add_any_attr(expr)`. 459 | pub(super) fn component_spread_tokens(attr: &SpreadAttr) -> TokenStream { attr.expr().clone() } 460 | -------------------------------------------------------------------------------- /leptos-mview-core/src/expand/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro_error2::{abort, emit_error}; 2 | use syn::{ext::IdentExt, parse_quote, spanned::Spanned}; 3 | 4 | #[allow(clippy::doc_markdown)] 5 | // just doing a manual implementation as theres only one need for this (slots). 6 | // Use the `paste` crate if more are needed in the future. 7 | /// `ident` must be an UpperCamelCase word with only ascii word characters. 8 | pub fn upper_camel_to_snake_case(ident: &str) -> String { 9 | let mut new = String::with_capacity(ident.len()); 10 | // all characters should be ascii 11 | for char in ident.chars() { 12 | // skip the first `_`. 13 | if char.is_ascii_uppercase() && !new.is_empty() { 14 | new.push('_'); 15 | }; 16 | new.push(char.to_ascii_lowercase()); 17 | } 18 | 19 | new 20 | } 21 | 22 | pub fn snake_case_to_upper_camel(ident: syn::Ident) -> syn::Ident { 23 | let str = ident.unraw().to_string(); 24 | let mut new = String::with_capacity(str.len()); 25 | let mut next_char_is_word_start = true; 26 | 27 | for char in str.chars() { 28 | match (char, next_char_is_word_start) { 29 | ('_', _) => { 30 | next_char_is_word_start = true; 31 | } 32 | (c, true) => { 33 | next_char_is_word_start = false; 34 | new.extend(c.to_uppercase()); 35 | } 36 | (c, false) => { 37 | new.push(c); 38 | } 39 | } 40 | } 41 | 42 | syn::Ident::new_raw(&new, ident.span()) 43 | } 44 | 45 | pub fn emit_error_if_modifier(m: Option<&syn::Ident>) { 46 | if let Some(modifier) = m { 47 | emit_error!( 48 | modifier.span(), 49 | "unknown modifier: modifiers are only supported on `on:` directives" 50 | ); 51 | } 52 | } 53 | 54 | /// Converts a [`syn::Path`] (which could include things like `Vec`) to 55 | /// always use the turbofish (like `Vec::`). 56 | pub fn turbofishify(mut path: syn::Path) -> syn::Path { 57 | path.segments 58 | .iter_mut() 59 | .for_each(|segment| match &mut segment.arguments { 60 | syn::PathArguments::None => (), 61 | syn::PathArguments::AngleBracketed(generics) => { 62 | generics.colon2_token.get_or_insert(parse_quote!(::)); 63 | } 64 | // this would probably never happen, not caring about recoverability. 65 | syn::PathArguments::Parenthesized(p) => { 66 | abort!(p.span(), "function generics are not allowed") 67 | } 68 | }); 69 | path 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use quote::{quote, ToTokens}; 75 | 76 | use super::turbofishify; 77 | 78 | #[test] 79 | fn add_turbofish() { 80 | let path = syn::parse2::(quote! { std::vec::Vec }).unwrap(); 81 | let path = turbofishify(path); 82 | assert_eq!( 83 | "std::vec::Vec::", 84 | path.to_token_stream().to_string().replace(' ', "") 85 | ); 86 | } 87 | 88 | #[test] 89 | fn leave_turbofish() { 90 | let path = syn::parse2::(quote! { std::vec::Vec:: }).unwrap(); 91 | let path = turbofishify(path); 92 | assert_eq!( 93 | "std::vec::Vec::", 94 | path.to_token_stream().to_string().replace(' ', "") 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /leptos-mview-core/src/kw.rs: -------------------------------------------------------------------------------- 1 | syn::custom_keyword!(class); 2 | syn::custom_keyword!(style); 3 | syn::custom_keyword!(attr); 4 | syn::custom_keyword!(on); 5 | syn::custom_keyword!(prop); 6 | syn::custom_keyword!(clone); 7 | syn::custom_keyword!(slot); 8 | -------------------------------------------------------------------------------- /leptos-mview-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::pedantic, clippy::nursery)] 2 | #![allow( 3 | clippy::option_if_let_else, 4 | clippy::or_fun_call, 5 | clippy::module_name_repetitions 6 | )] 7 | 8 | mod ast; 9 | mod error_ext; 10 | mod expand; 11 | mod kw; 12 | mod parse; 13 | mod span; 14 | 15 | use ast::{Child, Children}; 16 | use expand::root_children_tokens; 17 | use proc_macro2::{Span, TokenStream}; 18 | use proc_macro_error2::abort; 19 | use quote::quote; 20 | use syn::spanned::Spanned; 21 | 22 | #[must_use] 23 | pub fn mview_impl(input: TokenStream) -> TokenStream { 24 | // return () in case of any errors, to avoid "unexpected end of macro 25 | // invocation" e.g. when assigning `let res = mview! { ... };` 26 | proc_macro_error2::set_dummy(quote! { () }); 27 | 28 | let children = match syn::parse2::(input) { 29 | Ok(tree) => tree, 30 | Err(e) => return e.to_compile_error(), 31 | }; 32 | 33 | // If there's a single top level component, can just expand like 34 | // div().attr(...).child(...)... 35 | // If there are multiple top-level children, need to use the fragment. 36 | if children.len() == 1 { 37 | let child = children.into_vec().remove(0); 38 | match child { 39 | Child::Node(node) => quote! { 40 | { #[allow(unused_braces)] #node } 41 | }, 42 | Child::Slot(slot, _) => abort!( 43 | slot.span(), 44 | "slots should be inside a parent that supports slots" 45 | ), 46 | } 47 | } else { 48 | // look for any slots 49 | if let Some(slot) = children.slot_children().next() { 50 | abort!( 51 | slot.tag().span(), 52 | "slots should be inside a parent that supports slots" 53 | ); 54 | }; 55 | 56 | let fragment = root_children_tokens(children.node_children(), Span::call_site()); 57 | quote! { 58 | { 59 | #[allow(unused_braces)] 60 | #fragment 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /leptos-mview-core/src/parse.rs: -------------------------------------------------------------------------------- 1 | //! Mini helper functions for parsing 2 | 3 | use proc_macro2::TokenStream; 4 | use syn::parse::{discouraged::Speculative, Parse, ParseBuffer, ParseStream}; 5 | 6 | pub fn extract_parenthesized(input: ParseStream) -> syn::Result<(syn::token::Paren, ParseBuffer)> { 7 | let stream; 8 | let delim = syn::parenthesized!(stream in input); 9 | Ok((delim, stream)) 10 | } 11 | 12 | pub fn extract_bracketed(input: ParseStream) -> syn::Result<(syn::token::Bracket, ParseBuffer)> { 13 | let stream; 14 | let delim = syn::bracketed!(stream in input); 15 | Ok((delim, stream)) 16 | } 17 | 18 | pub fn extract_braced(input: ParseStream) -> syn::Result<(syn::token::Brace, ParseBuffer)> { 19 | let stream; 20 | let delim = syn::braced!(stream in input); 21 | Ok((delim, stream)) 22 | } 23 | 24 | pub fn bracketed_tokens(input: ParseStream) -> syn::Result<(syn::token::Bracket, TokenStream)> { 25 | let (delim, buf) = extract_bracketed(input)?; 26 | let ts = take_rest(&buf); 27 | Ok((delim, ts)) 28 | } 29 | 30 | pub fn braced_tokens(input: ParseStream) -> syn::Result<(syn::token::Brace, TokenStream)> { 31 | let (delim, buf) = extract_braced(input)?; 32 | let ts = take_rest(&buf); 33 | Ok((delim, ts)) 34 | } 35 | 36 | // these functions probably aren't going to change and it's difficult to make 37 | // them generic over the delimiter, so just leaving it with duplication. 38 | 39 | /// Parses an AST wrapped in braces. 40 | /// 41 | /// Does not advance the token stream if the inner stream does not completely 42 | /// match `T`, including if there are more tokens after the `T`. 43 | pub fn braced(input: ParseStream) -> syn::Result<(syn::token::Brace, T)> { 44 | let fork = input.fork(); 45 | if fork.peek(syn::token::Brace) { 46 | let (brace, inner) = extract_braced(&fork).expect("peeked brace"); 47 | let ast = inner.parse::()?; 48 | if inner.is_empty() { 49 | input.advance_to(&fork); 50 | Ok((brace, ast)) 51 | } else { 52 | Err(inner.error("found extra tokens trying to parse braced expression")) 53 | } 54 | } else { 55 | Err(input.error("no brace found")) 56 | } 57 | } 58 | 59 | /// Parses an AST wrapped in parens. 60 | /// 61 | /// Does not advance the token stream if the inner stream does not completely 62 | /// match `T`, including if there are more tokens after the `T`. 63 | pub fn parenthesized(input: ParseStream) -> syn::Result<(syn::token::Paren, T)> { 64 | let fork = input.fork(); 65 | if fork.peek(syn::token::Paren) { 66 | let (paren, inner) = extract_parenthesized(&fork).expect("peeked paren"); 67 | let ast = inner.parse::()?; 68 | if inner.is_empty() { 69 | input.advance_to(&fork); 70 | Ok((paren, ast)) 71 | } else { 72 | Err(inner.error("found extra tokens trying to parse parenthesized expression")) 73 | } 74 | } else { 75 | Err(input.error("no paren found")) 76 | } 77 | } 78 | 79 | pub fn rollback_err(input: ParseStream, parser: F) -> Option 80 | where 81 | F: Fn(ParseStream) -> syn::Result, 82 | { 83 | let fork = input.fork(); 84 | match parser(&fork) { 85 | Ok(val) => { 86 | input.advance_to(&fork); 87 | Some(val) 88 | } 89 | Err(_) => None, 90 | } 91 | } 92 | 93 | /// Equivalent to parsing a [`TokenStream`] and unwrapping. 94 | pub fn take_rest(input: ParseStream) -> TokenStream { 95 | TokenStream::parse(input).expect("parsing TokenStream should never fail") 96 | } 97 | -------------------------------------------------------------------------------- /leptos-mview-core/src/span.rs: -------------------------------------------------------------------------------- 1 | //! Mini helper functions for working with spans. 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use quote::quote; 5 | 6 | /// Tries to join two spans together, returning just the first span if 7 | /// unable to join. 8 | /// 9 | /// The spans are unable to join if the user is not on nightly or the spans 10 | /// are in different files. 11 | pub fn join(s1: Span, s2: Span) -> Span { s1.join(s2).unwrap_or(s1) } 12 | 13 | /// Gives each span of `spans` the color of a variable. 14 | /// 15 | /// Returns an iterator of [`TokenStream`]s that need to be expanded to 16 | /// somewhere in the macro. Each [`TokenStream`] contains `let _ = ();`, so 17 | /// putting it in a block is the easiest way. 18 | pub fn color_all(spans: impl IntoIterator) -> impl Iterator { 19 | spans.into_iter().map(|span| { 20 | let ident = syn::Ident::new("__x", span); 21 | quote! { let #ident = (); } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /leptos-mview-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leptos-mview-macro" 3 | description = "Proc macro export for leptos-mview" 4 | readme = "README.md" 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | proc-macro2.workspace = true 15 | proc-macro-error2.workspace = true 16 | leptos-mview-core = { path = "../leptos-mview-core", version = "0.4.4" } 17 | 18 | # needed for doctests to run 19 | [dev-dependencies] 20 | leptos.workspace = true 21 | 22 | [features] 23 | nightly = ["proc-macro-error2/nightly"] 24 | -------------------------------------------------------------------------------- /leptos-mview-macro/README.md: -------------------------------------------------------------------------------- 1 | This crate is an implementation detail. 2 | 3 | See `leptos-mview` for the macro instead. 4 | -------------------------------------------------------------------------------- /leptos-mview-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro_error2::proc_macro_error; 3 | 4 | /// A concise view macro for Leptos. 5 | /// 6 | /// See [module documentation](https://docs.rs/leptos-mview/) for more usage details. 7 | /// 8 | /// # Examples 9 | /// 10 | /// ``` 11 | /// # use leptos_mview_macro::mview; use leptos::prelude::*; 12 | /// let input = RwSignal::new(String::new()); 13 | /// let (red, set_red) = signal(true); 14 | /// 15 | /// mview! { 16 | /// !DOCTYPE html; 17 | /// h1.title("A great website") 18 | /// 19 | /// input 20 | /// #some-id 21 | /// type="text" 22 | /// bind:value={input} 23 | /// class:{red} // class:red={red} 24 | /// on:click={move |_| set_red(false)}; 25 | /// 26 | /// // {move || !input().is_empty()} 27 | /// Show when=[!input().is_empty()] ( 28 | /// Await 29 | /// future={fetch_from_db(input())} 30 | /// blocking 31 | /// |db_info| ( 32 | /// em("DB info is: " {*db_info}) 33 | /// // {move || format!("{:?}", input())} 34 | /// span("Query was: " f["{:?}", input()]) 35 | /// ) 36 | /// ) 37 | /// 38 | /// SlotIf cond={red} ( 39 | /// slot:Then("red") 40 | /// slot:ElseIf cond={Signal::derive(move || input().is_empty())} ("empty") 41 | /// slot:Fallback ("odd") 42 | /// ) 43 | /// } 44 | /// # ; 45 | /// 46 | /// async fn fetch_from_db(input: String) -> usize { input.len() } 47 | /// 48 | /// # #[slot] struct Then { children: ChildrenFn } 49 | /// # #[slot] struct ElseIf { #[prop(into)] cond: Signal, children: ChildrenFn } 50 | /// # #[slot] struct Fallback { children: ChildrenFn } 51 | /// # 52 | /// # #[component] 53 | /// # fn SlotIf( 54 | /// # #[prop(into)] cond: Signal, 55 | /// # then: Then, 56 | /// # #[prop(optional)] else_if: Vec, 57 | /// # #[prop(optional)] fallback: Option, 58 | /// # ) -> impl IntoView { 59 | /// # move || { 60 | /// # if cond() { 61 | /// # (then.children)().into_any() 62 | /// # } else if let Some(else_if) = else_if.iter().find(|i| (i.cond)()) { 63 | /// # (else_if.children)().into_any() 64 | /// # } else if let Some(fallback) = &fallback { 65 | /// # (fallback.children)().into_any() 66 | /// # } else { 67 | /// # ().into_any() 68 | /// # } 69 | /// # } 70 | /// # } 71 | /// ``` 72 | #[proc_macro_error] 73 | #[proc_macro] 74 | #[rustfmt::skip] 75 | pub fn mview(input: TokenStream) -> TokenStream { 76 | leptos_mview_core::mview_impl(input.into()).into() 77 | } 78 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | An alternative `view!` macro for [Leptos](https://github.com/leptos-rs/leptos/tree/main) inspired by [maud](https://maud.lambda.xyz/). 3 | 4 | # Example 5 | 6 | A little preview of the syntax: 7 | 8 | ``` 9 | use leptos::prelude::*; 10 | use leptos_mview::mview; 11 | 12 | #[component] 13 | fn MyComponent() -> impl IntoView { 14 | let (value, set_value) = signal(String::new()); 15 | let red_input = move || value().len() % 2 == 0; 16 | 17 | mview! { 18 | h1.title("A great website") 19 | br; 20 | 21 | input 22 | type="text" 23 | data-index=0 24 | class:red={red_input} 25 | prop:{value} 26 | on:change={move |ev| { 27 | set_value(event_target_value(&ev)) 28 | }}; 29 | 30 | Show 31 | when=[!value().is_empty()] 32 | fallback=[mview! { "..." }] 33 | ( 34 | Await 35 | future={fetch_from_db(value())} 36 | blocking 37 | |db_info| ( 38 | p("Things found: " strong({*db_info}) "!") 39 | p("Is bad: " f["{}", red_input()]) 40 | ) 41 | ) 42 | } 43 | } 44 | 45 | async fn fetch_from_db(data: String) -> usize { data.len() } 46 | ``` 47 | 48 |
49 | Explanation of the example: 50 | 51 | ``` 52 | use leptos::prelude::*; 53 | use leptos_mview::mview; 54 | 55 | #[component] 56 | fn MyComponent() -> impl IntoView { 57 | let (value, set_value) = signal(String::new()); 58 | let red_input = move || value().len() % 2 == 0; 59 | 60 | mview! { 61 | // specify tags and attributes, children go in parentheses. 62 | // classes (and ids) can be added like CSS selectors. 63 | // same as `h1 class="title"` 64 | h1.title("A great website") 65 | // elements with no children end with a semi-colon 66 | br; 67 | 68 | input 69 | type="text" 70 | data-index=0 // kebab-cased identifiers supported 71 | class:red={red_input} // non-literal values must be wrapped in braces 72 | prop:{value} // shorthand! same as `prop:value={value}` 73 | on:change={move |ev| { // event handlers same as leptos 74 | set_value(event_target_value(&ev)) 75 | }}; 76 | 77 | Show 78 | // values wrapped in brackets `[body]` are expanded to `{move || body}` 79 | when=[!value().is_empty()] // `{move || !value().is_empty()}` 80 | fallback=[mview! { "..." }] // `{move || mview! { "..." }}` 81 | ( // I recommend placing children like this when attributes are multi-line 82 | Await 83 | future={fetch_from_db(value())} 84 | blocking // expanded to `blocking=true` 85 | // children take arguments with a 'closure' 86 | // this is very different to `let:db_info` in Leptos! 87 | |db_info| ( 88 | p("Things found: " strong({*db_info}) "!") 89 | // bracketed expansion works in children too! 90 | // this one also has a special prefix to add `format!` into the expansion! 91 | // {move || format!("{}", red_input()} 92 | p("Is bad: " f["{}", red_input()]) 93 | ) 94 | ) 95 | } 96 | } 97 | 98 | // fake async function 99 | async fn fetch_from_db(data: String) -> usize { data.len() } 100 | ``` 101 | 102 |
103 | 104 | # Purpose 105 | 106 | The `view!` macros in Leptos is often the largest part of a component, and can get extremely long when writing complex components. This macro aims to be as **concise** as possible, trying to **minimise unnecessary punctuation/words** and **shorten common patterns**. 107 | 108 | # Compatibility 109 | 110 | This macro will be compatible with the latest stable release of Leptos. The macro references Leptos items using `::leptos::...`, no items are re-exported from this crate. Therefore, this crate will likely work with any Leptos version if no view-related items are changed. 111 | 112 | The below are the versions with which I have tested it to be working. It is likely that the macro works with more versions of Leptos. 113 | 114 | | `leptos_mview` version | Compatible `leptos` version | 115 | | ---------------------- | --------------------------- | 116 | | `0.1` | `0.5` | 117 | | `0.2` | `0.5`, `0.6` | 118 | | `0.3` | `0.6` | 119 | | `0.4` | `0.7` | 120 | 121 | This crate also has a feature `"nightly"` that enables better proc-macro diagnostics (simply enables the nightly feature in proc-macro-error2. Necessary while [this pr](https://github.com/GnomedDev/proc-macro-error-2/pull/5) is not yet merged). 122 | 123 | # Syntax details 124 | 125 | ## Elements 126 | 127 | Elements have the following structure: 128 | 129 | 1. Element / component tag name / path (`div`, `App`, `component::Codeblock`). 130 | 2. Any classes or ids prefixed with a dot `.` or hash `#` respectively. 131 | 3. A space-separated list of attributes and directives (`class="primary"`, `on:click={...}`). 132 | 4. Children in parens or braces (`("hi")` or `{ "hi!" }`), or a semi-colon for no children (`;`). 133 | 134 | Example: 135 | ``` 136 | # use leptos_mview::mview; use leptos::prelude::*; 137 | # let handle_input = |_| (); 138 | # #[component] fn MyComponent(data: i32, other: &'static str) -> impl IntoView {} 139 | mview! { 140 | div.primary(strong("hello world")) 141 | input type="text" on:input={handle_input}; 142 | MyComponent data=3 other="hi"; 143 | } 144 | # ; 145 | ``` 146 | 147 | Adding generics is the same as in Leptos: add it directly after the component name, with or without the turbofish. 148 | 149 | ``` 150 | # use leptos::prelude::*; use leptos_mview::mview; 151 | # use core::marker::PhantomData; 152 | #[component] 153 | pub fn GenericComponent(ty: PhantomData) -> impl IntoView { 154 | std::any::type_name::() 155 | } 156 | 157 | #[component] 158 | pub fn App() -> impl IntoView { 159 | mview! { 160 | // both with and without turbofish is supported 161 | GenericComponent:: ty={PhantomData}; 162 | GenericComponent ty={PhantomData}; 163 | GenericComponent ty={PhantomData}; 164 | } 165 | } 166 | ``` 167 | 168 | Note that due to [Reserving syntax](https://doc.rust-lang.org/edition-guide/rust-2021/reserving-syntax.html), the `#` for ids must have a space before it. 169 | 170 | ``` 171 | # use leptos_mview::mview; use leptos::prelude::*; 172 | mview! { 173 | nav #primary ("...") 174 | // not allowed: nav#primary ("...") 175 | } 176 | # ; 177 | ``` 178 | 179 | Classes/ids created with the selector syntax can be mixed with the attribute `class="..."` and directive `class:a-class={signal}` as well. 180 | 181 | There is also a special element `!DOCTYPE html;`, equivalent to ``. 182 | 183 | ## Slots 184 | 185 | [Slots](https://docs.rs/leptos/latest/leptos/attr.slot.html) ([another example](https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs)) are supported by prefixing the struct with `slot:` inside the parent's children. 186 | 187 | The name of the parameter in the component function must be the same as the slot's name, in snake case. 188 | 189 | Using the slots defined by the [`SlotIf` example linked](https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs): 190 | ``` 191 | use leptos::prelude::*; 192 | use leptos_mview::mview; 193 | 194 | #[component] 195 | pub fn App() -> impl IntoView { 196 | let (count, set_count) = signal(0); 197 | let is_even = Signal::derive(move || count() % 2 == 0); 198 | let is_div5 = Signal::derive(move || count() % 5 == 0); 199 | let is_div7 = Signal::derive(move || count() % 7 == 0); 200 | 201 | mview! { 202 | SlotIf cond={is_even} ( 203 | slot:Then ("even") 204 | slot:ElseIf cond={is_div5} ("divisible by 5") 205 | slot:ElseIf cond={is_div7} ("divisible by 7") 206 | slot:Fallback ("odd") 207 | ) 208 | } 209 | } 210 | # #[slot] struct Then { children: ChildrenFn } 211 | # #[slot] struct ElseIf { #[prop(into)] cond: Signal, children: ChildrenFn } 212 | # #[slot] struct Fallback { children: ChildrenFn } 213 | # 214 | # #[component] 215 | # fn SlotIf( 216 | # #[prop(into)] cond: Signal, 217 | # then: Then, 218 | # #[prop(optional)] else_if: Vec, 219 | # #[prop(optional)] fallback: Option, 220 | # ) -> impl IntoView { 221 | # move || { 222 | # if cond() { 223 | # (then.children)().into_any() 224 | # } else if let Some(else_if) = else_if.iter().find(|i| (i.cond)()) { 225 | # (else_if.children)().into_any() 226 | # } else if let Some(fallback) = &fallback { 227 | # (fallback.children)().into_any() 228 | # } else { 229 | # ().into_any() 230 | # } 231 | # } 232 | # } 233 | ``` 234 | 235 | ## Values 236 | 237 | There are (currently) 3 main types of values you can pass in: 238 | 239 | - **Literals** can be passed in directly to attribute values (like `data=3`, `class="main"`, `checked=true`). 240 | - However, children do not accept literal numbers or bools - only strings. 241 | ```compile_fail 242 | # use leptos_mview::mview; 243 | // does NOT compile. 244 | mview! { p("this works " 0 " times: " true) } 245 | # ; 246 | ``` 247 | 248 | - Everything else must be passed in as a **block**, including variables, closures, or expressions. 249 | ``` 250 | # use leptos_mview::mview; use leptos::prelude::*; 251 | # let input_type = "text"; 252 | # let handle_input = |_a: i32| (); 253 | mview! { 254 | input 255 | class="main" 256 | checked=true 257 | data-index=3 258 | type={input_type} 259 | on:input={move |_| handle_input(1)}; 260 | } 261 | # ; 262 | ``` 263 | 264 | This is not valid: 265 | ```compile_fail 266 | # use leptos_mview::mview; 267 | let input_type = "text"; 268 | // ❌ This is not valid! Wrap input_type in braces. 269 | mview! { input type=input_type } 270 | # ; 271 | ``` 272 | 273 | - Values wrapped in **brackets** (like `value=[a_bool().to_string()]`) are shortcuts for a block with an empty closure `move || ...` (to `value={move || a_bool().to_string()}`). 274 | ```rust 275 | # use leptos::prelude::*; use leptos_mview::mview; 276 | # let number = || 3; 277 | mview! { 278 | Show 279 | fallback=[()] // common for not wanting a fallback as `|| ()` 280 | when=[number() % 2 == 0] // `{move || number() % 2 == 0}` 281 | ( 282 | "number + 1 = " [number() + 1] // works in children too! 283 | ) 284 | } 285 | # ; 286 | ``` 287 | 288 | - Note that this always expands to `move || ...`: for any closures that take an argument, use the full closure block instead. 289 | ```compile_error 290 | # use leptos_mview::mview; 291 | # use leptos::logging::log; 292 | mview! { 293 | input type="text" on:click=[log!("THIS DOESNT WORK")]; 294 | } 295 | ``` 296 | 297 | Instead: 298 | ``` 299 | # use leptos_mview::mview; use leptos::prelude::*; 300 | # use leptos::logging::log; 301 | mview! { 302 | input type="text" on:click={|_| log!("THIS WORKS!")}; 303 | } 304 | # ; 305 | ``` 306 | 307 | The bracketed values can also have some special prefixes for even more common shortcuts! 308 | - Currently, the only one is `f` - e.g. `f["{:.2}", stuff()]`. Adding an `f` will add `format!` into the closure. This is equivalent to `[format!("{:.2}", stuff())]` or `{move || format!("{:.2}", stuff())}`. 309 | 310 | ## Attributes 311 | 312 | ### Key-value attributes 313 | 314 | Most attributes are `key=value` pairs. The `value` follows the rules from above. The `key` has a few variations: 315 | 316 | - Standard identifier: identifiers like `type`, `an_attribute`, `class`, `id` etc are valid keys. 317 | - Kebab-case identifier: identifiers can be kebab-cased, like `data-value`, `an-attribute`. 318 | - NOTE: on HTML elements, this will be put on the element as is: `div data-index="0";` becomes `
`. **On components**, hyphens are converted to underscores then passed into the component builder. 319 | 320 | For example, this component: 321 | ```ignore 322 | #[component] 323 | fn Something(some_attribute: i32) -> impl IntoView { ... } 324 | ``` 325 | 326 | Can be used elsewhere like this: 327 | ``` 328 | # use leptos::prelude::*; use leptos_mview::mview; 329 | # #[component] fn Something(some_attribute: i32) -> impl IntoView {} 330 | mview! { Something some-attribute=5; } 331 | # ; 332 | ``` 333 | 334 | And the `some-attribute` will be passed in to the `some_attribute` argument. 335 | 336 | - Attribute shorthand: if the name of the attribute and value are the same, e.g. `class={class}`, you can replace this with `{class}` to mean the same thing. 337 | ``` 338 | # use leptos_mview::mview; use leptos::prelude::*; 339 | let class = "these are classes"; 340 | let id = "primary"; 341 | mview! { 342 | div {class} {id} ("this has 3 classes and id='primary'") 343 | } 344 | # ; 345 | ``` 346 | 347 | See also: [kebab-case identifiers with attribute shorthand](#kebab-case-identifiers-with-attribute-shorthand) 348 | 349 | Note that the special `node_ref` or `ref` or `_ref` or `ref_` attribute in Leptos to bind the element to a variable is just `ref={variable}` in here. 350 | 351 | ### Boolean attributes 352 | 353 | Another shortcut is that boolean attributes can be written without adding `=true`. Watch out though! `checked` is **very different** to `{checked}`. 354 | ``` 355 | # use leptos::prelude::*; use leptos_mview::mview; 356 | // recommend usually adding #[prop(optional)] to all these 357 | #[component] 358 | fn LotsOfFlags(wide: bool, tall: bool, red: bool, curvy: bool, count: i32) -> impl IntoView {} 359 | 360 | mview! { LotsOfFlags wide tall red=false curvy count=3; } 361 | # ; 362 | // same as... 363 | mview! { LotsOfFlags wide=true tall=true red=false curvy=true count=3; } 364 | # ; 365 | ``` 366 | 367 | See also: [boolean attributes on HTML elements](#boolean-attributes-on-html-elements) 368 | 369 | ### Directives 370 | 371 | Some special attributes (distinguished by the `:`) called **directives** have special functionality. All have the same behaviour as Leptos. These include: 372 | - `class:class-name=[when to show]` 373 | - `style:style-key=[style value]` 374 | - `on:event={move |ev| event handler}` 375 | - `prop:property-name={signal}` 376 | - `attr:name={value}` 377 | - `clone:ident_to_clone` 378 | - `use:directive_name` or `use:directive_name={params}` 379 | - `bind:checked={rwsignal}` or `bind:value={(getter, setter)}` 380 | 381 | All of these directives except `clone` also support the attribute shorthand: 382 | 383 | ``` 384 | # use leptos::prelude::*; use leptos_mview::mview; 385 | let color = RwSignal::new("red".to_string()); 386 | let disabled = false; 387 | mview! { 388 | div style:{color} class:{disabled}; 389 | } 390 | # ; 391 | ``` 392 | 393 | The `class` and `style` directives also support using string literals, for more complicated names. Make sure the string for `class:` doesn't have spaces, or it will panic! 394 | 395 | ``` 396 | # use leptos::prelude::*; use leptos_mview::mview; 397 | let yes = move || true; 398 | mview! { 399 | div class:"complex-[class]-name"={yes} 400 | style:"doesn't-exist"="white"; 401 | } 402 | # ; 403 | ``` 404 | 405 | Note that the `use:` directive automatically calls `.into()` on its argument, consistent with behaviour from Leptos. 406 | 407 | ## Children 408 | 409 | You may have noticed that the `let:data` prop was missing from the previous section on directive attributes! 410 | 411 | This is replaced with a closure right before the children block. This way, you can pass in multiple arguments to the children more easily. 412 | 413 | ``` 414 | # use leptos::prelude::*; use leptos_mview::mview; 415 | # leptos::task::Executor::init_futures_executor().unwrap(); 416 | mview! { 417 | Await 418 | future={async { 3 }} 419 | |monkeys| ( 420 | p({*monkeys} " little monkeys, jumping on the bed.") 421 | ) 422 | } 423 | # ; 424 | ``` 425 | 426 | Note that you will usually need to add a `*` before the data you are using. If you forget that, rust-analyser will tell you to dereference here: `*{monkeys}`. This is obviously invalid - put it inside the braces. 427 | 428 | Children can be wrapped in either braces or parentheses, whichever you prefer. 429 | 430 | ``` 431 | # use leptos::prelude::*; use leptos_mview::mview; 432 | mview! { 433 | p { 434 | "my " strong("bold") " and " em("fancy") " text." 435 | } 436 | } 437 | # ; 438 | ``` 439 | 440 | Summary from the previous section on values in case you missed it: children can be literal strings (not bools or numbers!), blocks with Rust code inside (`{*monkeys}`), or the closure shorthand `[number() + 1]`. 441 | 442 | Children with closures are also supported on slots. 443 | 444 | # Extra details 445 | 446 | ## Kebab-case identifiers with attribute shorthand 447 | 448 | If an attribute shorthand has hyphens: 449 | - On components, both the key and value will be converted to underscores. 450 | ``` 451 | # use leptos::prelude::*; use leptos_mview::mview; 452 | # #[component] fn Something(some_attribute: i32) -> impl IntoView {} 453 | let some_attribute = 5; 454 | mview! { Something {some-attribute}; } 455 | # ; 456 | // same as... 457 | mview! { Something {some_attribute}; } 458 | # ; 459 | // same as... 460 | mview! { Something some_attribute={some_attribute}; } 461 | # ; 462 | ``` 463 | 464 | - On HTML elements, the key will keep hyphens, but the value will be turned into an identifier with underscores. 465 | ``` 466 | # use leptos_mview::mview; use leptos::prelude::*; 467 | let aria_label = "a good label"; 468 | mview! { input {aria-label}; } 469 | # ; 470 | // same as... 471 | mview! { input aria-label={aria_label}; } 472 | # ; 473 | ``` 474 | 475 | ## Boolean attributes on HTML elements 476 | 477 | Note the behaviour from Leptos: setting an HTML attribute to true adds the attribute with no value associated. 478 | ``` 479 | # use leptos::prelude::*; 480 | view! { } 481 | # ; 482 | ``` 483 | Becomes ``, NOT `checked="true"` or `data-smth="true"` or `not-here="false"`. 484 | 485 | To have the attribute have a value of the string "true" or "false", use `.to_string()` on the bool. Make sure that it's in a closure if you're working with signals too. 486 | ``` 487 | # use leptos::prelude::*; 488 | # use leptos_mview::mview; 489 | let boolean_signal = RwSignal::new(true); 490 | mview! { input type="checkbox" checked=[boolean_signal().to_string()]; } 491 | # ; 492 | // or, if you prefer 493 | mview! { input type="checkbox" checked=f["{}", boolean_signal()]; } 494 | # ; 495 | ``` 496 | 497 | # Contributing 498 | 499 | Please feel free to make a PR/issue if you have feature ideas/bugs to report/feedback :) 500 | 501 | */ 502 | 503 | // note: to transfer above to README.md, install `cargo-rdme` and run 504 | // `cargo rdme` 505 | // Some bits are slightly broken, fix up stray `compile_error`/ 506 | // `ignore`, missing `rust` annotations and remove `#` lines. 507 | 508 | pub use leptos_mview_macro::mview; 509 | 510 | /// Not for public use. Do not implement anything on this. 511 | #[doc(hidden)] 512 | pub struct MissingValueAfterEq; 513 | -------------------------------------------------------------------------------- /tests/component.rs: -------------------------------------------------------------------------------- 1 | use leptos::{prelude::*, task::Executor}; 2 | use leptos_mview::mview; 3 | mod utils; 4 | use utils::check_str; 5 | 6 | #[test] 7 | fn basic() { 8 | #[component] 9 | fn MyComponent( 10 | my_attribute: &'static str, 11 | another_attribute: Vec, 12 | children: Children, 13 | ) -> impl IntoView { 14 | mview! { 15 | div class="my-component" data-my-attribute={my_attribute} data-another=f["{another_attribute:?}"] { 16 | {children()} 17 | } 18 | } 19 | } 20 | 21 | _ = view! { 22 | 23 | "my child" 24 | 25 | } 26 | } 27 | 28 | #[test] 29 | fn clones() { 30 | #[component] 31 | fn Owning(children: ChildrenFn) -> impl IntoView { 32 | mview! { div { {children()} } } 33 | } 34 | 35 | let notcopy = String::new(); 36 | _ = mview! { 37 | Owning { 38 | Owning clone:notcopy { 39 | {notcopy.clone()} 40 | } 41 | } 42 | }; 43 | } 44 | 45 | // TODO: not sure why this is creating an untracked resource warning 46 | #[test] 47 | fn children_args() { 48 | Executor::init_futures_executor().unwrap(); 49 | _ = mview! { 50 | Await future={async { 3 }} |data| { 51 | p { {*data} " little monkeys, jumping on the bed." } 52 | } 53 | }; 54 | 55 | // clone should also work 56 | let name = String::new(); 57 | _ = mview! { 58 | Await 59 | future={async {"hi".to_string()}} 60 | clone:name 61 | |greeting| { 62 | {greeting.clone()} " " {name.clone()} 63 | } 64 | }; 65 | } 66 | 67 | #[test] 68 | fn generics() { 69 | use core::marker::PhantomData; 70 | // copied from https://github.com/leptos-rs/leptos/pull/1636 71 | #[component] 72 | pub fn GenericComponent(ty: PhantomData) -> impl IntoView { 73 | let _ty = ty; 74 | std::any::type_name::() 75 | } 76 | 77 | let result = mview! { 78 | GenericComponent ty={PhantomData}; 79 | GenericComponent ty={PhantomData}; 80 | GenericComponent ty={PhantomData}; 81 | }; 82 | 83 | check_str(result, ["alloc::string::String", "usize", "i32"].as_slice()); 84 | 85 | // also accept turbofish 86 | let result = mview! { 87 | GenericComponent:: ty={PhantomData}; 88 | GenericComponent:: ty={PhantomData}; 89 | GenericComponent:: ty={PhantomData}; 90 | }; 91 | 92 | check_str(result, ["alloc::string::String", "usize", "i32"].as_slice()); 93 | } 94 | 95 | #[test] 96 | fn qualified_paths() { 97 | let _result = mview! { 98 | leptos::control_flow::Show when=[true] { 99 | "a" 100 | } 101 | leptos::control_flow::Show when=[false] { 102 | "b" 103 | } 104 | }; 105 | 106 | // requires ssr feature to check the output 107 | // check_str(result, Contains::AllOfNoneOf([&["a"], &["b"]])) 108 | } 109 | 110 | // don't try parse slot:: as a slot 111 | mod slot { 112 | use leptos::prelude::*; 113 | 114 | #[component] 115 | pub fn NotASlot() -> impl IntoView {} 116 | } 117 | 118 | #[test] 119 | fn slot_peek() { 120 | _ = mview! { 121 | slot::NotASlot; 122 | } 123 | } 124 | 125 | #[test] 126 | fn let_patterns() { 127 | if false { 128 | let letters = ['a', 'b', 'c']; 129 | _ = mview! { 130 | For 131 | each=[letters.into_iter().enumerate()] 132 | key={|(i, _)| *i} 133 | |(i, letter)| { 134 | "letter " {i+1} " is " {letter} 135 | } 136 | }; 137 | } 138 | } 139 | 140 | #[component] 141 | fn TakesClass() -> impl IntoView { 142 | mview! { 143 | div class="takes-class" { 144 | "I take more classes!" 145 | } 146 | } 147 | } 148 | 149 | #[component] 150 | fn TakesIds() -> impl IntoView { 151 | mview! { 152 | div class="i-take-ids"; 153 | } 154 | } 155 | 156 | #[test] 157 | fn selectors() { 158 | let r = mview! { 159 | TakesClass.test1.test-2; 160 | }; 161 | 162 | check_str(r, r#"
= mview! { 25 | div { 26 | "hi" 27 | } 28 | }; 29 | check_str(result, r#"
hi
"#); 30 | } 31 | 32 | #[test] 33 | fn multi_element_is_fragment() { 34 | let _fragment: View<_> = mview! { 35 | div { "a" } 36 | span { "b" } 37 | }; 38 | } 39 | 40 | #[test] 41 | fn a_bunch() { 42 | let result = mview! { 43 | "hi" 44 | span class="abc" data-index={0} { 45 | strong { "d" } 46 | {3} 47 | } 48 | br; 49 | input type="checkbox" checked; 50 | }; 51 | 52 | view! { 53 | 54 | }; 55 | 56 | check_str( 57 | result, 58 | "hi\ 59 | \ 60 | d\ 61 | 3\ 62 | \ 63 |
\ 64 | ", 65 | ); 66 | } 67 | 68 | #[test] 69 | fn directive_before_attr() { 70 | let result = mview! { 71 | span class:exist=true class="dont override"; 72 | }; 73 | check_str(result, "dont override exist"); 74 | 75 | let result = mview! { 76 | span style:color="black" style="font-size: 1em;"; 77 | }; 78 | check_str(result, "font-size: 1em;;color:black;"); 79 | } 80 | 81 | #[test] 82 | fn multiple_directives() { 83 | let yes = move || true; 84 | let no = move || false; 85 | let color = move || "white"; 86 | let result = mview! { 87 | div 88 | class:here={yes} 89 | style:color={color} 90 | class:not={no} 91 | class:also-here=true 92 | class="normal" 93 | style="line-height: 1.5;" 94 | style:background-color="red"; 95 | }; 96 | 97 | check_str( 98 | result, 99 | r#"class="normal here also-here" style="line-height: 1.5;;color:white;background-color:red;""#, 100 | ); 101 | } 102 | 103 | #[test] 104 | fn string_directives() { 105 | let yes = move || true; 106 | let result = mview! { 107 | div 108 | class:"complex[class]-name"={yes} 109 | style:"doesn't-exist"="black" 110 | class:"not-here"=false; 111 | }; 112 | 113 | check_str( 114 | result, 115 | r#"class="complex[class]-name" style="doesn't-exist:black;""#, 116 | ) 117 | } 118 | 119 | #[test] 120 | fn mixed_class_creation() { 121 | let class: TextProp = "some-class another-class".into(); 122 | let r = mview! { 123 | div.always-here class=[class.get()]; 124 | }; 125 | 126 | check_str(r, r#"class="some-class another-class always-here""#); 127 | } 128 | 129 | #[test] 130 | fn custom_web_component() { 131 | let component = mview! { 132 | iconify-icon icon="a" class="something" { 133 | "b" 134 | } 135 | }; 136 | 137 | check_str( 138 | component, 139 | r#"b"#, 140 | ); 141 | } 142 | 143 | #[test] 144 | fn has_ref() { 145 | let node_ref = NodeRef::new(); 146 | mview! { 147 | div ref={node_ref}; 148 | }; 149 | } 150 | 151 | #[test] 152 | fn bindings() { 153 | let (name, set_name) = signal("Controlled".to_string()); 154 | let email = RwSignal::new("".to_string()); 155 | let spam_me = RwSignal::new(true); 156 | let group = RwSignal::new("one".to_string()); 157 | 158 | mview! { 159 | input type="text" bind:value={(name, set_name)}; 160 | input type="email" bind:value={email}; 161 | input type="checkbox" bind:checked={spam_me}; 162 | 163 | input type="radio" value="one" bind:group={group}; 164 | input type="radio" value="two" bind:group={group}; 165 | }; 166 | } 167 | 168 | #[test] 169 | fn doctype() { 170 | let doctype = mview! { 171 | !DOCTYPE html; 172 | div; 173 | }; 174 | 175 | check_str(doctype, "
"); 176 | } 177 | -------------------------------------------------------------------------------- /tests/paren_children.rs: -------------------------------------------------------------------------------- 1 | //! Test allowing parentheses to wrap the children as well. 2 | 3 | use leptos::prelude::*; 4 | use leptos_mview::mview; 5 | mod utils; 6 | use utils::check_str; 7 | 8 | #[test] 9 | fn html_child() { 10 | let res = mview! { 11 | strong("child") 12 | "go" em("to" a href="#" ("nowhere")) 13 | }; 14 | 15 | check_str( 16 | res, 17 | ["childgo"].as_slice(), 18 | ) 19 | } 20 | 21 | #[test] 22 | fn component_closure() { 23 | if false { 24 | _ = mview! { 25 | For each={|| [1, 2, 3]} 26 | key={|i| *i} 27 | |index| ( 28 | "i is " {index} 29 | ) 30 | }; 31 | } 32 | } 33 | 34 | #[test] 35 | fn component_child() { 36 | if false { 37 | _ = mview! { 38 | Show 39 | when={|| true} 40 | ( 41 | "hello!" 42 | ) 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/router.rs: -------------------------------------------------------------------------------- 1 | use leptos::{context::Provider, prelude::*}; 2 | use leptos_mview::mview; 3 | use leptos_router::{ 4 | components::{Route, Router, Routes}, 5 | location::RequestUrl, 6 | path, 7 | }; 8 | 9 | mod utils; 10 | use utils::check_str; 11 | 12 | #[test] 13 | fn router() { 14 | #[component] 15 | fn RouterContext(children: ChildrenFn, path: &'static str) -> impl IntoView { 16 | // `Router` panicks if it is not provided with a `RequestUrl` context 17 | mview! { 18 | Provider value={RequestUrl::new(path)} ( 19 | {children()} 20 | ) 21 | } 22 | } 23 | 24 | let router = || { 25 | mview! { 26 | Router { 27 | main { 28 | Routes 29 | fallback=[mview! { p("not found")}] 30 | ( 31 | Route 32 | path={path!("")} 33 | view=[mview! { p("root route") }]; 34 | 35 | Route 36 | path={path!("route2")} 37 | view=[mview! { p("you are on /route2") }]; 38 | ) 39 | } 40 | } 41 | } 42 | }; 43 | 44 | Owner::new().with(|| { 45 | let router_context1 = mview! { 46 | RouterContext path="/" ( 47 | {router()} 48 | ) 49 | }; 50 | 51 | let router_context2 = mview! { 52 | RouterContext path="/route2" ( 53 | {router()} 54 | ) 55 | }; 56 | 57 | let router_context3 = mview! { 58 | RouterContext path="/does-not-exist" ( 59 | {router()} 60 | ) 61 | }; 62 | 63 | check_str(router_context1, "

root route"); 64 | check_str(router_context2, "

you are on /route2"); 65 | check_str(router_context3, "

not found"); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /tests/slots.rs: -------------------------------------------------------------------------------- 1 | use leptos::{either::Either, prelude::*}; 2 | use leptos_mview::mview; 3 | use utils::{check_str, Contains}; 4 | mod utils; 5 | 6 | // same example as the one given in the #[slot] proc macro documentation. 7 | 8 | #[test] 9 | fn test_example() { 10 | #[slot] 11 | struct HelloSlot { 12 | #[prop(optional)] 13 | children: Option, 14 | } 15 | 16 | #[component] 17 | fn HelloComponent(hello_slot: HelloSlot) -> impl IntoView { 18 | if let Some(children) = hello_slot.children { 19 | Either::Left((children)().into_view()) 20 | } else { 21 | Either::Right(().into_view()) 22 | } 23 | } 24 | 25 | view! { 26 | 27 | "Hello, World!" 28 | 29 | }; 30 | 31 | let r = mview! { 32 | HelloComponent { 33 | slot:HelloSlot { 34 | "Hello, World!" 35 | } 36 | } 37 | }; 38 | 39 | check_str(r, "Hello, World!"); 40 | } 41 | 42 | // https://github.com/leptos-rs/leptos/blob/main/examples/slots/src/lib.rs 43 | 44 | #[slot] 45 | struct Then { 46 | children: ChildrenFn, 47 | } 48 | 49 | #[slot] 50 | struct ElseIf { 51 | #[prop(into)] 52 | cond: Signal, 53 | children: ChildrenFn, 54 | } 55 | 56 | #[slot] 57 | struct Fallback { 58 | children: ChildrenFn, 59 | } 60 | 61 | #[component] 62 | fn SlotIf( 63 | #[prop(into)] cond: Signal, 64 | then: Then, 65 | #[prop(optional)] else_if: Vec, 66 | #[prop(optional)] fallback: Option, 67 | ) -> impl IntoView { 68 | move || { 69 | if cond.get() { 70 | Either::Left((then.children)().into_view()) 71 | } else if let Some(else_if) = else_if.iter().find(|i| i.cond.get()) { 72 | Either::Left((else_if.children)().into_view()) 73 | } else if let Some(fallback) = &fallback { 74 | Either::Left((fallback.children)().into_view()) 75 | } else { 76 | Either::Right(().into_view()) 77 | } 78 | } 79 | } 80 | 81 | #[test] 82 | pub fn multiple_slots() { 83 | for (count, ans) in [(0, "even"), (5, "x5"), (45, "x5"), (9, "odd"), (7, "x7")] { 84 | let is_even = count % 2 == 0; 85 | let is_div5 = count % 5 == 0; 86 | let is_div7 = count % 7 == 0; 87 | 88 | let r = mview! { 89 | SlotIf cond={is_even} { 90 | slot:Then { "even" } 91 | slot:ElseIf cond={is_div5} { "x5" } 92 | slot:ElseIf cond={is_div7} { "x7" } 93 | slot:Fallback { "odd" } 94 | } 95 | }; 96 | 97 | check_str(r, ans); 98 | } 99 | } 100 | 101 | #[test] 102 | pub fn accept_multiple_use_single() { 103 | // else_if takes Vec, check if just giving a single one 104 | // (which should just pass a single ElseIf instead of a vec) 105 | // still works 106 | let r = mview! { 107 | SlotIf cond=false { 108 | slot:Then { "no!" } 109 | slot:ElseIf cond=true { "yes!" } 110 | slot:Fallback { "absolutely not" } 111 | } 112 | }; 113 | 114 | check_str(r, "yes!"); 115 | } 116 | 117 | #[test] 118 | pub fn optional_slots() { 119 | let no_other = mview! { 120 | SlotIf cond=true { 121 | slot:Then { "yay!" } 122 | } 123 | }; 124 | 125 | check_str(no_other, "yay!"); 126 | 127 | let no_fallback = mview! { 128 | div { 129 | SlotIf cond=false { 130 | slot:Then { "not here" } 131 | slot:ElseIf cond=false { "not this either" } 132 | } 133 | } 134 | }; 135 | 136 | check_str(no_fallback, ">

") 137 | } 138 | 139 | #[component] 140 | fn ChildThenIf( 141 | #[prop(into)] cond: Signal, 142 | children: ChildrenFn, 143 | #[prop(default=vec![])] else_if: Vec, 144 | #[prop(optional)] fallback: Option, 145 | ) -> impl IntoView { 146 | move || { 147 | if cond.get() { 148 | Either::Left((children)().into_view()) 149 | } else if let Some(else_if) = else_if.iter().find(|i| i.cond.get()) { 150 | Either::Left((else_if.children)().into_view()) 151 | } else if let Some(fallback) = &fallback { 152 | Either::Left((fallback.children)().into_view()) 153 | } else { 154 | Either::Right(().into_view()) 155 | } 156 | } 157 | } 158 | 159 | #[test] 160 | fn children_and_slots() { 161 | let then = mview! { 162 | ChildThenIf cond=true { 163 | "here" 164 | slot:ElseIf cond=true { "not :(" } 165 | } 166 | }; 167 | 168 | check_str( 169 | then, 170 | Contains::AllOfNoneOf([["here"].as_slice(), ["not :("].as_slice()]), 171 | ); 172 | 173 | let elseif = mview! { 174 | div { 175 | ChildThenIf cond=false { 176 | "not :(" 177 | slot:ElseIf cond=true { "yes!" } 178 | } 179 | } 180 | }; 181 | 182 | check_str( 183 | elseif, 184 | Contains::AllOfNoneOf([["yes!"].as_slice(), ["not :("].as_slice()]), 185 | ); 186 | 187 | let mixed = mview! { 188 | div { 189 | ChildThenIf cond=true { 190 | "here 1" 191 | slot:ElseIf cond=false { "not this" } 192 | "here 2" 193 | span { "here 3" } 194 | slot:ElseIf cond=true { "still not here" } 195 | 196 | ChildThenIf cond=false { 197 | "nested not here" 198 | slot:Fallback { "nested is here!" } 199 | } 200 | 201 | slot:Fallback { "this one is not present" } 202 | "yet another shown" 203 | } 204 | } 205 | }; 206 | 207 | check_str( 208 | mixed, 209 | Contains::AllOfNoneOf([ 210 | [ 211 | "here 1", 212 | "here 2", 213 | "here 3", 214 | "nested is here!", 215 | "yet another shown", 216 | ] 217 | .as_slice(), 218 | [ 219 | "not this", 220 | "still not here", 221 | "nested not here", 222 | "this one is not present", 223 | ] 224 | .as_slice(), 225 | ]), 226 | ); 227 | } 228 | 229 | #[test] 230 | fn clone_in_slot() { 231 | let notcopy = String::new(); 232 | _ = mview! { 233 | ChildThenIf cond=true { 234 | "yes" 235 | slot:Fallback { 236 | ChildThenIf cond=true { 237 | "no" 238 | slot:Fallback clone:notcopy { 239 | {notcopy.clone()} 240 | } 241 | } 242 | } 243 | } 244 | }; 245 | } 246 | -------------------------------------------------------------------------------- /tests/spread.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | use leptos_mview::mview; 3 | mod utils; 4 | use utils::check_str; 5 | 6 | #[test] 7 | fn spread_html_element() { 8 | let attrs = view! { <{..} data-index=0 class="c" data-another="b" /> }; 9 | let res = mview! { 10 | div {..attrs} data-yet-another-thing="z" { 11 | "children" 12 | } 13 | }; 14 | check_str( 15 | res, 16 | r#"
children
"#, 17 | ); 18 | } 19 | 20 | #[test] 21 | fn spread_in_component() { 22 | #[component] 23 | fn Spreadable() -> impl IntoView { 24 | mview! { 25 | div; 26 | } 27 | } 28 | 29 | let res = mview! { 30 | Spreadable attr:class="b" attr:contenteditable=true attr:data-index=0; 31 | }; 32 | check_str( 33 | res, 34 | r#"
"#, 35 | ); 36 | } 37 | 38 | #[test] 39 | fn spread_on_component() { 40 | #[component] 41 | fn Spreadable() -> impl IntoView { 42 | mview! { 43 | div; 44 | } 45 | } 46 | 47 | let attrs = view! { <{..} data-a="b" data-index=0 class="c" /> }; 48 | 49 | let res = mview! { 50 | Spreadable attr:contenteditable=true {..attrs}; 51 | }; 52 | check_str( 53 | res, 54 | r#"
"#, 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /tests/ui.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ui() { 3 | // FIXME: diagnostics are unavoidably bad right now 4 | // due to trait errors when there is an issue. 5 | // these trait errors span the entire macro output, 6 | // so there is no good way to scope errors to a specific span. 7 | // 8 | // not running any UI tests for now. 9 | 10 | // let t = trybuild::TestCases::new(); 11 | // t.pass("tests/ui/pass/*.rs"); 12 | // t.compile_fail("tests/ui/errors/*.rs"); 13 | } 14 | -------------------------------------------------------------------------------- /tests/ui/errors/com_builder_spans.rs: -------------------------------------------------------------------------------- 1 | //! Testing that there are no errors that cause the entire macro to error (i.e. 2 | //! call-site error) 3 | 4 | use leptos::*; 5 | use leptos_mview::mview; 6 | 7 | fn missing_args() { 8 | // missing `key` attribute 9 | _ = mview! { 10 | For each=[[1, 2, 3]] |i| { {i} } 11 | }; 12 | } 13 | 14 | fn incorrect_arg_value() { 15 | // Show takes `bool` not `&str` 16 | _ = mview! { 17 | Show when={"no"} { 18 | "hi" 19 | } 20 | }; 21 | } 22 | 23 | fn missing_closure() { 24 | _ = mview! { 25 | Show when={true} { 26 | "hi" 27 | } 28 | }; 29 | } 30 | 31 | fn incorrect_closure() { 32 | #[component] 33 | fn Thing(label: &'static str) -> impl IntoView { label } 34 | 35 | // `label` is not a closure 36 | _ = mview! { 37 | Thing label=[false]; 38 | }; 39 | } 40 | 41 | fn incorrect_closure_to_children() { 42 | #[component] 43 | fn Thing(children: Children) -> impl IntoView { children() } 44 | 45 | let s = String::new(); 46 | // `children` does not take a closure 47 | _ = mview! { 48 | Thing |s| { "hello" } 49 | }; 50 | } 51 | 52 | fn missing_closure_to_children() { 53 | // thought it would make an error at the `.children`, but it seem to accept it 54 | // and errors at the tag name instead. this test is just for notifying in case 55 | // this ever changes. 56 | _ = mview! { 57 | Await future=[async { 3 }] { "no args" } 58 | }; 59 | } 60 | 61 | fn main() {} 62 | -------------------------------------------------------------------------------- /tests/ui/errors/com_builder_spans.stderr: -------------------------------------------------------------------------------- 1 | warning: use of deprecated method `leptos::ForPropsBuilder::::build`: Missing required field key 2 | --> tests/ui/errors/com_builder_spans.rs:10:9 3 | | 4 | 10 | For each=[[1, 2, 3]] |i| { {i} } 5 | | ^^^ 6 | | 7 | = note: `#[warn(deprecated)]` on by default 8 | 9 | error[E0061]: this method takes 1 argument but 0 arguments were supplied 10 | --> tests/ui/errors/com_builder_spans.rs:10:9 11 | | 12 | 10 | For each=[[1, 2, 3]] |i| { {i} } 13 | | ^^^ an argument of type `ForPropsBuilder_Error_Missing_required_field_key` is missing 14 | | 15 | note: method defined here 16 | --> $CARGO/leptos-0.6.12/src/for_loop.rs 17 | | 18 | | #[component(transparent)] 19 | | ^^^^^^^^^^^^^^^^^^^^^^^^^ 20 | = note: this error originates in the derive macro `::leptos::typed_builder_macro::TypedBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 21 | help: provide the argument 22 | | 23 | 10 | For(/* ForPropsBuilder_Error_Missing_required_field_key */) each=[[1, 2, 3]] |i| { {i} } 24 | | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 25 | 26 | error[E0277]: expected a `Fn()` closure, found `str` 27 | --> tests/ui/errors/com_builder_spans.rs:17:14 28 | | 29 | 17 | Show when={"no"} { 30 | | ^^^^ expected an `Fn()` closure, found `str` 31 | | 32 | = help: the trait `Fn()` is not implemented for `str`, which is required by `&str: FnOnce()` 33 | = note: wrap the `str` in a closure with no arguments: `|| { /* code */ }` 34 | = note: required for `&str` to implement `FnOnce()` 35 | note: required by a bound in `ShowPropsBuilder::::when` 36 | --> $CARGO/leptos-0.6.12/src/show.rs 37 | | 38 | | when: W, 39 | | ---- required by a bound in this associated function 40 | ... 41 | | W: Fn() -> bool + 'static, 42 | | ^^^^ required by this bound in `ShowPropsBuilder::::when` 43 | 44 | error[E0277]: expected a `Fn()` closure, found `str` 45 | --> tests/ui/errors/com_builder_spans.rs:17:9 46 | | 47 | 17 | Show when={"no"} { 48 | | ^^^^ expected an `Fn()` closure, found `str` 49 | | 50 | = help: the trait `Fn()` is not implemented for `str`, which is required by `&str: Fn()` 51 | = note: wrap the `str` in a closure with no arguments: `|| { /* code */ }` 52 | = note: required for `&str` to implement `Fn()` 53 | note: required by a bound in `leptos::Show` 54 | --> $CARGO/leptos-0.6.12/src/show.rs 55 | | 56 | | pub fn Show( 57 | | ---- required by a bound in this function 58 | ... 59 | | W: Fn() -> bool + 'static, 60 | | ^^^^^^^^^^^^ required by this bound in `Show` 61 | 62 | error[E0599]: the method `children` exists for struct `ShowPropsBuilder<&str, ((), (&str,), ())>`, but its trait bounds were not satisfied 63 | --> tests/ui/errors/com_builder_spans.rs:18:13 64 | | 65 | 16 | _ = mview! { 66 | | _________- 67 | 17 | | Show when={"no"} { 68 | 18 | | "hi" 69 | | | -^^^^ method cannot be called on `ShowPropsBuilder<&str, ((), (&str,), ())>` due to unsatisfied trait bounds 70 | | |____________| 71 | | 72 | | 73 | = note: the following trait bounds were not satisfied: 74 | `str: std::ops::Fn<()>` 75 | which is required by `<&str as FnOnce<()>>::Output = bool` 76 | `str: FnOnce<()>` 77 | which is required by `<&str as FnOnce<()>>::Output = bool` 78 | 79 | error[E0277]: expected a `Fn()` closure, found `bool` 80 | --> tests/ui/errors/com_builder_spans.rs:25:14 81 | | 82 | 25 | Show when={true} { 83 | | ^^^^ expected an `Fn()` closure, found `bool` 84 | | 85 | = help: the trait `Fn()` is not implemented for `bool` 86 | = note: wrap the `bool` in a closure with no arguments: `|| { /* code */ }` 87 | note: required by a bound in `ShowPropsBuilder::::when` 88 | --> $CARGO/leptos-0.6.12/src/show.rs 89 | | 90 | | when: W, 91 | | ---- required by a bound in this associated function 92 | ... 93 | | W: Fn() -> bool + 'static, 94 | | ^^^^^^^^^^^^ required by this bound in `ShowPropsBuilder::::when` 95 | 96 | error[E0277]: expected a `Fn()` closure, found `bool` 97 | --> tests/ui/errors/com_builder_spans.rs:25:9 98 | | 99 | 25 | Show when={true} { 100 | | ^^^^ expected an `Fn()` closure, found `bool` 101 | | 102 | = help: the trait `Fn()` is not implemented for `bool`, which is required by `fn(ShowProps<_>) -> impl leptos::IntoView {leptos::Show::<_>}: leptos::Component<_>` 103 | = note: wrap the `bool` in a closure with no arguments: `|| { /* code */ }` 104 | = note: required for `ShowProps` to implement `leptos::Props` 105 | = note: required for `fn(ShowProps) -> impl leptos::IntoView {leptos::Show::}` to implement `leptos::Component>` 106 | note: required by a bound in `component_props_builder` 107 | --> $CARGO/leptos-0.6.12/src/lib.rs 108 | | 109 | | pub fn component_props_builder( 110 | | ----------------------- required by a bound in this function 111 | | _f: &impl Component

, 112 | | ^^^^^^^^^^^^ required by this bound in `component_props_builder` 113 | 114 | error[E0599]: the method `children` exists for struct `ShowPropsBuilder`, but its trait bounds were not satisfied 115 | --> tests/ui/errors/com_builder_spans.rs:26:13 116 | | 117 | 24 | _ = mview! { 118 | | _________- 119 | 25 | | Show when={true} { 120 | 26 | | "hi" 121 | | | -^^^^ method cannot be called on `ShowPropsBuilder` due to unsatisfied trait bounds 122 | | |____________| 123 | | 124 | | 125 | = note: the following trait bounds were not satisfied: 126 | `bool: std::ops::Fn<()>` 127 | `bool: FnOnce<()>` 128 | which is required by `>::Output = bool` 129 | 130 | error[E0308]: mismatched types 131 | --> tests/ui/errors/com_builder_spans.rs:37:21 132 | | 133 | 37 | Thing label=[false]; 134 | | ----- ^^^^^^^ expected `&str`, found closure 135 | | | 136 | | arguments to this method are incorrect 137 | | 138 | = note: expected reference `&'static str` 139 | found closure `{closure@$DIR/tests/ui/errors/com_builder_spans.rs:37:21: 37:28}` 140 | note: method defined here 141 | --> tests/ui/errors/com_builder_spans.rs:33:14 142 | | 143 | 33 | fn Thing(label: &'static str) -> impl IntoView { label } 144 | | ^^^^^-------------- 145 | 146 | error[E0308]: mismatched types 147 | --> tests/ui/errors/com_builder_spans.rs:48:15 148 | | 149 | 48 | Thing |s| { "hello" } 150 | | ^^^ expected `Box Fragment>`, found closure 151 | | 152 | = note: expected struct `Box<(dyn FnOnce() -> Fragment + 'static)>` 153 | found closure `{closure@$DIR/tests/ui/errors/com_builder_spans.rs:48:15: 48:18}` 154 | 155 | error[E0283]: type annotations needed 156 | --> tests/ui/errors/com_builder_spans.rs:57:9 157 | | 158 | 57 | Await future=[async { 3 }] { "no args" } 159 | | ^^^^^ cannot infer type of the type parameter `V` declared on the function `Await` 160 | | 161 | = note: cannot satisfy `_: leptos::IntoView` 162 | = help: the following types implement trait `leptos::IntoView`: 163 | &'static str 164 | &Fragment 165 | &leptos::View 166 | &std::string::String 167 | () 168 | (A, B) 169 | (A, B, C) 170 | (A, B, C, D) 171 | and $N others 172 | note: required by a bound in `leptos::Await` 173 | --> $CARGO/leptos-0.6.12/src/await_.rs 174 | | 175 | | pub fn Await( 176 | | ----- required by a bound in this function 177 | ... 178 | | V: IntoView, 179 | | ^^^^^^^^ required by this bound in `Await` 180 | help: consider specifying the generic arguments 181 | | 182 | 57 | Await:: future=[async { 3 }] { "no args" } 183 | | ++++++++++++++++++++ 184 | 185 | error[E0283]: type annotations needed 186 | --> tests/ui/errors/com_builder_spans.rs:57:9 187 | | 188 | 57 | Await future=[async { 3 }] { "no args" } 189 | | ^^^^^ cannot infer type of the type parameter `VF` declared on the function `Await` 190 | | 191 | = note: multiple `impl`s satisfying `_: ToChildren<{closure@$DIR/tests/ui/errors/com_builder_spans.rs:57:38: 57:47}>` found in the `leptos` crate: 192 | - impl ToChildren for Box<(dyn FnMut() -> Fragment + 'static)> 193 | where >::Output == Fragment, F: FnMut(), F: 'static; 194 | - impl ToChildren for Box<(dyn FnOnce() -> Fragment + 'static)> 195 | where >::Output == Fragment, F: FnOnce(), F: 'static; 196 | - impl ToChildren for Box<(dyn std::ops::Fn() -> Fragment + 'static)> 197 | where >::Output == Fragment, F: Fn(), F: 'static; 198 | - impl ToChildren for Rc<(dyn std::ops::Fn() -> Fragment + 'static)> 199 | where >::Output == Fragment, F: Fn(), F: 'static; 200 | help: consider specifying the generic arguments 201 | | 202 | 57 | Await:: future=[async { 3 }] { "no args" } 203 | | ++++++++++++++++++++ 204 | -------------------------------------------------------------------------------- /tests/ui/errors/com_dyn_classes.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_mview::mview; 3 | 4 | #[component] 5 | fn AComponent( 6 | #[prop(into, default="".into())] class: TextProp, 7 | #[prop(optional)] id: &'static str, 8 | ) -> impl IntoView { 9 | mview! { 10 | div class=f["my-class {}", class.get()] {id}; 11 | } 12 | } 13 | 14 | fn missing_closure() { 15 | _ = mview! { 16 | AComponent class:red=true; 17 | }; 18 | } 19 | 20 | fn incorrect_type() { 21 | _ = mview! { 22 | AComponent class:red=["not this"]; 23 | }; 24 | } 25 | 26 | #[component] 27 | fn Nothing() -> impl IntoView {} 28 | 29 | // these spans are actually fine, there's a blank info message at `mview!` for 30 | // some reason. 31 | 32 | fn no_attribute_reactive() { 33 | _ = mview! { 34 | Nothing class:red=[true]; 35 | }; 36 | } 37 | 38 | fn no_attribute_static() { 39 | _ = mview! { 40 | Nothing.red; 41 | }; 42 | } 43 | 44 | fn no_attribute_id() { 45 | _ = mview! { 46 | Nothing #unique; 47 | }; 48 | } 49 | 50 | fn main() {} 51 | -------------------------------------------------------------------------------- /tests/ui/errors/com_dyn_classes.stderr: -------------------------------------------------------------------------------- 1 | error[E0618]: expected function, found `bool` 2 | --> tests/ui/errors/com_dyn_classes.rs:16:30 3 | | 4 | 16 | AComponent class:red=true; 5 | | ^^^^ call expression requires function 6 | 7 | error[E0308]: mismatched types 8 | --> tests/ui/errors/com_dyn_classes.rs:22:30 9 | | 10 | 22 | AComponent class:red=["not this"]; 11 | | ^^^^^^^^^^^^ 12 | | | 13 | | expected `bool`, found `&str` 14 | | arguments to this function are incorrect 15 | | 16 | note: method defined here 17 | --> $RUST/core/src/bool.rs 18 | | 19 | | pub fn then_some(self, t: T) -> Option { 20 | | ^^^^^^^^^ 21 | 22 | error[E0599]: no method named `class` found for struct `EmptyPropsBuilder` in the current scope 23 | --> tests/ui/errors/com_dyn_classes.rs:34:17 24 | | 25 | 33 | _ = mview! { 26 | | _________- 27 | 34 | | Nothing class:red=[true]; 28 | | | -^^^^^ method not found in `EmptyPropsBuilder` 29 | | |________________| 30 | | 31 | 32 | error[E0599]: no method named `class` found for struct `EmptyPropsBuilder` in the current scope 33 | --> tests/ui/errors/com_dyn_classes.rs:40:16 34 | | 35 | 39 | _ = mview! { 36 | | _________- 37 | 40 | | Nothing.red; 38 | | | -^ method not found in `EmptyPropsBuilder` 39 | | |_______________| 40 | | 41 | 42 | error[E0599]: no method named `id` found for struct `EmptyPropsBuilder` in the current scope 43 | --> tests/ui/errors/com_dyn_classes.rs:46:17 44 | | 45 | 45 | _ = mview! { 46 | | _________- 47 | 46 | | Nothing #unique; 48 | | | -^ method not found in `EmptyPropsBuilder` 49 | | |________________| 50 | | 51 | -------------------------------------------------------------------------------- /tests/ui/errors/invalid_child.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn main() { 4 | let a = "a"; 5 | mview! { 6 | (a) 7 | }; 8 | } 9 | 10 | // checking that it doesn't suggest adding `|_| {value}` 11 | fn not_impl_intoview() { 12 | // &&str doesn't impl IntoView, &str does 13 | // should be `{*value}` 14 | let value: &&str = &"hi"; 15 | _ = mview! { 16 | span ( 17 | {value} 18 | ) 19 | }; 20 | 21 | // forgot to call `.collect_view()` 22 | let values: Vec<&'static str> = vec!["hi", "bye", "howdy", "hello", "hey"]; 23 | _ = mview! { 24 | ul { 25 | {values 26 | .into_iter() 27 | .map(|val: &str| { 28 | mview! { li({val}) } 29 | }) 30 | } 31 | } 32 | } 33 | } 34 | 35 | fn extra_semicolons() { 36 | _ = mview! { 37 | div { "hi there" }; 38 | span; 39 | }; 40 | } 41 | 42 | #[expect(dependency_on_unit_never_type_fallback, reason="probably fixed in leptos 0.7")] 43 | fn unreachable_code() { 44 | _ = mview! { 45 | div { 46 | {todo!()} 47 | } 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /tests/ui/errors/invalid_child.stderr: -------------------------------------------------------------------------------- 1 | error: invalid child: expected literal, block, bracket or element 2 | --> tests/ui/errors/invalid_child.rs:6:9 3 | | 4 | 6 | (a) 5 | | ^ 6 | 7 | error: extra semi-colon found 8 | --> tests/ui/errors/invalid_child.rs:37:27 9 | | 10 | 37 | div { "hi there" }; 11 | | ^ 12 | | 13 | = help: remove this semi-colon 14 | 15 | error[E0277]: the trait bound `&&str: IntoView` is not satisfied 16 | --> tests/ui/errors/invalid_child.rs:17:14 17 | | 18 | 17 | {value} 19 | | -^^^^^- 20 | | || 21 | | |the trait `Fn()` is not implemented for `str`, which is required by `&&str: IntoView` 22 | | required by a bound introduced by this call 23 | | 24 | = help: the trait `IntoView` is implemented for `&'static str` 25 | = note: required for `&str` to implement `FnOnce()` 26 | = note: required for `&&str` to implement `IntoView` 27 | note: required by a bound in `leptos::HtmlElement::::child` 28 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 29 | | 30 | | pub fn child(self, child: impl IntoView) -> Self { 31 | | ^^^^^^^^ required by this bound in `HtmlElement::::child` 32 | 33 | error[E0277]: the trait bound `std::iter::Map, {closure@$DIR/tests/ui/errors/invalid_child.rs:27:22: 27:33}>: IntoView` is not satisfied 34 | --> tests/ui/errors/invalid_child.rs:25:14 35 | | 36 | 25 | {values 37 | | ______________-^ 38 | | | ______________| 39 | 26 | || .into_iter() 40 | 27 | || .map(|val: &str| { 41 | 28 | || mview! { li({val}) } 42 | 29 | || }) 43 | | ||__________________^ the trait `Fn()` is not implemented for `std::iter::Map, {closure@$DIR/tests/ui/errors/invalid_child.rs:27:22: 27:33}>`, which is required by `std::iter::Map, {closure@$DIR/tests/ui/errors/invalid_child.rs:27:22: 27:33}>: IntoView` 44 | 30 | | } 45 | | |______________- required by a bound introduced by this call 46 | | 47 | = help: the following other types implement trait `IntoView`: 48 | &'static str 49 | &Fragment 50 | &leptos::View 51 | &std::string::String 52 | () 53 | (A, B) 54 | (A, B, C) 55 | (A, B, C, D) 56 | and $N others 57 | = note: required for `std::iter::Map, {closure@$DIR/tests/ui/errors/invalid_child.rs:27:22: 27:33}>` to implement `IntoView` 58 | note: required by a bound in `leptos::HtmlElement::::child` 59 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 60 | | 61 | | pub fn child(self, child: impl IntoView) -> Self { 62 | | ^^^^^^^^ required by this bound in `HtmlElement::::child` 63 | 64 | warning: unreachable call 65 | --> tests/ui/errors/invalid_child.rs:46:13 66 | | 67 | 46 | {todo!()} 68 | | ^-------^ 69 | | || 70 | | |any code following this expression is unreachable 71 | | unreachable call 72 | | 73 | = note: `#[warn(unreachable_code)]` on by default 74 | 75 | warning: unused variable: `a` 76 | --> tests/ui/errors/invalid_child.rs:4:9 77 | | 78 | 4 | let a = "a"; 79 | | ^ help: if this is intentional, prefix it with an underscore: `_a` 80 | | 81 | = note: `#[warn(unused_variables)]` on by default 82 | -------------------------------------------------------------------------------- /tests/ui/errors/invalid_directive.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_mview::mview; 3 | 4 | fn not_directive() { 5 | mview! { 6 | div something:yes="b" {} 7 | }; 8 | } 9 | 10 | fn not_class_name() { 11 | mview! { 12 | div class:("abcd") = true {} 13 | }; 14 | } 15 | 16 | fn not_style_name() { 17 | mview! { 18 | div style:[1, 2]="black" {} 19 | }; 20 | } 21 | 22 | fn not_event_name() { 23 | mview! { 24 | button on:clicky-click={move |_| ()}; 25 | }; 26 | } 27 | 28 | fn invalid_modifier() { 29 | mview! { 30 | button on:click:delegated={|_| ()}; 31 | }; 32 | } 33 | 34 | #[component] 35 | fn Com(#[prop(optional, into)] class: TextProp) -> impl IntoView { 36 | let _ = class; 37 | } 38 | 39 | fn invalid_parts() { 40 | _ = mview! { 41 | div class:this:undelegated=true; 42 | }; 43 | _ = mview! { 44 | div style:position:undelegated="absolute"; 45 | }; 46 | _ = mview! { 47 | input prop:value:something="input something"; 48 | }; 49 | _ = mview! { 50 | button use:directive:another; 51 | }; 52 | _ = mview! { 53 | button attr:type="submit"; 54 | }; 55 | 56 | let to_clone = String::new(); 57 | _ = mview! { 58 | Com clone:to_clone:undelegated; 59 | }; 60 | _ = mview! { 61 | Com clone:{to_clone}; 62 | }; 63 | _ = mview! { 64 | Com class:aaa:undelegated=[false]; 65 | }; 66 | } 67 | 68 | fn directive(_el: leptos::HtmlElement) {} 69 | 70 | fn main() {} 71 | -------------------------------------------------------------------------------- /tests/ui/errors/invalid_directive.stderr: -------------------------------------------------------------------------------- 1 | error: unknown directive 2 | --> tests/ui/errors/invalid_directive.rs:6:13 3 | | 4 | 6 | div something:yes="b" {} 5 | | ^^^^^^^^^ 6 | 7 | error: expected a kebab-cased ident 8 | --> tests/ui/errors/invalid_directive.rs:12:19 9 | | 10 | 12 | div class:("abcd") = true {} 11 | | ^ 12 | 13 | error: expected a kebab-cased ident 14 | --> tests/ui/errors/invalid_directive.rs:18:19 15 | | 16 | 18 | div style:[1, 2]="black" {} 17 | | ^ 18 | 19 | error: unknown modifier 20 | --> tests/ui/errors/invalid_directive.rs:30:25 21 | | 22 | 30 | button on:click:delegated={|_| ()}; 23 | | ^^^^^^^^^ 24 | | 25 | = help: :undelegated is the only known modifier 26 | 27 | error: unknown modifier: modifiers are only supported on `on:` directives 28 | --> tests/ui/errors/invalid_directive.rs:41:24 29 | | 30 | 41 | div class:this:undelegated=true; 31 | | ^^^^^^^^^^^ 32 | 33 | error: unknown modifier: modifiers are only supported on `on:` directives 34 | --> tests/ui/errors/invalid_directive.rs:44:28 35 | | 36 | 44 | div style:position:undelegated="absolute"; 37 | | ^^^^^^^^^^^ 38 | 39 | error: unknown modifier: modifiers are only supported on `on:` directives 40 | --> tests/ui/errors/invalid_directive.rs:47:26 41 | | 42 | 47 | input prop:value:something="input something"; 43 | | ^^^^^^^^^ 44 | 45 | error: unknown modifier: modifiers are only supported on `on:` directives 46 | --> tests/ui/errors/invalid_directive.rs:50:30 47 | | 48 | 50 | button use:directive:another; 49 | | ^^^^^^^ 50 | 51 | error: `attr:` is not supported on elements 52 | --> tests/ui/errors/invalid_directive.rs:53:16 53 | | 54 | 53 | button attr:type="submit"; 55 | | ^^^^ 56 | 57 | error: unknown modifier: modifiers are only supported on `on:` directives 58 | --> tests/ui/errors/invalid_directive.rs:58:28 59 | | 60 | 58 | Com clone:to_clone:undelegated; 61 | | ^^^^^^^^^^^ 62 | 63 | error: `clone:` does not take any values 64 | --> tests/ui/errors/invalid_directive.rs:61:19 65 | | 66 | 61 | Com clone:{to_clone}; 67 | | ^^^^^^^^^^ 68 | 69 | error: unknown modifier: modifiers are only supported on `on:` directives 70 | --> tests/ui/errors/invalid_directive.rs:64:23 71 | | 72 | 64 | Com class:aaa:undelegated=[false]; 73 | | ^^^^^^^^^^^ 74 | 75 | error[E0425]: cannot find value `clicky_click` in module `leptos::ev` 76 | --> tests/ui/errors/invalid_directive.rs:24:19 77 | | 78 | 24 | button on:clicky-click={move |_| ()}; 79 | | ^^^^^^^^^^^^ not found in `leptos::ev` 80 | 81 | warning: unused variable: `to_clone` 82 | --> tests/ui/errors/invalid_directive.rs:56:9 83 | | 84 | 56 | let to_clone = String::new(); 85 | | ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_to_clone` 86 | | 87 | = note: `#[warn(unused_variables)]` on by default 88 | -------------------------------------------------------------------------------- /tests/ui/errors/invalid_value.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn unwrapped() { 4 | _ = mview! { 5 | div a=a {} 6 | }; 7 | } 8 | 9 | fn no_spread() { 10 | _ = mview! { 11 | div {..}; 12 | }; 13 | } 14 | 15 | // ensure that it is spanned to the delims, not call site 16 | fn empty_value() { 17 | _ = mview! { 18 | a href={}; 19 | a href=(); 20 | a href=[]; 21 | }; 22 | } 23 | 24 | fn missing_value_no_remaining() { 25 | // nothing after the =, make sure that the error is on the = not call site 26 | _ = mview! { 27 | a href= 28 | }; 29 | } 30 | 31 | fn main() {} 32 | -------------------------------------------------------------------------------- /tests/ui/errors/invalid_value.stderr: -------------------------------------------------------------------------------- 1 | error: expected value after = 2 | --> tests/ui/errors/invalid_value.rs:5:15 3 | | 4 | 5 | div a=a {} 5 | | ^ 6 | | 7 | = help: you may have meant to wrap this in braces 8 | 9 | error: expected value after = 10 | --> tests/ui/errors/invalid_value.rs:19:16 11 | | 12 | 19 | a href=(); 13 | | ^ 14 | 15 | error: extra semi-colon found 16 | --> tests/ui/errors/invalid_value.rs:19:18 17 | | 18 | 19 | a href=(); 19 | | ^ 20 | | 21 | = help: remove this semi-colon 22 | 23 | error: expected value after = 24 | --> tests/ui/errors/invalid_value.rs:27:15 25 | | 26 | 27 | a href= 27 | | ^ 28 | 29 | error: unterminated element 30 | --> tests/ui/errors/invalid_value.rs:27:9 31 | | 32 | 27 | a href= 33 | | ^ 34 | | 35 | = help: add a `;` to terminate the element with no children 36 | 37 | error[E0277]: the trait bound `MissingValueAfterEq: IntoAttribute` is not satisfied 38 | --> tests/ui/errors/invalid_value.rs:5:15 39 | | 40 | 4 | _ = mview! { 41 | | _________- 42 | 5 | | div a=a {} 43 | | | ^ the trait `Fn()` is not implemented for `MissingValueAfterEq`, which is required by `MissingValueAfterEq: IntoAttribute` 44 | 6 | | }; 45 | | |_____- required by a bound introduced by this call 46 | | 47 | = help: the following other types implement trait `IntoAttribute`: 48 | &'static str 49 | &std::string::String 50 | Arguments<'_> 51 | Cow<'static, str> 52 | Nonce 53 | Oco<'static, str> 54 | Rc 55 | TextProp 56 | and $N others 57 | = note: required for `MissingValueAfterEq` to implement `IntoAttribute` 58 | note: required by a bound in `leptos::HtmlElement::::attr` 59 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 60 | | 61 | | pub fn attr( 62 | | ---- required by a bound in this associated function 63 | ... 64 | | attr: impl IntoAttribute, 65 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 66 | 67 | error[E0061]: this method takes 1 argument but 0 arguments were supplied 68 | --> tests/ui/errors/invalid_value.rs:11:14 69 | | 70 | 11 | div {..}; 71 | | ______________^^- 72 | 12 | | }; 73 | | |_____- an argument is missing 74 | | 75 | note: method defined here 76 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 77 | | 78 | | pub fn attrs( 79 | | ^^^^^ 80 | help: provide the argument 81 | | 82 | 10 | _ = ..(/* attrs */); 83 | | ~~~~~~~~~~~~~~~ 84 | 85 | error[E0277]: the trait bound `(): IntoAttribute` is not satisfied 86 | --> tests/ui/errors/invalid_value.rs:18:16 87 | | 88 | 17 | _ = mview! { 89 | | _________- 90 | 18 | | a href={}; 91 | | | ^^ the trait `Fn()` is not implemented for `()`, which is required by `(): IntoAttribute` 92 | 19 | | a href=(); 93 | 20 | | a href=[]; 94 | 21 | | }; 95 | | |_____- required by a bound introduced by this call 96 | | 97 | = help: the following other types implement trait `IntoAttribute`: 98 | &'static str 99 | &std::string::String 100 | Arguments<'_> 101 | Cow<'static, str> 102 | Nonce 103 | Oco<'static, str> 104 | Rc 105 | TextProp 106 | and $N others 107 | = note: required for `()` to implement `IntoAttribute` 108 | note: required by a bound in `leptos::HtmlElement::::attr` 109 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 110 | | 111 | | pub fn attr( 112 | | ---- required by a bound in this associated function 113 | ... 114 | | attr: impl IntoAttribute, 115 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 116 | 117 | error[E0277]: the trait bound `MissingValueAfterEq: IntoAttribute` is not satisfied 118 | --> tests/ui/errors/invalid_value.rs:19:16 119 | | 120 | 17 | _ = mview! { 121 | | _________- 122 | 18 | | a href={}; 123 | 19 | | a href=(); 124 | | | ^ the trait `Fn()` is not implemented for `MissingValueAfterEq`, which is required by `MissingValueAfterEq: IntoAttribute` 125 | 20 | | a href=[]; 126 | 21 | | }; 127 | | |_____- required by a bound introduced by this call 128 | | 129 | = help: the following other types implement trait `IntoAttribute`: 130 | &'static str 131 | &std::string::String 132 | Arguments<'_> 133 | Cow<'static, str> 134 | Nonce 135 | Oco<'static, str> 136 | Rc 137 | TextProp 138 | and $N others 139 | = note: required for `MissingValueAfterEq` to implement `IntoAttribute` 140 | note: required by a bound in `leptos::HtmlElement::::attr` 141 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 142 | | 143 | | pub fn attr( 144 | | ---- required by a bound in this associated function 145 | ... 146 | | attr: impl IntoAttribute, 147 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 148 | 149 | error[E0277]: expected a `Fn()` closure, found `()` 150 | --> tests/ui/errors/invalid_value.rs:20:16 151 | | 152 | 17 | _ = mview! { 153 | | _________- 154 | 18 | | a href={}; 155 | 19 | | a href=(); 156 | 20 | | a href=[]; 157 | | | ^^ expected an `Fn()` closure, found `()` 158 | 21 | | }; 159 | | |_____- required by a bound introduced by this call 160 | | 161 | = help: the trait `Fn()` is not implemented for `()`, which is required by `{closure@$DIR/tests/ui/errors/invalid_value.rs:20:16: 20:18}: IntoAttribute` 162 | = note: wrap the `()` in a closure with no arguments: `|| { /* code */ }` 163 | = help: the following other types implement trait `IntoAttribute`: 164 | &'static str 165 | &std::string::String 166 | Arguments<'_> 167 | Cow<'static, str> 168 | Nonce 169 | Oco<'static, str> 170 | Rc 171 | TextProp 172 | and $N others 173 | = note: required for `()` to implement `IntoAttribute` 174 | = note: 1 redundant requirement hidden 175 | = note: required for `{closure@$DIR/tests/ui/errors/invalid_value.rs:20:16: 20:18}` to implement `IntoAttribute` 176 | note: required by a bound in `leptos::HtmlElement::::attr` 177 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 178 | | 179 | | pub fn attr( 180 | | ---- required by a bound in this associated function 181 | ... 182 | | attr: impl IntoAttribute, 183 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 184 | 185 | error[E0277]: the trait bound `MissingValueAfterEq: IntoAttribute` is not satisfied 186 | --> tests/ui/errors/invalid_value.rs:27:15 187 | | 188 | 26 | _ = mview! { 189 | | _________- 190 | 27 | | a href= 191 | | | ^ the trait `Fn()` is not implemented for `MissingValueAfterEq`, which is required by `MissingValueAfterEq: IntoAttribute` 192 | 28 | | }; 193 | | |_____- required by a bound introduced by this call 194 | | 195 | = help: the following other types implement trait `IntoAttribute`: 196 | &'static str 197 | &std::string::String 198 | Arguments<'_> 199 | Cow<'static, str> 200 | Nonce 201 | Oco<'static, str> 202 | Rc 203 | TextProp 204 | and $N others 205 | = note: required for `MissingValueAfterEq` to implement `IntoAttribute` 206 | note: required by a bound in `leptos::HtmlElement::::attr` 207 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 208 | | 209 | | pub fn attr( 210 | | ---- required by a bound in this associated function 211 | ... 212 | | attr: impl IntoAttribute, 213 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 214 | -------------------------------------------------------------------------------- /tests/ui/errors/misc_partial.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn invalid_value() { 4 | _ = mview! { 5 | div class:x={true} { 6 | span class=test 7 | } 8 | } 9 | } 10 | 11 | fn incomplete_directive() { 12 | _ = mview! { 13 | div class:x={true} { 14 | span class: 15 | } 16 | } 17 | } 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /tests/ui/errors/misc_partial.stderr: -------------------------------------------------------------------------------- 1 | error: expected value after = 2 | --> tests/ui/errors/misc_partial.rs:6:24 3 | | 4 | 6 | span class=test 5 | | ^^^^ 6 | | 7 | = help: you may have meant to wrap this in braces 8 | 9 | error: unterminated element 10 | --> tests/ui/errors/misc_partial.rs:6:13 11 | | 12 | 6 | span class=test 13 | | ^^^^ 14 | | 15 | = help: add a `;` to terminate the element with no children 16 | 17 | error: unexpected end of input, expected a kebab-cased ident 18 | --> tests/ui/errors/misc_partial.rs:15:9 19 | | 20 | 15 | } 21 | | ^ 22 | 23 | error[E0277]: the trait bound `MissingValueAfterEq: IntoAttribute` is not satisfied 24 | --> tests/ui/errors/misc_partial.rs:6:24 25 | | 26 | 4 | _ = mview! { 27 | | _________- 28 | 5 | | div class:x={true} { 29 | 6 | | span class=test 30 | | | ^^^^ the trait `Fn()` is not implemented for `MissingValueAfterEq`, which is required by `MissingValueAfterEq: IntoAttribute` 31 | 7 | | } 32 | 8 | | } 33 | | |_____- required by a bound introduced by this call 34 | | 35 | = help: the following other types implement trait `IntoAttribute`: 36 | &'static str 37 | &std::string::String 38 | Arguments<'_> 39 | Cow<'static, str> 40 | Nonce 41 | Oco<'static, str> 42 | Rc 43 | TextProp 44 | and $N others 45 | = note: required for `MissingValueAfterEq` to implement `IntoAttribute` 46 | note: required by a bound in `leptos::HtmlElement::::attr` 47 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 48 | | 49 | | pub fn attr( 50 | | ---- required by a bound in this associated function 51 | ... 52 | | attr: impl IntoAttribute, 53 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 54 | -------------------------------------------------------------------------------- /tests/ui/errors/no_children_after_closure.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_mview::mview; 3 | 4 | fn main() { 5 | mview! { 6 | Await 7 | future=[async { 1 }] 8 | |data| "no" 9 | }; 10 | } -------------------------------------------------------------------------------- /tests/ui/errors/no_children_after_closure.stderr: -------------------------------------------------------------------------------- 1 | error: expected children block after closure arguments 2 | --> tests/ui/errors/no_children_after_closure.rs:8:16 3 | | 4 | 8 | |data| "no" 5 | | ^^^^ 6 | 7 | warning: use of deprecated method `leptos::AwaitPropsBuilder::::build`: Missing required field children 8 | --> tests/ui/errors/no_children_after_closure.rs:6:9 9 | | 10 | 6 | Await 11 | | ^^^^^ 12 | | 13 | = note: `#[warn(deprecated)]` on by default 14 | 15 | error[E0061]: this method takes 1 argument but 0 arguments were supplied 16 | --> tests/ui/errors/no_children_after_closure.rs:6:9 17 | | 18 | 6 | Await 19 | | ^^^^^ an argument of type `AwaitPropsBuilder_Error_Missing_required_field_children` is missing 20 | | 21 | note: method defined here 22 | --> $CARGO/leptos-0.6.12/src/await_.rs 23 | | 24 | | #[component] 25 | | ^^^^^^^^^^^^ 26 | = note: this error originates in the derive macro `::leptos::typed_builder_macro::TypedBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 27 | help: provide the argument 28 | | 29 | 6 | Await(/* AwaitPropsBuilder_Error_Missing_required_field_children */) 30 | | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | -------------------------------------------------------------------------------- /tests/ui/errors/non_str_child.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn main() { 4 | mview! { 5 | div { 3 } 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /tests/ui/errors/non_str_child.stderr: -------------------------------------------------------------------------------- 1 | error: only string literals are allowed in children 2 | --> tests/ui/errors/non_str_child.rs:5:15 3 | | 4 | 5 | div { 3 } 5 | | ^ 6 | -------------------------------------------------------------------------------- /tests/ui/errors/return_expression.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn main() { 4 | // should not get an "unexpected end of macro invocation" 5 | let expr = mview! { 6 | div class=; 7 | }; 8 | } -------------------------------------------------------------------------------- /tests/ui/errors/return_expression.stderr: -------------------------------------------------------------------------------- 1 | error: expected value after = 2 | --> tests/ui/errors/return_expression.rs:6:19 3 | | 4 | 6 | div class=; 5 | | ^ 6 | 7 | error[E0277]: the trait bound `MissingValueAfterEq: IntoAttribute` is not satisfied 8 | --> tests/ui/errors/return_expression.rs:6:19 9 | | 10 | 5 | let expr = mview! { 11 | | ________________- 12 | 6 | | div class=; 13 | | | ^ the trait `Fn()` is not implemented for `MissingValueAfterEq`, which is required by `MissingValueAfterEq: IntoAttribute` 14 | 7 | | }; 15 | | |_____- required by a bound introduced by this call 16 | | 17 | = help: the following other types implement trait `IntoAttribute`: 18 | &'static str 19 | &std::string::String 20 | Arguments<'_> 21 | Cow<'static, str> 22 | Nonce 23 | Oco<'static, str> 24 | Rc 25 | TextProp 26 | and $N others 27 | = note: required for `MissingValueAfterEq` to implement `IntoAttribute` 28 | note: required by a bound in `leptos::HtmlElement::::attr` 29 | --> $CARGO/leptos_dom-0.6.12/src/html.rs 30 | | 31 | | pub fn attr( 32 | | ---- required by a bound in this associated function 33 | ... 34 | | attr: impl IntoAttribute, 35 | | ^^^^^^^^^^^^^ required by this bound in `HtmlElement::::attr` 36 | -------------------------------------------------------------------------------- /tests/ui/errors/slot_builder_spans.rs: -------------------------------------------------------------------------------- 1 | //! Testing that there are no errors that cause the entire macro to error (i.e. 2 | //! call-site error). 3 | //! 4 | //! This file is for testing on the slot itself, see `com_builder_spans` for 5 | //! testing on components. 6 | 7 | use leptos::*; 8 | use leptos_mview::mview; 9 | 10 | #[slot] 11 | struct SChildren { 12 | an_attr: i32, 13 | children: ChildrenFn, 14 | } 15 | 16 | #[component] 17 | fn TakesSChildren(s_children: SChildren) -> impl IntoView { let _ = s_children; } 18 | 19 | fn missing_args() { 20 | _ = mview! { 21 | TakesSChildren { 22 | slot:SChildren { "hi" } 23 | } 24 | }; 25 | } 26 | 27 | fn incorrect_arg_value() { 28 | _ = mview! { 29 | TakesSChildren { slot:SChildren an_attr="no" { "what" } } 30 | }; 31 | } 32 | 33 | fn incorrect_closure_to_children() { 34 | let s = String::new(); 35 | _ = mview! { 36 | TakesSChildren { 37 | slot:SChildren an_attr=1 |s| { "this is " {s} } 38 | } 39 | }; 40 | } 41 | 42 | #[slot] 43 | struct SNoChildren { 44 | an_attr: i32, 45 | } 46 | 47 | #[component] 48 | fn TakesSNoChildren(s_no_children: SNoChildren) -> impl IntoView { let _ = s_no_children; } 49 | 50 | fn incorrect_children() { 51 | _ = mview! { 52 | TakesSNoChildren { 53 | slot:SNoChildren an_attr=5 { "hey!" } 54 | } 55 | }; 56 | } 57 | 58 | #[slot] 59 | struct SClosureChildren { 60 | children: Callback, 61 | } 62 | 63 | #[component] 64 | fn TakesSClosureChildren(s_closure_children: SClosureChildren) -> impl IntoView { 65 | let _ = s_closure_children; 66 | } 67 | 68 | // FIXME: these spans aren't ideal, but difficult to change without cluttering 69 | // the hover tooltips 70 | fn missing_closure_to_children() { 71 | _ = mview! { 72 | TakesSClosureChildren { 73 | slot:SClosureChildren { "hey!" } 74 | } 75 | }; 76 | } 77 | 78 | fn main() {} 79 | -------------------------------------------------------------------------------- /tests/ui/errors/slot_builder_spans.stderr: -------------------------------------------------------------------------------- 1 | warning: use of deprecated method `SChildrenBuilder::<((), __children)>::build`: Missing required field an_attr 2 | --> tests/ui/errors/slot_builder_spans.rs:22:18 3 | | 4 | 22 | slot:SChildren { "hi" } 5 | | ^^^^^^^^^ 6 | | 7 | = note: `#[warn(deprecated)]` on by default 8 | 9 | error[E0061]: this method takes 1 argument but 0 arguments were supplied 10 | --> tests/ui/errors/slot_builder_spans.rs:22:18 11 | | 12 | 22 | slot:SChildren { "hi" } 13 | | ^^^^^^^^^ an argument of type `SChildrenBuilder_Error_Missing_required_field_an_attr` is missing 14 | | 15 | note: method defined here 16 | --> tests/ui/errors/slot_builder_spans.rs:10:1 17 | | 18 | 10 | #[slot] 19 | | ^^^^^^^ 20 | = note: this error originates in the derive macro `::leptos::typed_builder_macro::TypedBuilder` (in Nightly builds, run with -Z macro-backtrace for more info) 21 | help: provide the argument 22 | | 23 | 22 | slot:SChildren(/* SChildrenBuilder_Error_Missing_required_field_an_attr */) { "hi" } 24 | | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 25 | 26 | error[E0308]: mismatched types 27 | --> tests/ui/errors/slot_builder_spans.rs:29:49 28 | | 29 | 29 | TakesSChildren { slot:SChildren an_attr="no" { "what" } } 30 | | ------- ^^^^ expected `i32`, found `&str` 31 | | | 32 | | arguments to this method are incorrect 33 | | 34 | note: method defined here 35 | --> tests/ui/errors/slot_builder_spans.rs:12:5 36 | | 37 | 12 | an_attr: i32, 38 | | ^^^^^^^----- 39 | 40 | error[E0308]: mismatched types 41 | --> tests/ui/errors/slot_builder_spans.rs:37:38 42 | | 43 | 37 | slot:SChildren an_attr=1 |s| { "this is " {s} } 44 | | ^^^ expected `Rc Fragment>`, found closure 45 | | 46 | = note: expected struct `Rc<(dyn std::ops::Fn() -> Fragment + 'static)>` 47 | found closure `{closure@$DIR/tests/ui/errors/slot_builder_spans.rs:37:38: 37:41}` 48 | 49 | error[E0599]: no method named `children` found for struct `SNoChildrenBuilder` in the current scope 50 | --> tests/ui/errors/slot_builder_spans.rs:53:42 51 | | 52 | 42 | #[slot] 53 | | ------- method `children` not found for this struct 54 | ... 55 | 51 | _ = mview! { 56 | | _________- 57 | 52 | | TakesSNoChildren { 58 | 53 | | slot:SNoChildren an_attr=5 { "hey!" } 59 | | | -^^^^^^ method not found in `SNoChildrenBuilder<((i32,),)>` 60 | | |_________________________________________| 61 | | 62 | 63 | error[E0277]: the trait bound `leptos::Callback: ToChildren<{closure@$DIR/tests/ui/errors/slot_builder_spans.rs:73:37: 73:43}>` is not satisfied 64 | --> tests/ui/errors/slot_builder_spans.rs:71:9 65 | | 66 | 71 | _ = mview! { 67 | | _________^ 68 | 72 | | TakesSClosureChildren { 69 | 73 | | slot:SClosureChildren { "hey!" } 70 | 74 | | } 71 | 75 | | }; 72 | | |_____^ the trait `ToChildren<{closure@$DIR/tests/ui/errors/slot_builder_spans.rs:73:37: 73:43}>` is not implemented for `leptos::Callback` 73 | | 74 | = help: the following other types implement trait `ToChildren`: 75 | Box<(dyn FnMut() -> Fragment + 'static)> 76 | Box<(dyn FnOnce() -> Fragment + 'static)> 77 | Box<(dyn std::ops::Fn() -> Fragment + 'static)> 78 | Rc<(dyn std::ops::Fn() -> Fragment + 'static)> 79 | = note: this error originates in the macro `mview` (in Nightly builds, run with -Z macro-backtrace for more info) 80 | -------------------------------------------------------------------------------- /tests/ui/errors/slot_unsupported_dirs.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_mview::mview; 3 | 4 | #[slot] 5 | struct Nothing {} 6 | 7 | #[component] 8 | fn TakesNothing(nothing: Nothing) -> impl IntoView { let _ = nothing; } 9 | 10 | fn try_bad_dirs() { 11 | let attrs: Vec<(&'static str, Attribute)> = Vec::new(); 12 | let _spread = mview! { 13 | TakesNothing { 14 | slot:Nothing {..attrs}; 15 | } 16 | }; 17 | 18 | let _on = mview! { 19 | TakesNothing { 20 | slot:Nothing on:click={|_| ()}; 21 | } 22 | }; 23 | 24 | let _attr = mview! { 25 | TakesNothing { 26 | slot:Nothing attr:something="something"; 27 | } 28 | }; 29 | 30 | fn a_directive(_el: HtmlElement) {} 31 | let _use = mview! { 32 | TakesNothing { 33 | slot:Nothing use:a_directive; 34 | } 35 | }; 36 | 37 | let _prop = mview! { 38 | TakesNothing { 39 | slot:Nothing prop:value="1"; 40 | } 41 | }; 42 | } 43 | 44 | fn main() {} 45 | -------------------------------------------------------------------------------- /tests/ui/errors/slot_unsupported_dirs.stderr: -------------------------------------------------------------------------------- 1 | error: spread syntax is not supported on slots 2 | --> tests/ui/errors/slot_unsupported_dirs.rs:14:26 3 | | 4 | 14 | slot:Nothing {..attrs}; 5 | | ^^^^^^^^^ 6 | 7 | error: `on:` is not supported on slots 8 | --> tests/ui/errors/slot_unsupported_dirs.rs:20:26 9 | | 10 | 20 | slot:Nothing on:click={|_| ()}; 11 | | ^^ 12 | 13 | error: `attr:` is not supported on slots 14 | --> tests/ui/errors/slot_unsupported_dirs.rs:26:26 15 | | 16 | 26 | slot:Nothing attr:something="something"; 17 | | ^^^^ 18 | 19 | error: `use:` is not supported on slots 20 | --> tests/ui/errors/slot_unsupported_dirs.rs:33:26 21 | | 22 | 33 | slot:Nothing use:a_directive; 23 | | ^^^ 24 | 25 | error: `prop:` is not supported on components/slots 26 | --> tests/ui/errors/slot_unsupported_dirs.rs:39:26 27 | | 28 | 39 | slot:Nothing prop:value="1"; 29 | | ^^^^ 30 | 31 | warning: unused variable: `attrs` 32 | --> tests/ui/errors/slot_unsupported_dirs.rs:11:9 33 | | 34 | 11 | let attrs: Vec<(&'static str, Attribute)> = Vec::new(); 35 | | ^^^^^ help: if this is intentional, prefix it with an underscore: `_attrs` 36 | | 37 | = note: `#[warn(unused_variables)]` on by default 38 | -------------------------------------------------------------------------------- /tests/ui/errors/unsupported_attrs.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_mview::mview; 3 | 4 | fn style_on_component() { 5 | mview! { 6 | Component style:color="white"; 7 | }; 8 | } 9 | 10 | fn prop_on_component() { 11 | mview! { 12 | Component prop:value="1"; 13 | }; 14 | } 15 | 16 | fn attr_on_element() { 17 | mview! { 18 | input attr:class="no" type="text"; 19 | }; 20 | } 21 | 22 | fn clone_on_element() { 23 | let notcopy = String::new(); 24 | mview! { 25 | div { 26 | span clone:notcopy { 27 | {notcopy.clone()} 28 | } 29 | } 30 | }; 31 | } 32 | 33 | #[component] 34 | fn Component() -> impl IntoView { 35 | mview! { 36 | button; 37 | }; 38 | } 39 | 40 | fn main() {} 41 | -------------------------------------------------------------------------------- /tests/ui/errors/unsupported_attrs.stderr: -------------------------------------------------------------------------------- 1 | error: `style:` is not supported on components/slots 2 | --> tests/ui/errors/unsupported_attrs.rs:6:19 3 | | 4 | 6 | Component style:color="white"; 5 | | ^^^^^ 6 | 7 | error: `prop:` is not supported on components/slots 8 | --> tests/ui/errors/unsupported_attrs.rs:12:19 9 | | 10 | 12 | Component prop:value="1"; 11 | | ^^^^ 12 | 13 | error: `attr:` is not supported on elements 14 | --> tests/ui/errors/unsupported_attrs.rs:18:15 15 | | 16 | 18 | input attr:class="no" type="text"; 17 | | ^^^^ 18 | 19 | error: `clone:` is not supported on elements 20 | --> tests/ui/errors/unsupported_attrs.rs:26:18 21 | | 22 | 26 | span clone:notcopy { 23 | | ^^^^^ 24 | -------------------------------------------------------------------------------- /tests/ui/errors/unterminated_element.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn main() { 4 | mview! { 5 | input type="text" 6 | "hi" 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /tests/ui/errors/unterminated_element.stderr: -------------------------------------------------------------------------------- 1 | error: unknown attribute 2 | --> tests/ui/errors/unterminated_element.rs:6:9 3 | | 4 | 6 | "hi" 5 | | ^^^^ 6 | 7 | error: child elements not found 8 | --> tests/ui/errors/unterminated_element.rs:5:9 9 | | 10 | 5 | / input type="text" 11 | 6 | | "hi" 12 | | |____________^ 13 | | 14 | = help: add a `;` at the end to terminate the element 15 | -------------------------------------------------------------------------------- /tests/ui/errors/unterminated_element_error.rs: -------------------------------------------------------------------------------- 1 | use leptos_mview::mview; 2 | 3 | fn main() { 4 | _ = mview! { 5 | div { 6 | "something" 7 | input.input type="text" 8 | } 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /tests/ui/errors/unterminated_element_error.stderr: -------------------------------------------------------------------------------- 1 | error: unterminated element 2 | --> tests/ui/errors/unterminated_element_error.rs:7:13 3 | | 4 | 7 | input.input type="text" 5 | | ^^^^^ 6 | | 7 | = help: add a `;` to terminate the element with no children 8 | -------------------------------------------------------------------------------- /tests/ui/errors/use_directive.rs: -------------------------------------------------------------------------------- 1 | use leptos::{html::AnyElement, HtmlElement}; 2 | use leptos_mview::mview; 3 | 4 | fn no_arg_dir(_el: HtmlElement) {} 5 | 6 | fn arg_dir(_el: HtmlElement, _argument: i32) {} 7 | 8 | fn missing_argument() { 9 | _ = mview! { 10 | div use:arg_dir; 11 | }; 12 | } 13 | 14 | fn extra_argument() { 15 | _ = mview! { 16 | span use:no_arg_dir=2; 17 | }; 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /tests/ui/errors/use_directive.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `i32: From<()>` is not satisfied 2 | --> tests/ui/errors/use_directive.rs:10:17 3 | | 4 | 10 | div use:arg_dir; 5 | | ^^^^^^^ the trait `From<()>` is not implemented for `i32`, which is required by `(): Into<_>` 6 | | 7 | = help: the following other types implement trait `From`: 8 | `i32` implements `From` 9 | `i32` implements `From` 10 | `i32` implements `From` 11 | `i32` implements `From` 12 | `i32` implements `From` 13 | = note: required for `()` to implement `Into` 14 | 15 | error[E0277]: the trait bound `(): From<{integer}>` is not satisfied 16 | --> tests/ui/errors/use_directive.rs:16:29 17 | | 18 | 15 | _ = mview! { 19 | | _________- 20 | 16 | | span use:no_arg_dir=2; 21 | | | ^ the trait `From<{integer}>` is not implemented for `()`, which is required by `{integer}: Into<_>` 22 | 17 | | }; 23 | | |_____- required by a bound introduced by this call 24 | | 25 | = help: the following other types implement trait `From`: 26 | `(T, T)` implements `From<[T; 2]>` 27 | `(T, T, T)` implements `From<[T; 3]>` 28 | `(T, T, T, T)` implements `From<[T; 4]>` 29 | `(T, T, T, T, T)` implements `From<[T; 5]>` 30 | `(T, T, T, T, T, T)` implements `From<[T; 6]>` 31 | `(T, T, T, T, T, T, T)` implements `From<[T; 7]>` 32 | `(T, T, T, T, T, T, T, T)` implements `From<[T; 8]>` 33 | `(T, T, T, T, T, T, T, T, T)` implements `From<[T; 9]>` 34 | and $N others 35 | = note: required for `{integer}` to implement `Into<()>` 36 | -------------------------------------------------------------------------------- /tests/ui/pass/many_braces.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_braces)] 2 | 3 | use leptos_mview::mview; 4 | 5 | fn main() { 6 | _ = mview! { 7 | div a={3} b={"aaaaa"} { 8 | {1234} 9 | span class={"braces not needed"} { "hi" } 10 | } 11 | }; 12 | 13 | _ = mview! { 14 | button class:primary-200={true}; 15 | button on:click={move |_| println!("hi")} { 16 | span 17 | style:background-color={"black"} 18 | style:color="white" 19 | { 20 | "inverted" 21 | } 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /tests/ui/pass/use_directive.rs: -------------------------------------------------------------------------------- 1 | use leptos::{*, html::AnyElement}; 2 | use leptos_mview::mview; 3 | 4 | fn no_arg_dir(_el: HtmlElement) {} 5 | 6 | fn arg_dir(_el: HtmlElement, _argument: i32) {} 7 | 8 | fn main() { 9 | _ = mview! { 10 | div use:no_arg_dir { 11 | span use:arg_dir=10; 12 | } 13 | }; 14 | 15 | _ = mview! { 16 | Component use:no_arg_dir; 17 | Component use:arg_dir=300; 18 | }; 19 | } 20 | 21 | #[component] 22 | fn Component() -> impl IntoView { 23 | mview! { button { "hi" } } 24 | } 25 | 26 | #[component] 27 | fn Spreadable(#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>) -> impl IntoView { 28 | mview! { 29 | div {..attrs}; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use leptos::prelude::*; 4 | 5 | #[track_caller] 6 | pub fn check_str<'a>(component: impl IntoView, contains: impl Into>) { 7 | let component_str = component.into_view().to_html(); 8 | match contains.into() { 9 | Contains::Str(s) => { 10 | assert!( 11 | component_str.contains(s), 12 | "expected \"{s}\" to be found in the component render.\n\ 13 | Found:\n\ 14 | {component_str}" 15 | ) 16 | } 17 | Contains::All(a) => a.into_iter().for_each(|s| { 18 | assert!( 19 | component_str.contains(s), 20 | "expected all of {a:?} to be found in the component render.\n\ 21 | did not find {s:?}\n\ 22 | Found:\n\ 23 | {component_str}" 24 | ) 25 | }), 26 | Contains::Not(s) => { 27 | assert!( 28 | !component_str.contains(s), 29 | "expected \"{s}\" to not be found in the component render.\n\ 30 | Found:\n\ 31 | {component_str}" 32 | ) 33 | } 34 | Contains::NoneOf(a) => a.into_iter().for_each(|s| { 35 | assert!( 36 | !component_str.contains(s), 37 | "expected none of {a:?} to be found in the component render.\n\ 38 | found {s:?} in the component:\n\ 39 | {component_str}" 40 | ) 41 | }), 42 | Contains::AllOfNoneOf([a, n]) => { 43 | a.into_iter().for_each(|s| { 44 | assert!( 45 | component_str.contains(s), 46 | "expected all of {a:?} to be found in the component render.\n\ 47 | did not find {s:?}\n\ 48 | Found:\n\ 49 | {component_str}" 50 | ); 51 | }); 52 | n.into_iter().for_each(|s| { 53 | assert!( 54 | !component_str.contains(s), 55 | "expected none of {n:?} to be found in the component render.\n\ 56 | found {s:?} in the component:\n\ 57 | {component_str}" 58 | ); 59 | }); 60 | } 61 | }; 62 | } 63 | 64 | pub enum Contains<'a> { 65 | Str(&'a str), 66 | All(&'a [&'a str]), 67 | Not(&'a str), 68 | NoneOf(&'a [&'a str]), 69 | AllOfNoneOf([&'a [&'a str]; 2]), 70 | } 71 | 72 | impl<'a> From<&'a str> for Contains<'a> { 73 | fn from(value: &'a str) -> Self { Self::Str(value) } 74 | } 75 | 76 | impl<'a> From<&'a [&'a str]> for Contains<'a> { 77 | fn from(value: &'a [&'a str]) -> Self { Self::All(value) } 78 | } 79 | -------------------------------------------------------------------------------- /tests/values.rs: -------------------------------------------------------------------------------- 1 | use leptos::prelude::*; 2 | use leptos_mview::mview; 3 | use utils::check_str; 4 | mod utils; 5 | 6 | #[test] 7 | fn f_value() { 8 | let yes = || true; 9 | let no = || false; 10 | let r = mview! { 11 | div aria-selected=f["{}", yes()] data-not-selected=f["{}", no()]; 12 | }; 13 | 14 | check_str(r, r#"